> 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.md).

# 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

{% columns %}
{% column %}

#### API

Quote routes, check status, and read supported coverage.
{% endcolumn %}

{% column %}

#### Smart Contracts

See `submitOrder`, cancellation, and on-chain behavior.
{% endcolumn %}
{% endcolumns %}

#### 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).<br>

### 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.<br>

### 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 `bytes32(0)` for the id unless the Glacis team issued you one. 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).

{% stepper %}
{% step %}

### 1. Request a quote

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

```jsx
const response = await fetch(`${ZD_API_ENDPOINT}/api/v1/quote`, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-apikey': ZD_API_KEY,
  },
  body: JSON.stringify({
    fromChainId: '130',          // Unichain
    toChainId: '42161',          // Arbitrum
    fromToken: 'USDC',
    toToken: 'USDT',
    fromAmount: '1000000000000', // 1,000,000 USDC (6 decimals)
    owner: await signer.getAddress(),        // optional: when set, the response includes escrowAddress + executionChainId
    destinationReceiver: RECIPIENT_ADDRESS,  // optional: screened alongside owner
  }),
});
// Surface documented HTTP errors (400/401/422/429/502) before touching the body.
if (!response.ok) {
  const body = await response.text();
  throw new Error(`Quote failed: HTTP ${response.status} ${body}`);
}
const { data } = await response.json();
```

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. 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; it does **not** populate the prepared structs, which always come back with both fields `null`. Fill both client-side with 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.
> {% endstep %}

{% step %}

### Approve and submit

`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.

```jsx
// Exact-amount approve assumes a non-fee-on-transfer token (true for the current stablecoin set).
// If a fee-on-transfer asset is ever onboarded, approve the gross amount or use an allowance buffer.
await erc20.approve(escrowAddress, data.orderRequests[0].bidTokenAmount);

// ZDLITE_ABI: from the ABI/OpenAPI package provided at onboarding (see Reference).
const escrow = new Contract(escrowAddress, ZDLITE_ABI, signer);

// data.orderRequests is the prepared ARRAY from the quote — submitOrder takes it as-is.
// Set owner and destinationReceiver on EVERY element: the contract reverts on a zero owner
// OR a zero destinationReceiver (ZDLite__ZeroAddress), and the quote returns them null.
const owner = await signer.getAddress();          // the address that will own the order (non-zero)
const requests = data.orderRequests.map((request) => ({
  ...request,
  owner,
  destinationReceiver: RECIPIENT_ADDRESS,         // who receives the asset on the destination chain (non-zero)
}));

// partnerId is attribution metadata only; pass the zero value unless Glacis issued you one.
const PARTNER_ID = '0x' + '00'.repeat(32);
const tx = await escrow.submitOrder(requests, PARTNER_ID, { value: data.gasCostSource });
const receipt = await tx.wait();

// orderId is not returned to off-chain callers from a normal send. Parse it from the
// OrderSubmitted event in the receipt. OrderSubmitted carries BOTH the indexed orderId and the
// full Order tuple — persist the whole tuple: it is the exact struct the cancel path needs, and
// before the order arrives on its execution chain it is the ONLY place to get it (see Cancellation).
// orderId is also the key for on-chain reads.
// A DIRECT bridge (quote.isDirect) emits OrderDirectBridged INSTEAD, stores no order,
// and is terminal at submit — there is no fill and nothing to cancel.
let orderId;
let order;
for (const log of receipt.logs) {
  try {
    const parsed = escrow.interface.parseLog(log);
    if (parsed && (parsed.name === 'OrderSubmitted' || parsed.name === 'OrderDirectBridged')) {
      orderId = parsed.args.orderId;
      order = parsed.args.order;   // full Order tuple — persist it, you need it to cancel
      break;
    }
  } catch (_) { /* not an escrow event; skip */ }
}
// viem equivalent: decodeEventLog({ abi: ZDLITE_ABI, ...log }) and match eventName.
```

> **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.
> {% endstep %}

{% step %}

### 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.

```jsx
// The read API returns a LOWERCASE status string, NOT the on-chain enum names.
// Poll while the status is non-terminal; treat anything else as terminal so a newly
// added status can never trap the loop. This is the full, code-verified non-terminal set.
const NON_TERMINAL_STATUSES = new Set([
  'pending', 'in_progress', 'outbound_bridging', 'cancel_pending', 'awaiting_claim',
  'continuing',
]);

