// Developers
API Reference
The RouterX REST API exposes same-chain quoting and swap calldata over HTTPS. Cross-chain quotes are built locally by the TypeScript SDK (rx.cross.quote); the Cross API stores transfer history.
Base URL#
https://api.routerx.exchangePaths are under /v1. Amounts are decimal strings in the token's smallest unit. Responses wrap payloads as { "code": 0, "data": … }.
Authentication#
Public meta routes need no key: /v1/:chain/dexList, trustedTokens, tokenList, gasPrice, tokenInfo, and rpcs, plus /healthz. Quote, quotePlan, and swap (GET and POST) take an x-api-key header when keys are configured on the server. The key is header-only; ?apiKey= is no longer accepted.
Prefer the SDK for Cross quotes
Same-chain/quote and /swap are HTTP. Cross quotes run in @routerx.exchange/sdk via rx.cross.quote (on-chain RXQuoter + Iris fees) — the Cross HTTP POST /v1/quote is disabled.GET /:chain/quote#
GET/v1/:chain/quote
Returns the best gas-adjusted route for a single swap. Reads go to RXQuoter (0x27fe…a666); data.router is the swap proxy. :chain accepts a chain ID (8453) or code (base).
The server builds candidates from its pair index (direct routes plus two-leg routes through the chain's hop tokens), ranks them by per-adapter USD reserve, and fans them out as Multicall3 batches of queryExactPath. A hit sets quoteMode to "narrow". If no candidate returns an amount, the response is 404 (no route); the server does not fall back to a full RXQuoter.query search. For a token the index has not seen, the server first waits up to about 1.5 seconds to pull its pools from GeckoTerminal. To run the same calls on your own RPC, use quotePlan below.
curl "https://api.routerx.exchange/v1/8453/quote?tokenIn=0x4200000000000000000000000000000000000006&tokenOut=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amountIn=1000000000000000000"Query parameters
| Parameter | Type | Description |
|---|---|---|
tokenIn | address | Input token. Required. Native uses 0xEeee…EEeE. |
tokenOut | address | Output token. Required. |
amountIn | string | Input amount in base units. Required. |
maxSteps | number | Max hops. Optional; defaults to the chain config. |
gasPrice | string | Gas price in wei for route scoring. Optional. |
split | string | Split routing: auto (default), force (skip the small-order gate), or off. Optional. |
Split routes
A large order can be cheaper across several pools. The server takes up to 4 candidates that share no pool (same adapter and token pair), quotes the full amount and one tenth of it, and measures price impact against a straight-line extrapolation of that tenth. The impact sets how far it goes:
| Impact | Behavior |
|---|---|
| under 10 bps | Stays on one path; no further probes. |
| 10–40 bps | Up to 2 legs; must beat the single path by 20 bps. |
| 40–100 bps | Up to 3 legs; must beat it by 10 bps. |
| 100 bps or more | Up to 4 legs; must beat it by 5 bps. |
When a split wins, the response adds legs and amountOut is their sum. offer stays the best single path; if no single path fills the amount, it is the largest leg. split is in bps and the legs add up to 10000. Each leg was quoted at floor(amountIn × split / 10000); on-chain the last leg takes the remainder. split=force skips the table above and probes the full budget, but still only splits when the total is at least the single path. The gain threshold does not count gas.
"legs": [
{ "split": 7000, "amountIn": "140000000000000000000", "amountOut": "334824958185", "gasEstimate": "…",
"hops": [{ "adapter": "0x…", "name": "UniswapV3", "tokenOut": "0x…", "extra": "0x…" }] },
{ "split": 3000, "amountIn": "60000000000000000000", "amountOut": "…", "gasEstimate": "…", "hops": [ … ] }
]RPC fallback
rpcSource is "primary"for the chain's configured RPC or "fallback". When the primary RPC is down or its breaker is open, the server retries on the fastest free RPCs from /v1/:chain/rpcs. A node that answers with no route is still a 404 and does not trigger the fallback.
Response
{
"code": 0,
"data": {
"chainId": 8453,
"tokenIn": "0x4200000000000000000000000000000000000006",
"tokenOut": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"amountIn": "1000000000000000000",
"amountOut": "2431382901",
"offer": {
"amounts": ["1000000000000000000", "2431382901"],
"adapters": [{ "address": "0x6A7c…", "name": "PancakeV3Adapter" }],
"path": ["0x4200…", "0x8335…"],
"gasEstimate": "0",
"extras": ["0x"]
},
"router": "0x1abe089620a6f3E3Ff802f2165AB8dbC10cd5CE8",
"cached": false,
"quoteMode": "narrow",
"rpcSource": "primary"
}
}GET /:chain/quotePlan#
GET/v1/:chain/quotePlan
Same query parameters as /quote. The server reads the in-memory pair index and returns calldata. It does not quote. You eth_call the plan on your own RPC, then POST the winning offer to /swap.
eth_calleach entry incalls(tois Multicall3). Decode the result as the return value ofaggregate3. RowimatchescandidateIds[i].- For each row with
success: true, decodereturnDataas the return value ofqueryExactPathand keep the offer whose lastamountsentry is largest. - If
callsis empty, or every candidate fails or returns 0, there is no route. Do not fall back to a full search. - POST the chosen
offer(amounts,adapters,path,extras) to/v1/:chain/swap.
Each aggregate3 holds up to 16 candidates, 64 in total. A candidate that uses UniswapV4 costs 20–30M gas, so it gets its own call, and a plan carries at most 3 of them. If your node rejects a call, split it in half and retry. Candidates are ordered by the minimum known USD reserve along the path (score).
poolsPending: true means the server is still pulling pools for one of the tokens in the background, so the plan may be missing routes; request it again in a few seconds for a fuller plan. fallback is null on every supported chain. It is only set when the pair index is turned off for a chain; then eth_call it and decode the result as the return value of query.
Split probes
split is null when split=off or fewer than 2 candidates can split. Otherwise run split.calls in the same round as calls. Row i of a split call is probes[i]: candidate candidateId quoted at units of steps (one unit is amountIn / steps). The full amount (units = steps) is that candidate's row in calls. candidateIds share no pool and are ordered best reserves first.
- Impact = 1 − (best single path at full amount) ÷ (its
units = 1output ×steps). Look it up in the table under/quoteto get how many candidates and legs to use and the gain threshold. Withmode: "force", or when no single path fills the amount, use every candidate andminGainBps. - Spread
stepsunits over the first N candidates to maximize total output (a grouped knapsack). Keep it if it uses 2 or more legs and beats the single path by the threshold. - POST
legsto/v1/:chain/swap, one per used candidate:{ split: units × 10000 / steps, offer }.
The SDK's rx.swap.quote does all of this. Probing costs about 9 extra queryExactPath per split candidate; pass split=off to skip it.
{
"code": 0,
"data": {
"chainId": 8453,
"tokenIn": "0x…",
"tokenOut": "0x…",
"amountIn": "1000000000000000000",
"path": { "tokenIn": "0x…", "tokenOut": "0x…" },
"maxSteps": 3,
"quoter": "0x27fe493F8A4F02e3049080d6aBD96646C7B1a666",
"router": "0x1abe089620a6f3E3Ff802f2165AB8dbC10cd5CE8",
"multicall3": "0xcA11bde05977b3631167028862bE2a173976CA11",
"candidates": [
{
"id": 0,
"kind": "direct",
"tokens": ["0x…", "0x…"],
"adapterIdx": [2],
"adapters": [{ "index": 2, "address": "0x…", "name": "UniswapV3" }],
"score": 8137595
}
],
"poolsPending": false,
"calls": [{ "to": "0xcA11…", "data": "0x82ad56cb…", "candidateIds": [0, 1] }],
"fallback": null,
"split": {
"mode": "auto",
"steps": 10,
"minGainBps": 5,
"candidateIds": [0, 3],
"calls": [{
"to": "0xcA11…",
"data": "0x82ad56cb…",
"probes": [{ "candidateId": 0, "units": 1, "amountIn": "100000000000000000" }]
}]
}
}
}ABI
The response does not carry ABIs. These fragments are fixed; they parse with viem's parseAbi and ethers' Interface. aggregate3 is Multicall3 (0xcA11…CA11, same address on every chain). queryExactPath and query are RXQuoter (0x27fe…a666).
export const QUOTE_PLAN_ABI = [
'function aggregate3((address target, bool allowFailure, bytes callData)[] calls) payable returns ((bool success, bytes returnData)[] returnData)',
'function queryExactPath(uint256 amountIn, address[] tokens, uint8[] adapterIdx) view returns ((uint256[] amounts, address[] adapters, address[] path, uint256 gasEstimate, bytes[] extras) offer)',
// only for a non-null fallback
'function query(uint256 amountIn, address tokenIn, address tokenOut, uint256 maxSteps, address[] hopTokens, uint256 gasPrice) view returns ((uint256[] amounts, address[] adapters, address[] path, uint256 gasEstimate, bytes[] extras) offer)',
] as constimport { createPublicClient, decodeFunctionResult, http, parseAbi } from 'viem'
import { base } from 'viem/chains'
const abi = parseAbi(QUOTE_PLAN_ABI)
const client = createPublicClient({ chain: base, transport: http() })
const plan = (await (await fetch(planUrl)).json()).data
let best
for (const call of plan.calls) {
const { data } = await client.call({ to: call.to, data: call.data })
const rows = decodeFunctionResult({ abi, functionName: 'aggregate3', data: data! })
rows.forEach((row, i) => {
if (!row.success) return
const offer = decodeFunctionResult({ abi, functionName: 'queryExactPath', data: row.returnData })
const out = offer.amounts[offer.amounts.length - 1]
if (out > 0n && (!best || out > best.amounts[best.amounts.length - 1])) best = offer
})
}
if (!best) throw new Error('no route')
const offer = {
amounts: best.amounts.map(String),
adapters: best.adapters,
path: best.path,
extras: best.extras,
gasEstimate: String(best.gasEstimate),
}
// POST { tokenIn, tokenOut, amountIn, to, offer } to /v1/:chain/swapGET /:chain/swap#
GET/v1/:chain/swap
Same as quote, plus executable transaction fields. Encodes swap / swapFromAVAX / swapToAVAX calldata on the CREATE2 router.
curl "https://api.routerx.exchange/v1/8453/swap?tokenIn=0x4200000000000000000000000000000000000006&tokenOut=0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913&amountIn=1000000000000000000&to=0xYourRecipient"Extra parameters
| Parameter | Type | Description |
|---|---|---|
to | address | Recipient. Required. |
slippage | number | Slippage percent. Default 0.5. |
fee | number | Fee in bps. Optional. Only fee-whitelisted senders are charged this value; everyone else pays the router DEFAULT_FEE (10 bps). |
Response adds minAmountOut, feeBps, and tx: { to, data, value }. Native tokenIn sets value = amountIn; ERC-20 input requires an allowance to the router first. A split quote encodes all of its legs into one Trade.
The router takes its fee out of amountIn before swapping, but the quote used the full amount, so minAmountOut removes the fee first. The server does not know who signs, so it assumes the larger fee:
feeBps = max(fee, 10)
minAmountOut = floor(amountOut × (1 − feeBps / 10000) × (1 − slippage / 100))
amountOut = 990, slippage = 1, no fee → 990 × 0.999 × 0.99 = 979.12 → 979POST /:chain/swap#
POST/v1/:chain/swap
Builds swap calldata from an offer you already quoted (quotePlan or your own RXQuoter call). The server does not quote again and does not send an RPC for an ERC-20 in, ERC-20 out trade. The response matches GET /swap, with quoteMode: "client".
{
"tokenIn": "0x…",
"tokenOut": "0x…",
"amountIn": "1000",
"to": "0x…",
"slippage": 0.5,
"fee": 0,
"offer": {
"amounts": ["1000", "990"],
"adapters": ["0x…"],
"path": ["0x…", "0x…"],
"extras": ["0x"],
"gasEstimate": "120000"
}
}Checks: path starts at tokenIn and ends at tokenOut (native addresses are the wrapped native), amounts[0] equals amountIn, every adapter is on the chain whitelist, and extras are hex. adapters may be addresses or { address }. minAmountOut is the last amount less the fee and slippage, as in GET /swap; the router enforces it on-chain.
Split legs
For a split, send legs instead of offer:
{
"tokenIn": "0x…", "tokenOut": "0x…", "amountIn": "1000", "to": "0x…", "slippage": 0.5,
"legs": [
{ "split": 6000, "offer": { "amounts": ["600", "595"], "adapters": ["0x…"], "path": ["0x…", "0x…"], "extras": ["0x"] } },
{ "split": 4000, "offer": { "amounts": ["400", "396"], "adapters": ["0x…"], "path": ["0x…", "0x…"], "extras": ["0x"] } }
]
}Extra checks: 2 to 8 legs; each splitis a positive integer and they sum to 10000; each leg's amounts[0] is floor(amountIn × split / 10000); and no pool (adapter plus token pair) appears twice, within a leg or across legs. amountOut is the sum of the legs, the response carries legs, and offer is the largest leg.
GET /:chain/tokenList#
GET/v1/:chain/tokenList
Official token list for a chain (address, symbol, name, decimals, icon). No API key. Related: /v1/:chain/dexList, /v1/:chain/gasPrice, /v1/:chain/tokenInfo?tokenAddress=, and /v1/chains.
curl "https://api.routerx.exchange/v1/8453/tokenList"GET /healthz#
GET/healthz
Health check. No API key. Returns 200 while the process is up; status is "degraded" and reasonslists why when PocketBase is not ready, an RPC breaker is not closed, or an enabled chain's pair index is empty or stale.
{
"status": "degraded",
"reasons": ["pair-index stale on base"],
"uptimeSec": 86400,
"pocketbase": { "ready": true, "lastError": null },
"breakers": { "8453": { "state": "closed", "total": 12, "failures": 0, "failRate": 0 } },
"pairIndex": { "8453": { "enabled": true, "pairs": 1520, "lastBuildAt": 1760900000000, "lastError": null, "stale": true } }
}Cross API#
https://cross-api.routerx.exchangerouterx-cross stores Cross transfer history. Quotes are not served over HTTP — use rx.cross.quote in @routerx.exchange/sdk (local RXQuoter legs + Circle Iris fees). Override the history host with crossApiUrl.
CCTP ∩ swap Composer sources
Ethereum, Optimism, Unichain, Polygon, Base, Arbitrum, Avalanche, Monad, World Chain, HyperEVM, and Arc. BSC, Robinhood, Katana, Plasma, and Elysium are swap-only and are not Cross sources. Arc Wave A is ERC-20 only.POST /v1/quote
POST/v1/quote
Disabled (HTTP 410). Kept as a stub for possible partner access later. Example SDK request shape for rx.cross.quote:
{
"srcChainId": 8453,
"dstChainId": 42161,
"tokenIn": "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
"tokenOut": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
"amountIn": "1000000000000000000",
"slippageBps": 50
}Transfers
| Method | Path | Description |
|---|---|---|
| POST | /v1/transfers | Create a transfer record |
| GET | /v1/transfers?account= | List by account |
| GET | /v1/transfers/:id | Get one transfer |
| PATCH | /v1/transfers/:id | Patch status / hashes |
| PATCH | /v1/transfers/by-burn | Patch by burn tx |
| POST | /v1/transfers/:id/refresh | One-shot Iris fill-in |
Errors#
Failed requests return an HTTP status with { "code": <status>, "message": "…" }.
| Status | Meaning |
|---|---|
| 400 | Missing or malformed parameters (e.g. invalid amountIn). |
| 401 | Invalid or missing API key when keys are required. |
| 404 | No route for the pair/size: no pair-index candidate returned an amount. There is no full-search fallback. |
| 502 | The chain RPC and every free fallback RPC failed while quoting. Retry, or quote via quotePlan on your own node. |
| 503 | That chain’s RPC circuit breaker is open and the fallback RPCs failed (check /healthz), or the server runs without API keys configured. |
| 500 | Internal error — retry or check /healthz breakers. |
{ "code": 400, "message": "invalid amountIn: abc(需为正整数字符串,含 decimals)" }