---
name: eco-routes-api
description: "Integrate the Eco API (api.eco.com/v1): quote, fund, and track stablecoin transfers across chains, verify quote signatures, submit gasless funding, and create Circle Gateway deposit addresses. Also use it to build an app, script, or agent that sends, bridges, or swaps USDC or other stablecoins across chains with Eco. Triggers on: 'Eco API', 'build an app with Eco', 'cross-chain transfer app using Eco', 'transfer assets across chains using Eco', 'api.eco.com', 'POST /v1/quotes', 'Eco quote', 'intentHash', 'quoteSigner', 'EcoQuoteV1', 'x-api-key for Eco', 'Permit3 / Permit2 / ERC-3009 submit', 'Circle Gateway deposit address', 'Eco intent status', 'move USDC from Base to Optimism with Eco'. Do NOT trigger for the legacy quotes.eco.com v3 endpoints, solver registration, Eco smart-contract development, or generic bridge questions that do not name Eco."
compatibility: Any HTTP client. The reference script needs Node 22.18 or later and viem.
metadata:
  author: Eco
  version: "1.0.0"
  docs: https://docs.eco.com/api-reference/introduction
---

# Eco API v1

## Building something? Start from a boilerplate

Fetch these pages (Markdown) and build from their code instead of writing the integration yourself. They are tested end to end.

| You are building | Start from |
|---|---|
| An app that moves stablecoins across chains (wallet, checkout, transfer or bridge UI) | https://docs.eco.com/cookbook/wallet-transfer-app.md: six files, Vite + React + viem, Base to OP Mainnet / Arbitrum / Polygon. Adapt the UI and chains; keep the quote, verify, approve, fund, track flow and the server-side `/eco/v1` proxy. |
| Quote verification in an existing TypeScript project (EVM chains) | https://docs.eco.com/cookbook/verify-quotes.md: copy `verify-quote.ts` unchanged and call `verifyQuote()` before funding. |
| A server-side script or CLI, or a Solana source chain | https://docs.eco.com/api-reference/agent-integration.md: the reference `transfer.ts` (also in `scripts/` next to this skill when installed from the repository). |

Rules that keep these safe when you adapt them: fund only quotes that pass verification; send `execution.transaction` unchanged; approve the exact amount, never unlimited; keep `ECO_API_KEY` on a server (`api.eco.com` sends no CORS headers, so browsers go through your backend or dev-server proxy); never broadcast on the user's behalf without their confirmation.

