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

# EVM swaps

> Quotes and unsigned transactions for swaps on Ethereum, BNB Chain, Base and Robinhood Chain, at router.raze.bot/evm, with the Solana router's API.

Under `https://router.raze.bot/evm` the router quotes swaps on four EVM chains and builds the transactions that execute them: the exact token approvals the swap needs, then the swap, simulated together at a recent block. It **never signs and never sends**. You sign with your own key and send the transactions, approvals first, through your own RPC.

The EVM routes speak the [Solana routes'](/router/quote-and-swap) API, which follows Jupiter's: `GET` or `POST /evm/quote` with `inputMint`, `outputMint` and `amount`, then the quote back unchanged as `quoteResponse` to `POST /evm/swap` with `userPublicKey`. What differs is what EVM needs: see [What differs from the Solana routes](#what-differs-from-the-solana-routes).

## Chains and venues

| Chain | `chain` | Chain id | Native coin | Venues (`dex`) |
| - | - | - | - | - |
| Ethereum | `eth` | `1` | ETH | Uniswap V2, Uniswap V3 |
| BNB Chain | `bsc` | `56` | BNB | PancakeSwap V2, PancakeSwap V3 |
| Base | `base` | `8453` | ETH | Uniswap V2, Uniswap V3 |
| Robinhood Chain | `robinhood` | `4663` | ETH | Uniswap V2, Uniswap V3 |

* **Exact-in only.** You say how much you spend; the quote says how much arrives (`outAmount`) and the least that will (`otherAmountThreshold`).
* **One venue per route**, direct or through up to two intermediate tokens: the chain's hubs (WETH, WBNB, USDC, USDT, …, listed by [`GET /evm/chains`](/api-reference/router/evm-chains)) and tokens recently seen paired with yours. No split routes, no exact-out, no platform fee.
* **Verified venues only.** A venue is quoted once the router has checked its contracts against the chain ([`GET /evm/venues`](/api-reference/router/evm-venues)). Its `dex` label is the `label` of quotes and what `dexes` / `excludeDexes` take; its id (`base-uniswap-v3`) works in the filters too.

## Routes

| Route | What |
| - | - |
| [`GET /evm/quote`](/api-reference/router/evm-quote), [`POST /evm/quote`](/api-reference/router/evm-quote-post) | Quote a swap: route, output, floor, expiry |
| [`POST /evm/swap`](/api-reference/router/evm-swap) | The approvals and the swap of a quote for your wallet, unsigned and simulated, with gas limits and fees |
| [`POST /evm/swap-instructions`](/api-reference/router/evm-swap-instructions) | The same handler as `/evm/swap` |
| [`GET /evm/venues`](/api-reference/router/evm-venues) | Each venue's label, contracts and verification |
| [`GET /evm/chains`](/api-reference/router/evm-chains) | Each chain's status, hubs and policy: quote lifetime, deadline, gas headroom, least tip |
| [`GET /evm/health`](/api-reference/router/evm-health), [`GET /evm/ready`](/api-reference/router/evm-ready) | Liveness and readiness, no key |

The key is the one of the market-data API and the Solana routes, sent the same ways (`x-api-key`, `Authorization: Bearer` or `?apiKey=`; see [Authentication](/get-started/authentication)). Every request the key lets in counts against your account's limit a minute and spends credits, like any other router request ([Rate limits and credits](/get-started/rate-limits)). A missing or unknown key is `401`, a request over the limit `429`, both with the API's body (`{"error": {"code": "unauthorized", "message": "…"}}`).

Requests are read like the Solana routes': parameters in the query of `GET /evm/quote` or in a JSON body (read as JSON whatever the `Content-Type`, at most 256 KiB), numbers as JSON numbers or decimal strings, unknown fields ignored. Every request is cut at 10 seconds.

## Quote, swap, sign, send

<Steps>
  <Step title="Quote">
    `GET` or `POST /evm/quote` with `chain`, `inputMint`, `outputMint`, `amount` and `slippageBps`. It answers `outAmount`, the floor `otherAmountThreshold`, the `routePlan`, a `quoteId` and `expiresAt`.
  </Step>

  <Step title="Build">
    `POST /evm/swap` with `userPublicKey` and the quote unchanged as `quoteResponse`, before `expiresAt` (15 seconds after the quote; 30 on `eth`). It answers `setupTransactions`, `swapTransaction`, `simulation` and `costs`.
  </Step>

  <Step title="Check the simulation">
    Send only when `simulation.status` is `passed`. The other statuses say what to do instead: see [Simulation](#simulation).
  </Step>

  <Step title="Send the approvals, then the swap">
    Sign each transaction with the wallet's key and send them in the order given: every entry of `setupTransactions`, then `swapTransaction`. Wait for each approval's receipt before the next transaction, or give them consecutive nonces. The swap must be mined before `deadline`.
  </Step>

  <Step title="Wait for the swap's receipt">
    A successful swap delivered at least `otherAmountThreshold` to the recipient. A reverted one moved nothing but gas: quote again.
  </Step>
</Steps>

```bash theme={null}
R=https://router.raze.bot/evm

# Quote 0.001 ETH to USDC on Base, 1% slippage
curl -s -H "x-api-key: $KEY" \
  "$R/quote?chain=base&inputMint=0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE&outputMint=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amount=1000000000000000&slippageBps=100" > quote.json

# Build it for the wallet that will sign: the quote goes back unchanged
jq -n --slurpfile q quote.json --arg w "$WALLET" '{userPublicKey: $w, quoteResponse: $q[0]}' |
  curl -s -H "x-api-key: $KEY" -H 'content-type: application/json' -d @- "$R/swap" > swap.json
```

The quote (a real answer of the public router):

```json theme={null}
{
  "inputMint": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
  "inAmount": "1000000000000000",
  "outputMint": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
  "outAmount": "2480114",
  "otherAmountThreshold": "2455312",
  "swapMode": "ExactIn",
  "slippageBps": 100,
  "priceImpactPct": "0",
  "routePlan": [
    {
      "swapInfo": {
        "ammKey": "0xb4cb800910b228ed3d0834cf79d697127bbb00e5",
        "label": "Uniswap V3",
        "inputMint": "0x4200000000000000000000000000000000000006",
        "outputMint": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
        "inAmount": "1000000000000000",
        "outAmount": "2480114",
        "feeTier": 100
      },
      "percent": 100,
      "bps": 10000
    }
  ],
  "contextSlot": 52397013,
  "timeTaken": 0.036435373,
  "quoteId": "b00fb006-1e11-4498-9d81-6d5d0857ef87",
  "chain": "base",
  "chainId": 8453,
  "router": "0x2626664c2603336e57b271c5c0b26f421741e481",
  "gasEstimate": "132900",
  "blockHash": "0x94b181d0afdb8e403980c281785a1d2d3df7a2cd0296551ec976fc907fe84233",
  "expiresAt": "2026-10-09T22:03:09.343704617Z",
  "candidates": 5
}
```

Its build for `0x…dEaD` (a burn address that happens to hold ETH on Base, used only so the simulation could pass; calldata trimmed). The input is native, so there is nothing to approve:

```json theme={null}
{
  "setupTransactions": [],
  "swapTransaction": {
    "chainId": 8453,
    "from": "0x000000000000000000000000000000000000dead",
    "to": "0x2626664c2603336e57b271c5c0b26f421741e481",
    "data": "0x5ae401dc000000000000000000000000000000000000000000000000000000006ac964ca…",
    "value": "1000000000000000",
    "gas": "149152",
    "maxFeePerGas": "11000000",
    "maxPriorityFeePerGas": "1000000"
  },
  "quoteId": "b00fb006-1e11-4498-9d81-6d5d0857ef87",
  "inAmount": "1000000000000000",
  "outAmount": "2480114",
  "otherAmountThreshold": "2455312",
  "swapMode": "ExactIn",
  "slippageBps": 100,
  "routePlan": [ "… as in the quote …" ],
  "simulation": { "status": "passed", "outAmount": "2480114", "gasUsed": ["124293"] },
  "costs": {
    "gas": "149152",
    "baseFeePerGas": "5000000",
    "maxPriorityFeePerGas": "1000000",
    "maxFeePerGas": "11000000",
    "gasPrice": "6000000",
    "maxNetworkFeeWei": "1640672000000"
  },
  "spender": null,
  "deadline": 1791583434,
  "chain": "base",
  "chainId": 8453,
  "contextSlot": 52397013,
  "blockHash": "0x94b181d0afdb8e403980c281785a1d2d3df7a2cd0296551ec976fc907fe84233",
  "expiresAt": "2026-10-09T22:03:09.343704617Z"
}
```

The swap is the venue router's `multicall(deadline, …)` with `exactInput` and `amountOutMinimum` 2455312, then `refundETH()`; `value` is the ETH spent. Selling 2 USDC for ETH from the same wallet needs an approval first (real answer, trimmed):

```json theme={null}
{
  "setupTransactions": [
    {
      "chainId": 8453,
      "from": "0x000000000000000000000000000000000000dead",
      "to": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
      "data": "0x095ea7b30000000000000000000000002626664c2603336e57b271c5c0b26f421741e48100000000000000000000000000000000000000000000000000000000001e8480",
      "value": "0",
      "gas": "66525",
      "maxFeePerGas": "11000000",
      "maxPriorityFeePerGas": "1000000"
    }
  ],
  "swapTransaction": {
    "chainId": 8453,
    "from": "0x000000000000000000000000000000000000dead",
    "to": "0x2626664c2603336e57b271c5c0b26f421741e481",
    "data": "0x5ae401dc000000000000000000000000000000000000000000000000000000006ac964be…",
    "value": "0",
    "gas": "166575",
    "maxFeePerGas": "11000000",
    "maxPriorityFeePerGas": "1000000"
  },
  "inAmount": "2000000",
  "outAmount": "806252011312846",
  "otherAmountThreshold": "798189491199717",
  "simulation": { "status": "passed", "outAmount": "806252011312846", "gasUsed": ["55437", "138812"] },
  "spender": "0x2626664c2603336e57b271c5c0b26f421741e481",
  "deadline": 1791583422
}
```

The approval is `approve(router, 2000000)`: exactly `inAmount`. The swap is `exactInput` with `amountOutMinimum` 798189491199717, then `unwrapWETH9(798189491199717, recipient)` to pay out ETH.

## The quote

| Parameter | |
| - | - |
| `chain` | `eth`, `bsc`, `base` or `robinhood` (also `ethereum`, `bnb`), or the chain id. Alias `chainId`. Required |
| `inputMint`, `outputMint` | Token addresses, `0x` and 40 hex digits in any case. The chain's coin is `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` or `native`. Different from each other; not the zero address |
| `amount` | What you spend, in the token's smallest unit (wei for the native coin): a decimal string of any size up to 2^256 − 1, or a JSON integer up to 2^64 − 1. No sign, fraction or exponent; `0` is `400 declined zero_amount` |
| `slippageBps` | 0–10000, default 50 |
| `maxHops` | 1–3, default 2: `1` direct only, `2` through at most one intermediate token, `3` two. `0` counts as 1, more than 3 as 3 |
| `onlyDirectRoutes` | `true` is `maxHops` 1 |
| `dexes`, `excludeDexes` | Only, or never, these venues: `dex` labels (`Uniswap V3`) or ids (`base-uniswap-v3`), matched ignoring case and punctuation, comma-separated or as an array (a repeated parameter in a query). Aliases `includeDex`, `excludeDex`. Unknown labels next to known ones are ignored; only unknown labels is `400` |
| `swapMode` | `ExactIn` only: `ExactOut` is `400` |
| `platformFeeBps` | `0` only: there is no platform fee on EVM, and above 0 is `400`, not ignored |

Answers write addresses in lower case, the native coin as `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`.

The router tries up to 48 routes (`candidates` says how many it priced) and prices each on the venue's own contracts, the V2 router's `getAmountsOut` or the V3 QuoterV2, all at one block: `contextSlot` is its number, `blockHash` its hash. The most output wins, then fewer hops, then less gas. `routePlan` has one step per hop, `percent` 100 and `bps` 10000 (no splits): the pool (`ammKey`), the venue (`label`), the hop's amounts and, on V3, `feeTier` in hundredths of a basis point (`100` is 0.01%). Inside a route the wrapped token stands for the native coin. `router` is the contract the swap will go to, `gasEstimate` is indicative (the build's simulation gives the real figure) and `priceImpactPct` is always `"0"`.

**Minimum output.** `otherAmountThreshold` is `outAmount × (10000 − slippageBps) / 10000`, rounded down. The build writes it into the swap's calldata, so the venue's router reverts the swap rather than deliver less. A floor of zero is refused with `422 declined amount_too_small`; an amount so small that no route gives anything is `404 declined no_route`.

**Lifetime.** A quote can be built until `expiresAt`: 15 seconds after quoting, 30 on `eth` (`quoteTtlSecs` in [`GET /evm/chains`](/api-reference/router/evm-chains)), as many times as you like until then. Quotes live in the router's memory: quote again rather than store them.

## The swap

`POST /evm/swap` (or `/evm/swap-instructions`, the same handler) takes:

| Field | |
| - | - |
| `userPublicKey` | The wallet that signs and sends every transaction and pays the input. Alias `wallet`. Required |
| `quoteResponse` | The quote, unchanged. Its `quoteId` names the quote the router builds |
| `quoteId` | Instead of `quoteResponse`: the quote's id alone |
| `destinationTokenAccount` | The address that receives the output, default `userPublicKey`. Alias `recipient` |
| `slippageBps` | Optional: a new floor for this build only, taken from the quote's `outAmount` |

Without `quoteResponse` or `quoteId`, the quote parameters at the top level (`chain`, `inputMint`, `outputMint`, `amount`, …) are quoted and built in the same call.

The router builds **only the quotes it made and still holds**: the route, the amounts, the spender and the calldata come from the stored quote, never from your body. A `quoteResponse` whose `chain`, `inputMint`, `outputMint`, `inAmount`, `outAmount`, `otherAmountThreshold` or `slippageBps` differs from the stored quote is `400 quote_mismatch` (`reason` names the field), not a different build: to change the slippage, send the top-level `slippageBps`. A quote past `expiresAt`, or one the router never made, is `409 route_expired`: quote again.

At a fresh block the router prices the quoted route again and reads the wallet's balance and allowance. If the route now gives less than `otherAmountThreshold`, the answer is `409 declined quote_stale`, with what it gives now (`outAmount`, `null` when the route no longer executes). The floor is never lowered: quote again.

| Field | |
| - | - |
| `setupTransactions` | The approvals, to send first, in order. Often empty |
| `swapTransaction` | The swap |
| `simulation` | What happened when the approvals and the swap ran at the build's block: see [Simulation](#simulation) |
| `costs` | Gas and fees in wei: the gas limits added up, the base fee, the tip, `maxFeePerGas`, a legacy `gasPrice`, and `maxNetworkFeeWei` (gas × `maxFeePerGas`); `null` where unknown |
| `otherAmountThreshold` | The floor in the swap's calldata: the quote's, or moved by this build's `slippageBps` |
| `outAmount` | The quote's. What arrives at the build's block is `simulation.outAmount` |
| `spender` | The venue's router, which the approvals are for. `null` with a native input, a wrap or an unwrap |
| `deadline` | Unix seconds: the swap reverts if mined later. 60 seconds after the build, 180 on `eth` |
| `contextSlot`, `blockHash` | The block the build was read and simulated at |

Each transaction is `{chainId, from, to, data, value, gas, maxFeePerGas, maxPriorityFeePerGas}`: `value` in wei as a decimal string, `gas` the gas limit, present only when the simulation passed, and the two fees present when the router could read them. Add the nonce, sign with `from`'s key, send.

### Approvals

* **Exact.** `approve(spender, inAmount)` on the input token, to the venue's router (the quote's `router`). Never an unlimited allowance.
* **Reset first.** When the wallet already has a smaller, non-zero allowance for that router, the build adds `approve(spender, 0)` before it: tokens like USDT refuse to change one non-zero allowance into another.
* **None needed.** `setupTransactions` is empty with a native input, for a wrap or an unwrap, and when the allowance already covers `inAmount`.
* **Not atomic with the swap.** They are separate transactions. Send them first, in order, and wait for each receipt, or give approvals and swap consecutive nonces. If the swap is never sent or fails, the allowance stays, at most `inAmount` for that router; send `approve(spender, 0)` yourself to take it back.

If the approvals take long to confirm, check `deadline` before you send the swap. When it is close or past, quote and build again: the allowance is in place by then, so the new build has no approvals.

### Native coin in and out

* **Spelling.** `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (any case) or `native` is the chain's coin, ETH or BNB, distinct from its wrapped token (WETH, WBNB). Answers write it in lower case. Inside a `routePlan` the pools trade the wrapped token, so the steps name it instead.
* **Native in.** `swapTransaction.value` is `inAmount` and there is nothing to approve. A V3 swap with native input ends with `refundETH()`: anything a pool does not take goes back to the wallet in the same transaction instead of staying in the router.
* **Native out.** The swap pays the native coin to the recipient, with the floor checked on the unwrap itself (`unwrapWETH9(otherAmountThreshold, recipient)` on V3).
* **Wrap and unwrap.** From the native coin to its wrapped token is a wrap, the other way an unwrap: one call to the wrapped-token contract, `deposit()` with `value` = `inAmount`, or `withdraw(inAmount)`. The quote has one `routePlan` step labelled `Wrap` or `Unwrap` whose `ammKey` is the wrapped token; it is 1:1 (`otherAmountThreshold` = `inAmount`, `candidates` 0, `slippageBps` has no effect, venue filters do not apply) and needs no approval. Both credit the caller, so `destinationTokenAccount` must be `userPublicKey` (`422 declined recipient_unsupported` otherwise).

### Gas and fees

* `gas` on each transaction is the gas the simulation used × the chain's headroom: 120%, 130% on `robinhood` (`gasHeadroomPct`). Use it as the gas limit.
* `maxFeePerGas` is twice the next block's base fee plus the tip; `maxPriorityFeePerGas` is the median tip of recent blocks, never below the chain's `minPriorityFeeWei` (0.05 gwei on `eth` and `bsc`, 0.001 gwei on `base`, none on `robinhood`). `costs.gasPrice` is a price for a legacy (type 0) transaction. When the router could not read the fees they are absent from the transactions and `null` in `costs`: use your own estimate. Any fee strategy works; the swap's floor and deadline do not depend on it.
* The wallet pays gas in the native coin on top of `inAmount`. The simulation does not check that it can, nor the nonce.

## Simulation

The build runs the approvals and the swap in order at its block (`eth_simulateV1`), between reads of the wallet's input balance and the recipient's output balance. It judges what moved, not what the router contract says it returned.

| `status` | Meaning | What to do |
| - | - | - |
| `passed` | Exactly `inAmount` left the wallet and at least `otherAmountThreshold` reached the recipient. `gasUsed` per transaction, `outAmount` what arrived | Send: approvals first, then the swap |
| `failed` | It would not work; `reason` below. `transaction` is the index of the failing one in `setupTransactions` + the swap (the swap is last) | Do not send |
| `requires_approval` | The router's node cannot simulate an approval followed by the swap, so nothing was simulated and no `gas` is given | Send the approvals alone (estimate their gas yourself) and wait for them, then quote and build again: with the allowance in place the new build has no approvals |
| `not_simulated` | The simulation could not run: `reason` is an RPC error code (`rpc_timeout`, …) or `balance_unreadable` | Build again (the quote can be built until it expires) or quote again. Do not send blind |

| `failed` `reason` | Meaning |
| - | - |
| `insufficient_balance` | The wallet holds less than `inAmount`. Nothing was simulated (`transaction` is `null`) |
| `approval_reverted` | An approval reverted |
| `swap_reverted` | The swap reverted; `revertReason` is the contract's message when it gave one |
| `partial_fill` | The pool took less than `inAmount`: quote a smaller amount |
| `output_below_minimum` | Less than the floor reached the recipient: a token that takes a tax on transfer |
| `unexpected_output`, `out_of_gas` | Only on a node without `eth_simulateV1` |

A wallet without the input gets the transactions anyway, unsimulated and without `gas`:

```json theme={null}
{ "simulation": { "status": "failed", "reason": "insufficient_balance", "transaction": null, "revertReason": null } }
```

A pass is true at the build's block. The chain moves before your transactions land: the floor and the deadline in the swap are what protect you on chain.

<Note>
  Fee-on-transfer, rebasing and honeypot tokens are not supported: their builds fail in simulation (`output_below_minimum`, `partial_fill`, `swap_reverted`). Quotes price the gross output and cannot tell.
</Note>

## Sign and send with viem

The build's transactions, signed by a local key and sent through your RPC, approvals first, each mined before the next:

```typescript theme={null}
import { createPublicClient, createWalletClient, http, type Address, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

const ROUTER = "https://router.raze.bot/evm";
const NATIVE = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE";

async function post<T>(path: string, body: unknown): Promise<T> {
  const res = await fetch(`${ROUTER}/${path}`, {
    method: "POST",
    headers: { "content-type": "application/json", "x-api-key": process.env.RAZE_API_KEY! },
    body: JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) {
    // The router answers {error, reason} ({error, reasons} when not ready);
    // the key gate's 401 and 429 answer {error: {code, message}}.
    const why = typeof json.error === "string"
      ? `${json.error}: ${json.reason ?? json.reasons?.join("; ")}`
      : json.error?.code;
    throw new Error(`${res.status} ${why}`);
  }
  return json as T;
}

type Tx = {
  chainId: number;
  from: Address;
  to: Address;
  data: Hex;
  value: string;
  gas?: string;
  maxFeePerGas?: string;
  maxPriorityFeePerGas?: string;
};
type Swap = {
  setupTransactions: Tx[];
  swapTransaction: Tx;
  deadline: number;
  simulation: { status: string; reason?: string };
};

const account = privateKeyToAccount(process.env.PRIVATE_KEY as Hex);
const transport = http(process.env.BASE_RPC_URL);
const wallet = createWalletClient({ account, chain: base, transport });
const client = createPublicClient({ chain: base, transport });

// 1. Quote: 2 USDC for ETH on Base, 1% slippage
const quote = await post<Record<string, unknown>>("quote", {
  chain: "base",
  inputMint: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  outputMint: NATIVE,
  amount: "2000000",
  slippageBps: 100,
});

// 2. Build it for this wallet right away (a quote lives 15 s on Base), the quote unchanged
const swap = await post<Swap>("swap", { userPublicKey: account.address, quoteResponse: quote });

// 3. Send only what the router could simulate
if (swap.simulation.status !== "passed") {
  throw new Error(`not sent: ${swap.simulation.status} ${swap.simulation.reason ?? ""}`);
}

// 4. Approvals first, then the swap, each mined before the next
for (const tx of [...swap.setupTransactions, swap.swapTransaction]) {
  if (tx === swap.swapTransaction && Date.now() / 1000 > swap.deadline - 5) {
    throw new Error("the deadline passed while approving: quote and swap again");
  }
  const fees =
    tx.maxFeePerGas && tx.maxPriorityFeePerGas
      ? { maxFeePerGas: BigInt(tx.maxFeePerGas), maxPriorityFeePerGas: BigInt(tx.maxPriorityFeePerGas) }
      : {}; // no fees from the router: viem estimates them
  const hash = await wallet.sendTransaction({
    to: tx.to,
    data: tx.data,
    value: BigInt(tx.value),
    gas: BigInt(tx.gas!), // present because the simulation passed
    ...fees,
  });
  const receipt = await client.waitForTransactionReceipt({ hash });
  if (receipt.status !== "success") throw new Error(`reverted: ${hash}`);
}
```

On another chain use the viem chain whose id is the transactions' `chainId`, and an RPC of that chain. To send without waiting between transactions, take the wallet's pending nonce and give the approvals and the swap consecutive nonces, in the same order.

## Errors

Errors have the Solana routes' shape, `{"error", "reason"}`, without `quoteId`: `error` is a closed code, `reason` a stable label after `declined` (or `rpc`) and a sentence otherwise. They never carry your input, a URL, a key or a provider's message. `not_ready` carries a list, `reasons`, instead of `reason`. The key gate's `401` and `429` use the API's body, `{"error": {"code", "message"}}`.

```json theme={null}
{ "error": "declined", "reason": "no_route", "candidates": 4 }
```

| HTTP | `error` | `reason` | Meaning | What to do |
| - | - | - | - | - |
| 400 | `bad_request` | A sentence | A missing or malformed field, a chain not served, a bad address, the same token twice, `ExactOut`, `slippageBps` above 10000, `dexes` naming no venue of the chain, a platform fee, a `quoteResponse` without `quoteId`, a body that is not a JSON object | Fix the request |
| 400 | `declined` | `zero_amount` | `amount` is 0 | |
| 400 | `quote_mismatch` | `<field> differs from the quote` | The `quoteResponse`, or a top-level `chain`, `inputMint`, `outputMint` or `amount`, says something else than the stored quote | Send the quote back unchanged |
| 401 | `unauthorized` | | Missing or unknown key, or no credits left (the API's body) | Send a valid key |
| 404 | `declined` | `no_route` | No route on the verified venues under your filters (`candidates`: routes tried) | Another pair, amount or filter |
| 404 | `not_found` | `no such route` | Unknown path (`/evm/build` and `/evm/deployments` are gone) | |
| 405 | | | A known path with the wrong method (`GET /evm/swap`): empty body, `Allow` names the right one | Use that method |
| 409 | `route_expired` | A sentence | The router does not hold that quote: past `expiresAt`, never made here, or made before a restart | Quote again |
| 409 | `declined` | `quote_stale` | At the build's block the route gives less than the floor: `outAmount` (now, or `null`) and `otherAmountThreshold` | Quote again |
| 413 | | | Body over 256 KiB (plain-text body) | Send less |
| 422 | `declined` | `amount_too_small` | The floor would be zero | A larger amount or a smaller slippage |
| 422 | `declined` | `recipient_unsupported` | A wrap or an unwrap to another address | Drop `destinationTokenAccount` |
| 429 | `rate_limited` | | Over your account's limit a minute (the API's body) | Wait for the next minute |
| 503 | `not_ready` | `reasons` | The chain has no RPC or no verified venue yet, none of the venues in `dexes` is verified, or too many quotes and builds are in flight (`Retry-After: 1`) | Retry with backoff, or drop `dexes` |
| 503 | `rpc` | `rpc_timeout`, `rpc_block_unavailable`, `rpc_unavailable`, `rpc_queue_full`, `request_timeout`, … | The router could not read the chain, or the request took more than 10 seconds | Retry with backoff |

`no_route` is about the market and `rpc` about the router's node: a missing pool is never reported as an RPC failure, nor the reverse.

## Readiness

[`GET /evm/ready`](/api-reference/router/evm-ready) answers `200` when at least one chain can trade (an RPC, a verified venue and a fresh block head) and `503` otherwise, with the Solana router's body: `ready`, every `reasons` it is not, and `warnings` for a chain that cannot trade while another can. `chains` lists every chain with its own `ready`, the `dex` labels of its verified venues and its newest block (`head`, from the router's stream with `ageMs`, or read over HTTP). Chains are independent: one that is not ready does not stop quotes and builds on the others. [`GET /evm/health`](/api-reference/router/evm-health) is plain `ok` while the process serves HTTP.

## What differs from the Solana routes

| | Solana routes | EVM routes |
| - | - | - |
| Chain | Implicit | `chain` required |
| Mints | base58; wrapped SOL `So111…112` | `0x` addresses; the native coin `0xEeee…EEeE` or `native`, the wrapped token inside routes |
| Amounts | u64 | uint256: decimal strings, or JSON integers up to 2^64 − 1 |
| `swapMode` | `ExactIn`, `ExactOut` | `ExactIn` only |
| `maxHops` | Up to 4 | Up to 3, default 2, one venue per route |
| Splits, platform fee | `allowSplit`; `platformFeeBps`, `feeBps` + `feeAccount` | None: a fee above 0 is `400` |
| `quoteResponse` | Its route is built as sent | Its `quoteId` names a quote the router holds (15 s, 30 s on `eth`); the route is priced again and any other value must match |
| Build answer | Instructions, and a v1 transaction with `serialize: true` | Unsigned transactions, always: `setupTransactions`, then `swapTransaction`, simulated |
| `destinationTokenAccount` | A wallet, paid through its token account | The address that receives the output |
| Fees | `prioritizationFeeLamports`, tips | `maxFeePerGas` / `maxPriorityFeePerGas` on each transaction (priority-fee fields are ignored) |
| `contextSlot`, `costs` | Slot; lamports | Block number; wei |
| `quoteId` | 16 hex digits, also on errors | A UUID; errors carry none |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.