> For the complete documentation index, see [llms.txt](https://docs.glacislabs.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.glacislabs.com/zero-delta/integration-guide/api.md).

# API

Complete reference for the ZeroDelta read API: quoting, transaction tracking, and chain/token/route/solver/protocol discovery. For an integration walkthrough see the Integration Guide.

### Conventions

* **Auth.** Most endpoints require an API key in the `x-apikey` request header (an `Authorization: Bearer <key>` form is also accepted, and `x-api-key` works as an alias). The health probes are public. The base URL and your API key are provided during onboarding (separate `dev` and `prod` hosts); they are not published here.
* **Environments.** Two deployments are live: **`prod`** (production) and **`dev`** (pre-production). Each is a separate on-chain deployment on the same nine mainnet chains — the escrow, roles and bridge-adapter addresses differ between them, and a key issued for one environment is not valid against the other. Read the contract addresses for your environment from `GET /api/v1/chains` against your own base URL; never hardcode them or copy them between environments.
* **Versioning.** Paths are namespaced under `/api/v1/`. Changes within `v1` are additive; breaking changes ship under a new version.
* **Roles.** Keys are `viewer` (all `/api/v1/*` read and quote endpoints) or `admin`. The admin role additionally reaches an operator-only management surface that is not part of the integration contract and is not documented here.
* **Envelope.** Every `/api/v1/*` response is wrapped in a standard envelope: the resource lives under `data`, errors under `error`, and list responses carry `meta` and `paging`. The examples below show the `data` payload; assume the envelope around it. See Models.
* **Amounts.** Token amounts are strings in the token's smallest unit (e.g. `"1000000"` = 1 USDC at 6 decimals). Chain IDs are serialized as strings in responses. Native fees are wei strings.
* **Pagination.** List endpoints accept `limit` (max 500; default 50 for transactions, 100 elsewhere) and `offset` (default 0). ⚠️ A bare call therefore returns only the **first page**, and a truncated list is indistinguishable from a complete one unless you check `meta.totalCount` against the number of rows you received.

> **Schemas live on their own pages.** Models covers the envelope, error codes, the status vocabulary, the compliance decision, and the quote/order shapes. Transaction Models covers the tracked transaction, its steps, and the chained-journey fields.

### Endpoints at a glance

<table><thead><tr><th>Method</th><th width="274">Path</th><th>Purpose</th></tr></thead><tbody><tr><td><code>POST</code></td><td><code>/api/v1/quote</code></td><td>Cross-chain swap quote</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/fees/balances</code></td><td>Unclaimed integrator fees, per recipient</td></tr><tr><td><code>POST</code></td><td><code>/api/v1/fees/payouts</code></td><td>Claim integrator-fee payouts</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/fees/payouts</code></td><td>Your payout requests</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/transactions</code></td><td>List tracked transactions</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/transactions/{txHash}</code></td><td>One transaction, with steps</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/transactions/{txHash}/orders</code></td><td>Batch orders in single transaction</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/transactions/{txHash}/gmp</code></td><td>On-demand GMP status enrichment</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/transactions/stats</code></td><td>Aggregated transaction statistics</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/chains</code></td><td>List supported chains</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/chains/{chainId}</code></td><td>One chain</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/tokens</code></td><td>List supported tokens</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/tokens/{tokenId}</code></td><td>One token by ID</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/tokens/lookup</code></td><td>One token by address + chain</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/tokens/configs</code></td><td>Token bridge configuration documents</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/prices</code></td><td>Cached USD prices by symbol</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/routes</code></td><td>List bridge rails (not the trade catalogue)</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/routes/{routeId}</code></td><td>One route</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/route-health</code></td><td>Measured lane health — build your picker from this</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/protocols</code></td><td>List supported GMP protocols</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/solvers</code></td><td>Solvers grouped by chain</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/solvers/{chainId}</code></td><td>Solvers on one chain</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/rwa/*</code></td><td>Tokenized real-world-asset vaults (5 routes)</td></tr><tr><td><code>GET</code></td><td><code>/api/v1/stakeflow/*</code></td><td>Stakeflow points and commitments (7 routes)</td></tr><tr><td><code>GET</code></td><td><code>/health</code></td><td>Liveness probe (no auth)</td></tr><tr><td><code>GET</code></td><td><code>/health/ready</code></td><td>Readiness probe (no auth)</td></tr><tr><td><code>GET</code></td><td><code>/metrics</code></td><td>Prometheus metrics (no auth)</td></tr></tbody></table>

***

### Quotes

#### `POST /api/v1/quote`

> **Wallet screening.** ZeroDelta screens wallets at two points. (1) **At quote time** — when you supply `owner` (and optionally `destinationReceiver`), both are screened by the compliance engine *before* a quote is returned; a blocked pair receives no quote or deposit instructions (`422 COMPLIANCE_BLOCKED`). (2) **At execution time** — the solver re-screens the pair through its pre-fill compliance gate *before filling* the order, so a pair that becomes blocked after quoting is still stopped before settlement. When `owner` is omitted the quote returns `complianceStatus: UNSCREENED` and the execution-time gate remains the enforcement point.

Calculates a firm cross-chain swap quote between two supported tokens. The quote is computed in-process: the engine races every configured liquidity provider in parallel, picks the best output, adds LayerZero/CCTP fees and a bridge-time estimate, and returns a prepared `orderRequests` array ready for `submitOrder`. The response carries a masked `provider` label (the specific market maker is not disclosed). This endpoint is per-key rate limited; max request body 1 MB.

**Same-token pairs.** A same-token pair across two different chains is quoted as a **direct bridge** (`isDirect: true`) when the token's mesh connects those chains: no liquidity-provider swap and no solver fill — `submitOrder` bridges straight to the receiver, `toAmount == finalAmount` is a minimum-delivered floor, and the resulting transfer is **not cancellable**. Only a same token on the *same* chain is rejected (`400 BAD_REQUEST`), since there is nothing to bridge or swap.

**Minimum trade size.** Orders below the per-pair floor are refused with `422 BELOW_MIN_TRADE_SIZE`, and `error.fieldErrors` carries a `fromAmount` entry with the exact minimum. The global floor is **$0.02**; USDe pairs carry a higher floor of **$0.11**.

> **A served quote is not proof a lane can be filled.** The quote gate refuses what it can see at pricing time — a closed vault, a throttled bridge, a firewalled lane, a lane measured broken while enforcement is on. It cannot refuse what it cannot see: a lane the prober marks `degraded` / `not-fillable` prices cleanly and still fails to fill. Filter your catalogue with `GET /api/v1/route-health` rather than relying on the quote alone.

**Request body** (QuoteRequest):

| Field                 | Required | Description                                                                                                                                                                                                                                                                                                                |
| --------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fromChainId`         | yes      | Source chain ID (string or number).                                                                                                                                                                                                                                                                                        |
| `toChainId`           | yes      | Destination chain ID.                                                                                                                                                                                                                                                                                                      |
| `fromToken`           | yes      | Source token symbol or contract address.                                                                                                                                                                                                                                                                                   |
| `toToken`             | yes      | Destination token symbol or contract address.                                                                                                                                                                                                                                                                              |
| `fromAmount`          | yes      | Amount to sell, in the source token's smallest unit.                                                                                                                                                                                                                                                                       |
| `owner`               | no       | Order owner address. When provided, the response also populates `escrowAddress` and `executionChainId`.                                                                                                                                                                                                                    |
| `destinationReceiver` | no       | Recipient wallet; screened alongside `owner` when present.                                                                                                                                                                                                                                                                 |
| `sender`              | no       | The wallet that pays for and signs the order: a hex address on an EVM source, a base58 public key on a Solana source. When provided, the response carries `txData`, and `owner` and `destinationReceiver` become required.                                                                                                 |
| `deadline`            | no       | Only with `sender`. The order's deadline that `txData` signs for, in unix seconds (decimal string). Absent: the quote's `recommendedDeadline`. `"0"`: no deadline. A past value is a `400`.                                                                                                                                |
| `feeRecipients`       | no       | Array of `FeeRecipient`, 1–8 entries. Your integrator fee: who is owed a share of this order, and how much, each in centibps of the output (100 = 1 bps). The total comes off the output after everything else the quote does. **Requires both `owner` and `destinationReceiver`.** Your API key must be enabled for fees. |

Both `owner` and `destinationReceiver` are optional inputs used for compliance screening, unless you name a `sender`. Supply both to receive a usable `partnerId` marker when the quote carries one; a supplied wallet must be a valid address for that lane. The returned `orderRequests[]` come back with `owner` and `destinationReceiver` filled from these fields, or `null` when they were omitted — make sure both are set on every element before submitting (see the Integration Guide).

**`feeRecipients` rules.** Each violation is a `400 VALIDATION_ERROR` with `fieldErrors`:

* 1 to 8 entries, each `recipient` unique.
* Each `feeCentibps` from 1 to 100,000.
* The total at most 100,000 (10%).
* Each `recipient` is either:
  * an EVM address: `0x` plus 40 hex digits, not the zero address, with a valid EIP-55 checksum when written in mixed case. It is echoed back checksummed. A mixed-case address with a wrong checksum is refused as a likely typo.
  * a Solana address: base58 that decodes to 32 bytes, not `11111111111111111111111111111111`. It is kept and compared **exactly as sent**, because base58 case is significant.

```json
{
  "fromChainId": "130",
  "toChainId": "42161",
  "fromToken": "USDC",
  "toToken": "USDT",
  "fromAmount": "1000000000000"
}
```

**Response** (`200`, `data` is a Quote; illustrative values):

<pre class="language-json"><code class="lang-json"><strong>{
</strong>  "data": {
    "fromChainId": "130",
    "toChainId": "42161",
    "fromToken": "USDC",
    "toToken": "USDT",
    "fromAmount": "1000000000000",
    "toAmount": "999850000000",
    "finalAmount": "999850000000",
    "bridge": "Circle",
    "feeBps": 0,
    "estimatedDuration": 600,
    "lifespanSeconds": 30,
    "swapTimeSeconds": 12,
    "gasCostSource": "0",
    "gasCostDest": "0",
    "swapCost": "",
    "inboundProtocolFee": "0",
    "provider": "zerodelta",
    "route": { "...": "see Route" },
    "orderRequests": [
      {
        "owner": null,
        "bidToken": "0x078D782b760474a361dDA0AF3839290b0EF57AD6",
        "bidTokenAmount": "1000000000000",
        "askToken": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9",
        "askTokenAmount": "999850000000",
        "destinationReceiver": null,
        "destinationChainId": "42161",
        "executionChainId": "1",
        "deadline": "0"
      }
    ],
    "complianceStatus": "SCREENED",
      "compliance": {
      "status": "SCREENED",
      "decision": "ALLOW",
      "screenedWallets": [
        "0x1111111111111111111111111111111111111111",
        "0x2222222222222222222222222222222222222222"
      ],
      "sourceJurisdiction": "US_EX_NY",
      "appliedPolicyIds": ["pol_01H..."],
      "appliedPolicyNames": ["Default OFAC + Jurisdiction"],
      "appliedLegislation": ["MICA_EU"],
      "riskCategories": [],
      "flaggedCategories": [],
      "blockingCategories": [],
      "reasons": [],
      "policyVersion": "2026-07-01",
      "checkId": "chk_01H...",
      "expiresAt": "2026-07-14T12:30:00Z"
    }
  }
}
</code></pre>

When `owner` is supplied in the request, the response additionally includes `escrowAddress`, `executionChainId`, and `executionEscrowAddress`.

**Ready-to-sign transactions (`sender`).** Name the wallet that will pay for and sign the order as `sender`, together with `owner` and `destinationReceiver`, and the quote carries `txData`: the unsigned transactions the order needs, in send order. On an EVM source they are calls — an `approve` on the bid token when the escrow's allowance from `sender` is short, then `submitOrder` on the escrow with `msg.value` already set. On a Solana source they are serialized transactions built from the wallet's state on the ZeroDelta program. Send each one and wait for it to confirm before sending the next. `orderRequests[0].deadline` is then the deadline the transactions sign for: the quote's `recommendedDeadline`, or the `deadline` you passed. Never re-send stored transactions after a failure or an expiry — request a new quote, and the gateway returns only what is still to do. Shapes: `EvmTransaction` and `SolanaTransaction` in Models.

**`partnerId`.** Some quotes carry a `partnerId`: every quote whose request carried `feeRecipients` (see Integrator-fee quotes below), and certain other quotes depending on your API key. When it is present:

* A `0x`-prefixed 32-byte hex **string** is the marker for this exact quote. Submit it verbatim as the `bytes32 partnerId` argument of `submitOrder` together with the quoted amounts and the same `owner` and `destinationReceiver` used at quote time. The first matching order consumes it.
* An **object** of the form `{ "error": "..." }` means the request omitted `owner` or `destinationReceiver`, so no marker could be bound. Show the price if useful, then request a new quote with both wallets before submitting. Never pass this object to the contract.
* If `partnerId` is absent, use `bytes32(0)`. It is always absent for a direct bridge (`isDirect: true`).

Changing a marked quote's wallets, amounts, or marker means the marker will not match the submitted order, even though the transaction itself may succeed. The order may then remain unfilled until the market meets its price or the owner cancels.

**Integrator-fee quotes.** A quote that carries `feeRecipients` always carries `partnerId`, for any API key, and on a fee quote it is always the **string** form, never the `{ "error": ... }` object (a fee request without both wallets is refused with `400 FEE_WALLETS_REQUIRED` instead). Submit it exactly as above. **An order submitted without the `partnerId` settles normally at the net amount, but no fee is collected on it.**

> **`compliance.decision` is `ALLOW` / `DENY` / `REVIEW` / `REMEDIATE`.** There is no `BLOCK` value — a client branching on one would treat every refusal as an allow. A successful quote is only ever returned for `ALLOW`. See Models.

**Order-request semantics:**

* `orderRequests` is an **array** — it is the exact argument `submitOrder` takes. One element for a normal or direct quote; **two** for a chained quote (`isChained: true`), where element 0's swap output funds element 1. Pass the whole array through unchanged, filling `owner` and `destinationReceiver` on **every** element.
* `orderRequests[].executionChainId` is the chain the order clears on. It is always populated — including on direct quotes, where the contract validates it before short-circuiting — and the contract reverts on `0`.
* On a chained quote, `orderRequests[1].bidTokenAmount` is an estimate the contract overwrites with actual receipts, and `orderRequests[1].deadline` is `"0"` by design (it is re-validated at continuation time).

**Fee and amount semantics:**

* `toAmount` is the on-chain `order.askTokenAmount` — the swap output threshold the solver must meet to fill the order, used to construct `orderRequests[].askTokenAmount`. It is not what the destination receiver gets when the outbound bridge charges a protocol fee — see `finalAmount`.
* `finalAmount` is what the destination receiver actually gets: `toAmount` minus the outbound bridge protocol fee (CCTP fast-track maxFee). It equals `toAmount` for LayerZero outbound and CCTP slow-track outbound, and is otherwise slightly lower. `finalAmount <= toAmount` always; always populated.
* `inboundProtocolFee` is the CCTP fast-track maxFee debited from the **input** before pricing: the inbound bridge delivers `fromAmount − inboundProtocolFee` to the execution chain and `toAmount` is quoted from that. `"0"` for LayerZero bids, local bids, and slow-track lanes.
* `feeBps` is the route protocol fee in basis points (CCTP fast-track fee for Circle lanes; `0` for LayerZero). Metadata only — it is **not** subtracted from `toAmount`. The actual fee shows up as the gap between `toAmount` and `finalAmount`.
* `recipientFees` (a `RecipientFees`, present only when the request carried `feeRecipients`) is the integrator fee taken off this quote. Amounts are raw units of the output token **on the execution chain** (`chainId`, `token`, `decimals`), the same scale as `toAmount`. `toAmount`, `finalAmount` and `orderRequests[0].askTokenAmount` are already net of it; `feeBps` is unaffected.
* `estimatedDuration` is the end-to-end transfer time in seconds, composed of inbound bridge finality + execution-chain swap window + outbound bridge finality, plus a per-leg fill-latency buffer that covers tx inclusion on each hop. CCTP legs use fast vs standard finality depending on the deployed adapter's slow-track setting.
* `lifespanSeconds` is how long the quote stays valid, in seconds, derived from the winning provider's signed expiry (expiry minus generation time, floored at 0). It is `0` when the provider does not sign an expiry or the quote has already lapsed. The on-chain fill threshold itself does **not** expire — this is the provider's price-validity window.
* `swapTimeSeconds` is the estimated time for the on-chain swap to settle on the execution chain. The swap executes in a single transaction, so this is one block of the execution chain (its average block time, which differs per chain); `0` when block-time data is unavailable.
* `gasCostSource` / `gasCostDest` are LayerZero native messaging fees in wei (inbound and outbound respectively), and are `0` for CCTP (USDC). Pass `gasCostSource` as `msg.value` on `submitOrder`.
* `swapCost` is a gas hint from the winning liquidity provider (decimal string; empty when the provider does not return one).
* `provider` is a masked liquidity-provider label, always `"zerodelta"` on a successful swap quote — the specific market maker the engine routes through is not disclosed. Omitted on direct-bridge quotes (there is no provider).
* `isDirect` marks a direct-bridge quote (same token, different chains): no swap, no solver fill, not cancellable, and the top-level `executionChainId` / `executionEscrowAddress` cancel coordinates are omitted.
* `isChained` marks a two-leg quote: `orderRequests` carries two elements and settlement spans two execution chains.

**Errors:** Omitting either wallet on a quote that carries `partnerId` is handled in the successful quote's `partnerId` object as described above.

<table data-header-hidden data-search="false"><thead><tr><th></th><th></th><th></th></tr></thead><tbody><tr><td>Status</td><td><code>error.code</code></td><td>Cause</td></tr><tr><td><code>400</code></td><td><code>BAD_REQUEST</code></td><td>Malformed body, missing required field, non-positive <code>fromAmount</code>, or the same token on the same chain (<code>fromToken</code> == <code>toToken</code> with <code>fromChainId</code> == <code>toChainId</code>).</td></tr><tr><td><code>400</code></td><td><code>VALIDATION_ERROR</code></td><td>A malformed <code>feeRecipients</code> (see the rules above), with <code>fieldErrors</code>.</td></tr><tr><td><code>400</code></td><td><code>WALLETS_REQUIRED</code></td><td>Depending on your API key: the request named an <code>owner</code> or <code>destinationReceiver</code> that a <code>partnerId</code> marker cannot bind: not an EVM address (a Solana destination takes its base58 wallet). Leaving a wallet out is not this error: the quote's <code>partnerId</code> is then an error object, as described above.</td></tr><tr><td><code>400</code></td><td><code>FEE_WALLETS_REQUIRED</code></td><td><code>feeRecipients</code> without both <code>owner</code> and <code>destinationReceiver</code>, or a wallet the fee cannot bind: not an EVM address (a Solana destination takes its base58 wallet).</td></tr><tr><td><code>400</code></td><td><code>FEE_UNSUPPORTED_LANE</code></td><td><code>feeRecipients</code> on a pair with no USD stablecoin side, a direct bridge, or a chained quote. The same request without <code>feeRecipients</code> prices.</td></tr><tr><td><code>401</code></td><td><code>UNAUTHORIZED</code></td><td>Missing or invalid API key.</td></tr><tr><td><code>403</code></td><td><code>FEE_NOT_ENABLED</code></td><td>The request carried <code>feeRecipients</code>, and your API key is not enabled for fees. The same request without them prices.</td></tr><tr><td><code>422</code></td><td><code>BELOW_MIN_TRADE_SIZE</code></td><td>Bid amount is under the per-pair minimum ($0.02 global, $0.11 for USDe pairs). <code>error.fieldErrors</code> carries a <code>fromAmount</code> entry with the exact minimum.</td></tr><tr><td><code>422</code></td><td><code>QUOTE_UNAVAILABLE</code></td><td>No registered route for the requested (token, source chain, destination chain) — e.g. the token is not enabled on that chain. Also returned when the request carried <code>feeRecipients</code> and the order is too small to carry at least one unit of fee.</td></tr><tr><td><code>422</code></td><td><code>BRIDGE_RATE_LIMITED</code></td><td>The lane exists, but a bridge the order would cross cannot carry it right now: the token's LayerZero pathway is throttled or closed by its issuer's rate limiter. When a smaller amount still fits, the error carries <code>error.limits[]</code> with the ceiling and a <code>fromAmount</code> field error ("Maximum: X SYM right now"). With no <code>limits</code>, the pathway is closed: retry later or choose another destination chain.</td></tr><tr><td><code>422</code></td><td><code>ROUTE_UNHEALTHY</code></td><td>The lane exists, but ZeroDelta's route-health prober has measured it failing to price for an extended period (a momentary provider outage is <code>UPSTREAM_ERROR</code> instead). Stop offering the pair; it quotes again on its own once it recovers.</td></tr><tr><td><code>422</code></td><td><code>VALIDATION_ERROR</code></td><td>Semantic validation failure, with per-field details.</td></tr><tr><td><code>422</code></td><td><code>COMPLIANCE_BLOCKED</code></td><td>Supplied <code>owner</code>/<code>destinationReceiver</code> pair failed screening; no quote or deposit instructions returned. The error <code>reasons[]</code> array is populated per the disclosure policy.</td></tr><tr><td><code>429</code></td><td><code>RATE_LIMITED</code></td><td>Rate limit exceeded; a <code>Retry-After</code> header (seconds) is returned.</td></tr><tr><td><code>502</code></td><td><code>UPSTREAM_ERROR</code></td><td>The quoting engine could not produce a quote (all liquidity providers failed, or bridge-fee estimation failed). Transient; retry with backoff.</td></tr><tr><td><code>503</code></td><td><code>SERVICE_UNAVAILABLE</code></td><td>Compliance engine unreachable while screening — the quote gate <strong>fails closed</strong> (only when <code>owner</code> is supplied and the gate is configured). Also returned when the request named a <code>sender</code> and the gateway cannot build transactions for the source chain.</td></tr><tr><td><code>503</code></td><td><code>FEE_UNAVAILABLE</code></td><td>The request carried <code>feeRecipients</code> and fees cannot be taken right now. Nothing about the request was wrong: retry, or quote without <code>feeRecipients</code>.</td></tr></tbody></table>

***

### Fees

Endpoints for the integrator fees you charge with `feeRecipients` on a quote (see Charging an integrator fee in the Integration Guide). Every route takes your API key; a key that is not enabled for fees gets `403 FEE_NOT_ENABLED` on each. Balances and payouts are scoped to the calling key: another key's fees are never visible. Shapes: `FeeBalancesListing`, `FeePayoutClaim` and `FeePayout` in Models.

#### `GET /api/v1/fees/balances`

Unclaimed fees per recipient: what each recipient named in this key's `feeRecipients` has collected since its last payout claim, in US dollars. Each fee is valued at the market price at its fill's block, and counts once the fill is confirmed and valued, usually within seconds.

* Returns a `FeeBalancesListing`: `rows[]` of `{ recipient, amountUsd, fills, updatedAt }`, and `measured`.
* `amountUsd` is an exact decimal string, up to 18 places. `fills` counts the fills since the last claim.
* `measured: false`, with no rows, means the balances could not be read right now. It **never** means "nothing collected".
* A recipient that has been claimed shows `amountUsd` 0 until it collects again.
* Not paginated: one row per recipient the key has ever paid.

**Errors:** `401 UNAUTHORIZED`, `403 FEE_NOT_ENABLED`, `429 RATE_LIMITED`.

#### `POST /api/v1/fees/payouts`

Claims payouts. Each named recipient's **whole** balance becomes one pending payout request, to be paid in **USDC** to that address on `chainId`. Its balance goes back to zero in the same step: a fee that lands during the claim is counted either in the request or in the new balance, never both and never neither. Payouts are made by hand.

```json
{
  "recipients": [
    { "recipient": "0xc0ffee254729296a45a3885639AC7E10F9d54979", "chainId": 8453 },
    { "recipient": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "chainId": 1151111081099710 }
  ]
}
```

* `201` with one `FeePayout` per recipient, in address order.
* 1 to 100 recipients. A recipient may appear only once per claim, in any spelling; a repeat is `400 VALIDATION_ERROR` "must be unique".
* `chainId` is an **integer** here and must be one of the chains below. The chain decides the address form: Solana takes a Solana address; every other chain takes an EVM address, in any case (a mixed-case one must carry a valid EIP-55 checksum).

| Chain ID         | Chain     |
| ---------------- | --------- |
| 1                | Ethereum  |
| 10               | Optimism  |
| 130              | Unichain  |
| 137              | Polygon   |
| 143              | Monad     |
| 146              | Sonic     |
| 196              | X Layer   |
| 1329             | Sei       |
| 1672             | Pharos    |
| 8453             | Base      |
| 9745             | Plasma    |
| 42161            | Arbitrum  |
| 43114            | Avalanche |
| 57073            | Ink       |
| 98866            | Plume     |
| 1151111081099710 | Solana    |

**All or nothing.** If any named recipient has nothing to claim, `409 FEE_NOTHING_TO_CLAIM` names them. If any has less than **$0.05**, `409 FEE_BELOW_MINIMUM` names them and their balances. Either way, nothing is claimed. A request cannot be withdrawn, edited or deleted once made.

**Errors:** `400 VALIDATION_ERROR` (malformed body, an invalid address, or a `chainId` outside the list, with `fieldErrors`), `401 UNAUTHORIZED`, `403 FEE_NOT_ENABLED`, `409 FEE_NOTHING_TO_CLAIM`, `409 FEE_BELOW_MINIMUM`, `429 RATE_LIMITED`, `503 FEE_UNAVAILABLE` (the claim could not be recorded; nothing was claimed).

#### `GET /api/v1/fees/payouts`

The payout requests this key has made, newest first. Returns an array of `FeePayout`.

**Query parameters:** `status` (`pending`, `completed` or `all`; default `pending`), `limit` (default 50, max 500), `offset`. `meta.totalCount` counts every match, and `paging` links the neighbouring pages.

A request's `status` moves from `pending` to `completed` once the payout has been sent; `completedAt` is then set.

**Errors:** `400` (a `status` other than `pending`, `completed` or `all` is `VALIDATION_ERROR`; a malformed `limit` or `offset` is `BAD_REQUEST`), `401 UNAUTHORIZED`, `403 FEE_NOT_ENABLED`, `429 RATE_LIMITED`.

***

### Transactions

#### `GET /api/v1/transactions`

Lists tracked cross-chain transactions, most recent first. Returns an array of TrackedTransaction (without the per-transaction `steps` array — fetch a single transaction for steps).

**Query parameters:**

<table data-search="false"><thead><tr><th>Parameter</th><th>Description</th></tr></thead><tbody><tr><td><code>status</code></td><td>Filter by TrackingStatus.</td></tr><tr><td><code>protocol</code></td><td>Filter by GMP protocol (e.g. <code>layerzero</code>).</td></tr><tr><td><code>chainId</code></td><td>Matches source <strong>or</strong> destination chain.</td></tr><tr><td><code>fromChainId</code></td><td>Filter by source chain.</td></tr><tr><td><code>toChainId</code></td><td>Filter by destination chain.</td></tr><tr><td><code>txHash</code></td><td>Filter by transaction hash (list filter, not the path lookup).</td></tr><tr><td><code>wallet</code></td><td>Filter by wallet address.</td></tr><tr><td><code>includeChainedLegs</code></td><td>Surface the second leg of a chained order as its own row. Default <code>false</code>; see the note below.</td></tr><tr><td><code>createdAfter</code> / <code>createdBefore</code></td><td>RFC 3339 time-window bounds.</td></tr><tr><td><code>sortBy</code></td><td><code>created_at</code> (default) or <code>updated_at</code>.</td></tr><tr><td><code>sortOrder</code></td><td><code>desc</code> (default) or <code>asc</code>.</td></tr><tr><td><code>limit</code> / <code>offset</code></td><td>Pagination (default limit 50, max 500).</td></tr></tbody></table>

> **`includeChainedLegs` defaults to `false`, and that default is deliberate.** A chained child is half of one user intent, so listing it beside its parent shows a single purchase as two transactions — one of which the user never submitted and cannot cancel. By default the parent carries it in `legs[]` instead, and the fold is **applied in SQL**, so `meta.totalCount`, pagination and `/transactions/stats` all agree with the rows you get back. Set it to `true` only when you specifically want per-leg rows, and expect your order counts to change.

**Errors:** `400 BAD_REQUEST`, `401 UNAUTHORIZED`.

#### `GET /api/v1/transactions/{txHash}`

Returns one TrackedTransaction keyed by **source transaction hash**, including the `steps` array. This is the endpoint the status poller calls (see the Integration Guide). Before the source tx is mined and indexed, this returns `404` — treat that as "not indexed yet" and keep polling.

**Path parameter:** `txHash` — the source-chain submit transaction hash.

> **`id` is not `orderId`.** The tracked transaction's `id` is a deployment-scoped uid (for example `"rwa:0x52b1…"`) because raw order ids are not unique across redeployments — a redeployed escrow restarts the nonce. The escrow's `cancel` / `claimCancellation` / `getOrder` calls take `orderId`; passing `id` there reverts.

**Errors:** `401 UNAUTHORIZED`, `404 NOT_FOUND`.

#### `GET /api/v1/transactions/{txHash}/orders`

Returns **all** orders sharing a submit transaction hash, most-recent first. A single submit tx can create more than one order (a batch); the single-object `GET /api/v1/transactions/{txHash}` collapses to the most-recent, while this returns the full set — each enriched identically to the single GET. Requires an API key.

**Path parameter:** `txHash` — the source-chain submit transaction hash.

**Response (200):** `data` = array of `TrackedTransaction` (each with its `steps`).

**Errors:** `401 UNAUTHORIZED`, `404 NOT_FOUND`.

#### `GET /api/v1/transactions/{txHash}/gmp`

On-demand GMP status enrichment for one transaction. Declared in the published spec; in deployed environments it may additionally be IP-restricted to internal services, so treat access as environment-dependent rather than guaranteed by your key.

**Errors:** `401 UNAUTHORIZED`, `404 NOT_FOUND`.

#### `GET /api/v1/transactions/stats`

Returns aggregated statistics over the transactions matching the filters. The `data` payload is a TransactionStats object.

**Query parameters:** `status`, `protocol`, `chainId` (matches source **or** destination chain), `fromChainId`, `toChainId`, `wallet`, `createdAfter`, `createdBefore` (same semantics as the list endpoint).

```json
{
  "data": {
    "total": 1234,
    "byStatus": { "delivered": 1100, "in_progress": 30 },
    "byProtocol": { "layerzero": 800, "cctp": 434 },
    "avgDeliveryMs": 412000.0,
    "bySourceChain": { "1": 600, "42161": 300 },
    "byDestChain": { "42161": 500, "8453": 200 }
  }
}
```

**Errors:** `401 UNAUTHORIZED`.

***

### Chains

#### `GET /api/v1/chains`

Lists supported chains. Returns an array of Chain. Each chain is enriched at response time with its contract addresses and execution-chain flag. This is the canonical source for the escrow, roles and adapter addresses **of the environment your key belongs to** — `dev` and `prod` return different addresses for the same chain.

**Query parameters:** `enabledOnly` (string; set to `false` to include disabled chains, default `true`), `limit`, `offset`.

**Errors:** `401 UNAUTHORIZED`.

#### `GET /api/v1/chains/{chainId}`

Returns one Chain.

**Path parameter:** `chainId` (integer).

**Errors:** `401 UNAUTHORIZED`, `404 NOT_FOUND`.

***

### Tokens

#### `GET /api/v1/tokens`

Lists supported tokens as per-chain rows. Returns an array of Token, each enriched with bridge metadata and its enabled outbound `routes`. This is the canonical source for per-(chain, token) contract addresses — do not hardcode them. Together with `GET /api/v1/route-health` it is what you build a route picker from.

> ⚠️ **Page this endpoint.** The default page is 100 rows and the catalogue is larger, so a bare call returns a **truncated** list that looks entirely healthy. Compare `meta.totalCount` against the rows you received and page with `limit` (max 500) and `offset` until they match — otherwise your picker silently omits whole tokens.

**Query parameters:** `chainId`, `symbol` (e.g. `USDC`), `isEnabled` (string; `false` includes disabled tokens, default `true`), `limit`, `offset`.

```json
{
  "data": [
    { "symbol": "USDC", "chainId": "42161", "address": "0xaf88d065e77c8cC2239327C5EDb3A432268e5831", "decimals": 6 },
    { "symbol": "USDT", "chainId": "42161", "address": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", "decimals": 6 }
  ]
}
```

**Errors:** `401 UNAUTHORIZED`.

#### `GET /api/v1/tokens/{tokenId}`

Returns one Token by its ID.

**Path parameter:** `tokenId` (string).

**Errors:** `401 UNAUTHORIZED`, `404 NOT_FOUND`.

#### `GET /api/v1/tokens/lookup`

Resolves a single Token by contract address and chain.

**Query parameters (both required):** `address`, `chainId`.

**Errors:** `400 BAD_REQUEST`, `401 UNAUTHORIZED`, `404 NOT_FOUND`.

#### `GET /api/v1/tokens/configs`

Returns the per-symbol token bridge configs (loaded from the token config directory), as an array of typed `TokenConfig` objects — one per token, sorted by `name`. `contracts` is keyed by chain ID. Useful for discovering bridge/standard metadata across chains in one call.

```json
{
  "data": [
    {
      "name": "USD Coin",
      "bridge": "Circle",
      "facet": "CCTPV2",
      "standard": "CCTPV2",
      "homeChainId": "1",
      "logoUrl": "https://...",
      "contracts": {
        "42161": { "impl": "0x…", "token": "0x…", "name": "USD Coin", "symbol": "USDC", "chainName": "arbitrum", "decimals": 6, "gmpParameters": "0x…" }
      }
    }
  ]
}
```

**Errors:** `401 UNAUTHORIZED`.

#### `GET /api/v1/prices`

Returns cached USD prices for the supported market and tokenized-asset symbols. The snapshot is cached and refreshed with stale-while-revalidate, so this is cheap to poll.

**Query parameter:** `symbols` — comma-separated token or native-currency symbols (max 100); omit for the full catalogue.

```json
{
  "data": {
    "prices": { "USDC": 1.0, "XAUT": 2412.35 },
    "fetchedAt": "2026-04-13T12:00:00Z",
    "stale": false,
    "ageSeconds": 42,
    "fallbackSymbols": [],
    "unavailableSymbols": [],
    "unsupportedSymbols": []
  }
}
```

`fallbackSymbols` lists USD-pegged assets priced at the explicit $1 fallback because no live observation was available.

**Errors:** `400 BAD_REQUEST`, `401 UNAUTHORIZED`.

***

### Routes and lane health

#### `GET /api/v1/routes`

Lists the **bridge rails**. Returns an array of Route. A route describes how **one** token moves between two chains — its bridge, adapter, standard, facet, fee and duration metadata.

> **This is not the trade catalogue.** A `Route` carries a single `tokenId` and no destination-token field, so it cannot express a trade between two different tokens, and it only covers the tokens that are bridge-configured. Build a route picker from `GET /api/v1/tokens` (the cells that exist) filtered by `GET /api/v1/route-health` (the lanes that work). See Discover what you can trade in the Integration Guide.

**Query parameters:**

<table data-search="false"><thead><tr><th>Parameter</th><th>Description</th></tr></thead><tbody><tr><td><code>fromChainId</code></td><td>Filter by source chain.</td></tr><tr><td><code>toChainId</code></td><td>Filter by destination chain.</td></tr><tr><td><code>token</code></td><td>Filter by token symbol, token name, token ID, or source/destination token address.</td></tr><tr><td><code>tokenId</code></td><td>Filter by token ID.</td></tr><tr><td><code>bridge</code></td><td>Filter by bridge provider (e.g. <code>layerzero</code>, <code>Circle</code>).</td></tr><tr><td><code>isEnabled</code></td><td>String; <code>false</code> includes disabled routes, default <code>true</code>.</td></tr><tr><td><code>limit</code> / <code>offset</code></td><td>Pagination.</td></tr></tbody></table>

**Errors:** `401 UNAUTHORIZED`.

#### `GET /api/v1/routes/{routeId}`

Returns one Route.

**Path parameter:** `routeId` (string).

**Errors:** `401 UNAUTHORIZED`, `404 NOT_FOUND`.

#### `GET /api/v1/route-health`

Measured lane health from the route-health prober, which re-quotes every catalogued lane on a sweep and records the verdict. **This is the endpoint a route picker is built from** — see Discover what you can trade in the Integration Guide for the full flow.

It also feeds `Route.isQuotable`: a lane the prober measured broken is demoted only when enforcement is on, and an **absent** `isQuotable` means "not measured", which must be treated as quotable.

> **`scope` has exactly two behaviours.** `scope=all` returns every measured lane. **Any other value — including omitting the parameter, and including `scope=healthy` — returns the unhealthy subset**, and the response echoes `"scope": "unhealthy"` whatever you sent. Send `scope=all` when you want the full picture, and read the echoed `scope` back rather than assuming your value was honoured.

**Response:** `data` carries `mode` (whether measured-broken lanes are currently being withheld from quoting), `scope`, a `summary` of lane counts by status, the sweep timestamps `lastSweepAt` / `nextSweepDueAt` / `measuredAt`, and the `lanes` array. The prober re-measures continuously, so counts and verdicts move between two calls seconds apart — treat every read as a snapshot.

<table data-search="false"><thead><tr><th>Lane field</th><th>Description</th></tr></thead><tbody><tr><td><code>laneKey</code></td><td>Stable identifier: source <code>chainId:address</code>, a greater-than sign, then destination <code>chainId:address</code>, both addresses lowercased. Key on this, not on symbols.</td></tr><tr><td><code>route</code></td><td>Human-readable label. Display only.</td></tr><tr><td><code>status</code></td><td><code>healthy</code>, <code>degraded</code>, <code>broken</code> or <code>unknown</code>.</td></tr><tr><td><code>kind</code></td><td>Why the lane is not healthy. Absent on healthy lanes.</td></tr><tr><td><code>message</code></td><td>The upstream reason, when there is one worth surfacing.</td></tr><tr><td><code>par</code></td><td>The ratio the prober last measured for this lane.</td></tr><tr><td><code>consecutiveFailures</code> / <code>firstFailedAt</code></td><td>Present on broken lanes.</td></tr><tr><td><code>lastProbedAt</code> / <code>lastOkAt</code></td><td>When the lane was last measured, and last measured working.</td></tr></tbody></table>

**`kind` values, and what to do with each:**

| `kind`                | Meaning                                                                 | Do                   |
| --------------------- | ----------------------------------------------------------------------- | -------------------- |
| `firewalled`          | Disabled by ZeroDelta operations. It will not quote.                    | Hide, do not retry   |
| `not-fillable`        | 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 reason.   | Hide, explain        |
| `value-floor`         | No acceptable price at the probed size. Size-dependent, not structural. | Offer with a warning |

> **Filter on `kind`, not on `status`.** The two are not interchangeable. A firewalled lane's `status` is `unknown`, **not** `broken` — the prober skips it rather than measuring it — so a filter that only hides `broken` keeps every disabled lane in your picker. In the other direction, `not-fillable` appears under both `degraded` and `broken`.

**Errors:** `401 UNAUTHORIZED`.

***

### Protocols

#### `GET /api/v1/protocols`

Lists the supported cross-chain messaging (GMP) protocols and their per-chain state. The `data` payload has `protocols` (a string array) and `states` (an array of ProtocolState).

```json
{
  "data": {
    "protocols": ["cctp", "layerzero"],
    "states": [
      { "chainId": "1", "executionChainId": "1", "isPaused": false, "updatedAt": "2026-04-13T12:00:00Z" }
    ]
  }
}
```

**Errors:** `401 UNAUTHORIZED`.

***

### Solvers

A solver is a wallet authorized to fill orders on a given chain. These endpoints expose the public solver registry (read-only).

#### `GET /api/v1/solvers`

Returns active solvers grouped by chain, as an array of ChainSolvers.

**Query parameter:** `enabledOnly` (boolean; set to `false` to include soft-deleted rows, default `true`).

**Errors:** `401 UNAUTHORIZED`.

#### `GET /api/v1/solvers/{chainId}`

Returns one ChainSolvers group for a single chain. Returns `404` for chain IDs not in the chain registry.

**Path parameter:** `chainId` (integer). **Query parameter:** `enabledOnly` (boolean, default `true`).

**Errors:** `400 BAD_REQUEST`, `401 UNAUTHORIZED`, `404 NOT_FOUND`.

***

### Tokenized assets (RWA)

Read-only views over the tokenized real-world-asset vaults ZeroDelta can clear into. Declared in the published spec; full response schemas ship in the OpenAPI package.

| Method + path                            | Purpose                                   | Query                     |
| ---------------------------------------- | ----------------------------------------- | ------------------------- |
| `GET /api/v1/rwa/vaults`                 | List tokenized RWA vaults                 | `status`                  |
| `GET /api/v1/rwa/vaults/{slug}`          | One vault by slug                         | —                         |
| `GET /api/v1/rwa/vaults/{slug}/history`  | Daily price, APY and TVL series           | —                         |
| `GET /api/v1/rwa/vaults/{slug}/activity` | Deposits and redemptions for one vault    | `cursor`, `days`, `limit` |
| `GET /api/v1/rwa/icons/{file}`           | Vault icon, proxied from the upstream CMS | —                         |

**Errors:** `400 BAD_REQUEST`, `401 UNAUTHORIZED`, `404 NOT_FOUND` (per route; the icon proxy needs no key).

***

### Stakeflow

Points and commitment views. Declared in the published spec; full response schemas ship in the OpenAPI package.

| Method + path                            | Purpose                                              | Query                        |
| ---------------------------------------- | ---------------------------------------------------- | ---------------------------- |
| `GET /api/v1/stakeflow/leaderboard`      | Ranked wallet totals (restricted — can return `403`) | `wallet`                     |
| `GET /api/v1/stakeflow/points/{address}` | A wallet's points position                           | —                            |
| `GET /api/v1/stakeflow/rates`            | Published earn rate per committable token            | —                            |
| `GET /api/v1/stakeflow/commitments`      | A wallet's commitments, active and revoked           | `wallet`                     |
| `GET /api/v1/stakeflow/balance`          | One wallet's live balance of a committable token     | `wallet`, `token`, `chainId` |
| `GET /api/v1/stakeflow/capacity`         | How much of one token a wallet could revoke          | `wallet`, `token`            |
| `GET /api/v1/stakeflow/revoke-preview`   | What a specific revocation would cost                | `wallet`, `token`, `amount`  |

**Errors:** `400 BAD_REQUEST`, `401 UNAUTHORIZED`, and `403` on the leaderboard.

***

### Health and metrics

#### `GET /health`

Liveness probe. Always returns `200`. No auth.

```json
{ "status": "ok", "time": "2026-04-13T12:00:00Z" }
```

#### `GET /health/ready`

Readiness probe. Probes downstream dependencies in parallel; only the database pools are critical. Returns `503` when a critical DB check fails, otherwise `200` — with `status: degraded` when a non-critical dependency is down. No auth.

```json
{
  "status": "ok",
  "time": "2026-04-13T12:00:00Z",
  "checks": {
    "database":      { "status": "ok", "critical": true, "latencyMs": 3 },
    "read_database": { "status": "ok", "critical": true, "latencyMs": 4 },
    "cache":         { "status": "ok", "critical": true }
  },
  "providers": {
    "a1b2c3d4e5f6": { "status": "ok", "critical": false, "latencyMs": 40, "ageSeconds": 5 },
    "9c0d1e2f3a4b": { "status": "ok", "critical": false }
  },
  "failures": []
}
```

> * `status` is `ok` or `degraded`.
> * `checks` holds infrastructure dependencies under generic role labels (`database`, `read_database`, `cache`).
> * `providers` holds third-party / internal-service dependencies under **opaque hashed keys** — the specific vendor is never disclosed.
> * Each entry has `status` (`ok | unavailable | stub | not_configured`), a `critical` flag, and optional `latencyMs` / `ageSeconds`. Per-check error detail is intentionally omitted.
> * `failures` lists the masked keys of non-ok checks (omitted when empty).

#### `GET /metrics`

Prometheus-format metrics. No auth; optionally IP-whitelisted per environment.

***

### Authentication and rate limits

* **Auth.** Send your key in the `x-apikey` header on every `/api/v1/*` request (`x-api-key` is accepted as an alias, as is `Authorization: Bearer <key>`). The base URL and key are provided during onboarding (separate `dev` and `prod` hosts). A missing or invalid key returns `401 UNAUTHORIZED`. The health and metrics probes need no key.
* **Environments.** `dev` and `prod` are independent deployments with independent contract addresses and independent keys — a `dev` key returns `401` against `prod`. Point each build at one environment's base URL and read its addresses from `GET /api/v1/chains`.
* **Rate limits.** A **global per-IP** limit of roughly **100 requests/second with a burst allowance of 200** applies to every route. A tighter **per-key** limit of roughly **50 requests/second, burst 100** applies to the pricing endpoints, `/api/v1/quote` above all. These are the deployed defaults; your key's exact limit is set at onboarding. On `429 RATE_LIMITED` the API returns a `Retry-After` header — in the current implementation it is always the literal value `1` (seconds), so treat it as a floor and add your own backoff rather than assuming it grows. If you run many concurrent sessions refetching quotes, tell the Glacis team your expected `/quote` rate so your key is sized for it.
* **A note for generated clients.** The published OpenAPI spec does not declare a `429` response on any path, so a client generated from it will have no rate-limit branch even though every endpoint is rate limited. Handle `429` explicitly.
