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

Integration Guide

Integrate the clearing house. One quote, one contract call.

Guide for engineering teams integrating ZeroDelta. Today the clearing house settles stablecoins across 9 chains; this is the surface you integrate now. A base integration is four calls (quote, approve, submitOrder, poll) with no proprietary SDK to learn. The same four calls clear stablecoins today and tokenized assets as coverage expands. Pre-req reading: Product Overview and Architecture.

Jump to reference

API

Quote routes, check status, and read supported coverage.

Smart Contracts

See submitOrder, cancellation, and on-chain behavior.

Before you start

The on-chain contracts and API use the internal name ZDLite (you will see it in error codes like ZDLite__UnsupportedToken and in the ZDLITE_ABI package); ZeroDelta is the product. The two names refer to the same thing.

Addresses in this guide are the prod deployment. ZeroDelta runs two deployments — prod (production) and dev (pre-production) — and every contract address rotates between them, so dev shares none of these addresses. Use the base URL, API key, and deployment you were given at onboarding, and read addresses from GET /api/v1/chains for that environment rather than hardcoding them.

These come from the Glacis team during onboarding, not from this doc. Line them up first:

  • API key (x-apikey header) and the base URL for your environment (dev or prod).

  • The ABI/OpenAPI package (ZDLITE_ABI, full request/response schemas).

  • The environment scope your key is granted (mainnet small-amount and/or an onboarding-provisioned dev environment; see Testing without mainnet funds).

  • Confirmed chain IDs, token addresses, and fee model for your routes (the live /chains, /tokens, /routes endpoints are canonical).

What ZeroDelta is, and isn't

Is:

  • Single-call cross-chain stablecoin clearing.

  • Audited contracts on every supported chain.

  • A quote-and-submit interface with deterministic order tracking.

Isn't:

  • A bridge for arbitrary ERC-20s (supported tokens only).

  • An atomic same-block swap (settlement spans cross-chain bridge windows).

  • A public/permissionless solver network (today).

  • A replacement for general-purpose bridges.

Quick start

A minimal integration is four steps: quote → approve → submitOrder → poll status.

You interact with one smart-contract function (submitOrder) on the source chain and the ZeroDelta API for quoting and tracking. Base URL and an API key are provided during onboarding (see Operational details).

Three things to know before you write the submit code. submitOrder takes an array of order requests plus a partnerId — pass the quote's orderRequests through unchanged and use the quote's partnerId when it is a 32-byte hex string; otherwise use bytes32(0). A partnerId error object means you must re-quote with both wallets before submitting. It is a state-changing transaction, so orderId is not returned to your off-chain call; you parse it from the OrderSubmitted event (shown in step 2). And msg.value must carry the bridge fee from the quote (gasCostSource), denominated in the source chain's native token (see step 2 for buffer and revert behavior).

1

1. Request a quote

Refetch the quote roughly every 10 seconds in any live interface to keep pricing current.

The /quote request requires { fromChainId, toChainId, fromToken, toToken, fromAmount } and accepts optional owner and destinationReceiver — both are used for compliance screening, and passing owner makes the response include the source escrowAddress and executionChainId (used in the approve + submit steps). The response also carries finalAmount (what the receiver gets on the destination chain), fee and ETA fields, and the prepared orderRequests array. The response can also carry partnerId: a string marker to pass to submitOrder, or an error object when either wallet is missing — in that case re-quote with both wallets to obtain the marker before submitting. A full annotated response is shown under Quote example.

You must set owner and destinationReceiver on every element of orderRequests before submitting. Passing them in the quote request screens the wallets, and the prepared structs come back with them filled in; omit them and both fields come back null. Either way, make sure every element carries real, non-zero addresses before calling submitOrder; the contract reverts with ZDLite__ZeroAddress on a zero owner or destinationReceiver (see step 2).

