Discover What You Can Trade
The tradeable surface is a cross product narrowed by live measurement, not a list of routes. Four calls, one copy-paste function, and the three filtering mistakes to design out.
Build your route picker from what the clearing house can actually settle right now. Four calls, one function to copy, and the three mistakes that otherwise reach production.
Three questions, three endpoints
ZeroDelta's tradeable surface is not a list of routes. It is a cross product — any enabled token on any connected chain, to any enabled token on any connected chain — narrowed by live measurement. Three endpoints answer three different questions, and conflating them is the root of most integration problems.
What exists?
GET /api/v1/chains, GET /api/v1/tokens
The chains we are connected to and the tokens enabled on each. Their cross product is your candidate surface.
What works?
GET /api/v1/route-health
One measured row per lane, from a prober that actually quotes each pair. This narrows the candidates.
What fills right now?
POST /api/v1/quote
The authoritative answer for one trade, at one size, at this moment.
/api/v1/routesis not the trade catalogue. It is the bridge-rail table: one row per token per chain pair, describing how that one token moves — its bridge, adapter, standard and facet. ARoutecarries a singletokenIdand no destination-token field, so it cannot express a trade between two different tokens at all, and it covers only the tokens that are bridge-configured. Read it to understand a rail; never to build a picker.
1. Authenticate
Every endpoint requires a key, sent as a request header. There is no public surface — an unauthenticated request returns 401 UNAUTHORIZED with missing API key, including for /chains.
X-API-Key: your-api-keyEvery /api/v1/* response uses the same envelope: data on success, plus meta and paging on list endpoints, and error carrying a code and message on failure. On an error there is no data key at all — check the status before reading it, as the sample below does.
2. Read the chains
GET /api/v1/chains. Each entry carries chainId, escrowContract, isExecutionChain and the contract set. You need the escrow address later to submit and to cancel, so treat this endpoint as canonical rather than hardcoding addresses.
3. Read the tokens
GET /api/v1/tokens. One row per (token, chain) cell, with address, decimals, isEnabled and the rail metadata. These cells are your candidate surface.
Page this endpoint. The default page size is 100 and the catalogue is larger, so a single bare call returns a truncated list and nothing in the response body looks wrong.
meta.totalCountis the real number — compare it against what you received, and page withlimit(max 500) andoffsetuntil they match. A picker built from the first page silently omits whole tokens.
4. Read the measured health, and filter
GET /api/v1/route-health?scope=all. One row per lane the prober has an opinion about. The top level carries summary with the live counts, measuredAt, and nextSweepDueAt. This endpoint is not paginated — it returns data only, with no meta or paging.
scopehas exactly two behaviours, and only one of them is spelled.scope=allreturns every measured lane. Any other value — including omitting the parameter entirely, and includingscope=healthy— returns the unhealthy subset, and the response echoes"scope": "unhealthy"whatever you asked for. Always sendscope=allwhen you want the full picture, and read the echoedscopeback rather than assuming your value was honoured.
Shape only — the values above are illustrative, not a census. Read summary from your own call for the live counts. A lane that is not healthy also carries kind, and usually a human-readable message; broken lanes add consecutiveFailures and firstFailedAt.
laneKey
Stable identifier. Source chainId:address, then a greater-than sign, then destination chainId:address, both addresses lowercased. Key on this.
route
Human-readable label. Display only.
status
healthy, degraded, broken or unknown.
kind
Why the lane is not healthy. Absent on healthy lanes. See the table below.
message
The upstream reason, when there is one worth surfacing.
par
The ratio the prober last measured for this lane.
lastProbedAt / lastOkAt
When the lane was last measured, and last measured working.
What to do with each kind
This table is the whole decision. kind tells you why a lane is not offerable, and each reason has a different correct response.
kind
What it means
Do
absent
Measured and working. The healthy majority carry no kind at all.
Offer
firewalled
Disabled by ZeroDelta operations; it will not quote. Not a transient failure.
Hide, do not retry
not-fillable
The lane prices cleanly but cannot actually be filled at the measured size.
Hide
lane-gap
No bridge wiring exists for this token pair.
Hide
bridge-throttled
The token's bridge path is rate-limited to zero upstream. Temporary.
Hide, retry later
persistent-upstream
Liquidity providers unavailable for this pair. Temporary.
Hide, retry later
vault-closed
An RWA vault is not accepting deposits. message carries the upstream reason.
Hide, explain
value-floor
No acceptable price at the probed size. Size-dependent, not structural.
Offer with a warning
Three mistakes to design out
Page
/tokens, or your catalogue is quietly incomplete. This is the failure that hides best: the default page is 100 cells, the response is well-formed, and the tokens you never received simply never appear in your picker. Readmeta.totalCountand page until you match it.
Filter on
kind, never onstatus. The two are not interchangeable, and the overlap is exactly where integrations break. A firewalled lane's status isunknown, notbroken— the prober skips it rather than measuring it — so "hide broken, show the rest" puts every disabled lane back into your picker, where it will be refused the moment a user selects it. In the other direction,not-fillableappears under bothdegradedandbroken. Build your allow and deny sets fromkind.
Key on
laneKey, never on a symbol pair. A symbol does not identify a cell: most symbols exist on several chains at once, so "USDC to AUSD" names many different lanes with different verdicts.laneKeypins both ends to a chain and an address. Symbol casing is a second reason to avoid them — it is not uniform across the catalogue (USDe,USDtb, and then- andde-prefixed tokenized assets are not upper-case), so an exact comparison against a hardcoded upper-case symbol misses them.
Copy this
Everything above, as the function most integrations end up writing anyway.
canTrade and whyNot take two cells from cells, not symbols. base is the environment base URL you were given at onboarding — a dev key is not valid against prod.
One pair is never tradeable and needs no measurement. The same token on the same chain is rejected with
400 BAD_REQUEST— there is nothing to bridge or swap. That is whatsameCellguards. The same token across different chains is fine: it is quoted as a direct bridge (isDirect: true), with no liquidity-provider swap and no solver fill, and it is not cancellable once submitted.
Health narrows the menu; the quote decides the trade
Use /route-health to decide what appears in your interface, and /quote to decide whether a specific trade proceeds. They are not redundant, and the quote is not a substitute for the health filter.
A served quote is not proof a lane can be filled.
/quoterefuses the cases it can see at pricing time: a closed vault, a throttled bridge, a disabled lane, a lane measured broken while enforcement is on. It cannot refuse what it cannot see — adegradedlane markednot-fillableprices cleanly and still fails to fill. If you skip the health filter and rely on the quote alone, you will publish lanes that quote and never settle.
The reverse also holds. Health is measured at a probe size, seconds to minutes ago; the quote is priced for your size, now. A lane that passes the filter can still return 422 — BELOW_MIN_TRADE_SIZE, COMPLIANCE_BLOCKED, or no acceptable price at your amount. Surface the error rather than treating it as a catalogue bug.
Refreshing
The health data moves continuously. The prober re-measures lanes all the time, so
summary,measuredAtand individual verdicts change between two calls seconds apart. Treat every read as a snapshot, not as a stable list.nextSweepDueAtis a sensible poll interval — roughly how long until the next full pass lands. Refreshing much faster than that mostly re-reads the same verdicts./chainsand/tokenschange on onboarding, not on a schedule. Cache them for hours.modetells you whether measurement is being enforced. Underenforce, lanes measured broken are already withheld from quoting, so your filter and the gateway agree.
Once you know what you can offer, continue with Request a quote. Field-level reference for every endpoint above is in the API reference.