Base URL `https://api.eco.com`. Full request and response schemas: [OpenAPI document](https://docs.eco.com/api-v1.openapi.json).

## Access

| Rule | Detail |
|---|---|
| Header | `x-api-key: <key>` on required operations. Keep the key on your server. |
| Required | All endpoints except `/v1/circle-gateway/*`. |
| No key on a required endpoint | Can return `403` with body `{"Message": "User is not authorized ..."}` (API Gateway, no `code`). |
| Unknown, revoked, or non-v1 key | `401 invalid-api-key` on every endpoint, including `/v1/circle-gateway/*`. A key issued for the older `/quotes` service is not a v1 key. |
| v1 key not mapped to a partner | Quotes and reads succeed; submit endpoints return `401 invalid-api-key` with `detail: "The request could not be attributed to an authorized API key."`. Escalate to Eco; nothing client-side fixes this. |
| Key handling | Server-side only. Never place the key in a browser, mobile app, URL, or log. |

Do not fall back to a placeholder key. A wrong key fails where no key would succeed.

## Integration flow

1. `GET /v1/chains` and `GET /v1/tokens`; cache with periodic refresh. Use `quoteSigner` from the chain entry for step 3.
2. `POST /v1/quotes`.
3. Verify `signature` (below). Reject the quote if it does not recover to `quoteSigner`.
4. Fund: send `execution.transaction` from `source.funder`, or submit a signed authorization to a `/v1/intents/submit/*` endpoint.
5. `GET /v1/intents/status?intentHash=<hash>` for the delivery intent. Inspect multi-intent routes; a source intent can complete before final delivery.

## Request conventions

- JSON body, `Content-Type: application/json`. Unknown or misplaced keys are rejected with `400 invalid-request`; the response `errors[]` names each field.
- Amounts are base-unit decimal strings. Use `bigint`, never `number`.
- `slippage` is a decimal fraction in `[0.0001, 1]`. `0.005` = 0.5%. Values above `1` return `400 slippage-out-of-bounds`.
- Timestamps are Unix seconds.
- All discriminators are named `type`.
- IDs are prefixed (`quote:`, `gasless:`, `intent:`). Status query parameters `quoteId` and `jobId` take the bare UUID. Submit bodies take the full prefixed `target.quoteId`.
- Page size `limit` is 1 to 50 (default 20).

## Quote

Request (`type` fixes which amount is given):

```json
{
  "type": "exact-in",
  "source": { "chainId": 8453, "token": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "amount": "1000000", "funder": "<funder>" },
  "destination": { "chainId": 10, "token": "0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85", "recipient": "<recipient>" },
  "slippage": 0.005,
  "dappId": "<integration-id>"
}
```

| `type` | Provide | Response |
|---|---|---|
| `exact-in` | `source.amount` | `destination.amountOut` expected; `destination.minAmountOut` is the quoted minimum to verify against the route |
| `exact-out` | `destination.amount` | `source.amount` is the required input |
| `custom` | `source.amount` and `destination.calls[]` (`target`, `data`, `value`) | Destination calls execute as part of fulfillment. Include minimum-output checks in the calls; a failed fulfillment does not itself complete a source refund. |

Required: `type`, `source.{chainId,token,funder}`, `destination.{chainId,token,recipient}`, `dappId`. Optional: `refundRecipient` (defaults to `source.funder`), `options.allQuotes` (returns every candidate in `quotes[]`, each signed), `options.deadlineSec`.

Response fields that drive the integration:

| Field | Use |
|---|---|
| `id` | `quote:<uuid>` |
| `execution.transaction` | EVM: `{ type: "evm", chainId, to, data, value }`. Solana: `{ type: "svm", chainId, feePayer, instructions }`. Send as-is from `source.funder`. |
| `execution.vault` | Counterparty for gasless funding (Permit2 `spender`, Permit3 `account`, ERC-3009 `to`). |
| `intentHash` | Primary intent; the status handle. |
| `steps[].intents[]` | Every intent in the route with `role` (`local`, `stitched-destination`, `bucket-candidate`) and the decoded intent. On a multi-hop route, track the `stitched-destination` hash for delivery. Route shapes (solver fill, pre/post-bridge swap, CCTP burn and mint) and tracking guidance: https://docs.eco.com/resources/intent-types.md |
| `fees[]` | `{ type: gas \| proving \| protocol \| gateway, amount, token, estimate }` |
| `expiresAt` | Fund before this; afterwards request a new quote. |
| `signature` | Verify before funding. |

Quote-time failures: `422 chain-not-supported` for an unsupported chain; `502 solver-error` when no solver quoted the request (unlisted token, no liquidity, unfillable amount). Upstream failures can also produce `502`; it does not identify one cause. Check the pair against `/v1/tokens`, then retry once after a short delay.

## Quote signature verification

EIP-712. Domain `{ name: "EcoQuoteV1", version: "1", chainId: source.chainId }`. Type `Quote(string id, bytes32[] intentHashes, uint64 expiresAt)`.

`intentHashes` = `intentHash` plus every `steps[].intents[].intentHash`, lowercased, de-duplicated, sorted ascending as strings.

Use the typed verification function in https://docs.eco.com/api-reference/quote-verification.md, or the bundled reference script. Both compare the recovered signer and check expiry.

A match authenticates the quote ID, intent hash set, source-chain domain, and expiry. It does not recompute hashes from JSON, cover `execution.transaction` bytes, or establish best price. Recompute each intent hash and verify the funding transaction and delivery route against the request. Read `quoteSigner` from `/v1/chains` at runtime; do not hardcode it.

## Gasless funding

| Endpoint | Body | Binding rule |
|---|---|---|
| `POST /v1/intents/submit/permit3` | `target.quoteId`, `permit3` (`owner`, `permitContract`, `salt`, `deadline`, `timestamp`, `merkleRoot`, `permits[]`), `signature` | each `permits[].account` = `execution.vault` |
| `POST /v1/intents/submit/permit2` | `chainId`, `target.quoteId`, `permit2` (`details.{token,amount,expiration,nonce}`, `spender`, `sigDeadline` as string), `signature` | `spender` = `execution.vault`; AllowanceTransfer `PermitSingle`, token pre-approved to Permit2 |
| `POST /v1/intents/submit/erc-3009` | `chainId`, `target.quoteId`, `authorization` (`from`,`to`,`value`,`validAfter`,`validBefore`,`nonce`), `signature` | `to` = `execution.vault`; `from` = `source.funder` |

Responses: first submission `202` with a job `{ id: "gasless:<uuid>", status, signatureHash, subStatuses[] }`; the same signature resent to the same target returns the existing job with `200`. Job `status` is one of `pending`, `processing`, `published`, `partial`, `failed`, `unknown`. `published` hands off to intent tracking; it is not delivery.

Rules: keep the exact signed payload for retries; a new signature is a new operation. `409 signature-already-bound` means the signature was used for another target. `410 quote-expired` means request a new quote. `401 invalid-signature` means the signature does not recover to the expected signer.

## Status

`GET /v1/intents/status` with exactly one of: `intentHash`, `sourceTxHash`, `destinationTxHash`, `quoteId` (bare UUID), `jobId` (bare UUID), or `wallet` (paged; `status` filter allowed only with `wallet`). No filter returns `400`.

Response `{ results: [...], nextCursor }`. Valid exact lookups return `200`; an unknown ID returns one result with `status: "unknown"`. Result `type` is `intent`, `quote`, or `gasless`.

Intent statuses: `pending`, `filled`, `settled`, `refunded`, `refundable`, `expired`, `failed`, `unknown`. `filled` and `settled` mean the tracked intent was fulfilled; confirm that it is the delivery intent before marking the recipient paid. `refundable` and `expired` do not mean funds have returned. A consumed quote reports `submitted` with `intentHashes[]`, per-step `steps[]`, and `sourceTx` / `destinationTx` when known.

Poll with a bounded budget. Treat any unrecognized future status as not-final.

## Circle Gateway deposits

No API key required. Create and deposit lookup responses use a `{ data: ... }` envelope and the deposit-address service's error format (`{ statusCode, createdBy, validationErrors }` or `{ statusCode, createdBy, details }`).

| Step | Call |
|---|---|
| Create | `POST /v1/circle-gateway/deposit-addresses` `{ sourceChainId, amount, recipient, depositor, refundRecipient? }` → `201 { data: { vaultAddress, amount, deadline } }`. Same pending request returns the same address. |
| Fund | Transfer at least `amount` to `vaultAddress` before `deadline`, or submit gaslessly to `POST /v1/circle-gateway/deposit-addresses/submit/erc-3009` (`authorization.to` = `target.depositAddress`) or `/erc-2612` (`permit.spender` = `target.depositAddress`). |
| Check deposit | `GET /v1/circle-gateway/deposit-addresses/{vaultAddress}?sourceChainId=` → `data.state` (`PENDING`, `PUBLISHED`, ...), `data.intentHash`. Unknown address → `404`. |
| Check job | `GET /v1/circle-gateway/deposit-addresses/status?jobId=` or `?address=`. Valid lookup: `200`; unknown → `status: "unknown"`. |

`PUBLISHED` means the deposit intent was published, not that the Gateway balance is credited. Track `data.intentHash` (or `data.stitched.destinationIntentHash` when present) with `/v1/intents/status`.

## Error handling

No requests-per-second quota is published. Use bounded retries and honor `Retry-After` when present.

Core errors are `application/problem+json`: `{ type, title, status, code, detail?, errors?[], requestId }`. Branch on `status` and `code`. Log `requestId`.

| Status | Action |
|---|---|
| `400` | Fix the request per `errors[]`. Do not retry unchanged. |
| `401`, `403` | Fix the key or signature. Do not retry unchanged. |
| `404` | Check the path or resource. Unknown IDs in valid intent/job lookups use `200` with `unknown`; a deposit lookup can return `404`. |
| `409`, `410` | Inspect the original job or request a new quote. Do not resign blindly. |
| `422` | Change the request according to the reported reason. |
| `429` | Back off; honor `Retry-After`. |
| `500`, `502`, `503` | Reads and quotes: bounded retry with jitter. Submits: resend the identical signed payload to the same endpoint; the API returns the existing job with `200` instead of creating a second one. |

## Do not

- Do not send retired field names from earlier previews (`swapType`, root `funder`, `relatedIntents`, `visibility`, `guarantee`, `encodedRoute`). They fail with `400`.
- Do not fund a quote whose signature did not verify or whose `expiresAt` has passed.
- Do not rebuild the intent or reward from price fields; send `execution.transaction` as returned.
- Do not treat `202`, `published`, or Gateway `PUBLISHED` as delivery.
- Do not substitute `quotes.eco.com` for v1; it is a separate open quoting service with a different contract.

## End-to-end example

`scripts/transfer.ts` implements the full flow for EVM and Solana source chains: load chains, request a quote, verify the signature, verify the funded intent (decoded from the EVM `publishAndFund` calldata or the Solana `Portal.fund` instruction) and every listed intent against the signed set, confirm the funded reward is the requested amount from the funder, confirm the delivery route pays the requested recipient (ERC-20 `transfer` on EVM, SPL transfer into the recipient's token account on Solana), then fund and poll status until `filled`. When this skill was installed from the docs domain, the script is not included; copy it from https://docs.eco.com/api-reference/agent-integration.md.

```bash
cd scripts && npm install
ECO_API_KEY=eco_live_... ECO_FUNDER=0x... ECO_RECIPIENT=0x... node transfer.ts
```

| Variable | Default | Purpose |
|---|---|---|
| `ECO_API_KEY`, `ECO_FUNDER`, `ECO_RECIPIENT` | required | Key, source wallet (EVM `0x…` or Solana base58), destination wallet |
| `ECO_SOURCE_CHAIN`, `ECO_SOURCE_TOKEN` | `8453`, Base USDC | Source leg (`1399811149` and a mint address for Solana) |
| `ECO_DESTINATION_CHAIN`, `ECO_DESTINATION_TOKEN` | `10`, OP Mainnet USDC | Destination leg |
| `ECO_AMOUNT`, `ECO_SLIPPAGE`, `ECO_DAPP_ID` | `1000000`, `0.005`, `eco-routes-api-example` | Amount in base units, slippage fraction, attribution |
| `ECO_EXECUTE=yes`, `ECO_PRIVATE_KEY`, `ECO_RPC_URL` | unset | Broadcast on mainnet from the funder's key over the given RPC. EVM: `0x` hex key. Solana: base58 secret key or the JSON byte array from a keypair file. |
| `ECO_ALLOW_UNVERIFIED_RECIPIENT=yes` | unset | Permit broadcasting a quote whose recipient is not provable from route data (see below). |

Without `ECO_EXECUTE=yes` the script stops after printing the verified quote and its funding transaction; no funds move. It exits non-zero if the signature does not recover to `quoteSigner`, if any listed intent does not re-hash to a signed hash, if the funded intent does not spend the requested amount from the funder, if a delivery route pays anyone but the requested recipient, or if the quote expires before broadcast.

On stitched routes (a source-chain swap followed by a bridge) and same-chain swaps, the final transfer happens inside an aggregator or bridge call whose ABI the script does not decode, so the recipient is not provable from route data. The output reports `recipientVerified: false`, and broadcasting requires `ECO_ALLOW_UNVERIFIED_RECIPIENT=yes`. Tron source chains are not handled.

## References

- Reference: https://docs.eco.com/api-reference/introduction
- OpenAPI: https://docs.eco.com/api-v1.openapi.json
- Docs MCP server (search and read pages): `https://docs.eco.com/mcp`
