For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.

Question
Endpoint
Answers

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/routes is 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. A Route carries a single tokenId and 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-key

Every /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.totalCount is the real number — compare it against what you received, and page with limit (max 500) and offset until 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.

scope has exactly two behaviours, and only one of them is spelled. scope=all returns every measured lane. Any other value — including omitting the parameter entirely, and including scope=healthy — returns the unhealthy subset, and the response echoes "scope": "unhealthy" whatever you asked for. Always send scope=all when you want the full picture, and read the echoed scope back 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.

Field
Meaning

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. Read meta.totalCount and page until you match it.

Filter on kind, never on status. The two are not interchangeable, and the overlap is exactly where integrations break. A firewalled lane's status is unknown, not broken — 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-fillable appears under both degraded and broken. Build your allow and deny sets from kind.

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. laneKey pins 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 the n- and de-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 what sameCell guards. 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. /quote refuses 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 — a degraded lane marked not-fillable prices 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, measuredAt and individual verdicts change between two calls seconds apart. Treat every read as a snapshot, not as a stable list.

  • nextSweepDueAt is a sensible poll interval — roughly how long until the next full pass lands. Refreshing much faster than that mostly re-reads the same verdicts.

  • /chains and /tokens change on onboarding, not on a schedule. Cache them for hours.

  • mode tells you whether measurement is being enforced. Under enforce, 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.