async function pollUntilDone(txHash) {
  while (true) {
    const res = await fetch(
      `${ZD_API_ENDPOINT}/api/v1/transactions/${txHash}`,
      { headers: { 'x-apikey': ZD_API_KEY } },
    );

    if (res.status === 404) {
      // Not indexed yet (tx not mined / indexer catching up). Back off and retry.
      await new Promise((r) => setTimeout(r, 10_000));
      continue;
    }
    if (res.status === 429) {
      // Honor the Retry-After header (seconds) on rate limit, then retry.
      const wait = (Number(res.headers.get('Retry-After')) || 10) * 1000;
      await new Promise((r) => setTimeout(r, wait));
      continue;
    }
    if (!res.ok) {
      throw new Error(`Status poll failed: HTTP ${res.status} ${await res.text()}`);
    }

    const { data } = await res.json();
    // Terminal once the JOURNEY status leaves the non-terminal set (delivered = success;
    // failed / expired = failure).
    //
    // Read journeyStatus, NOT status. On a chained order (an RWA buy is one) the parent's
    // `status` flips to 'delivered' as soon as LEG 1 lands — but the funds are parked mid-
    // journey, not in the user's wallet. journeyStatus reads 'continuing' for exactly that
    // window. Branching on `status` alone reports "done" on every in-flight chained order.
    // journeyStatus is absent on non-chained orders, so fall back to status.
    const effectiveStatus = data.journeyStatus ?? data.status;
    if (!NON_TERMINAL_STATUSES.has(effectiveStatus)) return data;
    await new Promise((r) => setTimeout(r, 10_000));
  }
}
```

**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:

```json
{
  "data": {
    "txHash": "0x...",
    "status": "in_progress",
    "journeyStatus": "in_progress",
    "sourceChainId": "130",
    "destChainId": "42161",
    "currentStep": "arrived",
    "completedSteps": 2,
    "totalSteps": 4,
    "steps": [
      { "stepName": "submitted", "stepStatus": "completed" },
      { "stepName": "arrived",   "stepStatus": "completed" },
      { "stepName": "filled",    "stepStatus": "pending" },
      { "stepName": "delivered", "stepStatus": "pending" }
    ]
  }
}
```

* `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.
{% endstep %}
{% endstepper %}

### 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](/zero-delta/integration-guide/discover-what-you-can-trade.md).

**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](/zero-delta/integration-guide/discover-what-you-can-trade.md).

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.<br>

### 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`

```solidity
function submitOrder(
    OrderRequest[] calldata requests,
    bytes32 partnerId
) external payable returns (bytes32 orderId, Order memory order);
```

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 attribution metadata only — no on-chain logic reads it. Pass `bytes32(0)` unless the Glacis team issued you an id; a non-zero value emits `PartnerOrder(partnerId)`.

**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:

```jsx
// The order's execution chain and escrow come from the quote (or the tracked transaction):
//   data.executionChainId / data.executionEscrowAddress
// In the prod deployment the escrow is 0x5e25c8ABc19b88d6A7Ab0D805C77A34987a68b40 on every
// chain — but that address is deployment-specific, so read it from GET /api/v1/chains for
// your environment rather than hardcoding it.
const execProvider = new JsonRpcProvider(EXECUTION_CHAIN_RPC_URL);
const execSigner = wallet.connect(execProvider);                        // signer with gas on that chain
const execEscrow = new Contract(data.executionEscrowAddress, ZDLITE_ABI, execSigner);

// PREFERRED: use the Order tuple you persisted from the OrderSubmitted event at submit time.
// It is valid immediately, including before the order arrives on the execution chain.
let order = persistedOrderTuple;   // from step 2

// FALLBACK, POST-ARRIVAL ONLY: read it back on the execution chain.
// Before arrival this returns a ZEROED struct; cancel(order) would then derive
// keccak256(0,0,0) and revert with ZDLite__UnauthorizedCaller. Guard on arrival first.
if (!order) {
  order = await execEscrow.getOrder(orderId);
  if (order.owner === ZeroAddress) {
    throw new Error('Order has not arrived on the execution chain yet — ' +
                    'use the Order tuple from the OrderSubmitted event.');
  }
}

// Cancel and reclaim (cancel post-arrival, or claimCancellation for a pre-arrival cancel).
await execEscrow.cancel(order);
```

`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.\ <br>

### 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.<br>

### 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.<br>

### 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.<br>

### Audit

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

### 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.