Two quote shapes worth branching on. isDirect: true is a same-token direct bridge — no swap, no solver fill, and not cancellable, so suppress cancel affordances. isChained: true returns two elements in orderRequests (leg 1's output funds leg 2); fill owner and receiver on both.

2

Approve and submit

Or let the gateway build the transactions. Add sender — the wallet that will sign — to the quote request, together with owner and destinationReceiver, and the quote carries txData: an approve (only when the escrow's allowance from sender is short) and the submitOrder call, with the order's wallets, deadline and msg.value already set. The wallet sends them in order, waiting for each to confirm. On a Solana source, txData holds the serialized transactions to sign instead. Decode and check them against the quote before signing; the shapes are in the API reference. The rest of this step is what those transactions do, for integrators who build the calls themselves.

submitOrder pulls the bid token from the caller, so the user approves the escrow first (standard ERC-20 approve). msg.value must cover the bridge adapter's native messaging fee, returned in the quote as gasCostSource.

On msg.value / gasCostSource:

  • Unit. gasCostSource is the source chain's native messaging fee in wei, not the asset being moved. Always pass { value: gasCostSource } from the quote.

  • Per rail. gasCostSource is 0 for CCTP-source orders (USDC) and nonzero for LayerZero-source orders (USDT/USDe and the other OFT stables). The worked example below is a USDC source, so its gasCostSource is 0; a USDT or USDe source returns a real native fee.

  • Buffer (LayerZero-source only). When gasCostSource is nonzero, send it as quoted or slightly above; because the messaging fee can move between quote and inclusion, a small buffer (for example +10-20%) reduces the chance of an underpayment revert on volatile-gas chains. The escrow passes msg.sender as the LayerZero refundAddress, so the OFT refunds the unused excess to the caller. Do not carry this buffer onto a CCTP (USDC) order: CCTP-source orders quote gasCostSource: 0, the CCTP adapter's refund-address parameter is unused, and any native value you attach to a USDC lane is stranded in the contract rather than refunded.

  • Underpayment. If msg.value is below what the bridge adapter requires, submitOrder reverts and no order is created. There is no ZDLite__ error for this case: the revert comes from the bridge adapter itself and is adapter-specific (CCTP vs LayerZero), so do not match it by a ZDLite__ name. Send gasCostSource (plus the buffer above) to avoid it.

  • Overpayment. Send gasCostSource as quoted (plus the small buffer above) and avoid large overpayments.

The ask side is bytes on-chain. askToken and destinationReceiver are chain-agnostic byte strings, so a non-EVM destination can be expressed. For EVM destinations you pass the plain 20-byte address — exactly the 0x-prefixed hex string the API returns — and ethers/viem encode it correctly. Any width other than 20 or 32 bytes reverts with ZDLite__UnsupportedReceiver.

OrderRequest.owner does not have to equal msg.sender. An integrator can submit on a user's behalf: pull the bid token from the user through your own flow, then submit with owner set to the user's address. Set both owner and destinationReceiver to the correct, non-zero addresses before you submit — the contract reverts (ZDLite__ZeroAddress) if either is the zero address.

3

Track status

Poll the read API by source transaction hash (about every 10 seconds) until the order reaches a terminal state. Immediately after broadcast, before the source tx is mined and indexed, the API has not seen the hash yet and returns 404. Treat 404 as "not indexed yet" and keep polling; start with a short initial backoff (about 5–10 seconds) to let the source tx mine and the indexer catch up.

Status vocabulary — read this before you write the terminal check. The read API returns a lowercase status string, not the on-chain enum names (Filled / Cancelled); comparing status === 'Filled' never matches. The full, code-verified set:

Status
Terminal?

pending

no

in_progress

no

outbound_bridging

no

cancel_pending

no

awaiting_claim

no

continuing

no

delivered

yes (success)

expired

yes

cancelled

yes

fill_failed

yes

delivery_failed

yes

failed

yes

Happy path: pending → in_progress → outbound_bridging → delivered (outbound_bridging appears only when the destination chain differs from the execution chain). The poller above stops only on a terminal status, so a newly added status can never trap the loop.

status is per-order; journeyStatus is per-journey. Branch on journeyStatus. A chained order (isChained: true, and every RWA buy) settles in two legs. When leg 1 lands, the parent order's status is genuinely delivered — that leg is done — but the value is parked on the destination chain waiting for leg 2, not in the receiver's wallet. journeyStatus reads continuing for that whole window and only reaches a terminal value when the last leg settles. Treat continuing as non-terminal and prefer journeyStatus over status in any completion check, or you will report success on every chained order still in flight.

A response looks like:

  • status is the lowercase lifecycle string in the table above, scoped to this order. journeyStatus is the same vocabulary scoped to the whole journey across chained legs — it is the field to branch on for completion (see the warning above). delivered is the success terminal. Keep polling while the status is in the non-terminal set; stop on any terminal status. currentStep and the steps[] array (stepName ∈ submitted / arrived / filled / delivered, stepStatus ∈ completed / pending / failed / recovered) give finer-grained progress.

  • The API is keyed by source transaction hash (txHash), not by orderId. Persist your source tx hash for API status polling; persist orderId (read from the OrderSubmitted event) for the on-chain cancel/read path.

  • id is not orderId. The tracked transaction's id is a deployment-scoped uid (for example "rwa:0x52b1…"), unique across redeployments; orderId is the raw on-chain id. The escrow's cancel/claim calls take orderId — passing id there reverts.

  • A batch submit can create more than one order under one tx hash — GET /api/v1/transactions/{txHash}/orders returns the full set.

The API is keyed by source transaction hash. The same lifecycle can be read on-chain by orderId: getOrder(orderId) returns the full Order struct, and orderStatuses(orderId) returns the on-chain status enum (Pending / Filled / Cancelled — a different vocabulary from the API string; see the Reference). To obtain orderId from a submit, read it from the OrderSubmitted event in the transaction receipt.

Charging an integrator fee

You can take a fee on the swaps you route through ZeroDelta, split between up to eight addresses. The fee comes out of what the user receives: the quote you show already has it deducted, and the user signs for that net amount.

1. Get your API key enabled for fees. Fees are off for every API key by default. Contact ZeroDelta to have them enabled on yours. Until then a quote carrying feeRecipients is refused with 403 FEE_NOT_ENABLED, and the same request without feeRecipients still prices.

2. Quote with feeRecipients, owner and destinationReceiver. Add them to the POST /api/v1/quote body:

feeCentibps is in centibps of the output: 100 = 1 bps, so this example takes 15 bps, 9 to the EVM address and 6 to the Solana address. Both wallets are required, because the fee is bound to them.

3. Read the fee off the response. toAmount, finalAmount and orderRequests[0].askTokenAmount are already net of the fee. recipientFees states exactly what was taken and each recipient's share. feeBps is unchanged: it is still the bridge's protocol fee.

4. Submit with the quote's partnerId. Pass it verbatim as the second argument of submitOrder(orderRequests, partnerId). If you quote with sender, the submit call in txData already carries it. An order submitted without the partnerId settles normally at the net amount, but no fee is collected on it. The marker binds this exact order (both wallets, both chains, both tokens and both amounts) and can be used once.

5. The fee is collected when the order fills. The user always receives the quoted net amount. The fee is paid from the amount the fill produced above that. In the usual case that covers the whole fee. If the market moved against the order between quote and fill, the fee collected can be lower, down to zero. Each fee is valued in US dollars at the market price at the fill's block, and is added to the recipient's balance, usually within seconds of the fill.

6. Check balances and claim payouts. GET /api/v1/fees/balances lists what each of your recipients has collected since their last claim, in US dollars. POST /api/v1/fees/payouts claims a recipient's whole balance, to be paid in USDC on the chain you name. Payouts are made by hand. Track your requests with GET /api/v1/fees/payouts. See Fees in the API reference for the rules.

Which orders can carry a fee. At least one of the two tokens must be a USD stablecoin, because the fee is valued in dollars through it. Buying or selling an RWA against a stablecoin takes a fee. A direct bridge (isDirect, the same token on both chains) and a chained two-leg quote cannot carry one: there is no single fill to take it from. These are refused with 400 FEE_UNSUPPORTED_LANE, and the same request without feeRecipients prices.

Supported coverage

The core bid stablecoins are USDC, USDT and USDe, with USDtb, AUSD, PYUSD and USDG also bridge-configured. Additional tokens are ask-only — they can be bought but not bridged, so they are delivered on the chain the order executes on (destinationChainId must equal executionChainId for those). Per-(chain, token) availability is not uniform — not every token is enabled on every chain. Query GET /api/v1/tokens for the live per-cell set and GET /api/v1/route-health for which lanes between those cells actually work, before you build a route picker — the full flow is in Discover what you can trade.

Bridge rail by token (the same regardless of chain):

Token
Rail

USDC

Circle CCTP V2 (burn-and-mint, native 1:1)

USDT, USDe, USDtb, AUSD, PYUSD, USDG

LayerZero V2 OFT

Ask-only tokens

none — delivered on the execution chain

Chains and IDs (code-verified against the prod deployment manifest; confirm against the live /chains API):

Chain
Chain ID
Role

Ethereum

1

Connected & default execution chain

Optimism

10

Connected

Arbitrum

42161

Connected & allowed execution chain

Base

8453

Connected & allowed execution chain

Ink

57073

Connected

Unichain

130

Connected

Plasma

9745

Connected

Sonic

146

Connected

Plume

98866

Connected & allowed execution chain

The dev deployment spans the same nine chains with the same execution-chain allowlist; only the contract addresses differ.

Execution chain is per order, not global. Each order carries an executionChainId that the escrow validates against its allowlist — Ethereum (1), Base (8453), Arbitrum (42161), Plume (98866). The API elects it for you and returns it on every element of orderRequests, plus as the top-level executionChainId / executionEscrowAddress you cancel against.

Do not assume every (chain × token) cell is enabled, or that an enabled cell is a tradeable lane. Token coverage differs by chain, and a cell being enabled says nothing about whether a lane between two cells can be filled. GET /api/v1/tokens returns the live cells; GET /api/v1/route-health returns the measured verdict for each lane between them. Gate your UI on those two. A quote for an unsupported cell returns 422 / ZDLite__UnsupportedToken on submit. GET /api/v1/routes is not the trade catalogue — it is the bridge-rail table, one row per token per chain pair, and a Route carries a single tokenId with no destination-token field, so it cannot express a trade between two different tokens at all. See Discover what you can trade.

Per-(chain, token) contract addresses are returned by GET /api/v1/tokens; treat that endpoint as the canonical source rather than hardcoding addresses. A representative /tokens response is shown under API reference.

New chains, tokens, and routes are onboarded on partner request. Always treat GET /api/v1/chains, /tokens and /route-health as the live source of truth for what is currently enabled and tradeable.

Smart contract interface

The escrow contract is deployed on every supported chain. On source chains it handles submitOrder and bridging to the order's execution chain. Integrators only need submitOrder on the source chain; full interface is in the Reference.

submitOrder

Transfers requests[0].bidToken from the caller, bridges it to that request's execution chain, and emits OrderSubmitted. If the source chain is already the execution chain, the order is stored directly without bridging. The function is payable; always include { value: gasCostSource } from the quote. Off-chain callers read orderId from the OrderSubmitted event (or an eth_call simulation); a normal send does not return values to the caller.

  • requests is length 1 for a normal order, or 2 for a chained order where request 0's swap output funds request 1. Pass the quote's orderRequests through unchanged.

  • partnerId is emitted on-chain as PartnerOrder(partnerId) when non-zero. When the quote returns a string partnerId, pass it verbatim: the off-chain listener verifies it against the submitted order. Use bytes32(0) when the quote has no partnerId. If the quote returns { "error": "..." }, request a new quote with owner and destinationReceiver before submitting.

Requirements:

  • Caller must approve requests[0].bidToken to the escrow before calling.

  • msg.value must cover the bridge adapter's native messaging fee.

  • bidToken and askToken must be registered/supported on their respective chains.

  • A bridge adapter must exist for the (bidToken, executionChainId) pair.

  • executionChainId must be non-zero and in the escrow's allowlist.

  • destinationReceiver must be 20 or 32 bytes — exactly 20 when the destination chain is the order's execution chain.

  • deadline must be 0 (no expiry) or a future timestamp.

  • The contract must not be paused.

Same-token direct bridge. When a single request's ask token is the same asset as its bid token on the destination chain, submitOrder bridges straight to destinationReceiver, stores no order state, and emits OrderDirectBridged instead of OrderSubmitted. Such a transfer never arrives, never fills, and cannot be cancelled; askTokenAmount acts as a minimum-delivered floor. The quote flags these with isDirect: true.

See Reference for the OrderRequest / Order structs, events, and error codes.

Cancellation

If an order cannot be filled (for example, the market moves beyond the quoted minimum), the user reclaims funds on the order's execution chain:

  • cancel(Order) — direct cancel by the order owner; funds are released immediately once the order has arrived.

  • claimCancellation(Order) — withdraw funds for an order cancelled before it arrived, once it lands.

  • cancelFor(Order, signature) — the same cancel, authorized off-chain by the owner and relayed by anyone (the owner pays no gas).

Which to call: check the status steps (step 3). If the arrived step is completed, use cancel. If you cancelled while the order was still pre-arrival, call claimCancellation once it lands.

Persist the Order tuple at submit — pre-arrival, it is the only copy that exists. Every cancel function takes the full populated Order struct. getOrder(orderId) on the execution-chain escrow only returns it after the order has bridged and arrived; call it earlier and it returns a zeroed struct. Passing that zeroed struct to cancel() derives keccak256(0, 0, 0) — an id that belongs to no one — and the call reverts with ZDLite__UnauthorizedCaller, which reads like a permissions bug and is not one. Pre-arrival is exactly when users most want out, so capture the Order tuple from the OrderSubmitted event at submit time (step 2) and keep it. Read it back with getOrder only as a post-arrival fallback.

Gasless cancel (cancelFor). The owner signs an EIP-712 message and a relayer submits it:

  • Domain: name = "ZDLiteEscrow", version = "1", chainId = the execution chain, verifyingContract = that chain's escrow. The contract's domainSeparator() view returns the same value for verification.

  • Typed struct: Cancel(address owner,uint64 orderNonce,uint64 sourceChainId) — the identity triple that derives orderId.

  • Replay protection is the order's one-way status machine, not a separate nonce: once the cancel lands the status is no longer Pending/Nonexistent, so replaying the signature reverts. Contract wallets (EIP-1271) are supported.

Chained orders park their intermediate funds on the destination chain instead of paying out. If the follow-up leg cannot proceed, the owner calls claimChain(prevOrderId) on that chain's escrow to recover the parked funds; continueChain(prevOrderId) is permissionless and pushes the chain forward.

Cancel functions take the full populated Order struct, which the contract assigns at submit (it sets nonce and sourceChainId). You already have this struct from the OrderSubmitted event (it carries the full Order tuple), so persist it at submit time; otherwise — and only once the order has arrived — read it back from the execution chain, where the canonical record lives after the order bridges. Query the execution-chain escrow, not the source-chain one:

getOrder(orderId) is a view on the execution-chain escrow; it returns the canonical Order (see the Reference) once the order has arrived. Calling it on a source-chain instance returns Nonexistent after the order bridges, so switch your provider first. Because nonce and sourceChainId are contract-assigned, do not reconstruct the struct by hand; persist it from the event, or read it back after arrival.

Solver model

Solvers are operated by Glacis today, vetted and approved to fill orders. Liquidity on the other side is sourced from vetted private market makers under Glacis operation. There is nothing for an integrator to run or operate; you submit orders and the network fills them. An open, third-party solver network is on the roadmap. If you want to run a solver as an independent party, contact the Glacis team.

Risk model

ZeroDelta's contracts have been audited by Halborn and Sherlock. The operational risks an integrator should be aware of:

Risk
Detail
Mitigation / forward path

Upgradeable contracts

The contracts are currently upgradeable under admin roles held by Glacis (a disclosed operational risk). Compromise of admin keys could put escrowed funds at risk.

We intend to move admin roles toward multisig control, with progressive decentralization of governance as a forward direction.

Quote drift

Quotes are estimates; if the market moves significantly mid-flight, a solver may be unable to fill at the quoted minimum.

The order stays Pending; the user cancels and reclaims funds on the execution chain.

Bridge / relayer dependence

Cross-chain settlement relies on third-party messaging infrastructure (Circle CCTP for USDC, LayerZero for the OFT stables). Outages can delay a cross-chain leg.

These are widely used, production cross-chain providers; a stuck leg completes once the provider recovers.

Token-level pauses

A stablecoin or its bridge implementation with pause features can halt a transfer mid-flight.

Limited to assets with such features; surfaced in transaction status.

FAQ

Do I need to run a solver or hold inventory? No. You quote and submit; the Glacis-operated solver network fills.

Can I submit on behalf of my users? Yes. Set OrderRequest.owner to the user's address and submit from your own contract or wallet (msg.sender need not equal owner).

What happens if the order can't be filled? It stays Pending; nothing auto-refunds. The user (or you on their behalf) calls cancel and reclaims funds on the execution chain. With deadline = 0 (the default) there is no expiry; set a deadline for unattended flows so an order has a defined failure point. See Cancellation.

Is it atomic / instant? No. Settlement spans cross-chain bridge windows. The quote's estimatedDuration (seconds) is the expected time for that route; actual finality depends on the chains and assets involved and is bounded by the underlying bridge (Circle CCTP V2 for USDC, LayerZero V2 OFT for the OFT stables). ZeroDelta optimizes for certainty at size, not sub-minute UX.

Which stablecoins and chains are supported? USDC, USDT and USDe are the core set (with USDtb, AUSD, PYUSD and USDG also bridged, plus a set of ask-only tokens) across 9 chains today; query /chains, /tokens and /route-health for the live set.

How long does integration take? The surface is small: four calls (quote, approve, submitOrder, poll) and no proprietary SDK to learn, so once you have the prerequisites (see Before you start) the build is short. The gating step is onboarding (key, base URL, ABI package, environment scope), not the code.

Can I get more chains/tokens/routes? Yes, on request. Contact the Glacis team.

Operational details

  • Environments: two deployments, dev (pre-production) and prod (production), each with its own base URL and API key, provided on onboarding. They are independent on-chain deployments: the addresses in this guide are prod, and dev uses a different address for every contract. A key is valid against one environment only. See Testing without mainnet funds.

  • Auth & rate limits: x-apikey header; per-key rate limits apply to pricing endpoints. See Errors and limits for 429 / Retry-After and retry guidance.

  • Versioning: additive within /api/v1/; breaking changes ship under a new version with advance notice (see Auth and environments).

  • Support & incidents: support and status channels provided on onboarding.

Audit

Halborn and Sherlock have audited the ZeroDelta (ZDLite) smart contracts. All important findings have been addressed. The reports are available on request.

Roadmap

Next-version capabilities include deeper network settlement that tightens pricing as orderflow grows, an open solver network, RWA support, and the partner capabilities described in the Product Overview. The internal technical reference is available under NDA. To start an integration or discuss your use case, contact the Glacis team.

Last updated