> ## Documentation Index
> Fetch the complete documentation index at: https://docs.eco.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verifying quote signatures

> Every quote returned by the Eco API is signed. Recover the signer client-side and compare it with the published quoteSigner.

Every quote from `POST /v1/quotes` carries a top-level `signature`: an EIP-712 signature by the address published as `quoteSigner` on the source chain's entry in [`GET /v1/chains`](/api-reference/v1/chains). Verification is one hash and one signature recovery, with no API key and no extra call. With `options.allQuotes`, each entry in `quotes` carries its own signature.

## What is signed

```text theme={null}
domain:  { name: "EcoQuoteV1", version: "1", chainId: <source.chainId> }
Quote(string id, bytes32[] intentHashes, uint64 expiresAt)
```

| Field            | Taken from                                                                                                           |
| ---------------- | -------------------------------------------------------------------------------------------------------------------- |
| `id`             | `quote.id`, including the `quote:` prefix                                                                            |
| `intentHashes`   | `quote.intentHash` plus every `steps[].intents[].intentHash`: lowercased, de-duplicated, sorted ascending as strings |
| `expiresAt`      | `quote.expiresAt`, Unix seconds                                                                                      |
| domain `chainId` | `quote.source.chainId`                                                                                               |

An intent hash already commits to the route, reward, recipient, and amounts, so changing any of them changes the hash and breaks the signature. The funding calldata is intentionally outside the signature. The source chain lives in the domain, so a signature for a Base quote does not verify as an OP Mainnet quote.

## Verify

```typescript theme={null}
import { hashTypedData, recoverAddress } from 'viem';

const intentHashes = [quote.intentHash, ...quote.steps.flatMap((s) => s.intents.map((i) => i.intentHash))]
  .filter(Boolean)
  .map((h) => h.toLowerCase());

const digest = hashTypedData({
  domain: { name: 'EcoQuoteV1', version: '1', chainId: quote.source.chainId },
  types: {
    Quote: [
      { name: 'id', type: 'string' },
      { name: 'intentHashes', type: 'bytes32[]' },
      { name: 'expiresAt', type: 'uint64' },
    ],
  },
  primaryType: 'Quote',
  message: {
    id: quote.id,
    intentHashes: [...new Set(intentHashes)].sort(),
    expiresAt: BigInt(quote.expiresAt),
  },
});

const signer = await recoverAddress({ hash: digest, signature: quote.signature });
// Compare case-insensitively with chains[source.chainId].quoteSigner from GET /v1/chains.
```

The same recovery applies whether the source chain is EVM or Solana; only the domain `chainId` differs.

## What a match proves

A matching signer proves the quote was issued by Eco, names exactly those intents, and has not been altered or extended past `expiresAt`. It does not prove the price is the best available, and it does not cover the bytes of `execution.transaction`.

Read `quoteSigner` from `GET /v1/chains` and cache it with a refresh rather than pinning it in code. The published signer can rotate.
