# Overview

<figure><img src="https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2FfO4nwM6YyR537eTdnnkD%2FLogo%20for%20docs.png?alt=media&amp;token=d9f47953-4294-4f31-a879-70b357366c6e" alt=""><figcaption></figcaption></figure>

Make digital assets make sense.

[**Glacis Labs**](https://glacislabs.com/) builds the cross-chain infrastructure that moves digital assets at institutional scale. Our products share one foundation and one goal: settlement that holds when real size is on the line.

### Products

* **ZeroDelta** — *flagship, latest.* The clearing house for RWAs. Match institutional orderflow and settle any stablecoin into any other, across any chain, with firm, zero-slippage execution at scale. **Start here if you're integrating.** [Full docs ↗](/zero-delta/why-zerodelta)
* **Airlift** — *in production.* A universal token registry for cross-chain token standards (OFT, NTT, CCT, xERC20, and more) — integrate any burn-and-mint token through one API. [Full docs ↗](https://docs.glacislabs.com/airlift/why-airlift)
* **Glacis Core** — *the foundation.* A router and firewall for cross-chain messages: build cross-chain apps independent of any single GMP, with quorum-based security. [Full docs ↗](https://docs.glacislabs.com/glacis-core/why-glacis)

### How they fit together

Glacis Core carries the messages and enforces the security model. Airlift moves the tokens. ZeroDelta clears and settles on top of both — its architecture is, literally, "built on Glacis Core messaging and Glacis Airlift token transfers."

```javascript
ZeroDelta      flagship · clears & settles (RWAs, stablecoins)
    │ builds on
    ├── Airlift        interchain token transfers (Circle CCTP, LayerZero OFT)
    └── Glacis Core    secure cross-chain messaging / GMP + security

Glacis Labs — the lab that builds and owns all three.
```

### Start here

* **Integrate ZeroDelta** → [Product Overview](/zero-delta/why-zerodelta) · [Architecture ](/zero-delta/architecture)· [Integration Guide](/zero-delta/integration-guide)&#x20;
* **Move a token cross-chain (Airlift)** → [Airlift overview](/airlift/why-airlift)
* **Build on the messaging layer (Glacis Core)** → [Glacis Core overview](/glacis-core/why-glacis)
* **Partner or co-design** → [contact the Glacis Labs team](https://t.me/glacisofficial)


# Why ZeroDelta?

The clearing house for RWAs. ZeroDelta matches institutional RWA orderflow, delivering compliant, zero-slippage execution for capital moving at scale.

* **Neutral by design.** No token, no preferred routes. One request, one firm settlement that holds at size. The clearing house works for the market, not against it.
* **Proven operator.** Built by [Glacis Labs](https://www.glacislabs.com/), the team behind Airlift, our universal interchain token-bridging product.
* **Audited.** Smart contracts audited by Halborn and Sherlock; all important findings addressed. Over 1B already cleared.
* **Built on tier-1 infrastructure.** Circle CCTP V2 and LayerZero V2 OFT underneath, not an untested bridge of our own.
* **Non-custodial.** Funds move only through audited contracts under conditions the contracts enforce.

***

### Moving a billion dollars should mean moving a billion dollars

The emergence of digital assets has hyper-fragmented liquidity across dozens of chains, dozens of stablecoins, and a fast-growing universe of tokenized real-world assets (RWAs). For institutions moving real size, execution means severe slippage, endless integrations, and broken compliance. DeFi is simply not built for institutional scale.

Today ZeroDelta clears stablecoins and tokenized assets across 9 chains, and the architecture is built to extend the same clearing model to any tokenized asset. We clear multi-chain orderflow as a single venue, and unlike an aggregator our pricing improves with size rather than decaying against pool depth.

**Live today:** eight bridged assets — USDC, USDT, USDe, USDtb, AUSD, PYUSD, USDG and XAUT — clearing across 9 chains with firm quotes, plus a substantially larger **ask-only** set that can be bought but not bridged (delivered on the order's execution chain). **Chained two-leg orders, which is what a tokenized-asset purchase is, are live in production.** Query `GET /api/v1/routes` for the authoritative live set; the Integration Guide carries the coverage matrix. **Direction (roadmap, not live):** chains and issuers at scale, compliance at the protocol layer, and an open solver network. The sections below mark which is which.

Because the biggest players don't need a dollar to equal a dollar. They need a billion to equal a billion.

***

### A clearing house, not an aggregator

<figure><img src="https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2FbSIPXv0QyF7TSCjN1qfI%2Fimage.png?alt=media&#x26;token=07c4cf4c-d174-4644-8568-b899ba580c32" alt=""><figcaption></figcaption></figure>

This is the distinction that defines the category.

An aggregator routes and quotes. It searches existing pools for the best path and hands the trade back to the same fragmented liquidity everyone else is drawing on. Its per-order economics flatten as it scales, because every order still pays the spread on the venue it lands in.

A clearing house clears orderflow centrally and commits to a firm price before the user signs. That is why ZeroDelta wins the trades that break an aggregator: execution gets cheaper and tighter with size rather than worse, and the price you are quoted is the price you settle, held at size.

Concretely: a small order on an aggregator pays the spread on whatever venue it lands in, and that cost holds or worsens as the order grows. A large order cleared through ZeroDelta is priced as a single firm quote, so the marginal cost of size falls instead of rising.

We do not disclose how the matching is computed. What matters for an integrator is the behavior: a single firm quote, held to settlement, that improves rather than degrades as the order grows.

***

### One integration, one settlement, any supported asset and chain

ZeroDelta abstracts the entire fragmented surface of chains, bridges, and issuers behind a single interface. You send one request and get one firm settlement that holds at size, regardless of how many venues, bridges, and issuers sit underneath it. Today it clears stablecoins and tokenized assets across 9 chains; the architecture is built to scale to hundreds of assets, issuers, and chains (direction, not current scale).

* **One integration, complete coverage.** Any supported asset, any supported chain, through a single interface. No bilateral integration per route.
* **Firm quotes.** The price you are quoted is the price you settle, with no quote-versus-execution drift; the solver must fill at or above the quoted threshold on-chain.
* **Deterministic delivery.** Every order has one traceable lifecycle from submission to settlement, with a canonical on-chain record on the order's execution chain that is publicly verifiable.
* **Honest about depth.** An order larger than the depth currently available for a route is declined at quote rather than filled at a worse price.

***

### Compliance, moving to the protocol layer

Institutional flow needs rails whose compliance posture matches the assets moving across them. ZeroDelta is designed to move compliance into the protocol layer instead of leaving it to bespoke integrations.

Today, access is permissioned in practice: integrators are onboarded and KYB'd by the Glacis team, and asset eligibility is controlled at the contract layer. Wallet screening runs at quote time and again at fill time, so a pair that becomes blocked after quoting is still stopped before settlement.

These protocol-layer capabilities are rolling out as the clearing layer matures:

* **Permissioned by default.**
* **Protocol-enforced issuer rules.**
* **Travel-rule support built in.**

The goal is a settlement layer for compliance-forward issuers and the integrators that serve them, one that fits the regulatory era they actually operate in rather than one they have to retrofit.

***

### Built for RWAs. Overkill for stablecoins.

Tokenized real-world assets are where fragmentation hurts most: NAV-locked windows, multi-jurisdiction compliance, asset-class diversity, counterparty permissioning, and audit-traceable lifecycles all compound at once. ZeroDelta is built to clear that — and clears it today: a tokenized-asset purchase settles as a chained two-leg order, live in production, where leg 1 moves the user's stablecoin to the execution chain and leg 2 swaps it into the asset and delivers it.

The same clearing house also settles stablecoins across 9 chains, which is the simplest case it handles. Whether you are moving stablecoins or tokenized assets, the distribution, convertibility, and coverage are the same single integration.

***

### Built for

| Partner                         | What ZeroDelta enables                                                                        |
| ------------------------------- | --------------------------------------------------------------------------------------------- |
| **Wallets and exchanges**       | Complete asset coverage for users without building or maintaining bidirectional integrations. |
| **Stablecoin and RWA issuers**  | Distribution and convertibility across every supported chain through a single integration.    |
| **Funds and payment platforms** | Large trades and cross-border settlement executed without slippage.                           |
| **Treasury and OTC desks**      | Audit-traceable rebalancing with firm quotes at institutional size.                           |

***

### The ambition

ZeroDelta is not a point solution for one swap on one route. The goal is to fix the way digital assets move: clearing any supported asset into any other, at scale, with compliance moving into the protocol layer and a price that holds. Stablecoins and the first tokenized assets today; the full RWA frontier next.

If your team is moving stablecoins, FX, or tokenized assets at institutional size, the technical deep-dive and partner co-design conversations are available under NDA.

***

### Talk to us

* **Integrate** → [Integration Guide](/zero-delta/integration-guide)
* **Partner with us** → [Glacis integrations team](https://t.me/+CXFgMhEqkE85N2Nh)
* **Technical deep-dive** → under NDA


# Architecture

### Three components

| Component                   | Who runs it                                         | What it does                                                                                                                                                                                                                                                  |
| --------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Users / integrators**     | You and your users                                  | Submit orders on-chain and receive the delivered asset. An integrator can submit on a user's behalf.                                                                                                                                                          |
| **Escrow contracts**        | Audited contracts deployed on every supported chain | Hold the user's asset between submission and settlement, and enforce the user's declared terms: destination, minimum output, deadline. The contract is permissionless to call; which assets are eligible is gated at the contract layer (`isSupportedToken`). |
| **Clearing and settlement** | Operated by Glacis today; opens over time           | Matches incoming orderflow, settles the difference, and delivers the destination asset.                                                                                                                                                                       |

Orders clear on an **execution chain**. Because an asset of a given ticker is treated as equivalent across chains through canonical cross-chain transfer (CCTP burn-and-mint for USDC, LayerZero OFT for USDT, USDe and the other OFT stables), ZeroDelta can match and settle wherever the equivalent asset lives and deliver wherever the user wants it. The execution chain is chosen **per order** — every order carries an `executionChainId` that the escrow validates against its allowlist, and the API elects it for you (Ethereum is the default; Base, Arbitrum and Plume are also allowed in the current deployment).

**Most orders are cross-asset.** An order moves one stablecoin into a *different* stablecoin (for example USDC → USDT); the assets are not interchangeable, so a price has to be set. That price is the firm quote: you request it from the API and submit against it, you do not price the leg yourself. The response carries `askTokenAmount` (the on-chain fill threshold the solver must meet) and `finalAmount` (the expected delivery to the receiver). **Same-ticker moves across chains** (for example USDC → USDC) are quoted too, as a **direct bridge**: `isDirect: true`, no swap and no solver fill, the funds leave in the submit transaction itself, and the transfer is **not cancellable**. Only a same token on the *same* chain is rejected, with **`400 BAD_REQUEST`** — a malformed pair rather than a pricing failure, so it does not join the `422` family the other quote refusals belong to. There is nothing to bridge or swap. How a swap price is matched is out of scope here; what matters for integration is that the quoted price is the price the order settles at.

***

### How an order clears

```mermaid
flowchart LR
    U[User / Integrator] -->|1. quote| API[ZeroDelta API]
    U -->|2. approve + submitOrder| ESC[Source-chain escrow<br/>holds order until filled or cancelled]
    ESC -->|3. forward order to execution chain| HUB[Execution-chain escrow<br/>order routing + fulfillment]
    HUB -->|4. orders picked up| SOLV[Glacis-approved solvers]
    SOLV -->|source liquidity| LP[Liquidity providers<br/>various sources, best rate]
    SOLV -->|5. deliver| R[Receiver on destination chain]
    HUB -.->|status events| API
    API -.->|poll status| U
    U -.->|user cancels, then reclaims| HUB
```

*Cross-chain legs move over ZeroDelta's own CCTP V2 and LayerZero V2 OFT bridge adapters; Glacis Core provides the GMP status layer.*

The dashed cancel path is user-initiated recovery: if an order cannot be settled, the user (or an integrator on their behalf) cancels and reclaims the locked funds on the execution chain. Nothing auto-refunds; the escrow never returns funds on its own. Recovery runs on the order's execution chain — the one named in the quote's `executionChainId` — so it incurs that chain's gas regardless of which chain the order originated on. Which function to call depends on whether the order has arrived yet (`cancel` post-arrival, `claimCancellation` pre-arrival), and a relayer can submit the owner's signed cancel with `cancelFor` — see Cancellation for the decision and Trust model.

1. **Quote.** You request a firm quote from the API for a `(fromChain, fromToken) → (toChain, toToken, amount)` pair. The quote carries `lifespanSeconds` — its price-validity window, from the winning provider's signed expiry (`0` when the provider does not sign one, or the quote has lapsed). Treat a quote as short-lived: refetch when `lifespanSeconds` elapses, or periodically (about every 10 seconds) to keep pricing current. The on-chain fill threshold itself does not expire.
2. **Approve and submit.** The user approves the asset to the source-chain escrow (standard ERC-20 `approve`), then calls `submitOrder`. The escrow pulls the asset and locks it. `submitOrder` is the only Zero-Delta-specific call; the preceding approve is a standard ERC-20 transaction.
3. **Settle.** The order is filled on the execution chain at or above the user's declared minimum output (`askTokenAmount`); the escrow verifies the fill meets that floor before releasing funds. Underneath, the source and destination legs move over Circle CCTP V2 (USDC) and LayerZero V2 OFT (USDT, USDe and the other OFT stables).
4. **Deliver.** The destination asset is delivered to the chosen receiver on the destination chain.

On-chain, the order status moves `Pending → Filled` on the happy path, or `Cancelled` if the user cancels (the `OrderStatus` enum). The **read API uses a different, lowercase status vocabulary** (`pending` / `in_progress` / `outbound_bridging` / `delivered`, plus `continuing` and the terminal failure states) — so if you poll the API, compare against its values, not the enum names. You track status either by polling the read API by source transaction hash, or on-chain by `orderId` via `getOrder` / `orderStatuses`; the Reference lists the full status set and the Integration Guide shows a correct poller.

> **A chained order has two statuses, and only one of them answers "is the user done?"** A chained (two-leg) order — which is what every RWA purchase is — exposes a per-order `status` and a journey-scoped `journeyStatus`. When leg 1 lands, `status` reads `delivered`: that leg genuinely finished. But the value is parked on the destination escrow awaiting leg 2, not in the receiver's wallet, and `journeyStatus` reads `continuing` for that entire window. Completion checks belong on `journeyStatus`, with `status` as the fallback for non-chained orders.

***

### The front end is deliberately standard

There is nothing proprietary to learn on the front end. The deposit pattern is the same **ERC-20 approve + escrow-lock** that many on-chain protocols use:

```jsx
// data is the quote response; see the Integration Guide for the runnable version.
// Exact-amount approve assumes a non-fee-on-transfer token (true for the current stablecoin set).
await erc20.approve(data.escrowAddress, data.orderRequests[0].bidTokenAmount);
// The quote returns owner/destinationReceiver as null on every element; set both to
// non-zero addresses first, or submitOrder reverts (ZDLite__ZeroAddress).
const requests = data.orderRequests.map((request) => ({ ...request, owner, destinationReceiver }));
// submitOrder takes the ARRAY plus a partnerId (attribution only — pass the zero value).
// value covers the bridge messaging fee in the source chain's native token (gasCostSource);
// it is 0 for a USDC/CCTP source. See the Integration Guide for buffer and revert behavior.
await escrow.submitOrder(requests, PARTNER_ID, { value: data.gasCostSource });
```

The user signs an approval and one `submitOrder` transaction. Everything after that, matching, settlement, and delivery, is handled by the protocol and ZeroDelta's infrastructure.

***

### What you can rely on

* **Firm quotes.** The price you are quoted is the price the order settles at. No quote-versus-execution drift.
* **A declared floor.** The escrow enforces the user's destination, minimum output, and deadline. An order that cannot meet the declared minimum does not settle.
* **Settlement that holds at size.** Pricing improves with order size rather than decaying against pool depth. An order larger than the depth currently available for a route is declined at quote (`422`) rather than filled at a worse price.
* **A traceable lifecycle.** Every order carries a deterministic ID (`orderId`) and an on-chain record; the canonical record resolves on the order's execution chain once it bridges, and the read API mirrors it keyed by source transaction hash.

***

### Built on the Glacis Labs stack

* **Glacis Core** — secure cross-chain messaging / GMP abstraction; the Glacis Labs foundation. ZeroDelta uses it for **cross-chain message status**, which is what drives the tracked-transaction lifecycle. See Glacis Core.
* **ZeroDelta's own bridge adapters** — the cross-chain token legs themselves move through adapters that ship with the ZeroDelta contracts (`CctpV2BridgeAdapter` for USDC, `LayerZeroOFTAdapter` for the OFT stables), not through a separate transfer product.
* **Circle CCTP V2** (USDC; burn-and-mint, native 1:1) and **LayerZero V2 OFT** (USDT, USDe and the other OFT stables) — the bridge rails underneath, which govern each cross-chain leg and its timing.
* **Glacis Airlift** is a sibling Glacis Labs product for interchain token transfers. ZeroDelta does **not** route its cross-chain legs through it. See Airlift.

***

### Trust model

* **Non-custodial.** User funds move only through audited contracts under conditions the contracts enforce. **ZeroDelta** never holds user balances in an account it controls.
* **Recoverable, but user-initiated.** If an order cannot be settled, the user cancels and reclaims funds on the execution chain. Recovery is not automatic. Direct-bridge transfers are the exception: they complete in the submit transaction and have nothing to recover.

> **Operational default worth a risk-committee note:** with `deadline = 0` (the default, no expiry), an unfilled order sits `Pending` indefinitely and **nothing auto-refunds** — funds are recovered only by an explicit user-initiated cancel on the execution chain. For unattended or automated flows, set a real `deadline` so every order has a defined failure point.

* **Upgradeable contracts.** The contracts are currently upgradeable under administrative roles held by Glacis; this is disclosed as an operational risk in the Integration Guide. We intend to move admin roles toward multisig control, with progressive decentralization of governance as a forward direction.
* **Audited.** Smart contracts audited by Halborn and Sherlock; all important findings addressed.


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

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

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.


# Discover What You Can Trade

The tradeable surface is a cross product narrowed by live measurement, not a list of routes. Four calls, one copy-paste function, and the three filtering mistakes to design out.

Build your route picker from what the clearing house can actually settle right now. Four calls, one function to copy, and the three mistakes that otherwise reach production.

### Three questions, three endpoints

ZeroDelta's tradeable surface is **not a list of routes**. It is a cross product — any enabled token on any connected chain, to any enabled token on any connected chain — narrowed by live measurement. Three endpoints answer three different questions, and conflating them is the root of most integration problems.

| Question              | Endpoint                                   | Answers                                                                                                       |
| --------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
| What exists?          | `GET /api/v1/chains`, `GET /api/v1/tokens` | The chains we are connected to and the tokens enabled on each. Their cross product is your candidate surface. |
| What works?           | `GET /api/v1/route-health`                 | One measured row per lane, from a prober that actually quotes each pair. This narrows the candidates.         |
| What fills right now? | `POST /api/v1/quote`                       | The authoritative answer for one trade, at one size, at this moment.                                          |

> **`/api/v1/routes` is not the trade catalogue.** It is the bridge-rail table: one row per token per chain pair, describing how that *one* token moves — its bridge, adapter, standard and facet. A `Route` carries a single `tokenId` and no destination-token field, so it cannot express a trade between two different tokens at all, and it covers only the tokens that are bridge-configured. Read it to understand a rail; never to build a picker.

### 1. Authenticate

Every endpoint requires a key, sent as a request header. There is no public surface — an unauthenticated request returns `401 UNAUTHORIZED` with `missing API key`, including for `/chains`.

```
X-API-Key: your-api-key
```

Every `/api/v1/*` response uses the same envelope: `data` on success, plus `meta` and `paging` on list endpoints, and `error` carrying a `code` and `message` on failure. On an error there is **no `data` key at all** — check the status before reading it, as the sample below does.

### 2. Read the chains

`GET /api/v1/chains`. Each entry carries `chainId`, `escrowContract`, `isExecutionChain` and the contract set. You need the escrow address later to submit and to cancel, so treat this endpoint as canonical rather than hardcoding addresses.

### 3. Read the tokens

`GET /api/v1/tokens`. One row per *(token, chain)* cell, with `address`, `decimals`, `isEnabled` and the rail metadata. These cells are your candidate surface.

> **Page this endpoint.** The default page size is 100 and the catalogue is larger, so a single bare call returns a **truncated** list and nothing in the response body looks wrong. `meta.totalCount` is the real number — compare it against what you received, and page with `limit` (max 500) and `offset` until they match. A picker built from the first page silently omits whole tokens.

### 4. Read the measured health, and filter

`GET /api/v1/route-health?scope=all`. One row per lane the prober has an opinion about. The top level carries `summary` with the live counts, `measuredAt`, and `nextSweepDueAt`. This endpoint is not paginated — it returns `data` only, with no `meta` or `paging`.

> **`scope` has exactly two behaviours, and only one of them is spelled.** `scope=all` returns every measured lane. **Any other value — including omitting the parameter entirely, and including `scope=healthy` — returns the unhealthy subset**, and the response echoes `"scope": "unhealthy"` whatever you asked for. Always send `scope=all` when you want the full picture, and read the echoed `scope` back rather than assuming your value was honoured.

```json
{
  "mode": "enforce",
  "scope": "all",
  "summary": { "healthy": 128, "degraded": 3, "broken": 11, "unknown": 6, "total": 148 },
  "measuredAt": "2026-09-11T08:06:23Z",
  "lastSweepAt": "2026-09-11T08:06:23Z",
  "nextSweepDueAt": "2026-09-11T08:21:23Z",
  "lanes": [
    {
      "laneKey": "10:0x01bf...071 then 10:0x0b2c...f85",
      "route": "USDT@10 to USDC@10",
      "status": "healthy",
      "par": 0.9994,
      "lastProbedAt": "2026-09-11T08:06:23Z",
      "lastOkAt": "2026-09-11T08:06:23Z"
    }
  ]
}
```

Shape only — the values above are illustrative, not a census. Read `summary` from your own call for the live counts. A lane that is not healthy also carries `kind`, and usually a human-readable `message`; broken lanes add `consecutiveFailures` and `firstFailedAt`.

| Field                       | Meaning                                                                                                                                            |
| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `laneKey`                   | Stable identifier. Source `chainId:address`, then a greater-than sign, then destination `chainId:address`, both addresses lowercased. Key on this. |
| `route`                     | Human-readable label. Display only.                                                                                                                |
| `status`                    | `healthy`, `degraded`, `broken` or `unknown`.                                                                                                      |
| `kind`                      | Why the lane is not healthy. Absent on healthy lanes. See the table below.                                                                         |
| `message`                   | The upstream reason, when there is one worth surfacing.                                                                                            |
| `par`                       | The ratio the prober last measured for this lane.                                                                                                  |
| `lastProbedAt` / `lastOkAt` | When the lane was last measured, and last measured working.                                                                                        |

### What to do with each `kind`

This table is the whole decision. `kind` tells you *why* a lane is not offerable, and each reason has a different correct response.

| `kind`                | What it means                                                                  | Do                   |
| --------------------- | ------------------------------------------------------------------------------ | -------------------- |
| *absent*              | Measured and working. The healthy majority carry no `kind` at all.             | Offer                |
| `firewalled`          | Disabled by ZeroDelta operations; it will not quote. Not a transient failure.  | Hide, do not retry   |
| `not-fillable`        | The lane prices cleanly but cannot actually be filled at the measured size.    | Hide                 |
| `lane-gap`            | No bridge wiring exists for this token pair.                                   | Hide                 |
| `bridge-throttled`    | The token's bridge path is rate-limited to zero upstream. Temporary.           | Hide, retry later    |
| `persistent-upstream` | Liquidity providers unavailable for this pair. Temporary.                      | Hide, retry later    |
| `vault-closed`        | An RWA vault is not accepting deposits. `message` carries the upstream reason. | Hide, explain        |
| `value-floor`         | No acceptable price at the probed size. Size-dependent, not structural.        | Offer with a warning |

### Three mistakes to design out

> **Page `/tokens`, or your catalogue is quietly incomplete.** This is the failure that hides best: the default page is 100 cells, the response is well-formed, and the tokens you never received simply never appear in your picker. Read `meta.totalCount` and page until you match it.

> **Filter on `kind`, never on `status`.** The two are not interchangeable, and the overlap is exactly where integrations break. A firewalled lane's status is `unknown`, **not** `broken` — the prober skips it rather than measuring it — so "hide broken, show the rest" puts every disabled lane back into your picker, where it will be refused the moment a user selects it. In the other direction, `not-fillable` appears under both `degraded` and `broken`. Build your allow and deny sets from `kind`.

> **Key on `laneKey`, never on a symbol pair.** A symbol does not identify a cell: most symbols exist on several chains at once, so "USDC to AUSD" names many different lanes with different verdicts. `laneKey` pins both ends to a chain and an address. Symbol casing is a second reason to avoid them — it is not uniform across the catalogue (`USDe`, `USDtb`, and the `n`- and `de`-prefixed tokenized assets are not upper-case), so an exact comparison against a hardcoded upper-case symbol misses them.

### Copy this

Everything above, as the function most integrations end up writing anyway.

```jsx
// Lanes that should never reach a user. 'value-floor' is deliberately absent:
// it is a size problem, not a broken lane.
const HIDE = new Set([
  'firewalled', 'not-fillable', 'lane-gap',
  'bridge-throttled', 'persistent-upstream', 'vault-closed',
]);

const cell = (t) => t.chainId + ':' + t.address.toLowerCase();
const laneKey = (from, to) => cell(from) + '>' + cell(to);
const sameCell = (from, to) => cell(from) === cell(to);

// On failure the envelope carries 'error', not 'data'. Check the status first, or a bad
// key surfaces as "data is not iterable" three frames away from the cause. These endpoints
// are rate limited per key, so honour 429 and its Retry-After header.
async function get(base, path, headers) {
  for (let attempt = 0; ; attempt++) {
    const res = await fetch(base + path, { headers });
    if (res.status === 429 && attempt < 5) {
      const retryAfter = Number(res.headers.get('Retry-After')) || 1;
      await new Promise((r) => setTimeout(r, retryAfter * 1000 * (attempt + 1)));
      continue;
    }
    const body = await res.json();
    if (!res.ok) {
      const err = (body && body.error) || {};
      throw new Error(path + ' failed: HTTP ' + res.status + ' ' + err.code + ' ' + err.message);
    }
    return body;
  }
}

// List endpoints page at 100 by default, and a truncated page is indistinguishable from a
// complete one. Page until the rows you hold match meta.totalCount.
async function pageAll(base, path, headers) {
  const out = [];
  for (let offset = 0; ; offset += 500) {
    const body = await get(base, path + '?limit=500&offset=' + offset, headers);
    out.push(...body.data);
    if (body.data.length === 0) return out;
    const total = body.meta && body.meta.totalCount;
    if (total != null ? out.length >= total : body.data.length < 500) return out;
  }
}

export async function loadCatalogue(base, apiKey) {
  const headers = { 'X-API-Key': apiKey, Accept: 'application/json' };
  const [tokens, health] = await Promise.all([
    pageAll(base, '/api/v1/tokens', headers),
    get(base, '/api/v1/route-health?scope=all', headers),
  ]);

  const hidden = new Map();
  for (const lane of health.data.lanes) {
    if (HIDE.has(lane.kind)) hidden.set(lane.laneKey, lane.message || lane.kind);
  }

  return {
    // Every (token, chain) cell you may offer.
    cells: tokens.filter((t) => t.isEnabled),
    // Refresh when the next sweep is due, not on a timer of your own.
    refreshAt: health.data.nextSweepDueAt,
    canTrade: (from, to) => !sameCell(from, to) && !hidden.has(laneKey(from, to)),
    whyNot: (from, to) => hidden.get(laneKey(from, to)),
  };
}
```

`canTrade` and `whyNot` take two cells from `cells`, not symbols. `base` is the environment base URL you were given at onboarding — a `dev` key is not valid against `prod`.

> **One pair is never tradeable and needs no measurement.** The same token on the same chain is rejected with `400 BAD_REQUEST` — there is nothing to bridge or swap. That is what `sameCell` guards. The same token across *different* chains is fine: it is quoted as a direct bridge (`isDirect: true`), with no liquidity-provider swap and no solver fill, and it is not cancellable once submitted.

### Health narrows the menu; the quote decides the trade

Use `/route-health` to decide what appears in your interface, and `/quote` to decide whether a specific trade proceeds. They are not redundant, and the quote is not a substitute for the health filter.

> **A served quote is not proof a lane can be filled.** `/quote` refuses the cases it can see at pricing time: a closed vault, a throttled bridge, a disabled lane, a lane measured broken while enforcement is on. It cannot refuse what it cannot see — a `degraded` lane marked `not-fillable` prices cleanly and still fails to fill. If you skip the health filter and rely on the quote alone, you will publish lanes that quote and never settle.

The reverse also holds. Health is measured at a probe size, seconds to minutes ago; the quote is priced for *your* size, now. A lane that passes the filter can still return `422` — `BELOW_MIN_TRADE_SIZE`, `COMPLIANCE_BLOCKED`, or no acceptable price at your amount. Surface the error rather than treating it as a catalogue bug.

### Refreshing

* **The health data moves continuously.** The prober re-measures lanes all the time, so `summary`, `measuredAt` and individual verdicts change between two calls seconds apart. Treat every read as a snapshot, not as a stable list.
* `nextSweepDueAt` is a sensible poll interval — roughly how long until the next full pass lands. Refreshing much faster than that mostly re-reads the same verdicts.
* `/chains` and `/tokens` change on onboarding, not on a schedule. Cache them for hours.
* `mode` tells you whether measurement is being enforced. Under `enforce`, lanes measured broken are already withheld from quoting, so your filter and the gateway agree.

Once you know what you can offer, continue with [Request a quote](/zero-delta/integration-guide). Field-level reference for every endpoint above is in the [API reference](/zero-delta/integration-guide/api).


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

Both `owner` and `destinationReceiver` are optional inputs used for compliance screening. The returned `orderRequests[]` still come back with `owner` and `destinationReceiver` as `null` regardless — fill both client-side before submitting (see the Integration Guide).

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

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

<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>401</code></td><td><code>UNAUTHORIZED</code></td><td>Missing or invalid API key.</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.</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).</td></tr></tbody></table>

***

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


# Models

Schema reference for the ZeroDelta read API: envelope, error codes, status vocabulary, compliance decision, quote and order shapes, and the tracked transaction with its chained-journey fields.

Schema reference for the ZeroDelta read API. For the endpoints that return these shapes, see the API reference.

### Envelope

Every `/api/v1/*` response uses this wrapper. `data` carries the result (an object or array); `error` is present only on failure; `meta` and `paging` appear on list responses.

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>data</code></td><td>object or array</td><td>The resource payload.</td></tr><tr><td><code>error.code</code></td><td>ErrorCode</td><td>Machine-readable error code.</td></tr><tr><td><code>error.message</code></td><td>string</td><td>Human-readable message.</td></tr><tr><td><code>error.fieldErrors[]</code></td><td>array</td><td>Per-field errors, each <code>{ field, message }</code>.</td></tr><tr><td><code>error.reasons[]</code></td><td>array</td><td>Machine-readable decision reason codes, populated on <code>COMPLIANCE_BLOCKED</code> per the server's disclosure policy (<code>POLICY_GUARD_REASON_DISCLOSURE</code>).</td></tr><tr><td><code>meta.page</code></td><td>integer</td><td>Current page.</td></tr><tr><td><code>meta.perPage</code></td><td>integer</td><td>Items per page.</td></tr><tr><td><code>meta.totalCount</code></td><td>integer</td><td>Total matching items.</td></tr><tr><td><code>paging.self</code></td><td>string</td><td>URL of the current page.</td></tr><tr><td><code>paging.next</code></td><td>string or null</td><td>URL of the next page, or null.</td></tr><tr><td><code>paging.prev</code></td><td>string or null</td><td>URL of the previous page, or null.</td></tr></tbody></table>

On error, only the `error` object is populated:

```json
{ "error": { "code": "UNAUTHORIZED", "message": "missing or invalid API key" } }
```

Branch on `error.code` (machine-readable), not on `error.message`.

### ErrorCode

<table data-search="false"><thead><tr><th>Value</th><th>Typical status</th></tr></thead><tbody><tr><td><code>INTERNAL_ERROR</code></td><td>500</td></tr><tr><td><code>BAD_REQUEST</code></td><td>400</td></tr><tr><td><code>NOT_FOUND</code></td><td>404</td></tr><tr><td><code>CONFLICT</code></td><td>409</td></tr><tr><td><code>VALIDATION_ERROR</code></td><td>422</td></tr><tr><td><code>UNAUTHORIZED</code></td><td>401</td></tr><tr><td><code>FORBIDDEN</code></td><td>403</td></tr><tr><td><code>RATE_LIMITED</code></td><td>429</td></tr><tr><td><code>SERVICE_UNAVAILABLE</code></td><td>503</td></tr><tr><td><code>QUOTE_UNAVAILABLE</code></td><td>422</td></tr><tr><td><code>UPSTREAM_ERROR</code></td><td>502</td></tr><tr><td><code>BELOW_MIN_TRADE_SIZE</code></td><td>422</td></tr><tr><td><code>COMPLIANCE_BLOCKED</code></td><td>422</td></tr></tbody></table>

### TrackingStatus

Lifecycle status of a tracked cross-chain transaction. Returned as a lowercase string (not the on-chain `OrderStatus` enum — see the Smart Contract Reference). When polling, treat any value outside the non-terminal set as terminal so a newly added status cannot trap your loop.

<table data-search="false"><thead><tr><th>Status</th><th>Terminal?</th><th>Meaning</th></tr></thead><tbody><tr><td><code>pending</code></td><td>no</td><td>Submitted; awaiting arrival on the execution chain.</td></tr><tr><td><code>in_progress</code></td><td>no</td><td>Arrived on the execution chain; awaiting fill.</td></tr><tr><td><code>outbound_bridging</code></td><td>no</td><td>Filled; bridging out to the destination chain.</td></tr><tr><td><code>cancel_pending</code></td><td>no</td><td>User pre-cancelled; awaiting arrival to claim.</td></tr><tr><td><code>awaiting_claim</code></td><td>no</td><td>Arrived after a pre-cancel; user can claim funds.</td></tr><tr><td><code>continuing</code></td><td>no</td><td>A chained journey's leg 1 landed and the connector is parked on the destination escrow, awaiting leg 2. Appears on <code>journeyStatus</code>.</td></tr><tr><td><code>delivered</code></td><td>yes</td><td>Success — receiver got the asset.</td></tr><tr><td><code>expired</code></td><td>yes</td><td>Deadline elapsed in transit (claim required).</td></tr><tr><td><code>cancelled</code></td><td>yes</td><td>Order cancelled; funds returned.</td></tr><tr><td><code>fill_failed</code></td><td>yes</td><td>Solver gave up before filling.</td></tr><tr><td><code>delivery_failed</code></td><td>yes</td><td>Outbound bridge permanently failed.</td></tr><tr><td><code>failed</code></td><td>yes</td><td>Generic unrecoverable failure.</td></tr></tbody></table>

Happy path: `pending → in_progress → outbound_bridging → delivered`.

> **`status` is per-order; `journeyStatus` is per-journey.** On a chained (two-leg) order — which is what a tokenized-asset purchase is — the parent's `status` reads `delivered` as soon as leg 1 lands, because that leg genuinely finished. The value is parked mid-journey, not in the receiver's wallet, and `journeyStatus` reads `continuing` for that whole window. Put completion checks on `journeyStatus`, falling back to `status` when it is absent (it is absent on non-chained orders).

### Compliance

The complete compliance-engine decision surfaced on a quote — the result of screening the owner (and receiver, when supplied) before deposit instructions are returned. Benign decision metadata is always present. The risk and sanctions signals (`riskCategories`, `flaggedCategories`, `blockingCategories`, `reasons`) are disclosure-gated by the server's `POLICY_GUARD_REASON_DISCLOSURE` setting (`all` | `policy` | `none`) to avoid tipping off, so they may be omitted even when the upstream engine populated them.

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>status</code></td><td>string</td><td><code>SCREENED</code> when the wallet pair was checked and approved; <code>UNSCREENED</code> when no <code>owner</code> was supplied. Always present.</td></tr><tr><td><code>decision</code></td><td>string</td><td>Engine verdict: <code>ALLOW</code>, <code>DENY</code>, <code>REVIEW</code> or <code>REMEDIATE</code>. A successful quote is only ever returned for <code>ALLOW</code>. There is no <code>BLOCK</code> value — a client branching on one would treat every refusal as an allow.</td></tr><tr><td><code>screenedWallets[]</code></td><td>string[]</td><td>The addresses screened — <code>owner</code>, plus the receiver when supplied.</td></tr><tr><td><code>sourceJurisdiction</code></td><td>string</td><td>Jurisdiction resolved for the owner (e.g. <code>EU</code>, <code>US_EX_NY</code>).</td></tr><tr><td><code>appliedPolicyIds[]</code> / <code>appliedPolicyNames[]</code></td><td>string[]</td><td>The policies the decision was evaluated against.</td></tr><tr><td><code>appliedLegislation[]</code></td><td>string[]</td><td>In-scope legislation overlays applied (e.g. <code>["MICA_EU"]</code>).</td></tr><tr><td><code>riskCategories[]</code></td><td>string[]</td><td>Risk categories detected. Disclosure-gated.</td></tr><tr><td><code>flaggedCategories[]</code></td><td>string[]</td><td>Non-blocking flagged categories. Disclosure-gated.</td></tr><tr><td><code>blockingCategories[]</code></td><td>string[]</td><td>Categories that would block. Disclosure-gated.</td></tr><tr><td><code>reasons[]</code></td><td>string[]</td><td>Machine-readable decision reason codes. Disclosure-gated.</td></tr><tr><td><code>policyVersion</code></td><td>string</td><td>Content-addressed version of the composed policy used.</td></tr><tr><td><code>checkId</code></td><td>string</td><td>Unique id for this compliance check (audit-trail anchor).</td></tr><tr><td><code>expiresAt</code></td><td>string</td><td>When this decision stops being valid (RFC 3339).</td></tr></tbody></table>

### QuoteRequest

| Field                 | Type   | Required | Description                                                                                  |
| --------------------- | ------ | -------- | -------------------------------------------------------------------------------------------- |
| `fromChainId`         | string | yes      | Source chain ID (string or number accepted).                                                 |
| `toChainId`           | string | yes      | Destination chain ID.                                                                        |
| `fromToken`           | string | yes      | Source token symbol or contract address.                                                     |
| `toToken`             | string | yes      | Destination token symbol or address.                                                         |
| `fromAmount`          | string | yes      | Amount to sell, in smallest unit.                                                            |
| `owner`               | string | no       | Order owner address; when set, the response includes `escrowAddress` and `executionChainId`. |
| `destinationReceiver` | string | no       | Recipient wallet; screened alongside `owner` when present.                                   |

### Quote

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>fromChainId</code></td><td>string</td><td>Source chain ID.</td></tr><tr><td><code>toChainId</code></td><td>string</td><td>Destination chain ID.</td></tr><tr><td><code>fromToken</code></td><td>string</td><td>Source token.</td></tr><tr><td><code>toToken</code></td><td>string</td><td>Destination token.</td></tr><tr><td><code>fromAmount</code></td><td>string</td><td>Amount sold, smallest unit.</td></tr><tr><td><code>toAmount</code></td><td>string</td><td>On-chain <code>order.askTokenAmount</code> — the swap output threshold the solver must meet to fill the order; used to construct <code>orderRequests[].askTokenAmount</code>. NOT what the receiver gets when the outbound bridge charges a protocol fee — see <code>finalAmount</code>.</td></tr><tr><td><code>finalAmount</code></td><td>string</td><td>What the receiver gets: <code>toAmount</code> minus the outbound CCTP fast-track maxFee. <code>&#x3C;= toAmount</code> (equal for LayerZero outbound / CCTP slow-track). Always populated.</td></tr><tr><td><code>bridge</code></td><td>string</td><td>Bridge provider for the route (e.g. <code>Circle</code>).</td></tr><tr><td><code>feeBps</code></td><td>integer</td><td>Route protocol fee in basis points (CCTP fast-track fee for Circle lanes; <code>0</code> for LayerZero). Metadata only — NOT subtracted from <code>toAmount</code>; the actual fee is the gap between <code>toAmount</code> and <code>finalAmount</code>.</td></tr><tr><td><code>inboundProtocolFee</code></td><td>string</td><td>CCTP fast-track maxFee debited from the input before pricing: the inbound bridge delivers <code>fromAmount − inboundProtocolFee</code>. <code>"0"</code> for LayerZero bids, local bids, slow-track lanes.</td></tr><tr><td><code>estimatedDuration</code></td><td>integer</td><td>End-to-end transfer time in seconds: inbound bridge finality + execution-chain swap window + outbound bridge finality, plus a per-leg fill-latency buffer for tx inclusion.</td></tr><tr><td><code>lifespanSeconds</code></td><td>integer</td><td>Quote validity in seconds, from the winning provider's signed expiry (floored at 0). <code>0</code> when the provider does not sign an expiry or the quote has lapsed. The on-chain fill threshold itself does not expire.</td></tr><tr><td><code>swapTimeSeconds</code></td><td>integer</td><td>Estimated time for the on-chain swap to settle on the execution chain — one block of the execution chain (average block time, differs per chain). <code>0</code> when block-time data is unavailable.</td></tr><tr><td><code>route</code></td><td>Route</td><td>The route this quote priced.</td></tr><tr><td><code>gasCostSource</code></td><td>string</td><td>LayerZero inbound native fee in wei; <code>0</code> for CCTP. Pass as <code>msg.value</code>.</td></tr><tr><td><code>gasCostDest</code></td><td>string</td><td>LayerZero outbound native fee in wei; <code>0</code> for CCTP.</td></tr><tr><td><code>swapCost</code></td><td>string</td><td>Gas hint from the winning liquidity provider (decimal string; empty when not provided).</td></tr><tr><td><code>provider</code></td><td>string</td><td>Masked liquidity-provider label — always <code>"zerodelta"</code> on a successful swap quote; omitted on direct-bridge quotes.</td></tr><tr><td><code>orderRequests</code></td><td>OrderRequest[]</td><td>Pre-built argument array for <code>submitOrder</code>. One element normally, two when <code>isChained</code>.</td></tr><tr><td><code>isDirect</code></td><td>boolean</td><td>Direct-bridge quote (same token, different chains): no swap, no solver fill, not cancellable; <code>provider</code> and the top-level cancel coordinates are omitted.</td></tr><tr><td><code>isChained</code></td><td>boolean</td><td>Two-leg quote: <code>orderRequests</code> carries two elements and settlement spans two execution chains.</td></tr><tr><td><code>escrowAddress</code></td><td>string</td><td>Source-chain escrow address (present when <code>owner</code> is supplied).</td></tr><tr><td><code>executionChainId</code></td><td>string</td><td>Chain ID where settlement executes and where <code>cancel</code> runs (present when <code>owner</code> is supplied; omitted on direct quotes).</td></tr><tr><td><code>executionEscrowAddress</code></td><td>string</td><td>Escrow address on the execution chain.</td></tr><tr><td><code>complianceStatus</code></td><td>string</td><td><code>SCREENED</code> or <code>UNSCREENED</code>.</td></tr><tr><td><code>compliance</code></td><td>Compliance</td><td>Full screening decision; present only when the gate is configured and an <code>owner</code> was supplied.</td></tr></tbody></table>

### OrderRequest

Pre-built Solidity `OrderRequest` struct, one element of the array `submitOrder()` takes. `owner` and `destinationReceiver` come back `null` — the frontend must fill both on **every** element with real, non-zero addresses before calling the contract (see the Integration Guide).

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>owner</code></td><td>string or null</td><td><code>null</code> — fill with the user's wallet address.</td></tr><tr><td><code>bidToken</code></td><td>string</td><td>Resolved source token contract address.</td></tr><tr><td><code>bidTokenAmount</code></td><td>string</td><td>Amount in smallest unit. On <code>orderRequests[1]</code> of a chained quote this is an estimate the contract overwrites with actual receipts.</td></tr><tr><td><code>askToken</code></td><td>string</td><td>Resolved destination token contract address (passed on-chain as <code>bytes</code>; a 20-byte hex string for EVM).</td></tr><tr><td><code>askTokenAmount</code></td><td>string</td><td>On-chain fill threshold — the swap output the solver must meet to fill the order. Always equals <code>Quote.toAmount</code>. NOT what the destination receiver gets when the outbound bridge charges a protocol fee — see <code>Quote.finalAmount</code>.</td></tr><tr><td><code>destinationReceiver</code></td><td>string or null</td><td><code>null</code> — fill with the recipient address (20 bytes for EVM; the contract rejects any width other than 20 or 32).</td></tr><tr><td><code>destinationChainId</code></td><td>string</td><td>Target chain ID.</td></tr><tr><td><code>executionChainId</code></td><td>string</td><td>Chain this order clears on. Always populated; the contract rejects <code>0</code> and any chain outside its allowlist.</td></tr><tr><td><code>deadline</code></td><td>string</td><td>Unix seconds; <code>"0"</code> means no deadline.</td></tr></tbody></table>

### Chain

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>chainId</code></td><td>string</td><td>Chain ID (serialized as a string).</td></tr><tr><td><code>name</code></td><td>string</td><td>Internal name (e.g. <code>arbitrum</code>).</td></tr><tr><td><code>displayName</code></td><td>string</td><td>Human label (e.g. <code>Arbitrum One</code>).</td></tr><tr><td><code>explorerUrl</code></td><td>string</td><td>Block explorer base URL.</td></tr><tr><td><code>nativeCurrency</code></td><td>string</td><td>Native currency symbol.</td></tr><tr><td><code>isTestnet</code></td><td>boolean</td><td>Testnet flag.</td></tr><tr><td><code>isEnabled</code></td><td>boolean</td><td>Whether the chain is enabled.</td></tr><tr><td><code>isExecutionChain</code></td><td>boolean</td><td>True when this is a configured execution chain.</td></tr><tr><td><code>isChainedDestination</code></td><td>boolean</td><td>True when a chained (two-leg) order may terminate on this chain — leg 2 swaps the connector into the ask here, so an ask token the escrow owns no bridge adapter for is still deliverable. Absent when chained quoting is off.</td></tr><tr><td><code>contracts</code></td><td>object</td><td>Contract addresses for this chain (string → address).</td></tr><tr><td><code>escrowContract</code></td><td>string</td><td>Escrow contract address.</td></tr><tr><td><code>rolesContract</code></td><td>string</td><td>Roles contract address.</td></tr><tr><td><code>providerIds</code></td><td>integer[]</td><td>Provider IDs available on this chain.</td></tr><tr><td><code>slug</code></td><td>string</td><td>Chain slug.</td></tr><tr><td><code>createdAt</code> / <code>updatedAt</code></td><td>string</td><td>RFC 3339 timestamps.</td></tr></tbody></table>

### Token

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>id</code></td><td>string</td><td>Token ID.</td></tr><tr><td><code>symbol</code></td><td>string</td><td>Symbol (e.g. <code>USDC</code>).</td></tr><tr><td><code>name</code></td><td>string</td><td>Token name.</td></tr><tr><td><code>chainId</code></td><td>string</td><td>Chain ID.</td></tr><tr><td><code>chainName</code></td><td>string</td><td>Chain name.</td></tr><tr><td><code>address</code></td><td>string</td><td>Contract address on this chain.</td></tr><tr><td><code>decimals</code></td><td>integer</td><td>Token decimals.</td></tr><tr><td><code>logoUrl</code></td><td>string</td><td>Logo URL.</td></tr><tr><td><code>isEnabled</code></td><td>boolean</td><td>Whether the token is enabled.</td></tr><tr><td><code>isChainedDestination</code></td><td>boolean</td><td>True only when this exact token deployment is allowlisted for a chained destination-local swap.</td></tr><tr><td><code>isSellSource</code></td><td>boolean</td><td>True only when this exact token deployment is allowlisted as a quote's bid (sell) token.</td></tr><tr><td><code>implAddress</code></td><td>string</td><td>Per-chain implementation address (e.g. OFT impl).</td></tr><tr><td><code>bridge</code></td><td>string</td><td>Bridge provider (e.g. <code>Circle</code>, <code>LayerZero</code>).</td></tr><tr><td><code>standard</code></td><td>string</td><td>Token standard (e.g. <code>CCTPV2</code>, <code>LayerZeroV2OFT</code>).</td></tr><tr><td><code>facet</code></td><td>string</td><td>Diamond facet name.</td></tr><tr><td><code>homeChainId</code></td><td>string</td><td>Home chain ID of the canonical token.</td></tr><tr><td><code>homeTokenAddress</code></td><td>string</td><td>Canonical token address on the home chain.</td></tr><tr><td><code>gmpParameters</code></td><td>string</td><td>GMP-encoded parameters for this token (hex).</td></tr><tr><td><code>routes</code></td><td>Route[]</td><td>Enabled outbound routes from this token.</td></tr></tbody></table>

### Route

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>id</code></td><td>string</td><td>Route ID.</td></tr><tr><td><code>tokenId</code></td><td>string</td><td>Token ID.</td></tr><tr><td><code>tokenName</code></td><td>string</td><td>Token name.</td></tr><tr><td><code>fromChainId</code> / <code>fromChainName</code></td><td>string</td><td>Source chain.</td></tr><tr><td><code>toChainId</code> / <code>toChainName</code></td><td>string</td><td>Destination chain.</td></tr><tr><td><code>fromAddress</code> / <code>toAddress</code></td><td>string</td><td>Source / destination token addresses.</td></tr><tr><td><code>homeChainId</code></td><td>string</td><td>Home chain ID of the canonical token.</td></tr><tr><td><code>homeTokenAddress</code></td><td>string</td><td>Canonical token address on the home chain.</td></tr><tr><td><code>decimals</code></td><td>integer</td><td>Token decimals.</td></tr><tr><td><code>bridge</code></td><td>string</td><td>Bridge provider.</td></tr><tr><td><code>adapter</code></td><td>string</td><td>Bridge adapter address.</td></tr><tr><td><code>standard</code></td><td>string</td><td>Token standard (e.g. <code>CCTPV2</code>).</td></tr><tr><td><code>facet</code></td><td>string</td><td>Diamond facet name.</td></tr><tr><td><code>estimatedDuration</code></td><td>integer</td><td>Estimated settlement time, seconds.</td></tr><tr><td><code>minAmount</code> / <code>maxAmount</code></td><td>string</td><td>Route amount bounds. <strong>Advisory only</strong> — <code>maxAmount</code> is serialized but enforced nowhere, so do not rely on it as a limit; the quote engine declines an order it cannot price.</td></tr><tr><td><code>feeBps</code></td><td>integer</td><td>Route protocol fee in basis points.</td></tr><tr><td><code>isQuotable</code></td><td>boolean</td><td>Present only when the route-health prober measured this exact lane broken and enforcement is on (<code>false</code>). <strong>Absent means not measured and must be treated as quotable</strong> — only an explicit <code>false</code> demotes a lane.</td></tr><tr><td><code>isEnabled</code></td><td>boolean</td><td>Whether the route is enabled.</td></tr></tbody></table>

### Solver

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>id</code></td><td>string (uuid)</td><td>Solver ID.</td></tr><tr><td><code>chainId</code></td><td>integer</td><td>Chain the solver operates on.</td></tr><tr><td><code>chainName</code></td><td>string</td><td>Resolved chain name.</td></tr><tr><td><code>address</code></td><td>string</td><td>0x-prefixed EVM address.</td></tr><tr><td><code>name</code></td><td>string</td><td>Solver name.</td></tr><tr><td><code>operator</code></td><td>string</td><td>Operator name.</td></tr><tr><td><code>operatorType</code></td><td>string</td><td><code>internal</code> or <code>third-party</code>.</td></tr><tr><td><code>description</code></td><td>string</td><td>Optional description.</td></tr><tr><td><code>website</code></td><td>string</td><td>Optional website.</td></tr><tr><td><code>contact</code></td><td>string</td><td>Optional contact.</td></tr><tr><td><code>isActive</code></td><td>boolean</td><td>Whether the solver is active.</td></tr><tr><td><code>createdBy</code></td><td>string</td><td>Creator.</td></tr><tr><td><code>createdAt</code> / <code>updatedAt</code></td><td>string</td><td>RFC 3339 timestamps.</td></tr></tbody></table>

### ChainSolvers

Per-chain group in the `/solvers` response.

| Field       | Type      | Description            |
| ----------- | --------- | ---------------------- |
| `chainId`   | integer   | Chain ID.              |
| `chainName` | string    | Chain name.            |
| `solvers`   | Solver\[] | Solvers on this chain. |

### ProtocolState

| Field              | Type    | Description                                   |
| ------------------ | ------- | --------------------------------------------- |
| `chainId`          | string  | Chain ID.                                     |
| `executionChainId` | string  | Configured execution chain.                   |
| `isPaused`         | boolean | Whether the protocol is paused on this chain. |
| `updatedAt`        | string  | RFC 3339 timestamp.                           |


# Transaction Models

The tracked transaction and its steps, including the chained-journey fields that describe a two-leg order as one user intent.

The shapes returned by the `/transactions` endpoints. For the other schemas, see Models.

### TrackedTransaction

Key fields (a single-transaction GET additionally returns the `steps` array):

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>id</code></td><td>string</td><td>Deployment-scoped uid (for example <code>"rwa:0x52b1…"</code>), unique across redeployments. <strong>This is not <code>orderId</code></strong> — see below.</td></tr><tr><td><code>orderId</code></td><td>string</td><td>The raw on-chain order id. This is what the escrow's <code>cancel</code> / <code>claimCancellation</code> / <code>getOrder</code> calls take; passing <code>id</code> there reverts. Returned by the API but <strong>not yet declared in the published OpenAPI spec</strong>, so a generated client may not surface it.</td></tr><tr><td><code>txHash</code></td><td>string</td><td>Source transaction hash (the lookup key).</td></tr><tr><td><code>protocol</code></td><td>string[]</td><td>GMP protocol(s) involved.</td></tr><tr><td><code>sourceChainId</code> / <code>destChainId</code></td><td>string</td><td>Source / destination chain IDs.</td></tr><tr><td><code>wallet</code></td><td>string</td><td>Order owner wallet.</td></tr><tr><td><code>status</code></td><td>TrackingStatus</td><td>Lifecycle status <strong>of this order</strong>. For a chained order see <code>journeyStatus</code>.</td></tr><tr><td><code>claimable</code></td><td>boolean</td><td>True iff <code>claimCancellation()</code> on the execution-chain escrow would succeed right now (status is <code>awaiting_claim</code> or <code>expired</code>).</td></tr><tr><td><code>fromTokenAddress</code> / <code>fromTokenSymbol</code> / <code>fromTokenDecimals</code></td><td>—</td><td>Source token metadata.</td></tr><tr><td><code>fromTokenAmount</code> / <code>fromTokenRawAmount</code></td><td>string</td><td>Human-readable / raw source amount.</td></tr><tr><td><code>toTokenAddress</code> / <code>toTokenSymbol</code> / <code>toTokenDecimals</code></td><td>—</td><td>Destination token metadata. On a chained leg 1 this is the internal <strong>connector</strong>, not what the user bought — see the <code>finalToToken*</code> fields.</td></tr><tr><td><code>toTokenAmount</code> / <code>toTokenRawAmount</code></td><td>string</td><td>Human-readable / raw destination amount.</td></tr><tr><td><code>completedSteps</code> / <code>totalSteps</code></td><td>integer</td><td>Step progress.</td></tr><tr><td><code>currentStep</code></td><td>string</td><td>Current step name.</td></tr><tr><td><code>submittedAt</code> / <code>arrivedAt</code> / <code>filledAt</code> / <code>deliveredAt</code> / <code>failedAt</code></td><td>string or null</td><td>Lifecycle timestamps.</td></tr><tr><td><code>failedReason</code> / <code>failedStep</code></td><td>string</td><td>Failure detail.</td></tr><tr><td><code>steps</code></td><td>TransactionStep[]</td><td>Step entries (single-transaction GET only).</td></tr><tr><td><code>sourceExplorerLink</code> / <code>destinationExplorerLink</code></td><td>string</td><td>Explorer links.</td></tr><tr><td><code>destinationTx</code></td><td>string</td><td>Destination transaction hash.</td></tr><tr><td><code>gmpStatus</code> / <code>gmpMessageId</code></td><td>string</td><td>GMP status / message ID.</td></tr><tr><td><code>orderNonce</code></td><td>string</td><td>Per-source-chain order nonce (decimal string); part of the identity triple that derives <code>orderId</code>.</td></tr><tr><td><code>destinationReceiver</code></td><td>string</td><td>Receiver of the ask token on the destination chain.</td></tr><tr><td><code>orderDeadline</code></td><td>string</td><td>Order deadline (unix seconds, decimal string); <code>"0"</code> means none.</td></tr><tr><td><code>executionChainId</code></td><td>string</td><td>Chain where the escrow lives (where <code>cancel</code> / <code>claimCancellation</code> are called).</td></tr><tr><td><code>executionEscrowAddress</code></td><td>string</td><td>Escrow proxy address on the execution chain.</td></tr><tr><td><code>createdAt</code> / <code>updatedAt</code></td><td>string</td><td>RFC 3339 timestamps.</td></tr></tbody></table>

#### Chained-journey fields

A chained (two-leg) order is the shape used when the asset being bought exists only on the chained destination chain — an RWA vault share on Plume, for example. It is **one user intent settled as two orders**, and these fields describe the journey rather than the individual leg.

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>isDirect</code></td><td>boolean</td><td>True for a direct-bridged transfer (<code>OrderDirectBridged</code>): a same-token order bridged straight to the destination at submit time — no solver fill, no execution-chain round-trip, and not cancellable.</td></tr><tr><td><code>isChained</code></td><td>boolean</td><td>True for <strong>either</strong> leg of a chained order. On leg 1 the <code>toToken*</code> fields describe the internal connector, not the asset the user bought.</td></tr><tr><td><code>chainedParentOrderId</code></td><td>string</td><td>Order ID of leg 1, present only on the child. Rows carrying it are excluded from <code>GET /transactions</code> unless <code>includeChainedLegs=true</code>.</td></tr><tr><td><code>chainedChildOrderId</code></td><td>string</td><td>Order ID of leg 2, once <code>continueChain</code> has created it. Absent during the park window between leg 1's fill and leg 2's creation.</td></tr><tr><td><code>journeyStatus</code></td><td>TrackingStatus</td><td>Status of the <strong>user's intent</strong>, which for a chained order is not the status of any one leg: leg 1 reaching <code>delivered</code> means the connector was parked on the destination escrow, not that the user received anything. Reads <code>continuing</code> for that window. <strong>Branch completion checks on this field</strong>, falling back to <code>status</code> when it is absent.</td></tr><tr><td><code>journeyStartedAt</code></td><td>string</td><td>When leg 1 was submitted — the start of the whole journey.</td></tr><tr><td><code>journeyCompletedAt</code></td><td>string or null</td><td>When the <strong>final</strong> leg delivered. Null until the journey actually completes. It means the user got their asset at this time, not that movement stopped.</td></tr><tr><td><code>legs[]</code></td><td>TransactionLeg[]</td><td>The whole journey, index 0 being this order. Present <strong>only</strong> on a chained parent and omitted entirely for a single-leg order, so the single-leg response shape is unchanged.</td></tr><tr><td><code>finalToTokenAddress</code></td><td>string</td><td>The destination the user actually asked for, as opposed to the internal connector in <code>toToken*</code>. Correct from the moment of submit, including before leg 2 exists — leg 2's ask is ABI-encoded into <code>Order.nextOrders</code> on-chain at submit.</td></tr><tr><td><code>finalToTokenSymbol</code> / <code>finalToTokenDecimals</code></td><td>—</td><td>Metadata for the asset the user actually bought.</td></tr><tr><td><code>finalToTokenAmount</code> / <code>finalToTokenRawAmount</code></td><td>string</td><td>Human-readable / on-chain raw amount of that asset.</td></tr><tr><td><code>finalDestChainId</code></td><td>string</td><td>The chain the user's asset actually lands on. On a chained order this is not <code>destChainId</code> of leg 1.</td></tr><tr><td><code>deliveryEvidence</code></td><td>string</td><td>Qualifies a <code>delivered</code> status; absent for any order not claiming delivery. <code>observed</code> means the delivery was seen — a solver-confirmed outbound row, a same-chain direct transfer, or a fill that was itself the delivery. <code>presumed</code> means it was inferred.</td></tr><tr><td><code>sourceEventMissing</code></td><td>boolean</td><td>The source-chain submit event was never indexed, so the row was reconstructed. Returned by the API but <strong>not yet declared in the published OpenAPI spec</strong>.</td></tr></tbody></table>

> **A chained order spans three chains, and the execution chain is neither end.** Leg 1 is a same-symbol cross-chain move of the bid asset (a USDC to USDC hop is correct, not a bug) onto the execution chain; leg 2 swaps that connector into the ask and delivers it. A destination that looks wrong in a UI is usually the invisible middle hop — read `finalDestChainId` and `finalToToken*`, not `destChainId` and `toToken*`.

### TransactionLeg

One leg of a chained journey, as returned in `TrackedTransaction.legs[]`.

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>index</code></td><td>integer</td><td>Position in the journey, 0-based.</td></tr><tr><td><code>role</code></td><td>string</td><td>What the leg is <strong>for</strong>, so a consumer can label it without re-deriving intent from the token pair. <code>source-swap</code> turns the user's bid into the internal connector and parks it on the destination chain — its ask side is not what the user bought, and its final step is a bridge-and-park rather than a delivery. <code>delivery</code> turns the connector into the asset the user asked for and delivers it.</td></tr><tr><td><code>orderId</code></td><td>string</td><td>On-chain order id for this leg.</td></tr><tr><td><code>txHash</code></td><td>string</td><td>Submit transaction hash for this leg.</td></tr><tr><td><code>sourceChainId</code> / <code>destChainId</code></td><td>string</td><td>Chain IDs for this leg, serialized as strings.</td></tr><tr><td><code>fromTokenAddress</code> / <code>fromTokenSymbol</code> / <code>fromTokenAmount</code></td><td>—</td><td>What this leg sold.</td></tr><tr><td><code>toTokenAddress</code> / <code>toTokenSymbol</code> / <code>toTokenAmount</code></td><td>—</td><td>What this leg bought.</td></tr><tr><td><code>status</code></td><td>TrackingStatus</td><td>Status of this leg alone.</td></tr><tr><td><code>completedSteps</code> / <code>totalSteps</code></td><td>integer</td><td>Step progress for this leg.</td></tr><tr><td><code>submittedAt</code> / <code>arrivedAt</code> / <code>filledAt</code> / <code>deliveredAt</code></td><td>string or null</td><td>Lifecycle timestamps for this leg.</td></tr><tr><td><code>steps</code></td><td>TransactionStep[]</td><td>Step entries for this leg.</td></tr></tbody></table>

### TransactionStep

<table data-search="false"><thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead><tbody><tr><td><code>stepNumber</code></td><td>integer</td><td>Step index.</td></tr><tr><td><code>stepName</code></td><td>string</td><td>One of <code>submitted</code>, <code>arrived</code>, <code>filled</code>, <code>delivered</code>.</td></tr><tr><td><code>stepStatus</code></td><td>string</td><td>One of <code>completed</code>, <code>pending</code>, <code>failed</code>, <code>recovered</code>.</td></tr><tr><td><code>eventType</code></td><td>string</td><td>Underlying event type.</td></tr><tr><td><code>chainName</code> / <code>chainId</code></td><td>string</td><td>Chain this step occurred on.</td></tr><tr><td><code>txHash</code></td><td>string</td><td>Step transaction hash.</td></tr><tr><td><code>explorerLink</code> / <code>protocolExplorerLink</code></td><td>string</td><td>Explorer links.</td></tr><tr><td><code>blockNumber</code></td><td>integer</td><td>Block number.</td></tr><tr><td><code>blockTimestamp</code></td><td>string or null</td><td>Block timestamp.</td></tr><tr><td><code>gasUsed</code></td><td>integer</td><td>Gas units consumed (omitted when unknown).</td></tr><tr><td><code>gasPriceWei</code></td><td>string</td><td>Effective gas price in wei (omitted when unknown).</td></tr></tbody></table>

### TransactionStats

| Field                           | Type           | Description                            |
| ------------------------------- | -------------- | -------------------------------------- |
| `total`                         | integer        | Total matching transactions.           |
| `byStatus`                      | object         | Count keyed by TrackingStatus.         |
| `byProtocol`                    | object         | Count keyed by protocol.               |
| `avgDeliveryMs`                 | number or null | Average delivery time in milliseconds. |
| `bySourceChain` / `byDestChain` | object         | Counts keyed by chain ID.              |


# Smart Contracts

Contract interface, data types, events, error codes, deployments, and coverage. For the REST API, see the API Reference. Compiler: Solidity 0.8.19 (optimizer enabled, 200 runs). License: BUSL-1.1

The contracts and error codes use the **ZDLite** identifier (the on-chain name for the product branded ZeroDelta). Contract sources carry an SPDX `BUSL-1.1` header (source-available); test sources are MIT. Integration and use are permitted.

> **Per-(chain, token) addresses:** the canonical source for token contract addresses on each chain is `GET /api/v1/tokens` (see Coverage and the Integration Guide). Addresses change as chains/tokens are onboarded; do not hardcode them.

### Data types

```solidity
struct OrderRequest {
    address owner;                // Order owner (the user); need not equal msg.sender
    address bidToken;             // Token being sold — always a local EVM address on the source chain
    uint256 bidTokenAmount;       // Amount of bidToken to sell
    bytes   askToken;             // Token being bought, on destinationChainId (20 packed bytes for EVM)
    uint256 askTokenAmount;       // Minimum acceptable output (fill threshold)
    bytes   destinationReceiver;  // Recipient on the destination chain (20 bytes EVM, 32 bytes Solana-style)
    uint64  destinationChainId;   // Target chain ID
    uint64  executionChainId;     // Chain this order clears on; must be in allowedExecutionChains
    uint64  deadline;             // Unix expiry (0 = no deadline)
}

struct Order {
    address owner;
    uint64  nonce;                // Auto-incremented, set by the contract (the first order from a chain is nonce 1)
    uint64  deadline;
    uint64  sourceChainId;        // Set by the contract
    uint64  destinationChainId;
    bytes   bidToken;
    uint256 bidTokenAmount;       // Actual amount escrowed (balance-based accounting)
    bytes   askToken;
    uint256 askTokenAmount;       // Minimum acceptable output (fill threshold)
    bytes   destinationReceiver;
    bytes   nextOrders;           // abi.encode(OrderRequest[]) of chained follow-ups; empty for normal orders
}

// Returned by getPendingChain(prevOrderId) for a chained order whose leg 1 has
// landed and whose funds are parked awaiting leg 2.
struct PendingChain {
    address owner;                // Who may claimChain() the parked funds
    address token;                // The parked asset (leg 1's output, leg 2's bid token)
    uint256 amount;               // Parked amount. ZERO means already continued or claimed.
    bytes   requests;             // abi.encode(OrderRequest[]) of the follow-up leg
}

enum OrderStatus {
    Nonexistent,
    Pending,
    Filled,
    Cancelled
}
```

**The ask side is `bytes`, not `address`.** `askToken` and `destinationReceiver` are chain-agnostic byte strings so a non-EVM destination can be expressed at all. For every EVM destination you pass the plain 20-byte address as `bytes` — a `0x`-prefixed 20-byte hex string in ethers/viem, exactly what the API returns in `orderRequests[]`. The contract rejects any other width than 20 or 32 bytes (`ZDLite__UnsupportedReceiver`), and requires exactly 20 when the destination chain is the order's own execution chain. `bidToken` stays an `address`: it is pulled with `IERC20` from `msg.sender`, so the bid side is always local and always EVM.

**Chain IDs are `uint64`** on both structs, and there is no `data` field: the old pass-through `bytes data` is now `Order.nextOrders`, which the contract owns (it carries the chained follow-up requests and is empty for a normal order).

**`PendingChain.amount == 0` is the resolved sentinel.** `getPendingChain` returns a zeroed struct for a chain that was never parked *and* for one already continued or claimed — the two are indistinguishable from the return value alone. Treat a zero `amount` as "nothing to act on" rather than as "claimable", and drive UI from the `ChainedOrderParked` / `ChainedOrderContinued` / `ChainedOrderClaimed` events when you need to tell those cases apart.

**`executionChainId` is per order.** Each request names the chain it clears on, and the contract checks it against the `allowedExecutionChains` registry — there is no single protocol-wide setting. The API fills this in for you (`orderRequests[].executionChainId`); a `0` or non-allowlisted value reverts (`ZDLite__InvalidExecutionChainId` / `ZDLite__ExecutionChainNotAllowed(uint256)`).

**On-chain enum vs. read-API status — two different vocabularies.** The on-chain `OrderStatus` enum above (`Nonexistent` / `Pending` / `Filled` / `Cancelled`) is what `orderStatuses(orderId)` returns. The **read API** (`GET /api/v1/transactions/{txHash}`) returns its own lowercase status string, **not** the enum names — if you poll the API, compare against the lowercase values, not `Filled` / `Cancelled`. The full read-API set (code-verified), with terminal classification:

<table data-search="false"><thead><tr><th>Status</th><th>Terminal?</th></tr></thead><tbody><tr><td><code>pending</code></td><td>no</td></tr><tr><td><code>in_progress</code></td><td>no</td></tr><tr><td><code>outbound_bridging</code></td><td>no</td></tr><tr><td><code>cancel_pending</code></td><td>no</td></tr><tr><td><code>awaiting_claim</code></td><td>no</td></tr><tr><td><code>continuing</code></td><td>no</td></tr><tr><td><code>delivered</code></td><td>yes (success)</td></tr><tr><td><code>expired</code></td><td>yes</td></tr><tr><td><code>cancelled</code></td><td>yes</td></tr><tr><td><code>fill_failed</code></td><td>yes</td></tr><tr><td><code>delivery_failed</code></td><td>yes</td></tr><tr><td><code>failed</code></td><td>yes</td></tr></tbody></table>

Happy path: `pending → in_progress → outbound_bridging → delivered`. This is the canonical set the Integration Guide poller mirrors; treat anything outside the non-terminal set (the first six) as terminal.

> **`continuing` belongs to `journeyStatus`, not `status`.** On a chained order the parent's per-order `status` reads `delivered` the moment leg 1 lands — that leg really is done — while the journey-scoped `journeyStatus` reads `continuing` because the value is parked awaiting leg 2, not in the receiver's wallet. Branch completion checks on `journeyStatus` (falling back to `status` when it is absent, as it is on non-chained orders). See the Integration Guide poller.

The order is identified by `orderId`, an immutable identity derived by the contract from `owner`, `nonce`, and `sourceChainId` (not a function of amounts). The contract computes it as `keccak256(abi.encode(owner, nonce, sourceChainId))` (code-verified against the live contract).

Do not precompute or reconstruct `orderId`. `nonce` and `sourceChainId` are assigned by the contract at submit, so an off-chain caller cannot know them in advance, and under concurrency a precomputed id can be wrong. Obtain `orderId` after submission from the `OrderSubmitted` event in the transaction receipt, or from an `eth_call` simulation of `submitOrder`. A normal state-changing send does not return the value to an off-chain caller.

On-chain events key on `orderId`; the read API is queryable by source transaction hash. Persist both: `orderId` for the on-chain cancel/read path (`getOrder`, `orderStatuses`), the source tx hash for API status polling. Note that the read API's `id` field is a **deployment-scoped uid** (for example `"rwa:0x52b1…"`), not `orderId` — the escrow's cancel/claim calls take `orderId`, and passing `id` there reverts.

***

### Integrator-relevant functions

```solidity
// ── Source chain ───────────────────────────────────────────
// Submit one or more chained orders. Pulls requests[0].bidToken from msg.sender,
// bridges it to that request's execution chain, emits OrderSubmitted.
// msg.value covers the bridge fee. Returns (orderId, order) on-chain; off-chain
// callers do NOT receive these from a normal send — read orderId from the
// OrderSubmitted event, or use eth_call to simulate.
//
//   requests   — length 1 (normal order) or 2 (chained: request 0's swap output
//                funds request 1). The API returns exactly this array as
//                `orderRequests`; pass it through unchanged.
//   partnerId  — attribution metadata only, read by no on-chain logic. Pass
//                bytes32(0) unless the Glacis team issued you an id; a non-zero
//                value emits PartnerOrder(partnerId).
function submitOrder(OrderRequest[] calldata requests, bytes32 partnerId)
    external payable returns (bytes32 orderId, Order memory order);

// ── Execution chain: cancel / claim ───────────────────────────────
function cancel(Order calldata order) external;                  // owner cancels; funds released once arrived
function claimCancellation(Order calldata order) external;       // withdraw a pre-arrival cancellation once it lands
function cancelFor(Order calldata order, bytes calldata signature) external; // gasless: a relayer submits the owner's signed cancel

// ── Execution chain: chained orders (only when order.nextOrders is non-empty) ─
// continueChain is PERMISSIONLESS and PAYABLE: anyone may push the journey
// forward, and the caller pays leg 2's bridge fee out of their own msg.value.
// claimChain is OWNER-ONLY: only the parked order's owner can take the funds back.
function continueChain(bytes32 prevOrderId) external payable returns (bytes32 orderId, Order memory order);
function claimChain(bytes32 prevOrderId) external;               // owner takes the parked funds back instead

// ── Views ─────────────────────────────────────────────
// getOrder / orderStatuses resolve the canonical order state on the order's
// EXECUTION chain. A source-chain instance will not hold the canonical record
// post-bridge (returns Nonexistent).
function getOrder(bytes32 orderId) external view returns (Order memory);
function orderStatuses(bytes32 orderId) external view returns (OrderStatus);
function getPendingChain(bytes32 prevOrderId) external view returns (PendingChain memory);
function isSupportedToken(uint256 chainId, address token) external view returns (bool);
function isSupportedToken(uint256 chainId, bytes memory token) external view returns (bool);
function getBridgeAdapter(address token, uint256 destChainId) external view returns (address);
function getEquivalentToken(uint256 destChainId, address destToken) external view returns (address);
function getEquivalentToken(uint256 destChainId, bytes memory destToken) external view returns (address);
function allowedExecutionChains(uint256 chainId) external view returns (bool);
function domainSeparator() external view returns (bytes32);      // EIP-712 domain, for cancelFor relayers
```

`getOrder` and `orderStatuses` are **execution-chain** views. Every order names its own execution chain in `OrderRequest.executionChainId`; the quote surfaces the resolved pair as `executionChainId` + `executionEscrowAddress`, and that is the contract to read and to cancel against. Point a provider at that chain — not at the source chain — before calling. See the cancel snippet in the Integration Guide.

> **`getOrder` is only populated after arrival.** Before the order bridges and arrives on its execution chain, `getOrder(orderId)` returns a zeroed struct. Passing that to `cancel()` derives `keccak256(0, 0, 0)` and reverts with `ZDLite__UnauthorizedCaller` — which looks like a permissions problem and is not one. Persist the `Order` tuple from the `OrderSubmitted` event at submit time; pre-arrival it is the only copy in existence.

**Chained orders: who may call what.** `continueChain(prevOrderId)` is **permissionless** — any address may call it to push a parked journey forward — and it is **`payable`**, because the caller funds leg 2's bridge messaging fee from their own `msg.value`. That is deliberate: a relayer or the counterparty can complete a journey the user has walked away from. `claimChain(prevOrderId)` is the opposite: **owner-only**, and it abandons the follow-up leg to return the parked funds. Both are no-ops once the chain is resolved (`PendingChain.amount == 0`).

**Direct-bridged orders store no state.** When a single request's ask token is the same asset as its bid token on the destination chain (a same-ticker lane, e.g. USDC → USDC), `submitOrder` bridges straight to `destinationReceiver` and emits **only** `OrderDirectBridged`. No order is stored, nothing arrives, nothing fills — and there is nothing to cancel: `cancel()` on such an id registers a harmless pre-cancellation and `claimCancellation` reverts `ZDLite__OrderNotArrived`. `askTokenAmount` acts as a minimum-delivered floor. The API flags these quotes with `isDirect: true`; suppress cancel affordances for them.

**Gasless cancel (`cancelFor`).** The owner signs an EIP-712 `Cancel` message and any relayer submits it. Domain: `name = "ZDLiteEscrow"`, `version = "1"`, `chainId` = the execution chain, `verifyingContract` = that chain's escrow (read `domainSeparator()` to verify). Typed struct: `Cancel(address owner,uint64 orderNonce,uint64 sourceChainId)` — the same 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 a replay reverts. Contract signatures (EIP-1271) are accepted.

The contract also exposes a `nonce()` view, but do not use it to precompute `orderId`: `nonce` is contract-assigned and advances as orders are submitted, so a read-then-derive sequence is racy under concurrency. Always read `orderId` back from the `OrderSubmitted` event instead.

The contract also exposes administrative functions (token/adapter registration, execution-chain allowlisting, pause, role management) that are operator-only and not part of the integration surface. The full interface is available in the OpenAPI/ABI package provided on onboarding.

***

### Events

| Event                                              | Indexed     | Emitted when                                                                                               |
| -------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------- |
| `OrderSubmitted(bytes32 orderId, Order order)`     | `orderId`   | An order is submitted on the source chain.                                                                 |
| `OrderDirectBridged(bytes32 orderId, Order order)` | `orderId`   | A same-token direct bridge is submitted — emitted **instead of** `OrderSubmitted`, and terminal at submit. |
| `OrderArrived(bytes32 orderId, Order order)`       | `orderId`   | An order arrives on the execution chain (or same-chain submit).                                            |
| `OrderFilled(bytes32 orderId, Order order)`        | `orderId`   | An order is filled.                                                                                        |
| `OrderCancelled(bytes32 orderId, Order order)`     | `orderId`   | A cancel returns funds.                                                                                    |
| `PartnerOrder(bytes32 partnerId)`                  | `partnerId` | A submit carried a non-zero `partnerId` (attribution only).                                                |

Pre-arrival cancellations and claims emit their own events (`OrderPreCancelled`, `EscrowClaimed`), and chained orders emit `ChainedOrderParked` / `ChainedOrderContinued` / `ChainedOrderClaimed`, all keyed by the funding order's id. See the ABI package for the complete list.

***

### Error codes

The errors integrators encounter most often:

All names below are code-verified against the deployed contract source. Match by selector against these exact signatures — **including parameters**: an error that carries arguments hashes to a different selector than its bare name, so deriving a selector from the name alone will never match the revert data.

<table data-search="false"><thead><tr><th>Error</th><th>Meaning</th></tr></thead><tbody><tr><td><code>ZDLite__UnsupportedToken</code></td><td>The bid or ask token is not registered on that chain.</td></tr><tr><td><code>ZDLite__BridgeAdapterNotFound</code></td><td>No adapter is registered for the <code>(token, destChain)</code> route.</td></tr><tr><td><code>ZDLite__ZeroAddress</code></td><td>A required address (<code>owner</code> or <code>destinationReceiver</code>) is the zero address; both must be real addresses.</td></tr><tr><td><code>ZDLite__ZeroAmount</code></td><td><code>bidTokenAmount</code> (or a required amount) is zero.</td></tr><tr><td><code>ZDLite__UnsupportedReceiver</code></td><td><code>destinationReceiver</code> is not 20 or 32 bytes — or not exactly 20 when the destination chain is the order's execution chain.</td></tr><tr><td><code>ZDLite__InvalidExecutionChainId</code></td><td><code>executionChainId</code> is <code>0</code>.</td></tr><tr><td><code>ZDLite__ExecutionChainNotAllowed(uint256)</code></td><td><code>executionChainId</code> is not in the escrow's allowlist. The revert data carries the rejected chain id, so the selector is that of the <code>(uint256)</code> signature — not of the bare name.</td></tr><tr><td><code>ZDLite__NoOrderRequests</code></td><td>The <code>requests</code> array is empty.</td></tr><tr><td><code>ZDLite__TooManyOrderRequests</code></td><td>More than two requests were submitted (max chain length is 2).</td></tr><tr><td><code>ZDLite__InvalidDeadline</code></td><td><code>deadline</code> is in the past (and not <code>0</code>).</td></tr><tr><td><code>ZDLite__OrderExpired</code></td><td>The order's deadline passed before it could be filled.</td></tr><tr><td><code>ZDLite__InsufficientAskToken(expected, actual)</code></td><td>A fill could not meet the user's minimum output; the fill reverts.</td></tr><tr><td><code>ZDLite__DirectBridgeInsufficientAmount(askTokenAmount, actualAmount)</code></td><td>A direct bridge would deliver less than the <code>askTokenAmount</code> floor.</td></tr><tr><td><code>ZDLite__OrderNotPending</code></td><td>The order is not in <code>Pending</code> state for the requested action.</td></tr><tr><td><code>ZDLite__OrderNotArrived</code></td><td>A cancel/claim was attempted before the order arrived on the execution chain.</td></tr><tr><td><code>ZDLite__OrderAlreadyArrived</code></td><td>Re-delivery guard (bridge replay protection).</td></tr><tr><td><code>ZDLite__OrderAlreadyFilled</code></td><td>The order was already filled.</td></tr><tr><td><code>ZDLite__OrderNotCancellable</code></td><td>The order is not in a cancellable state.</td></tr><tr><td><code>ZDLite__UnauthorizedCaller</code></td><td>Caller is not permitted for that action (e.g. cancel by non-owner) — also what a zeroed <code>Order</code> struct produces, see the <code>getOrder</code> note above.</td></tr><tr><td><code>ZDLite__InvalidSignature</code></td><td>A <code>cancelFor</code> signature did not validate against <code>owner</code>.</td></tr><tr><td><code>ZDLite__ChainedTokenMismatch</code> / <code>ZDLite__ChainedChainMismatch</code></td><td>A two-request submit does not satisfy <code>askToken[0] == bidToken[1]</code> and <code>destinationChainId[0] == executionChainId[1]</code>.</td></tr><tr><td><code>ZDLite__ChainAlreadyParked</code></td><td>A chained order's funds are already parked for that funding order id — the park step cannot run twice.</td></tr><tr><td><code>ZDLite__ChainNotParked</code> / <code>ZDLite__ChainAlreadyResolved</code></td><td><code>continueChain</code> / <code>claimChain</code> was called for a chain that is not parked, or was already continued or claimed.</td></tr></tbody></table>

These are the common subset an integrator hits; the complete error set ships in the ABI package.

**On underpaid `msg.value`:** there is no `ZDLite__` error for an insufficient bridge fee. If `msg.value` does not cover the bridge adapter's native messaging fee, `submitOrder` reverts with the bridge adapter's own error, which is adapter-specific (CCTP vs LayerZero) — do not match it by a `ZDLite__` name. Send `gasCostSource` from the quote. See the Integration Guide.

> **Buffer the fee on LayerZero lanes only — never on CCTP.** On a LayerZero-source order the escrow passes `msg.sender` as the OFT `refundAddress`, so the adapter refunds whatever excess you send; a small buffer (say +10–20%) is cheap insurance against the fee moving between quote and inclusion. CCTP (USDC) lanes are the opposite: they quote `gasCostSource: 0`, and the CCTP adapter's refund-address parameter is declared but **unused**, so native value attached to a USDC order is **stranded in the contract, not refunded**. Send exactly the quoted `gasCostSource` on CCTP lanes.

**On a paused contract:** pause uses OpenZeppelin `PausableUpgradeable` (`whenNotPaused`), so a submission against a paused contract reverts with OpenZeppelin's `EnforcedPause` selector (older deployments: the `"Pausable: paused"` revert string), not a `ZDLite__` error. Note that the pause also freezes `cancel`, `claimCancellation`, `continueChain`, `claimChain`, and the bridge-delivery entrypoints.

***

### Deployments

ZeroDelta runs two live deployments: **`prod`** (production) and **`dev`** (pre-production). They are independent on-chain deployments — same nine chains, same execution-chain allowlist, entirely different addresses — and are reached through different API base URLs and API keys. The addresses below are the **`prod` deployment**; `dev` addresses are not published here, and integrators read them from `GET /api/v1/chains` on their `dev` base URL.

The escrow is deployed at the same address across all 9 supported chains, and Ethereum is the default execution chain.

* **Escrow (`prod`, all chains):** `0x5e25c8ABc19b88d6A7Ab0D805C77A34987a68b40`
* **Roles (`prod`, all chains):** `0x4248EA01aB83541d1406e23250FCAE6c29D2fF13`
* **Default execution chain:** Ethereum (`chainId 1`).
* **Allowed execution chains:** Ethereum (`1`), Base (`8453`), Arbitrum (`42161`), Plume (`98866`). An order's `executionChainId` must be one of these; the API picks it for you.

> **Confirm your environment before you wire addresses in.** The deployment name scopes the CREATE2 salt, so **every address rotates between deployments** — the `dev` deployment does not share a single address with `prod`, even for the same contract on the same chain. Use the base URL and deployment you were given at onboarding, and read addresses from `GET /api/v1/chains` for that environment rather than hardcoding them.

`GET /api/v1/chains` is canonical: per chain it returns the escrow address, the bridge adapter addresses, the roles-contract address, and whether the chain is an execution chain. That makes the `submitOrder` requirement "a bridge adapter must exist for `(bidToken, executionChainId)`" checkable from the API before you submit. Keep the chain set in sync with the Integration Guide coverage matrix.

***

### Coverage

* **Bridged bid stablecoins:** USDC (Circle CCTP V2), USDT and USDe (LayerZero V2 OFT) are the core set; USDtb, AUSD, PYUSD and USDG are also bridge-configured over LayerZero OFT.
* **Ask-only outputs:** additional tokens can be bought but not bridged — DAI, RLUSD, USD1, USDS, wM, rUSDY, PAXG, USDY, sUSDf, USDf, USD3 and PRIME — plus the tokenized-asset set (13: eleven Nest `n*` vault tokens and two Centrifuge `de*` tokens). They are delivered on the chain the order executes on, so `destinationChainId` must equal `executionChainId` for those. XAUT is the exception among tokenized assets — it carries its own OFT mesh and can be delivered cross-chain.
* **Chains:** Ethereum (default execution chain), Optimism, Arbitrum, Base, Ink, Unichain, Plasma, Sonic, Plume. Chain IDs and the per-cell bridge matrix are in the Integration Guide.
* **Per-(chain, token) addresses:** retrieved from `GET /api/v1/tokens` (e.g. `?chainId=42161`), which returns `symbol`, `chainId`, `address`, and `decimals` per token. This is the canonical source; addresses are not hardcoded here because they change as coverage expands.
* Coverage is not uniform per (chain, token) cell — `GET /api/v1/routes` returns the live per-cell set. New chains, tokens, and routes are onboarded on partner request. The live `/chains`, `/tokens`, and `/routes` endpoints are the source of truth for what is enabled in your environment.

***

### Audit

Halborn and Sherlock have audited the ZeroDelta (ZDLite) smart contracts; all important findings addressed. Report available on request.


# Widget

Prebuilt UI for cross-chain stablecoin and real-world-asset settlement. Drop it in; the user's wallet always signs.

The ZeroDelta Widget is a set of prebuilt UI components that put the clearing house into your app. It is the four-call integration from the [Integration Guide](/zero-delta/integration-guide) — quote, approve, `submitOrder`, track — already built, tested and themed.

One npm package, **zero runtime dependencies**, and no iframe: it renders in your page's own realm, styled with your tokens.

{% columns %}
{% column %}

#### Install & Embed

React, `mount()`, a `<script>` tag, or Next.js.
{% endcolumn %}

{% column %}

#### The Keyless Proxy

The one thing you must deploy before any of it works.
{% endcolumn %}
{% endcolumns %}

> **The package uses the internal name StableGenie.** You will see it in the package id (`@glacislabs/stablegenie-zerodelta`), the component names (`<StableGeniePay>`) and the browser global (`window.StableGenieWidget`); **ZeroDelta** is the product. The express-checkout pill is branded **ZeroDelta Link**. The names refer to the same thing — the same relationship as `ZDLite` and ZeroDelta on the contract side.

### Before you start

ZeroDelta is permissioned: integrators are onboarded and KYB'd by the Glacis team, so you cannot self-serve a key. Two things come from that onboarding, and you need both before the widget can price anything:

* a **ZeroDelta API key** (a viewer key — your proxy holds it server-side, never the browser), and
* the **base URL** for your environment.

**Ask for them here —** [**Glacis integrations team**](https://t.me/+CXFgMhEqkE85N2Nh)**.** The same key and base URL work for a direct API integration, so if your team already has them from the [Integration Guide](/zero-delta/integration-guide), you are ready.

### Try it before you install

The [widget playground](https://playground.glacislabs.com/) runs every component against the production gateway — no key needed on your side. Configure a surface, a theme and a pair, then copy the exact React, `mount()` or `<script>` snippet for it.

### Pick a surface

| Component                | What it does                                                                                           | Typical host                 |
| ------------------------ | ------------------------------------------------------------------------------------------------------ | ---------------------------- |
| `StableGenieExchange`    | The full exchange card — pick both sides, see routes and fees                                          | a dApp swap page             |
| `StableGenieRwaExchange` | The same card over real-world assets, one leg each side                                                | a tokenized-asset app        |
| `StableGeniePay`         | **Exact-receive**: the recipient gets exactly what you asked for, funded from whatever the payer holds | checkout, deposits, invoices |
| `StableGenieSwap`        | Exact-in, two selects, minimal chrome                                                                  | a compact sidebar            |
| `PaymentIsland`          | **ZeroDelta Link** — a collapsed express-checkout pill that expands to a one-click card                | a product page or cart       |

The first four take a display `variant`; `pill` on any of them renders the ZeroDelta Link surface.

### What you get

* **Five surfaces**, three layouts (`compact`, `wide`, `pill`) and two asset universes (stablecoins, real-world assets).
* **Nine source chains**, and many more destinations — receiving does not require an escrow on the far side.
* **A live catalogue.** Chains, tokens and routes are read from the gateway on every load and cross-checked against measured lane health, so the widget never offers a pair it cannot fill.
* **Full theming** — 21 tokens, light and dark, isolated from your page CSS in both directions.
* **Your wallet stack, not ours.** Pass your EIP-1193 provider and the user never connects twice; pass nothing and the widget discovers a wallet via EIP-6963.
* **Compliance screened and disclosed** — every route carries the decision the gateway made, shown to the payer rather than hidden behind a generic failure.
* **ESM, CJS, types and a prebuilt IIFE**, MIT licensed, published on [npm](https://www.npmjs.com/package/@glacislabs/stablegenie-zerodelta).

Tested with React 18 and 19, Next.js, Vite, webpack, and wallet stacks including wagmi, RainbowKit and injected providers.

### The user's wallet always signs

The widget never holds keys and never submits silently. It also refuses to trust the API it talks to:

* the escrow address and every token's decimals are **pinned offline** and cross-checked — a mismatch fails closed;
* every quote is **re-validated** against what the user actually selected;
* every transaction is **re-decoded before signing**, and approvals are exact-amount, never unbounded;
* settlement is **confirmed by receipt** over an independent RPC, so `onSuccess` means the money moved.

There is no telemetry. The only network calls are to your proxy, the verification RPC, and token artwork.

See [The Keyless Proxy](/zero-delta/widget/keyless-proxy) for the full trust model.

### Start here

{% stepper %}
{% step %}

### Get a key

ZeroDelta is permissioned — ask the [Glacis integrations team](https://t.me/+CXFgMhEqkE85N2Nh) for a viewer key and your base URL.
{% endstep %}

{% step %}

### Install the package

`npm i @glacislabs/stablegenie-zerodelta` — then pick React, `mount()` or a `<script>` tag. See [Install & Embed](/zero-delta/widget/install).
{% endstep %}

{% step %}

### Deploy the keyless proxy

One handler, one environment variable — the key from step 1, held server-side. The widget cannot price anything without it. See [The Keyless Proxy](/zero-delta/widget/keyless-proxy).
{% endstep %}

{% step %}

### Configure the surface

Choose the component, the mode and the product; wire your wallet provider and the four callbacks. See [Configure](/zero-delta/widget/configure).
{% endstep %}

{% step %}

### Match your brand

21 tokens, light and dark, four ready-made palettes. See [Theming](/zero-delta/widget/theming).
{% endstep %}

{% step %}

### Look up anything else

The full export surface, the headless engine and the known limits are in the [API Reference](/zero-delta/widget/api-reference).
{% endstep %}
{% endstepper %}

### What is not shipped

Stated plainly, so you do not design around something that does not exist yet.

|                                                            | Status                                                                                      |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
| Fiat on-ramp and off-ramp (`mode: "onramp"` / `"offramp"`) | Roadmap — the card shows an explicit coming-soon panel rather than a quote it cannot honour |
| Bank settlement (`settlement: "bank"`)                     | Roadmap, same treatment                                                                     |
| Integrator fees or revenue share                           | Not available — the widget takes no fee and passes no partner id                            |
| Localisation                                               | Not available — all copy is English                                                         |

### Talk to us

* **Get an API key and a base URL** → [Glacis integrations team](https://t.me/+CXFgMhEqkE85N2Nh)
* **New chains, tokens or routes** → [Glacis integrations team](https://t.me/+CXFgMhEqkE85N2Nh) — onboarded on request, the same process as a direct API integration
* **Integrate the API yourself instead** → [Integration Guide](/zero-delta/integration-guide)


# Install & Embed

Four ways onto the page — React, mount(), a script tag, or Next.js — plus the compatibility floor.

```sh
npm i @glacislabs/stablegenie-zerodelta
```

`react` and `react-dom` (>= 18) are **peer dependencies** for the main entry — they are kept external in the ESM and CJS builds, so your React is reused and nothing is double-bundled. The package itself has **zero runtime dependencies**.

> **CSS is auto-injected. There is no stylesheet to import.** The widget writes its own styles into the document on first render, scoped to its own root.

> **You also need the keyless proxy.** The widget's default `apiBase` is a same-origin endpoint you deploy, which injects the ZeroDelta key server-side. Without it every request answers `503 proxy_misconfigured`. This is not optional — see [The Keyless Proxy](/zero-delta/widget/keyless-proxy).

### Entry points

| Import                              | Contains                                                                                                  | React required              |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------- |
| `@glacislabs/stablegenie-zerodelta` | the React components, `mount`, the theme helpers, `createEngine`                                          | **yes**                     |
| `…/core`                            | the framework-agnostic engine — quoting, routing, encoding, the pre-sign validator, the on-chain verifier | **no**                      |
| `…/mount`                           | imperative mounting for non-React hosts                                                                   | yes (bundled by your build) |
| `…/standalone`                      | the prebuilt IIFE with React bundled in, for plain `<script>` embeds                                      | no                          |

Use `…/core` if you want the routing and validation engine without pulling in React — it is verifiably React-free.

### React

```tsx
import { StableGenieExchange } from "@glacislabs/stablegenie-zerodelta";

// `provider`: your app's EIP-1193 provider (wagmi connector, window.ethereum…) so the user does not connect twice.
<StableGenieExchange
  variant="wide"
  defaultFromChainId={42161}
  defaultFromAsset="USDT"
  defaultToChainId={8453}
  defaultToAsset="USDC"
  provider={provider}
/>
```

An exact-receive deposit instead:

```tsx
import { StableGeniePay } from "@glacislabs/stablegenie-zerodelta";

// recipient = your deposit address (required — the widget ships no default and never falls back to the payer).
<StableGeniePay
  recipient="0x0000000000000000000000000000000000000000"
  toChainId={42161}
  toAsset="USDC"
  amount="250.00"
  provider={provider}
  onSuccess={({ hash }) => console.log("paid", hash)}
/>
```

### Imperative `mount()`

For React hosts that do not write JSX at the embed point.

```ts
import { mount } from "@glacislabs/stablegenie-zerodelta";

const unmount = mount(document.getElementById("sg"), {
  variant: "exchange",
  defaultFromChainId: 42161,
  defaultFromAsset: "USDT",
  defaultToChainId: 8453,
  defaultToAsset: "USDC",
});
// later: unmount();
```

> **`variant` means two different things, and this is the sharpest footgun in the package.** In `mount()` it selects the **component** — `"pay"` (default), `"swap"`, `"exchange"` or `"rwa"`. In a component it selects the **layout** — `"compact"`, `"wide"` or `"pill"`. `mount()` strips its own selector before rendering, so it **cannot express a layout**: there is no way to get a `pill` or `wide` card through `mount()`. Use JSX, or the script-tag helpers below, which pass the display variant through.

### Plain `<script>`

No React on the host at all. The standalone build bundles its own.

```html
<div id="sg"></div>
<!-- Pin the version. The standalone build bundles React; self-host it if your CSP disallows third-party scripts. -->
<script src="https://unpkg.com/@glacislabs/stablegenie-zerodelta@4.0.0/dist/standalone.global.js"></script>
<script>
  StableGenieWidget.mountExchange(document.getElementById("sg"), {
    variant: "wide",
    defaultFromChainId: 42161,
    defaultFromAsset: "USDT",
    provider: window.ethereum,
  });
</script>
```

The global exposes five helpers, each taking the same props as the matching React component — including the display `variant` — and returning an unmount function:

| Helper                        | Renders                                     |
| ----------------------------- | ------------------------------------------- |
| `mountPay(el, props)`         | `StableGeniePay`                            |
| `mountSwap(el, props)`        | `StableGenieSwap`                           |
| `mountExchange(el, props)`    | `StableGenieExchange`                       |
| `mountRwaExchange(el, props)` | `StableGenieExchange` with `product: "rwa"` |
| `mountExpress(el, props)`     | `PaymentIsland` — ZeroDelta Link            |

**jsDelivr** works too: `https://cdn.jsdelivr.net/npm/@glacislabs/stablegenie-zerodelta@4.0.0/dist/standalone.global.js`

> **Pin an exact version, and prefer self-hosting if your CSP disallows third-party script origins — this widget sits in a payment path.** The file ships inside the package at `dist/standalone.global.js`, so you can copy it into your own static assets at build time:
>
> ```jsonc
> "scripts": {
>   "vendor:widget": "cp node_modules/@glacislabs/stablegenie-zerodelta/dist/standalone.global.js public/vendor/"
> }
> ```

### Next.js and server rendering

The widget is a client component. Importing it on the server does not crash — style injection, `localStorage` and wallet discovery are all guarded on `typeof window` — but it renders nothing useful there.

1. **The package ships no `"use client"` directive.** Wrap it in your own client component:

```tsx
// app/components/Checkout.tsx
"use client";
import { StableGeniePay } from "@glacislabs/stablegenie-zerodelta";

export function Checkout({ amount }: { amount: string }) {
  return <StableGeniePay recipient="0x…" toChainId={42161} toAsset="USDC" amount={amount} />;
}
```

2. **Or skip SSR entirely** where you would rather not ship it in the server bundle:

```tsx
import dynamic from "next/dynamic";
const Checkout = dynamic(() => import("./Checkout").then((m) => m.Checkout), { ssr: false });
```

3. **Route the proxy as a Next handler.** Mount it at `app/api/widget/v1/[...path]/route.ts` so the widget's default `apiBase` resolves without configuration. The proxy logic is plain data in and plain data out — see [The Keyless Proxy](/zero-delta/widget/keyless-proxy) for a complete handler.

### Compatibility

|                      |                                                                    |
| -------------------- | ------------------------------------------------------------------ |
| React                | 18 and 19 (peer dependency)                                        |
| Module formats       | ESM + CJS, with `.d.ts` and `.d.cts` types                         |
| Bundlers             | Vite, webpack, Next.js, Rollup, esbuild — nothing special required |
| Node                 | >= 20, for building only                                           |
| Runtime dependencies | none                                                               |
| Licence              | MIT                                                                |

**Browser APIs used:** `fetch`, `BigInt`, `URL` / `URLSearchParams`, `Promise.all`, `CustomEvent`, and `localStorage` (guarded — it degrades cleanly in private or sandboxed contexts). Any browser with those needs no polyfill; there is no `viem` or `ethers` to shim because neither is used.

### When something does not render

| Symptom                                                 | Cause                                                                                                                                               |
| ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Every request answers `503 proxy_misconfigured`         | The proxy has no `STABLEGENIE_API_KEY` set                                                                                                          |
| `403 origin_not_allowed`                                | Cross-origin embed with the origin missing from `WIDGET_ALLOWED_ORIGINS`                                                                            |
| Card renders but no chains or tokens load               | The `/api/widget/v1/*` route is not mounted or not rewritten to your handler                                                                        |
| **Direct settlement never appears as a route**          | Your proxy does not allow `screen/wallet` — the engine cannot screen, so it fails closed. See [The Keyless Proxy](/zero-delta/widget/keyless-proxy) |
| A payment succeeds on-chain but `onSuccess` never fires | Your CSP `connect-src` does not allow the verification RPC — see [The Keyless Proxy](/zero-delta/widget/keyless-proxy)                              |
| `variant: "pill"` is ignored                            | You used `mount()`, which cannot express a display variant                                                                                          |

### Try a configuration first

The [widget playground](https://playground.glacislabs.com/) renders every component against the production gateway and prints the exact React, `mount()` and `<script>` snippet for whatever you configure. The snippets on this page are the same ones it generates.


# Configure

Every prop the widget accepts — shared config, per-component props, events, wallets, coverage and compliance.

All five components extend one shared config object, `StableGenieConfig`. You can pass it as props on a single widget, or once on `StableGenieProvider` for several. Component-specific props sit on top.

### Shared configuration

Every widget accepts these.

| Prop               | Type                                | Default            | Notes                                                                                                                                  |
| ------------------ | ----------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
| `apiBase`          | `string`                            | `/api/widget/v1`   | The same-origin keyless proxy you deploy. See [The Keyless Proxy](/zero-delta/widget/keyless-proxy).                                   |
| `apiKey`           | `string`                            | —                  | Not needed against the keyless proxy. Attached **only** to a same-origin `apiBase`, or to an absolute one matching `trustedApiOrigin`. |
| `trustedApiOrigin` | `string`                            | —                  | The single absolute origin permitted to receive `apiKey`, e.g. `https://pay.example.com`. Omit and no absolute origin is trusted.      |
| `provider`         | `Eip1193Provider`                   | EIP-6963 discovery | Inject your app's wallet provider so the user does not connect twice.                                                                  |
| `rpcOverrides`     | `Record<number, string>`            | registry / bundled | Per-chain RPC used for settlement verification and balance reads only.                                                                 |
| `theme`            | `Partial<StableGenieTheme>`         | light              | See [Theming](/zero-delta/widget/theming).                                                                                             |
| `title`            | `string`                            | per component      | Card heading.                                                                                                                          |
| `onQuote`          | `(plan: Plan) => void`              | —                  | Fires when a plan is priced.                                                                                                           |
| `onSuccess`        | `(info: SuccessInfo) => void`       | —                  | Fires **only** on a receipt-confirmed settlement. See [Events](#events).                                                               |
| `onError`          | `(error: Error) => void`            | —                  | Carries the unsanitized error for your logging.                                                                                        |
| `onTransaction`    | `(entry: SanitizedTxEntry) => void` | —                  | Display-safe record of the settling transaction.                                                                                       |

#### Sharing config across widgets

```tsx
import { StableGenieProvider, StableGenieExchange, StableGeniePay } from "@glacislabs/stablegenie-zerodelta";

<StableGenieProvider apiBase="/api/widget/v1" provider={provider} theme={{ mode: "dark" }}>
  <StableGenieExchange />
  <StableGeniePay recipient="0x…" toChainId={42161} toAsset="USDC" amount="250.00" />
</StableGenieProvider>
```

> **Props win over context, and `theme` is the one field that merges.** A widget's own props replace the provider's values, except `theme`, which is shallow-merged (`{ ...context.theme, ...props.theme }`). So a provider can set `mode: "dark"` globally and one widget can override `accent` alone.

### Three axes, not one

`StableGenieExchange` is configured along three independent axes. They are easy to confuse because two of them are commonly called "variant" elsewhere.

| Axis     | Prop      | Values                            | What it decides                  |
| -------- | --------- | --------------------------------- | -------------------------------- |
| Market   | `mode`    | `exchange` · `onramp` · `offramp` | What the card trades **against** |
| Universe | `product` | `stable` · `rwa`                  | **Which assets** are selectable  |
| Layout   | `variant` | `compact` · `wide` · `pill`       | How the card **looks**           |

Only `mode: "exchange"` is live. `onramp` and `offramp` render an explicit coming-soon panel — the widget never quotes a rail it cannot execute.

|                    | `product: "stable"` | `product: "rwa"` |
| ------------------ | ------------------- | ---------------- |
| `mode: "exchange"` | ✅ live              | ✅ live           |
| `mode: "onramp"`   | ○ coming soon       | ○ coming soon    |
| `mode: "offramp"`  | ○ coming soon       | ○ coming soon    |

`StableGenieRwaExchange` is the same component with `product="rwa"` pre-set — same card, same pickers, same flow, same engine. Only the catalogue and the pair rule differ.

### Component props

#### `StableGenieExchange` / `StableGenieRwaExchange`

| Prop                                      | Type                                  | Default                     |
| ----------------------------------------- | ------------------------------------- | --------------------------- |
| `mode`                                    | `"exchange" \| "onramp" \| "offramp"` | `"exchange"`                |
| `product`                                 | `"stable" \| "rwa"`                   | `"stable"`                  |
| `variant`                                 | `"compact" \| "wide" \| "pill"`       | `"compact"`                 |
| `heightPx`                                | `number`                              | fit-content                 |
| `assets`                                  | `SupportedAsset[]`                    | the product's full universe |
| `defaultFromChainId` / `defaultToChainId` | `number`                              | —                           |
| `defaultFromAsset` / `defaultToAsset`     | `SupportedAsset`                      | —                           |
| `defaultAmount`                           | `string`                              | —                           |
| `recipient`                               | `string`                              | the payer                   |
| `fiatCurrency`                            | `"USD" \| "EUR" \| "GBP" \| "CHF"`    | —                           |

`StableGenieRwaExchange` takes the same props minus `product`.

#### `StableGeniePay` — exact-receive

The recipient receives exactly `amount` of `toAsset` on `toChainId`; the payer funds it from whatever they hold.

| Prop              | Type                     | Default                      |                                                      |
| ----------------- | ------------------------ | ---------------------------- | ---------------------------------------------------- |
| `recipient`       | `string`                 | —                            | **required**                                         |
| `toChainId`       | `number`                 | —                            | **required**                                         |
| `toAsset`         | `SupportedAsset`         | —                            | **required**                                         |
| `amount`          | `string`                 | —                            | **required**, decimal string                         |
| `fundingAssets`   | `SupportedAsset[]`       | the seven executable stables | narrows what may fund the payment                    |
| `heightPx`        | `number`                 | fit-content                  |                                                      |
| `variant`         | `"pill"`                 | full card                    |                                                      |
| `moneyDisplay`    | `string`                 | the crypto amount            | pill face, e.g. `"$99.00"`                           |
| `defaultExpanded` | `boolean`                | `false`                      | pill only                                            |
| `settlement`      | `"stablecoin" \| "bank"` | `"stablecoin"`               | how **you** are settled, never a payer choice        |
| `bankAccount`     | `BankPayoutAccount`      | —                            | display-only; a masked tail, never an account number |

> **`recipient` has no default and never falls back to the payer.** The widget will not quote without one. This is deliberate: a deposit widget that silently paid the payer's own wallet would be a silent loss of funds.

> **`settlement: "bank"` is not shipped.** It is a roadmap rail, so the card parks with a coming-soon panel rather than quoting a settlement the engine cannot perform.

#### `StableGenieSwap` — exact-in

A plain card with two selects. `StableGenieExchange` is the richer surface; use `StableGenieSwap` when you want the smaller one.

| Prop                 | Type             | Default   |
| -------------------- | ---------------- | --------- |
| `defaultFromChainId` | `number`         | `42161`   |
| `defaultFromAsset`   | `SupportedAsset` | `"USDT"`  |
| `defaultToChainId`   | `number`         | `8453`    |
| `defaultToAsset`     | `SupportedAsset` | `"USDC"`  |
| `defaultAmount`      | `string`         | `""`      |
| `recipient`          | `string`         | the payer |
| `variant`            | `"pill"`         | full card |

#### `PaymentIsland` — ZeroDelta Link

The express-checkout pill: a collapsed button that morphs into a one-click card. `variant="pill"` on any of the three widgets renders this component; you can also mount it directly.

| Prop              | Type                            | Notes                                     |
| ----------------- | ------------------------------- | ----------------------------------------- |
| `kind`            | `"pay" \| "swap" \| "exchange"` | **required** — which shape the card takes |
| `defaultExpanded` | `boolean`                       | ramp modes force this on                  |
| `moneyDisplay`    | `string`                        | the collapsed pill face                   |
| `onIdentitySave`  | `(id: LinkIdentity) => void`    | host persistence seam                     |
| `onDone`          | `() => void`                    | payer dismissed a settled receipt         |

It also accepts the pay-shaped props (`recipient`, `toChainId`, `toAsset`, `amount`, `fundingAssets`, `settlement`, `bankAccount`) and the exchange-shaped ones (`mode`, `product`, `assets`, the `default*` fields).

> **The saved identity is opt-in, client-side and namespaced per wallet.** A returning payer's preferences live in `localStorage` under `sg:link:v1:<wallet address>`, hold crypto preferences only, and are forgettable from the card's own menu. Nothing is sent anywhere.

### Wallets

Pass your app's provider and the widget reuses the existing connection silently:

```tsx
// wagmi
const { connector } = useAccount();
const provider = await connector?.getProvider();

<StableGenieExchange provider={provider} />
```

With no `provider`, the widget discovers one itself: **EIP-6963** announcement first (300 ms window, first wallet announced), then legacy `window.ethereum`, then an install prompt. It follows `accountsChanged` and `chainChanged`, and adopts a provider injected mid-session.

There is no `wagmi` or `viem` runtime dependency — the package has **zero** runtime dependencies.

> **Network switching on Plume.** The widget bundles `wallet_addEthereumChain` metadata for eight of the nine source chains. Plume (98866) has none, so a wallet that does not already know Plume must add it manually. Every other chain can be added from the widget.

### Events

| Callback        | Payload                                  | Fires when                                             |
| --------------- | ---------------------------------------- | ------------------------------------------------------ |
| `onQuote`       | `Plan`                                   | a route has been priced                                |
| `onSuccess`     | `SuccessInfo` `{ hash, verdict, route }` | the settling transaction is **confirmed by receipt**   |
| `onError`       | `Error`                                  | any failure; carries the original, unsanitized message |
| `onTransaction` | `SanitizedTxEntry`                       | a settling transaction was submitted                   |

> **`onSuccess` means the money moved.** It fires only when the transaction is mined with a success receipt (`verdict.status === "confirmed"`). A broadcast-but-unmined transaction resolves as `unconfirmed` and a failed one as `reverted`; **neither fires `onSuccess`.** Treat the callback as settlement, not as submission — because that is what it is.

`onTransaction` receives a whitelisted, display-safe record: no API keys, no calldata, no wallet secrets, every string length-bounded. It records the **settling** transaction only — never the ERC-20 `approve`. Errors thrown from your callback never break the payment flow.

### Coverage

#### Source chains

Payments are sent from the nine chains where the ZeroDelta escrow is deployed.

| Chain    | ID    |   | Chain    | ID    |
| -------- | ----- | - | -------- | ----- |
| Ethereum | 1     |   | Ink      | 57073 |
| Optimism | 10    |   | Unichain | 130   |
| Arbitrum | 42161 |   | Plasma   | 9745  |
| Base     | 8453  |   | Sonic    | 146   |
| Plume    | 98866 |   |          |       |

> **Destinations are not limited to that set.** The escrow writes the destination chain into the order as a `uint64` and the bridge delivers there, so the widget needs no RPC, escrow or wallet on the receiving chain. Many more chains can receive than can send.

#### Assets

Three nested allow-lists bound what can be named. `SupportedAsset` is their union — 26 symbols.

| List                                 | Members                                                                                                    |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
| Executable stables (stable ⇄ stable) | USDC, USDT, USDE, USDTB, AUSD, PYUSD, USDG                                                                 |
| Additional RWA-side stables          | USD1, WM, USDS, DAI, RLUSD                                                                                 |
| Tradeable RWA                        | XAUT, NALPHA, NTBILL, NBASIS, NCREDIT, NOPAL, NWISDOM, NACRDX, NLCRD, NAXI, NCLOA, NFALCON, DEJAAA, DESPXA |

The second list is deliberately wider than the first: a stable that cannot execute a stable-to-stable destination leg can still fund an RWA order, which settles on the execution chain it is already on.

> **These are allow-lists, not the catalogue.** What is actually selectable is derived on every page load from the live `/chains`, `/tokens`, `/routes` and `/route-health` endpoints. The constants only *bound* it. Never hardcode a token list from this table — read `engine.fetchCatalogue()` (see [API Reference](/zero-delta/widget/api-reference)) or let the widget do it for you.

A read that fails yields a catalogue marked `ready: false` — meaning "not known yet", never "nothing is supported". Only a positive answer may remove an option.

### Real-world assets

With `product="rwa"`, exactly one leg of the pair is the real-world asset and the other narrows to stables. Two settlement shapes exist and the engine handles both:

* **Single-leg** — XAUT and the Ethereum-home assets settle on the execution chain.
* **Chained** — the vault shares and the Centrifuge pair are delivered in two legs. `/quote` answers `isChained: true` with two order requests, and the wallet signs them as **one** `submitOrder`. The engine binds leg to leg before it will encode anything.

See [Integration Guide](/zero-delta/integration-guide) for what a chained order looks like at the API and contract level.

### Compliance

Every quote carries the gateway's compliance decision, and from **v4.0.0** the widget *discloses* it rather than only enforcing it. All four surfaces render a one-line chip under the route, expandable to the controls that actually ran — which policies applied, which jurisdiction, which wallets were screened. You do not configure this; it is on.

The five controls a decision can report:

| Check       | Covers                                              |
| ----------- | --------------------------------------------------- |
| `location`  | source jurisdiction, OFAC and geo policy            |
| `screening` | wallet screening across the payment's addresses     |
| `risk`      | risk categories returned by the screening providers |
| `asset`     | the asset's own policy                              |
| `limits`    | amount and velocity policy                          |

> **Absent is not "passed", and empty is not "none found".** A gateway with no compliance gate configured returns no decision at all — the widget renders that as *no decision returned*, never as green ticks. Separately, a key may be scoped so that sanction categories are withheld from the response, because naming a category back to the subject can amount to tipping off; an empty list therefore means "not disclosed to this key", never "clean". If you build your own UI on the engine, use the exported `complianceView()` and `complianceChecks()` rather than reimplementing this — `ComplianceView.blocked` is true **only** for an explicit deny and is never inferred from an absence.

#### Direct settlement is screened too

A direct settlement is a plain transfer: it never calls `/quote`, so it never reaches the gateway's pre-quote screen. It is gated instead by a standalone wallet screen, which your proxy must expose as `screen/wallet`. **Omit that path and direct settlement disappears from the picker** — the engine reads the resulting 404 as "could not screen" and fails closed. Verdicts are cached for five minutes per address.

#### Handling a block

A rejection arrives as a `422` with the structured code `COMPLIANCE_BLOCKED`, which the widget relabels into a machine-readable block plus a sentence the payer can act on:

```ts
interface QuoteBlock {
  blocked: true;
  category: string;   // STOLEN_FUNDS | SANCTIONS | TERRORIST_FINANCING | FRAUD | SCAM | COMPLIANCE
  reason: string;     // the raw upstream reasons
  message: string;    // the user-facing sentence the widget renders
}
```

| Category                            | Rendered as "the recipient is …"        |
| ----------------------------------- | --------------------------------------- |
| `STOLEN_FUNDS`                      | flagged as associated with stolen funds |
| `SANCTIONS` / `SANCTIONED` / `OFAC` | on a sanctions list                     |
| `TERRORIST_FINANCING`               | flagged for terrorist financing         |
| `FRAUD`                             | flagged for fraud                       |
| `SCAM`                              | flagged as a scam address               |
| anything else                       | flagged by compliance screening         |

A block is **never bypassed**. Do not retry it and do not offer a workaround — surface the message and let the user change wallet or recipient.

Upstream error text is scrubbed of URLs, host:port pairs, filesystem paths and stack frames and bounded to 160 characters before display; the original still reaches `onError` for your logs.


# Theming

Twenty-one tokens, twenty CSS variables, and the isolation rule that makes them the only way in.

Pass a partial theme to any widget, or once to `StableGenieProvider`:

```tsx
import { StableGenieExchange } from "@glacislabs/stablegenie-zerodelta";

<StableGenieExchange
  theme={{ mode: "dark", accent: "#8b93ff", accentText: "#0b0d12", radius: "14px" }}
/>
```

`resolveTheme()` picks `darkTheme` when `mode === "dark"` and `lightTheme` otherwise, then spreads your overrides on top. You only name what you want to change.

> **The `theme` prop is the only way in.** Tokens are emitted as CSS custom properties written **inline on the widget's own root element, never on `:root`**. Page CSS cannot leak into the widget and the widget cannot leak out. A host stylesheet targeting the widget's internal class names is unsupported and will break without warning.

### Tokens

`StableGenieTheme` is flat — 21 fields, all optional when you pass a partial.

#### Colour

| Field        | CSS variable       | Light     | Dark      |
| ------------ | ------------------ | --------- | --------- |
| `accent`     | `--sg-accent`      | `#4358e1` | `#7b8cff` |
| `accentText` | `--sg-accent-text` | `#ffffff` | `#0b0d12` |
| `bg`         | `--sg-bg`          | `#ffffff` | `#0e1116` |
| `surface`    | `--sg-surface`     | `#f7f8fc` | `#161a22` |
| `border`     | `--sg-border`      | `#e6e8f0` | `#252b36` |
| `text`       | `--sg-text`        | `#0e1116` | `#eef1f6` |
| `textMuted`  | `--sg-text-muted`  | `#6b7280` | `#9aa4b2` |
| `danger`     | `--sg-danger`      | `#df1b41` | `#ff5470` |
| `success`    | `--sg-success`     | `#12805c` | `#3ddc97` |

#### Shape and type

| Field           | CSS variable          | Default                                                     | Applies to           |
| --------------- | --------------------- | ----------------------------------------------------------- | -------------------- |
| `radius`        | `--sg-radius`         | `20px`                                                      | the outer card       |
| `radiusControl` | `--sg-radius-control` | `10px`                                                      | inputs, rows, panels |
| `radiusCta`     | `--sg-radius-cta`     | `999px`                                                     | the primary button   |
| `font`          | `--sg-font`           | `'Helvetica Neue', Helvetica, Arial, system-ui, sans-serif` | everything           |

#### Motion

Identical in light and dark — motion does not theme. Every rule that uses these is `prefers-reduced-motion` guarded, so a user who asks for less motion gets it regardless of what you set.

| Field              | CSS variable             | Default                          | Used for             |
| ------------------ | ------------------------ | -------------------------------- | -------------------- |
| `motionFast`       | `--sg-motion-fast`       | `160ms`                          | small state flips    |
| `motionBase`       | `--sg-motion-base`       | `220ms`                          | expand / collapse    |
| `motionSlow`       | `--sg-motion-slow`       | `320ms`                          | settling the receipt |
| `motionDeliberate` | `--sg-motion-deliberate` | `480ms`                          | the success sweep    |
| `easeOutSoft`      | `--sg-ease-out-soft`     | `cubic-bezier(0.22,0.61,0.36,1)` | entrances            |
| `easeInOut`        | `--sg-ease-in-out`       | `cubic-bezier(0.4,0,0.2,1)`      | reversible morphs    |
| `easeSpring`       | `--sg-ease-spring`       | `cubic-bezier(0.34,1.56,0.64,1)` | the pill pop         |

#### `mode`

`mode` is the only field with no CSS variable. It selects which base palette the other twenty are spread over.

### Dark mode is yours to decide

The widget does **not** read `prefers-color-scheme` itself — it renders the mode you give it, so it always matches your page rather than fighting it. Follow the OS setting like this:

```tsx
import { useEffect, useState } from "react";
import { StableGenieExchange } from "@glacislabs/stablegenie-zerodelta";

function useColorScheme() {
  const [dark, setDark] = useState(false);
  useEffect(() => {
    const mq = window.matchMedia("(prefers-color-scheme: dark)");
    const sync = () => setDark(mq.matches);
    sync();
    mq.addEventListener("change", sync);
    return () => mq.removeEventListener("change", sync);
  }, []);
  return dark;
}

export function Widget() {
  const dark = useColorScheme();
  return <StableGenieExchange theme={{ mode: dark ? "dark" : "light" }} />;
}
```

### Presets

Four palettes, all contrast-checked to WCAG AA. Copy the object you want.

```ts
const light = { mode: "light" };

const dark = { mode: "dark" };

const indigo = {
  mode: "dark",
  accent: "#8b93ff", accentText: "#0b0d12",
  bg: "#131316", surface: "#1c1c22", border: "#28282f",
  text: "#f2f2f6", textMuted: "#9d9da8",
};

const emerald = {
  mode: "light",
  accent: "#0b7d55", accentText: "#ffffff",
  surface: "#f2faf7", border: "#dcefe7",
};
```

Indigo measures at least 6:1 for text on its surfaces and 8:1 for the CTA label on the accent; emerald at least 4.6:1 on both counts.

### Corner sets

The three radii move together. Spread one of these over any palette:

```ts
const rounded = {};                                                     // the default
const sharp   = { radius: "10px", radiusControl: "8px",  radiusCta: "10px" };
const pill    = { radius: "26px", radiusControl: "16px", radiusCta: "999px" };
```

### Picking `accentText` for a brand colour

If you set `accent` to your own brand colour, set `accentText` to whichever of black or white is legible on it. This is the helper the playground uses:

```ts
/** Relative luminance (sRGB) — picks black or white CTA text for a custom accent. */
function contrastText(hex: string): string {
  const m = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
  if (!m) return "#ffffff";
  const lin = (c: string) => {
    const v = Number.parseInt(c, 16) / 255;
    return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
  };
  const l = 0.2126 * lin(m[1]!) + 0.7152 * lin(m[2]!) + 0.0722 * lin(m[3]!);
  return (l + 0.05) / 0.05 > 1.05 / (l + 0.05) ? "#0b0d12" : "#ffffff";
}

<StableGenieExchange theme={{ accent: brand, accentText: contrastText(brand) }} />
```

### Sizing

The card is capped at a readable width and centres in its container. Set `heightPx` to fix the height and scroll the content inside — useful in a fixed checkout panel. Omit it for fit-content.

```tsx
<StableGenieExchange variant="wide" heightPx={640} />
```

### What you cannot theme

Layout, copy, the ZeroDelta brand mark, the ordering of funding routes, and the success visual — which is accent-tinted by design and never a loud green surface. These are fixed so that a payment surface looks and behaves the same wherever it is embedded.

See every token live, with a copyable snippet, in the [widget playground](https://playground.glacislabs.com/).


# The Keyless Proxy

The same-origin proxy you deploy so the browser never holds a ZeroDelta key — contract, reference handler and CSP.

The widget never talks to the ZeroDelta gateway directly. It calls a **same-origin endpoint you deploy**, which injects your API key server-side and forwards a fixed set of requests. The browser holds no credential at all.

```
browser  ──►  https://your-app.com/api/widget/v1/*   ──►  https://zd-api.prod.glacis-infra.network/api/v1/*
           (same origin, no key)                      (your key, server-side)
```

This is a hard prerequisite. Until it is deployed, every widget request answers `503 proxy_misconfigured`.

### Getting an API key

ZeroDelta is permissioned — there is no self-serve signup. Integrators are onboarded and KYB'd by the Glacis team, who issue:

* a **viewer key**, which this proxy holds server-side and sends upstream as `x-api-key`, and
* the **base URL** for your environment.

**Ask for both here —** [**Glacis integrations team**](https://t.me/+CXFgMhEqkE85N2Nh)**.** It is the same key and base URL a direct API integration uses, so if your team already completed the [Integration Guide](/zero-delta/integration-guide) onboarding, reuse what you have.

> **The key is a server-side credential.** It goes in your deployment's environment, never in the browser bundle and never in your repository. If you ever need the browser to hold one, read [If you cannot be same-origin](#if-you-cannot-be-same-origin) first — and prefer not to.

### The contract

Mount a handler at `/api/widget/v1/:path*`. It must implement exactly this.

#### Allowed requests

Everything else is rejected **before any upstream call**. This is never an open relay that attaches your key to an arbitrary path.

| Resource        | Method | Purpose                                                           |
| --------------- | ------ | ----------------------------------------------------------------- |
| `chains`        | `GET`  | the enabled chain registry                                        |
| `tokens`        | `GET`  | per-chain token addresses and decimals                            |
| `routes`        | `GET`  | the live route matrix                                             |
| `route-health`  | `GET`  | measured lane health, so the widget stops offering broken lanes   |
| `quote`         | `POST` | pricing, and the gateway's pre-quote compliance screen            |
| `screen/wallet` | `POST` | the standalone wallet screen — **required for direct settlement** |

> **`screen/wallet` is not optional, and forgetting it fails closed loudly.** A direct settlement never calls `/quote`, so it never reaches the gateway's pre-quote screen; this endpoint is its only compliance control. A deployment that omits the entry answers `404`, the engine reads that as "could not screen", and **direct settlement disappears from the picker**. That is the intended failure direction — but it looks like a missing feature, so check this first if direct routes vanish.
>
> Note it is a **POST because it writes upstream**: it bills a screening provider and appends an audit row. The engine caches and de-duplicates verdicts for that reason — do not call it per keystroke, and count it against your daily cap alongside `/quote`.

#### Environment

| Variable                   | Required | Default                                    | Notes                                                                                               |
| -------------------------- | -------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------- |
| `STABLEGENIE_API_KEY`      | **yes**  | —                                          | The viewer key from onboarding, sent upstream as `x-api-key`. Alias: `ZERODELTA_API_KEY`.           |
| `ZD_API_UPSTREAM`          | no       | `https://zd-api.prod.glacis-infra.network` | https only (http allowed on loopback). A trailing `/api/v1` is ignored. Alias: `ZERODELTA_API_URL`. |
| `WIDGET_ALLOWED_ORIGINS`   | no       | —                                          | Comma-separated origins allowed to call the proxy cross-origin. Same-origin is always allowed.      |
| `RATE_LIMIT_PER_MIN`       | no       | `60`                                       | Per-IP request budget.                                                                              |
| `GLOBAL_QUOTE_CAP_PER_DAY` | no       | `5000`                                     | Daily ceiling on the resources that cost money upstream: `quote` **and** `screen/wallet`.           |

> **Never prefix these with a client-side bundler prefix.** `VITE_`, `NEXT_PUBLIC_`, `REACT_APP_` and their equivalents inline the value into the public bundle — which is precisely the leak this proxy exists to prevent.

#### Responses

Every rejection fails closed and carries a machine-readable `code`:

| Status | `code`                | Meaning                                                             |
| ------ | --------------------- | ------------------------------------------------------------------- |
| `403`  | `origin_not_allowed`  | Cross-origin request from an origin not in `WIDGET_ALLOWED_ORIGINS` |
| `404`  | `not_found`           | Path is not one of the six allowed resources                        |
| `405`  | `method_not_allowed`  | Wrong method for that resource                                      |
| `429`  | `rate_limited`        | Per-IP or daily budget exhausted; carries `Retry-After`             |
| `503`  | `proxy_misconfigured` | No API key configured                                               |
| `503`  | `service_unavailable` | No valid upstream configured                                        |
| `504`  | `PROXY_TIMEOUT`       | The gateway did not respond in time                                 |
| `502`  | `PROXY_ERROR`         | Upstream request failed                                             |

On success, the gateway's status and JSON body pass through unchanged.

> **Never answer with `access-control-allow-origin: *`.** Echo the specific allowed origin and set `Vary: Origin`. CORS is not authentication — the surface is read-only pricing plus a screening call, and every payment is wallet-signed, so the blast radius of abuse is cost and quota, never funds — but these controls bound it, and they only work if each one fails closed.

### Two rules that keep the compliance signal honest

Both are about **what you forward**, and both were tightened in v4.0.0. Get them wrong and the gateway screens the wrong party.

> **Forward the end user's IP, but only from an edge-verified header.** The gateway resolves geo and OFAC jurisdiction from it. If you forward nothing, the gateway sees *your serverless function's* IP and screens that instead of the payer. Trust `x-vercel-forwarded-for` or `x-real-ip` (your platform writes these from the real TCP peer and overwrites any client copy). A raw `x-forwarded-for` is a client-appendable chain — if it is your only signal, take the **last** entry, never the first. With no trustworthy signal, emit **no header at all**; never a placeholder.

> **Never forward a caller-supplied country header.** `cf-ipcountry` and `cf-connecting-ip` are only meaningful behind Cloudflare. On any other host nothing sets or strips them, so they are entirely attacker-controlled — and because the gateway prefers a country header over the raw-IP fallback, forwarding one lets a payer **choose the jurisdiction their payment is judged in**. Forward only a header your own edge writes. An absent signal must never be mistaken for a benign jurisdiction.

### Reference handler

A complete implementation for the Vercel Node runtime. No dependencies; only the request and response objects are platform-shaped.

```ts
// api/widget.ts  —  route /api/widget/v1/:path* here
import type { VercelRequest, VercelResponse } from "@vercel/node";

const ALLOWED: Record<string, string[]> = {
  chains: ["GET"],
  tokens: ["GET"],
  routes: ["GET"],
  "route-health": ["GET"],
  quote: ["POST"],
  "screen/wallet": ["POST"],
};

const DEFAULT_UPSTREAM = "https://zd-api.prod.glacis-infra.network";
const UPSTREAM_TIMEOUT_MS = 13_000;

/** https only — or http on loopback for a local gateway. A bad value fails closed as "no upstream"
 *  rather than sending the key over the wire. */
function parseUpstream(value?: string): string {
  try {
    const u = new URL((value || DEFAULT_UPSTREAM).trim());
    const loopback = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(u.hostname);
    if (u.protocol !== "https:" && !(u.protocol === "http:" && loopback)) return "";
    // The proxy appends /api/v1 itself; do not double it.
    return u.origin + u.pathname.replace(/\/+$/, "").replace(/\/api\/v1$/i, "");
  } catch {
    return "";
  }
}

const env = {
  apiKey: (process.env.STABLEGENIE_API_KEY || process.env.ZERODELTA_API_KEY || "").trim(),
  upstream: parseUpstream(process.env.ZD_API_UPSTREAM || process.env.ZERODELTA_API_URL),
  allowedOrigins: (process.env.WIDGET_ALLOWED_ORIGINS || "").split(",").map((s) => s.trim()).filter(Boolean),
  perMin: Number(process.env.RATE_LIMIT_PER_MIN) || 60,
  dailyCap: Number(process.env.GLOBAL_QUOTE_CAP_PER_DAY) || 5000,
};

// Best-effort, per warm instance. Use a shared store (Redis, Upstash) if you need a hard limit.
const ipWindow = new Map<string, { count: number; resetAt: number }>();
let billedDay = { day: "", count: 0 };

function rateLimit(ip: string, billsUpstream: boolean): number {
  const now = Date.now();
  const cur = ipWindow.get(ip);
  if (!cur || now >= cur.resetAt) {
    ipWindow.set(ip, { count: 1, resetAt: now + 60_000 });
  } else if (++cur.count > env.perMin) {
    return Math.ceil((cur.resetAt - now) / 1000);
  }
  if (billsUpstream) {
    const day = new Date(now).toISOString().slice(0, 10);
    if (billedDay.day !== day) billedDay = { day, count: 0 };
    if (++billedDay.count > env.dailyCap) return 3600;
  }
  if (ipWindow.size > 10_000) for (const [k, v] of ipWindow) if (now >= v.resetAt) ipWindow.delete(k);
  return 0;
}

/** Edge-VERIFIED end-user IP only. `x-vercel-forwarded-for` and `x-real-ip` are written by the
 *  platform from the real TCP peer; a raw `x-forwarded-for` is client-appendable, so take its LAST
 *  hop (the one the edge appended), never the first. Returns undefined rather than a placeholder. */
function clientIp(h: VercelRequest["headers"]): string | undefined {
  const first = (v: unknown) => {
    const s = Array.isArray(v) ? v[0] : typeof v === "string" ? v : undefined;
    return s ? s.split(",")[0]?.trim() || undefined : undefined;
  };
  const last = (v: unknown) => {
    const s = Array.isArray(v) ? v[v.length - 1] : typeof v === "string" ? v : undefined;
    if (!s) return undefined;
    const parts = s.split(",");
    return parts[parts.length - 1]?.trim() || undefined;
  };
  return first(h["x-vercel-forwarded-for"]) ?? first(h["x-real-ip"]) ?? last(h["x-forwarded-for"]);
}

function fail(res: VercelResponse, status: number, code: string, message: string) {
  res.status(status).setHeader("cache-control", "no-store");
  res.json({ error: { code, message } });
}

export default async function handler(req: VercelRequest, res: VercelResponse) {
  const method = (req.method || "GET").toUpperCase();
  const host = String(req.headers.host || "");
  const origin = String(req.headers.origin || "");

  // ── origin anchor: same-origin always; cross-origin must be listed; never `*` ──
  let sameOrigin = false;
  try {
    sameOrigin = !!origin && new URL(origin).host === host;
  } catch {}
  if (origin && !sameOrigin) {
    if (!env.allowedOrigins.includes(origin)) {
      return fail(res, 403, "origin_not_allowed", "This origin is not allowed to use the widget proxy.");
    }
    res.setHeader("access-control-allow-origin", origin);
    res.setHeader("vary", "Origin");
    res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
    res.setHeader("access-control-allow-headers", "content-type");
    res.setHeader("access-control-max-age", "600");
  }
  if (method === "OPTIONS") return res.status(204).end();

  // ── path + method allowlist ──
  //
  // Match a canonical parse, then forward a string from OUR OWN table: `base` is passed on only
  // after comparing exactly equal to a key of ALLOWED, so the upstream URL interpolates one of our
  // literals and never the caller's string. Dot-segments, percent-escapes and backslashes are
  // refused before the lookup, which keeps `..` and `%2F` impossible by construction rather than by
  // a property of the table's current contents. (`screen/wallet` contains a slash, so a blanket
  // "reject any slash" test cannot be used here.)
  const segs = (req.query as Record<string, string | string[]>).path;
  const base = (Array.isArray(segs) ? segs.join("/") : String(segs || "")).split("?")[0].replace(/\/+$/, "");
  const canonical = !/(^|\/)\.\.?(\/|$)/.test(base) && !base.includes("%") && !base.includes("\\");
  if (!base || !canonical || !Object.hasOwn(ALLOWED, base)) {
    return fail(res, 404, "not_found", "Unknown widget resource.");
  }
  if (!ALLOWED[base].includes(method)) {
    return fail(res, 405, "method_not_allowed", `${method} is not allowed for /${base}.`);
  }

  // ── fail-closed configuration gates ──
  if (!env.apiKey) return fail(res, 503, "proxy_misconfigured", "Widget proxy is not configured for this environment.");
  if (!env.upstream) return fail(res, 503, "service_unavailable", "Widget proxy upstream is not configured.");

  const ip = clientIp(req.headers);
  const retryAfter = rateLimit(ip || req.socket?.remoteAddress || "unknown", base === "quote" || base === "screen/wallet");
  if (retryAfter) {
    res.setHeader("retry-after", String(retryAfter));
    return fail(res, 429, "rate_limited", "Too many requests — please slow down.");
  }

  // ── forward ──
  // Headers are built by ENUMERATION. Nothing from the browser (cookies, authorization, a client
  // key) is forwarded; the server key is the only credential that ever reaches the gateway. The
  // end-user IP and country go through ONLY when edge-verified — see the two rules above.
  const headers: Record<string, string> = {
    accept: "application/json",
    "content-type": "application/json",
    "x-api-key": env.apiKey,
  };
  if (ip) headers["x-vercel-forwarded-for"] = ip;
  const country = String(req.headers["x-vercel-ip-country"] || "");   // never cf-ipcountry
  if (country) headers["x-vercel-ip-country"] = country;

  const params = new URLSearchParams();
  for (const [k, v] of Object.entries(req.query as Record<string, string | string[]>)) {
    if (k === "path" || v === undefined) continue;
    for (const x of Array.isArray(v) ? v : [v]) params.append(k, String(x));
  }
  const qs = params.toString();
  const url = `${env.upstream}/api/v1/${base}${qs ? `?${qs}` : ""}`;

  try {
    const upstream = await fetch(url, {
      method,
      headers,
      body: method === "GET" ? undefined : JSON.stringify(req.body ?? {}),
      signal: AbortSignal.timeout(UPSTREAM_TIMEOUT_MS),
      redirect: "manual",
    });
    const text = await upstream.text();
    res.status(upstream.status);
    res.setHeader("content-type", "application/json; charset=utf-8");
    res.setHeader("cache-control", "no-store");
    return res.send(text);
  } catch (e) {
    const err = e as { name?: string; message?: string };
    if (err?.name === "TimeoutError" || err?.name === "AbortError") {
      return fail(res, 504, "PROXY_TIMEOUT", "ZeroDelta did not respond in time");
    }
    return fail(res, 502, "PROXY_ERROR", err?.message || "Upstream request failed");
  }
}
```

On Vercel, route it with a rewrite:

```json
{
  "rewrites": [{ "source": "/api/widget/v1/:path*", "destination": "/api/widget" }],
  "functions": { "api/widget.ts": { "memory": 128, "maxDuration": 15 } }
}
```

On Next.js App Router, place the same logic in `app/api/widget/v1/[...path]/route.ts` and export `GET` and `POST`.

> **On a host that is not Vercel**, keep the shape but swap the header names for whichever ones *your* edge writes and overwrites. The rule is the header's provenance, not its spelling.

#### Verify it

```sh
curl -s https://your-app.com/api/widget/v1/chains | head -c 200
```

A JSON chain list means you are done. `{"error":{"code":"proxy_misconfigured"}}` means the key is not reaching the function.

> **The escrow pin is scoped to production.** The widget verifies the escrow address offline against a bundled production map, so pointing `ZD_API_UPSTREAM` at a non-production gateway makes that check fail closed on all nine chains at once. That is by design, not a bug.

### If you cannot be same-origin

Passing `apiKey` in the browser is supported but constrained. The key is attached **only** when `apiBase` is a same-origin relative path, or an absolute URL whose origin matches `trustedApiOrigin`:

```tsx
<StableGenieExchange
  apiBase="https://pay.example.com/api/widget/v1"
  trustedApiOrigin="https://pay.example.com"
  apiKey={viewerKey}
/>
```

> **There is no default trusted origin, on purpose.** The correct value is whichever proxy *you* deploy, so only your integration can know it. Set it to an origin you control — it is the sole destination permitted to receive the key. Protocol-relative and backslash-prefixed values (`//evil.example`, `/\evil.example`) are rejected: they pass a naive "starts with a slash" test but resolve to a foreign origin.

Prefer the same-origin proxy. It is the only configuration where the browser holds no credential at all.

### Content Security Policy

The widget confirms settlement by reading the transaction receipt over a public RPC. If your CSP blocks that call, **a payment that actually succeeded reports as unconfirmed and `onSuccess` never fires.**

Add the nine source-chain RPCs to `connect-src`, alongside your proxy:

```
connect-src 'self'
  https://ethereum-rpc.publicnode.com
  https://optimism-rpc.publicnode.com
  https://arb1.arbitrum.io
  https://mainnet.base.org
  https://mainnet.unichain.org
  https://rpc-gel.inkonchain.com
  https://rpc.plasma.to
  https://rpc.soniclabs.com
  https://rpc.plume.org;
```

Override any of them with `rpcOverrides` if you run your own nodes — then list yours instead.

Token artwork is loaded from the registry's image URLs, so `img-src` needs `https:` (or your own allow-list). Blocked images degrade to a letters glyph; nothing else changes.

The rest of a working policy, for reference:

```
default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none';
form-action 'self'; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self';
img-src 'self' data: https:;
```

`style-src` needs `'unsafe-inline'` because the widget injects its stylesheet and writes its theme tokens as inline custom properties.

### What the widget verifies for you

The proxy is one half of the trust model. The other half runs in the browser, and none of it depends on the API being honest:

* **The escrow address is pinned offline.** `0x5e25c8ABc19b88d6A7Ab0D805C77A34987a68b40` on every supported chain, cross-checked against the quote and the chain registry. Because both of those come from the same API, the offline pin is what makes the check meaningful — the widget fails closed on a mismatch.
* **Token decimals are pinned offline** for every supported asset. An inflated decimals value from a compromised registry would move the amount scaling and mint bad calldata; it fails closed instead.
* **Every quote is re-validated against your selection** — owner, receiver, both tokens, both chains, both amounts, and a bounded deadline. Minimum-received is enforced on the encoded calldata, not trusted from the response.
* **Every planned transaction is re-decoded before signing**, with its `value` bound to the plan. Approvals are **exact-amount, never unbounded**.
* **Settlement is confirmed by receipt** over a bundled RPC — never the registry's own — against a nonce baseline captured before broadcast.
* **The compliance decision is disclosed, not just enforced.** Every route carries the checks the gateway actually ran, and the widget asserts only what the server reported — see [Configure](/zero-delta/widget/configure).

The user's wallet always owns the final signature. The widget never holds keys and never submits silently.

#### Settlement verdicts

| Verdict       | Meaning                                                                       |
| ------------- | ----------------------------------------------------------------------------- |
| `confirmed`   | Mined with a success receipt. The only verdict that fires `onSuccess`.        |
| `reverted`    | Mined with a failure receipt — a real on-chain failure.                       |
| `mismatch`    | The mined transaction does not match the plan.                                |
| `unconfirmed` | Could not establish either. **Must not be presented as a completed payment.** |

### No telemetry

The only network calls the widget makes are to your `apiBase`, to the RPC used for verification, and image loads for token artwork. There is no analytics endpoint, no beacon and no third-party script.

Upstream error messages are scrubbed of URLs, host:port pairs, filesystem paths and stack frames and bounded to 160 characters before they reach a payer's screen. The original still reaches `onError` for your own logging.

### Known limits

* The reference rate limiter is **in-memory per warm instance** — best-effort. A shared store is the production upgrade if you need a hard ceiling.
* The proxy forwards the end user's IP and country so the gateway can screen, and nothing else from the browser.

### Still stuck?

If the proxy is deployed and `curl` still does not return a chain list, or your key is not working, the [Glacis integrations team](https://t.me/+CXFgMhEqkE85N2Nh) is the fastest route — the same channel that issued the key.


# API Reference

The complete export surface, the headless engine, and what the widget deliberately does not do.

Package `@glacislabs/stablegenie-zerodelta`, version **4.0.0**. MIT, zero runtime dependencies.

### Exports

#### `@glacislabs/stablegenie-zerodelta`

| Kind       | Exports                                                                                                                           |
| ---------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Components | `StableGeniePay` · `StableGenieSwap` · `StableGenieExchange` · `StableGenieRwaExchange` · `PaymentIsland` · `StableGenieProvider` |
| Mounting   | `mount`                                                                                                                           |
| Theme      | `lightTheme` · `darkTheme` · `resolveTheme`                                                                                       |
| Compliance | `CompliancePanel` · `complianceChecks` · `complianceView` · `complianceControlsLine` · `humanizeComplianceReason`                 |
| Engine     | `createEngine` · `symbolKey`                                                                                                      |
| Catalogue  | `assetClassOf` · `brokenLaneReason` · `counterpartTokens` · `destinationAvailability`                                             |
| Constants  | `EXECUTABLE_STABLE_ASSETS` · `STABLE_ASSETS` · `TRADEABLE_RWA_ASSETS` · `SUPPORTED_ASSETS` · `SUPPORTED_CHAIN_IDS`                |

#### `…/core`

The framework-agnostic engine — no React, no DOM. Everything above except the components, plus the primitives (address, units, ABI encoding, escrow, route solving, order normalisation, transaction validation, compliance derivation, the API client and the verifier).

#### `…/mount`

`mount(el, config)` for non-React hosts. Returns an unmount function.

#### `…/standalone`

The IIFE build. Sets `window.StableGenieWidget` with `mountPay`, `mountSwap`, `mountExchange`, `mountRwaExchange` and `mountExpress`.

### Types

| Type                                                                                               | Shape                                                                                                                                                                                                        |
| -------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `StableGenieConfig`                                                                                | The shared config — see [Configure](/zero-delta/widget/configure)                                                                                                                                            |
| `StableGeniePayProps` · `StableGenieSwapProps` · `StableGenieExchangeProps` · `PaymentIslandProps` | Per-component props                                                                                                                                                                                          |
| `SupportedAsset`                                                                                   | Union of 26 symbols (below)                                                                                                                                                                                  |
| `ExchangeMode`                                                                                     | `"exchange" \| "onramp" \| "offramp"`                                                                                                                                                                        |
| `ExchangeProduct`                                                                                  | `"stable" \| "rwa"`                                                                                                                                                                                          |
| `PaymentIslandKind`                                                                                | `"pay" \| "swap" \| "exchange"`                                                                                                                                                                              |
| `SettlementMethod`                                                                                 | `"stablecoin" \| "bank"`                                                                                                                                                                                     |
| `MountConfig`                                                                                      | Discriminated union on `variant: "pay" \| "swap" \| "exchange" \| "rwa"`                                                                                                                                     |
| `StableGenieTheme`                                                                                 | 21 theme tokens — see [Theming](/zero-delta/widget/theming)                                                                                                                                                  |
| `SuccessInfo`                                                                                      | `{ hash: string; verdict: VerifyResult; route: RouteResult }`                                                                                                                                                |
| `VerifyResult`                                                                                     | `{ status: "confirmed" \| "mismatch" \| "reverted" \| "unconfirmed"; reason?: string }`                                                                                                                      |
| `Compliance`                                                                                       | The gateway's decision for a quote's wallet pair — status, decision, screened wallets, jurisdiction, applied policies and legislation, risk / flagged / blocking categories, reasons, `checkId`, `expiresAt` |
| `ComplianceCheck`                                                                                  | One control: `id` (`location` \| `screening` \| `risk` \| `asset` \| `limits`), `label`, `ok`, `reasons`, evidence                                                                                           |
| `ComplianceView`                                                                                   | The derived display state: `tone`, `label`, `headline`, `blocked`, `needsAction`, `screened`                                                                                                                 |
| `ComplianceTone`                                                                                   | `"ok" \| "warn" \| "blocked" \| "neutral"`                                                                                                                                                                   |
| `WalletScreen`                                                                                     | A standalone `/screen/wallet` verdict — the direct-settlement gate                                                                                                                                           |
| `SanitizedTxEntry`                                                                                 | The display-safe record passed to `onTransaction`                                                                                                                                                            |
| `QuoteBlock`                                                                                       | `{ blocked: true; category: string; reason: string; message: string }`                                                                                                                                       |
| `Catalogue` · `Availability` · `BrokenLane`                                                        | The derived selectable set and why a lane is unavailable                                                                                                                                                     |
| `Chain` · `Token` · `Route`                                                                        | Registry shapes                                                                                                                                                                                              |
| `Plan` · `RouteResult` · `SwapInput`                                                               | Engine planning shapes. `Quote` and `RouteResult` carry `compliance?`                                                                                                                                        |
| `LinkIdentity` · `BankPayoutAccount`                                                               | ZeroDelta Link shapes                                                                                                                                                                                        |
| `Eip1193Provider`                                                                                  | The provider interface accepted by `config.provider`                                                                                                                                                         |

#### `SupportedAsset`

```ts
type SupportedAsset =
  // Stables that can execute a stable-to-stable destination leg
  | "USDC" | "USDT" | "USDE" | "USDTB" | "AUSD" | "PYUSD" | "USDG"
  // Stables that can fund an RWA order but not execute a stable-to-stable leg
  | "USD1" | "WM" | "USDS" | "DAI" | "RLUSD"
  // Real-world assets
  | "XAUT"
  | "NALPHA" | "NTBILL" | "NBASIS" | "NCREDIT" | "NOPAL" | "NWISDOM"
  | "NACRDX" | "NLCRD" | "NAXI" | "NCLOA" | "NFALCON"
  | "DEJAAA" | "DESPXA";
```

The union bounds what can be **named**. What is **selectable** is derived per load — see [Configure → Coverage](/zero-delta/widget/configure).

### The headless engine

`createEngine()` gives you the routing, encoding and verification layer without any UI. Use it to build your own picker or checkout on top of the same guarantees the widget enforces.

```ts
import { createEngine } from "@glacislabs/stablegenie-zerodelta/core";

const engine = createEngine({ apiBase: "/api/widget/v1" });

const catalogue = await engine.fetchCatalogue();     // what a picker should render
const preview = await engine.quoteSwapPreview({
  fromChainId: 42161, fromToken: "USDT",
  toChainId: 8453,    toToken: "USDC",
  amountIn: "100",
});
```

#### `EngineConfig`

| Field              | Type                                | Default          |
| ------------------ | ----------------------------------- | ---------------- |
| `apiBase`          | `string`                            | `/api/widget/v1` |
| `apiKey`           | `string`                            | —                |
| `getApiKey`        | `() => string \| Promise<string>`   | —                |
| `trustedApiOrigin` | `string`                            | —                |
| `fetchImpl`        | `typeof fetch`                      | global `fetch`   |
| `rpcOverrides`     | `Record<number, string>`            | —                |
| `onTransaction`    | `(entry: SanitizedTxEntry) => void` | —                |

#### `Engine`

| Method                                         | Returns                                           |
| ---------------------------------------------- | ------------------------------------------------- |
| `fetchChains()`                                | the enabled chain registry                        |
| `fetchTokens()`                                | per-chain token addresses and decimals            |
| `fetchRoutes()`                                | the published route matrix                        |
| `fetchRouteHealth()`                           | measured lane health, or `null` — never rejects   |
| `fetchQuote(body)`                             | a priced quote, carrying the compliance decision  |
| `fetchCatalogue(allowedAssets?, product?)`     | the selectable `Catalogue`                        |
| `fetchPaymentPlan(request, payer, wallet?)`    | an exact-receive `Plan`                           |
| `quoteSwapPreview(swap, owner?)`               | an exact-in preview                               |
| `buildSwapPlan(swap, payer, wallet?)`          | an exact-in `Plan`                                |
| `payerNonceBaseline(...)`                      | the pre-broadcast nonce baseline                  |
| `verifySubmittedTx(...)`                       | a `VerifyResult`                                  |
| `readBalances(...)` · `readNativeBalance(...)` | wallet balances                                   |
| `recordTransaction(entry)`                     | the sanitized history entry                       |
| `api` · `verifier` · `apiBase`                 | the underlying client, verifier and resolved base |

Resource reads are cached for 60 seconds per engine; a wallet screening verdict is held for 5 minutes, because it is a property of the address rather than of the market. `fetchCatalogue` never rejects: a failed read yields `ready: false`, which means "not known yet", not "nothing is supported".

### Rendering the compliance decision yourself

If you build your own checkout on `…/core`, use the same derivation the widget uses rather than reimplementing it — `complianceChecks()`, `complianceView()` and `CompliancePanel` are exported for exactly that. See [Configure → Compliance](/zero-delta/widget/configure) for the rule that governs all of them.

### Fees and attribution

The widget takes **no fee**. There is no fee hook, no integrator string and no revenue-share configuration — what the quote prices is what the payer pays.

It also passes `partnerId = bytes32(0)` on every `submitOrder`. The escrow's only use of that argument is to emit an attribution event when it is non-zero; there is no on-chain logic behind it. If you need order attribution, contact the Glacis team — it is not something you can configure from the widget today.

### Known limits

| Limit                            | Detail                                                                                      |
| -------------------------------- | ------------------------------------------------------------------------------------------- |
| No localisation                  | Every string is English. There is no locale seam or message catalogue.                      |
| No fee or attribution hook       | See above.                                                                                  |
| `mount()` cannot set a layout    | Its `variant` selects the component; use JSX or the script-tag helpers for `pill` / `wide`. |
| On- and off-ramp are not shipped | `mode: "onramp"` / `"offramp"` render a coming-soon panel.                                  |
| Bank settlement is not shipped   | `settlement: "bank"` parks the same way.                                                    |
| Plume network switching          | No `wallet_addEthereumChain` metadata is bundled for Plume (98866); users add it manually.  |
| Reference proxy rate limiter     | In-memory per warm instance — best-effort, not a hard ceiling.                              |

### Versioning

Releases are semantic and automated from `main`. **Pin an exact version in production** — this component sits in a payment path.

Four majors have carried breaking changes:

| Version   | Change                                                                                                                                                                                                                                           |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **4.0.0** | Direct settlements are screened, and the compliance decision is disclosed in the UI. Deployments must add `screen/wallet` to the proxy allow-list or direct settlement fails closed — see [The Keyless Proxy](/zero-delta/widget/keyless-proxy). |
| **3.0.0** | The exported `PayMethod` union drops `"recommended"` and `"anyasset"`; the default method is `"stablecoin"`. The any-asset funding rail is no longer reachable from the UI.                                                                      |
| **2.0.0** | Closed an API-key leak and readied the package for public npm.                                                                                                                                                                                   |
| **1.0.0** | `DEFAULT_API_BASE` is no longer exported. Hosts passing an absolute `apiBase` together with an `apiKey` must now also set `trustedApiOrigin`, or the key is withheld. Consumers on the default same-origin proxy are unaffected.                 |

The full changelog ships with the package on [npm](https://www.npmjs.com/package/@glacislabs/stablegenie-zerodelta).

***

*Verified against `@glacislabs/stablegenie-zerodelta` v4.0.0.*


# Points Program

Hold supported tokens in your own wallet and earn ZeroDelta Points. How it works, how points are calculated, and the rules.

Hold supported stablecoins and tokenized assets in **your own wallet**, sign a commitment, and earn **ZeroDelta Points** for as long as you keep holding. Nothing is deposited, locked or bridged — your funds never leave your wallet.

{% hint style="info" %}
Points are a record of participation. They carry no promise of any future distribution, value or entitlement.
{% endhint %}

> The ZeroDelta app and API use the internal name **Stakeflow** for this program. The names refer to the same thing.

**Take part in the ZeroDelta app →** [**zerodelta.glacislabs.com/stakeflow**](https://www.zerodelta.glacislabs.com/stakeflow)

### At a glance

|                |                                                                                       |
| -------------- | ------------------------------------------------------------------------------------- |
| **What earns** | The supported tokens you commit, held in your wallet on a supported chain             |
| **Rate**       | Currently **1 point per US dollar of value held, per day**, for every supported token |
| **Minimum**    | **$10** of each committed token — to join, and to keep earning                        |
| **Custody**    | None. No deposit, no lock, no Points contract. Move your funds whenever you like      |
| **Leaving**    | Revoke at any time with a signature. Points already earned are never taken away       |
| **Wallets**    | EVM wallets (including ERC-1271 smart-contract wallets) and Solana wallets            |

The live list of supported tokens and chains, with each token's rate, is shown in the [ZeroDelta app](https://www.zerodelta.glacislabs.com/stakeflow).

***

### How it works

```mermaid
sequenceDiagram
    participant W as Your wallet
    participant A as ZeroDelta app
    participant P as Points service (off-chain)
    participant C as Public blockchain
    W->>A: Pick a token and an amount
    A->>P: Request a commitment
    P-->>W: Terms and message to sign
    W->>P: Signature (no transaction, no gas)
    P->>C: Check the wallet holds at least $10
    P-->>A: Commitment recorded
    loop At unannounced times
        P->>C: Read the wallet's committed token balances
        P->>P: Credit points for the period since the last reading
    end
```

{% stepper %}
{% step %}

#### Commit

In the [ZeroDelta app](https://www.zerodelta.glacislabs.com/stakeflow), choose a supported token on a supported chain and the amount you intend to hold. You sign a short statement — EIP-712 typed data on EVM, a signed message on Solana. It is a signature, not a transaction: it costs no gas and moves nothing. The full terms are shown before you sign.
{% endstep %}

{% step %}

#### Hold

Keep the tokens in the same wallet. The Points service reads your balance of each committed token from the public blockchain at times that are not announced in advance.
{% endstep %}

{% step %}

#### Earn

Each reading credits points for the time since the previous reading.
{% endstep %}

{% step %}

#### Revoke (optional)

Sign a revocation for a token whenever you want. Earning on that token stops; everything already earned stays.
{% endstep %}
{% endstepper %}

***

### How points are calculated

Points accrue per wallet, per chain, per committed token. For each period between two consecutive readings:

```
points = min(balance at previous reading, balance at this reading, committed amount)
         × token USD price × rate × days in the period
```

* **The lower balance counts.** A balance earns for a period only if it was there at both ends. Tokens added part-way start earning from the next period; tokens removed stop earning straight away. A token's first reading after it is committed therefore earns nothing — it starts the first period.
* **The commitment is a cap, not a lock.** Hold less than you committed and you earn in proportion to what you hold. Hold more and the excess earns nothing until you sign a new commitment for the larger amount, which replaces the old one. A new commitment or a revocation applies to the whole period in progress.
* **Valued in dollars.** Every token, stablecoins included, is valued at its market USD price, so a dollar of one supported token earns the same as a dollar of another.
* **Weighted by time.** Points scale with the time elapsed in each period.
* **Precise.** Points are computed in high-precision decimal arithmetic and kept at full precision. Each chain's total is shown rounded down to whole points, and a wallet's total adds those chain totals together.

#### Worked example

A wallet commits **1,000 USDC** (price $1.00, rate 1). Readings are shown one day apart to keep the numbers simple — real reading times are not published.

| Reading   | Balance | Earning basis                        | Points for the period |
| --------- | ------- | ------------------------------------ | --------------------- |
| 1         | 1,000   | — first reading                      | 0                     |
| 2         | 1,200   | min(1,000, 1,200, cap 1,000) = 1,000 | 1,000                 |
| 3         | 400     | min(1,200, 400, cap 1,000) = 400     | 400                   |
| 4         | 1,000   | min(400, 1,000, cap 1,000) = 400     | 400                   |
| **Total** |         |                                      | **1,800**             |

***

### Rules

Every rule applies **per token and per chain**. The same token on two chains is two separate commitments.

#### Joining

* **You join per chain.** Commit on each chain where you want your balances read; the same address on another chain is not read until it commits there.
* When your signed commitment is submitted, the wallet must hold at least **$10** of that token on that chain, and the committed amount must also be at least $10. For this check a stablecoin counts as $1 per token; other assets use their USD price.
* You can commit several tokens on the same chain. Signing again for a token replaces its previous commitment.
* Sign-ups on a chain, including rejoining after dropping out, are subject to a program-wide daily limit.

#### What is read

Only the tokens you have committed are read. Holding other tokens in the same wallet costs nothing and earns nothing:

| Token on that chain                             | Read?                     | Earns on                                |
| ----------------------------------------------- | ------------------------- | --------------------------------------- |
| Committed                                       | Yes                       | its balance, up to the committed amount |
| Not committed                                   | No                        | nothing                                 |
| Revoked, or ended by dropping below the minimum | No, from the next reading | nothing, until it is committed again    |

#### Staying eligible

* At every reading, **each committed token** must be worth **$10 or more** in the wallet on that chain. Balances of different tokens are never added together.
* If a committed token is below that, its final period is still paid in proportion to what was held, and that commitment ends. Your other commitments on the chain are not affected.
* To earn on that token again, commit it again. A **24-hour cooldown** applies before recommitting a token that dropped out. If it was your last commitment on that chain, the cooldown covers every token there.

#### Leaving

* Revoke a token at any time by signing a revocation. Earning on that token stops, and the period in progress is not credited for it.
* When no commitment remains on a chain, the wallet stops being read there.
* **Points are never deducted** — not when you revoke, not when you drop below the minimum, and not when you move your funds.

#### Wallet notes

* **Smart-contract wallets** must already be deployed on the chain you commit on.
* **Solana:** balances are read from the wallet's associated token account for each token, which is where modern wallets keep them. Tokens held in any other token account are not counted.

***

### Architecture

The program runs entirely **off-chain**. There is no Points smart contract: the service only verifies signatures and reads public blockchain state.

```mermaid
flowchart TB
    W[Your wallet] -->|signs commitment or revocation| APP[ZeroDelta app and API]
    APP --> ENR
    subgraph PS[Points service - off-chain]
        ENR[Enrolment<br/>verify signature and minimum] --> LED[(Points ledger<br/>append-only)]
        SNAP[Snapshot engine<br/>read committed balances, credit points] --> LED
    end
    CH[(Public blockchains<br/>EVM and Solana)] -->|balances| SNAP
    PR[USD price feed] -->|prices| SNAP
    LED -.->|points, rates, commitments| APP
```

| Component           | What it does                                                                                                                                                             |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Enrolment**       | Issues a single-use, short-lived challenge carrying the terms, verifies the signature for the address, checks the minimum, and records the commitment or revocation.     |
| **Snapshot engine** | Reads each participating wallet's committed token balances from chain nodes at unannounced times, values them in USD, and credits the period since the previous reading. |
| **Points ledger**   | An append-only record of every credited period. Nothing in it can reduce a total, and credited periods are never recalculated.                                           |
| **ZeroDelta API**   | Serves points, rates and commitments to the app and to integrators — see the Stakeflow endpoints in the [API reference](/zero-delta/integration-guide/api).              |

***

### Fair play

* **Unannounced readings.** Reading times are not published, so a balance cannot be timed around them.
* **Both ends must hold.** Only the lower of two consecutive readings earns, so a short-lived top-up earns nothing.
* **Real funds per commitment.** The $10 minimum applies to every committed token, and the cooldown stops a token from cycling in and out.
* **Your signature only.** Committing and revoking both require the wallet's own signature, and each challenge can be used once.
* **Changes are forward-only.** Rates, supported tokens and minimums may change over time. A change applies from the next reading — including to existing participants when the minimum changes — and points already credited stay as they are.


# Why Airlift?

AirLift is a universal token registry that allows any burn and mint tokens to quickly be integrated with a single API.

The current implementation of each token standard ([LayerZero](https://layerzero.network/) OFT, [Wormhole](https://wormholescan.io/) NTT, [Chainlink](https://ccip.chain.link/) CCT, [Hyperlane](https://www.hyperlane.xyz/) WarpRoutes, [Circle](https://www.circle.com/) CCTP, [M0LitePortal](https://www.m0.org/) M0, xERC20) requires bespoke parameters for each of the assets being issued on these frameworks. While they leverage standardized validation and transport via the parent protocol, the configuration of the security and required data to send and receive is not.

This means for any protocol that wants to leverage these assets as potential routes has to manually integrate, test and potentially update each one in real time to maximize availability for their users. This becomes a nontrivial amount of development work and resource expenditure.

For services like [Li.fi](http://Li.fi) and Jumper, AirLift allows the routing process to include thousands of new potential routes to satisfy the user request.

#### Integration is Simple

* Contact us if you want availability to the Airlift APIs. We will give you API specs that help your frontend estimate route paths.
* The smart contract interface has only 2 functions: quote & send!
* If you are already a LIFI user you will automagically get the benefit of these highly efficient routes.

***

### How it Works

Glacis has partnered with LiFi to distribute Airlift to all of their integrators. For integrators that wish to use Airlift directly, please contact us directly.

{% stepper %}
{% step %}

### DApp Uses LiFi or Integrates Directly

A DApp or user can integrate LiFi or Glacis Airlift via our APIs. LiFi provides an aggregation of cross-chain token routes, while Airlift provides efficient cross-chain routes.
{% endstep %}

{% step %}

### Airlift Is Selected

To send a token cross-chain, Airlift will be selected as the best provider.
{% endstep %}

{% step %}

### User Sends Tokens Through Airlift

Tokens will be directed through the Airlift product, allowing the user to get their tokens on the selected destination chain.
{% endstep %}
{% endstepper %}

<figure><img src="https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2F7uLWpJ3wnt4oxgC2g3To%2FScreenshot%202025-03-13%20at%202.05.58%E2%80%AFPM.png?alt=media&amp;token=91dac016-c9d3-4798-9004-06f570990ade" alt=""><figcaption></figcaption></figure>

Under the hood, Airlift is a series of smart contract that abstracts the special configurations of each bridge & token standard into a single interface, paired with an API that helps with introspection of these configurations & quoting of routes.

***

### Market Challenges Addressed By Airlift

**For smaller orders**

* Intents and solvers can provide fast execution but begin suffering at larger order sizes outside of the major assets due to inventory management risk and opportunity cost of holding those assets.
* Solvers, when re-balancing inventory cross chain would be able to leverage Airlift for better settlement options.
* Limited liquidity on solver networks making people having to wait incredibly long times to get fills or have to pay very high slippage
* Users experience high slippage because of multiple swaps at Dex. Raking in tons of fees and hurting user experience cross chain.
* People have become accustomed to using intermediary assets like USDT, USDC to transfer assets across chain instead of efficient routes of minting and burning which lot of the tokens support. This requires multiple swaps and gas fees, slippage and also learning multiple tools and interfaces which also adds to security risks.

**Long tail Assets and Project tokens :**

* Solvers might not want to hold inventory of these assets and AirLift still allows execution with zero slippage and provides a cost savings compared to the current experience.

**How Airlift solves:**

* No Slippage routes: what you send is what you receive ( minus small fees ) Very very low fees, in our research we would be beating the market on lot of the routes by orders of magnitude savings.
* Single interface without interfacing with multiple token contracts and or bridges yourself. Since the routes are burn - mint routes, user provides the liquidity and there is no limit to this operation. Only limitation is user's balance.
* Assuages worries about liquidity at Dex for assets. LIFI will optimize if it is better to swap and then transfer or transfer and then swap at destination.


# Supported Chains & Tokens

## Chains

| Chain ID | Chain Name        | Airlift Address                            |
| -------- | ----------------- | ------------------------------------------ |
| 1        | Ethereum          | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 10       | Optimism          | 0x568c2c0C94B85B23E1C3Cf3E79D51b1566C8F663 |
| 14       | Flare             | 0x0905E21AEf470adEf9Cf9dCf7922431610c875b5 |
| 30       | Rootstock (RSK)   | 0xB303FEb7c38B7fbee5Bcb0dba2aCC1171478E6D5 |
| 56       | BNB Chain         | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 88       | Viction           | 0x2aEEC3AD32B39Fc67a1284Ea23a59eD986c3C47b |
| 100      | Gnosis Chain      | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 130      | Unichain          | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 137      | Polygon PoS       | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 143      | Monad             | 0x290D54179960984599F16F77DCDA81320301b158 |
| 146      | Sonic             | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 252      | Fraxtal           | 0x290D54179960984599F16F77DCDA81320301b158 |
| 324      | zkSync Era        | 0xA0246202aC2537C9978BE8940D51dc70b3d38426 |
| 592      | Astar             | 0x290D54179960984599F16F77DCDA81320301b158 |
| 747      | Flow EVM          | 0x51729fd7638111E05Ca5f30435e26da53b08816a |
| 988      | Stable            | 0x51729fd7638111E05Ca5f30435e26da53b08816a |
| 999      | Hyperliquid       | 0x290D54179960984599F16F77DCDA81320301b158 |
| 1101     | Polygon zkEVM     | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 1135     | Lisk              | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 1329     | Sei               | 0x290D54179960984599F16F77DCDA81320301b158 |
| 1480     | Islander / Vana   | 0x290D54179960984599F16F77DCDA81320301b158 |
| 1672     | Pharos            | 0x290D54179960984599F16F77DCDA81320301b158 |
| 1868     | Soneium           | 0x290D54179960984599F16F77DCDA81320301b158 |
| 1923     | Swell             | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 2020     | Ronin             | 0x290D54179960984599F16F77DCDA81320301b158 |
| 2741     | Abstract          | 0x5501e28f588b3A246a590a391A7Ae7B2bA065ddE |
| 4217     | Tempo             | 0x444b2Cf29df464894411e9e558384Caa683C2204 |
| 4326     | MegaETH           | 0xa908db975dCC10c41AF59572FD40AeE46942630E |
| 4663     | Robinhood         | 0x290D54179960984599F16F77DCDA81320301b158 |
| 5000     | Mantle            | 0x686893D73fA3817A13217E858652A4866B096dCa |
| 8453     | Base              | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 9745     | Plasma            | 0x290D54179960984599F16F77DCDA81320301b158 |
| 34443    | Mode              | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 42161    | Arbitrum One      | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 42793    | Etherlink         | 0x290D54179960984599F16F77DCDA81320301b158 |
| 42220    | Celo              | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 43114    | Avalanche C-Chain | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 50104    | Sophon            | 0x51729fd7638111E05Ca5f30435e26da53b08816a |
| 57073    | Ink               | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 59144    | Linea             | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 60808    | Bob               | 0x290D54179960984599F16F77DCDA81320301b158 |
| 747474   | Katana            | 0x290D54179960984599F16F77DCDA81320301b158 |
| 80094    | Bera              | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 81457    | Blast             | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 534352   | Scroll            | 0x023fa838682C115c2cFBA96Ef3791CB5Bd931Fc7 |
| 5734951  | Jovay             | 0x290D54179960984599F16F77DCDA81320301b158 |
| 21000000 | Corn              | 0x290D54179960984599F16F77DCDA81320301b158 |

***

## Tokens

{% hint style="info" %}
This list is incomplete, and is being deprecated in lieu of our [Glacis Ecosystem page](https://glacislabs.com/ecosystem), which provides real-time updates to our available routes the moment they are published.
{% endhint %}

<table><thead><tr><th width="189.96875">Name</th><th>Token Contract</th></tr></thead><tbody><tr><td>weETH</td><td><a href="https://etherscan.io/address/0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee">0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee</a></td></tr><tr><td>USDe</td><td><a href="https://etherscan.io/address/0x4c9edd5852cd905f086c759e8383e09bff1e68b3">0x4c9edd5852cd905f086c759e8383e09bff1e68b3</a></td></tr><tr><td>sUSDe</td><td><a href="https://etherscan.io/address/0x9d39a5de30e57443bff2a8307a4256c8797a3497">0x9d39a5de30e57443bff2a8307a4256c8797a3497</a></td></tr><tr><td>STONE</td><td><a href="https://etherscan.io/token/0x7122985656e38bdc0302db86685bb972b145bd3c#code">0x7122985656e38bdc0302db86685bb972b145bd3c</a></td></tr><tr><td>rsETH</td><td><a href="https://etherscan.io/address/0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7">0xA1290d69c65A6Fe4DF752f95823fae25cB99e5A7</a></td></tr><tr><td>ZRO</td><td><a href="https://etherscan.io/address/0x6985884c4392d348587b19cb9eaaf157f13271cd#code">0x6985884c4392d348587b19cb9eaaf157f13271cd</a></td></tr><tr><td>MIM</td><td><a href="https://etherscan.io/token/0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3#code">0x99D8a9C45b2ecA8864373A26D1459e3Dff1e17F3</a></td></tr><tr><td>PEPE</td><td><a href="https://arbiscan.io/address/0x25d887Ce7a35172C62FeBFD67a1856F20FaEbB00">0x25d887Ce7a35172C62FeBFD67a1856F20FaEbB00</a></td></tr><tr><td>WBTC</td><td><a href="https://etherscan.io/address/0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599">0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599</a></td></tr><tr><td>ANGLE</td><td><a href="https://etherscan.io/address/0x31429d1856ad1377a8a0079410b297e1a9e214c2">0x31429d1856ad1377a8a0079410b297e1a9e214c2</a></td></tr><tr><td>USDA</td><td><a href="https://etherscan.io/address/0x0000206329b97DB379d5E1Bf586BbDB969C63274">0x0000206329b97DB379d5E1Bf586BbDB969C63274</a></td></tr><tr><td>EURA</td><td><a href="https://etherscan.io/address/0x1a7e4e63778b4f12a199c062f3efdd288afcbce8">0x1a7e4e63778B4f12a199C062f3eFdD288afCBce8</a></td></tr><tr><td>BTC.b</td><td><a href="https://snowtrace.io/address/0x152b9d0FdC40C096757F570A51E494bd4b943E50/contract/43114/writeContract?chainid=43114">0x152b9d0FdC40C096757F570A51E494bd4b943E50</a></td></tr><tr><td>USDtb</td><td>0xC139190F447e929f090Edeb554D95AbB8b18aC1C</td></tr><tr><td>W</td><td><a href="https://etherscan.io/address/0xc072b1aef336edde59a049699ef4e8fa9d594a48#readProxyContract">0xc072B1AEf336eDde59A049699Ef4e8Fa9D594A48</a></td></tr><tr><td>LINK</td><td>0x514910771AF9Ca656af840dff83E8264EcF986CA</td></tr><tr><td>wstETH</td><td><a href="https://etherscan.io/token/0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0">0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0</a></td></tr><tr><td>ENA</td><td>0x57e114B691Db790C35207b2e685D4A43181e6061</td></tr><tr><td>AVAIL</td><td><a href="https://basescan.org/address/0xd89d90d26b48940fa8f58385fe84625d468e057a#readProxyContract">0xd89d90d26b48940fa8f58385fe84625d468e057a</a></td></tr><tr><td>stAVAIL</td><td>0x3742f3Fcc56B2d46c7B8CA77c23be60Cd43Ca80a</td></tr><tr><td>USDT</td><td><a href="https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7">0xdac17f958d2ee523a2206206994597c13d831ec7</a></td></tr><tr><td>GHO</td><td>0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f</td></tr><tr><td>Bold</td><td><a href="https://etherscan.io/address/0xb01dd87B29d187F3E3a4Bf6cdAebfb97F3D9aB98">0xb01dd87B29d187F3E3a4Bf6cdAebfb97F3D9aB98</a></td></tr><tr><td>frxETH</td><td>0xFC00000000000000000000000000000000000006</td></tr><tr><td>frxUSD</td><td>0xFc00000000000000000000000000000000000001</td></tr><tr><td>LBTC</td><td>0x8236a87084f8b84306f72007f36f2618a5634494</td></tr><tr><td>sfrxETH</td><td>0xFC00000000000000000000000000000000000005</td></tr><tr><td>sfrxUSD</td><td>0xfc00000000000000000000000000000000000008</td></tr><tr><td>stTAO</td><td>0xb60acd2057067dc9ed8c083f5aa227a244044fd6</td></tr><tr><td>tETH</td><td>0xd11c452fc99cf405034ee446803b6f6c1f6d5ed8</td></tr><tr><td>uniBTC</td><td>0x004e9c3ef86bc1ca1f0bb5c7662861ee93350568</td></tr><tr><td>VRTX</td><td>0xd0728f5b1f53a834f8dcd1b86f62ceb8726eb0a0</td></tr><tr><td>wOETH</td><td>0xdcee70654261af21c44c093c300ed3bb97b78192</td></tr><tr><td>xSolvBTC</td><td>0xd9d920aa40f578ab794426f5c90f6c731d159def</td></tr><tr><td>L3</td><td><a href="https://etherscan.io/address/0x88909D489678dD17aA6D9609F89B0419Bf78FD9a">0x88909D489678dD17aA6D9609F89B0419Bf78FD9a</a></td></tr><tr><td>BRZ</td><td>0x01d33FD36ec67c6Ada32cf36b31e88EE190B1839</td></tr><tr><td>SolvBTC</td><td>0x7A56E1C57C7475CCf742a1832B028F0456652F97</td></tr><tr><td>USD0</td><td><a href="https://etherscan.io/address/0x73A15FeD60Bf67631dC6cd7Bc5B6e8da8190aCF5">0x73A15FeD60Bf67631dC6cd7Bc5B6e8da8190aCF5</a></td></tr><tr><td>USD0++</td><td><a href="https://etherscan.io/token/0x35D8949372D46B7a3D5A56006AE77B215fc69bC0">0x35D8949372D46B7a3D5A56006AE77B215fc69bC0</a></td></tr><tr><td>USUAL</td><td><a href="https://etherscan.io/address/0xC4441c2BE5d8fA8126822B9929CA0b81Ea0DE38E">0xC4441c2BE5d8fA8126822B9929CA0b81Ea0DE38E</a></td></tr><tr><td>DOLO</td><td>0x0F81001eF0A83ecCE5ccebf63EB302c70a39a654</td></tr><tr><td>FLUID</td><td><a href="https://etherscan.io/address/0x6f40d4a6237c257fff2db00fa0510deeecd303eb">0x6f40d4a6237c257fff2db00fa0510deeecd303eb</a></td></tr><tr><td>USDM</td><td><a href="https://etherscan.io/token/0x59d9356e565ab3a36dd77763fc0d87feaf85508c">0x59d9356e565ab3a36dd77763fc0d87feaf85508c</a></td></tr><tr><td>LUMIA</td><td><a href="https://etherscan.io/token/0xD9343a049D5DBd89CD19DC6BcA8c48fB3a0a42a7">0xD9343a049D5DBd89CD19DC6BcA8c48fB3a0a42a7</a></td></tr><tr><td>BETS</td><td><a href="https://etherscan.io/address/0x94025780a1ab58868d9b2dbbb775f44b32e8e6e5">0x94025780a1ab58868d9b2dbbb775f44b32e8e6e5</a></td></tr><tr><td>USD1</td><td><a href="https://etherscan.io/token/0x8d0d000ee44948fc98c9b98a4fa4921476f08b0d">0x8d0d000ee44948fc98c9b98a4fa4921476f08b0d</a></td></tr><tr><td>USR</td><td><a href="https://etherscan.io/address/0x66a1e37c9b0eaddca17d3662d6c05f4decf3e110">0x66a1e37c9b0eaddca17d3662d6c05f4decf3e110</a></td></tr><tr><td>RED</td><td><a href="https://etherscan.io/token/0xc43C6bfeDA065fE2c4c11765Bf838789bd0BB5dE">0xc43C6bfeDA065fE2c4c11765Bf838789bd0BB5dE</a></td></tr><tr><td>STAR</td><td><a href="https://basescan.org/address/0xC19669A405067927865B40Ea045a2baabbbe57f5">0xC19669A405067927865B40Ea045a2baabbbe57f5</a></td></tr><tr><td>HYPER</td><td><a href="https://etherscan.io/token/0x93a2db22b7c736b341c32ff666307f4a9ed910f5">0x93a2db22b7c736b341c32ff666307f4a9ed910f5</a></td></tr><tr><td>CNDY</td><td><a href="https://explorer.etherlink.com/address/0x6b43732a9AE9F8654d496c0A075Aa4Aa43057A0B">0x6b43732a9AE9F8654d496c0A075Aa4Aa43057A0B</a></td></tr><tr><td>WXTZ</td><td><a href="https://explorer.etherlink.com/address/0xc9B53AB2679f573e480d01e0f49e2B5CFB7a3EAb">0xc9B53AB2679f573e480d01e0f49e2B5CFB7a3EAb</a></td></tr></tbody></table>


# Architecture

The Airlift operation is built on a dual-layer architecture that includes both off-chain and on-chain components, working together to enable seamless cross-chain token transfers.

### Off-Chain Components

* API:\
  Acts as the primary interface for users and integrators. It provides endpoints for discovering supported routes, retrieving fee quotes, initiating transfers, and tracking their progress. The API ensures abstraction and simplicity for applications interacting with the Airlift system.
* Indexer:\
  A continuously running service that monitors all supported blockchains in real time. It listens for relevant smart contract events (e.g., token transfers, bridging events) and provides the API with up-to-date data, such as route availability, estimated transfer duration, and gas costs. The indexer ensures accurate and efficient off-chain coordination.

### On-Chain Components

* Airlift Smart Contracts:\
  Deployed on each supported chain, these contracts are the core of the bridging mechanism. They expose functions such as `send(...)`, which initiates a cross-chain transfer. The smart contracts handle token-specific bridging logic depending on the token Standard.


# On-Chain Interface

Airlift is available on the following Mainnet chains (in order of chain ID):

{% columns %}
{% column %}

* Ethereum&#x20;
* Optimism
* Flare
* Rootstock
* Binance Smart Chain
* Gnosis
* Unichain
* Polygon PoS
* Sonic
* Fraxtal
* zkSync
* Astar
* HyperEVM
* Polygon zkEVM
* Lisk
* Sei
* Vana
* Soneium
* Swellchain
* Ronin
  {% endcolumn %}

{% column %}

* Abstract
* Mantle
* Base
* Plasma
* Mode
* Arbitrum One
* Etherlink
* Celo
* Avalanche C-Chain
* Sophon
* Ink
* Linea
* BOB
* Katana
* Berachain
* Blast
* Scroll
* Corn
* Solana
  {% endcolumn %}
  {% endcolumns %}

{% hint style="warning" %}
If you would like to use Airlift on a chain that was not listed above, please contact us.
{% endhint %}

There is a single point of entry for Airlift's smart contracts: the `send` function.

```solidity
function send(
    address token, 
    uint256 amount, 
    bytes32 receiver, 
    uint256 destinationChainId, 
    address refundAddress,
    bytes32 outputToken
) payable returns (bytes memory)
```

This send function allows you to send any token registered to Airlift across chains. The parameters are as follows:

* `token` - The address of the token that you wish to send across chains.
* `amount` - The amount of the token that you wish to send across chains.
* `receiver` - The address of the receiver in bytes32 format. If an ethereum based wallet, encode the address as packed.
* `destinationChainId` - The EVM Chain ID of the destination chain. If the destination chain is not an EVM, special IDs will be allocated.
* `refundAddress` - The EVM address that the GMP addresses should refund unused cross-chain gas to in case too much was provided.
* `outputToken` - The address of the token you expect to receive on the destination chain in bytes32 format. If an ethereum based address, encode the address as packed.

Note that the send function is payable. Native gas currency must be included to pay for the cross-chain transfer, since the destination chain transfer must be incentivized. The amount required can be estimated via our API's quote endpoint.


# Off-Chain Interface

## Overview

The Airlift API provides programmatic access to available\
routes for bridging assets across chains, fee quotes for transfers, and transaction status.\
It supports fetching transaction metadata, querying available routes, and\
estimating costs for operations like bridging.

Base URL: <https://airlift.prod.glacis-api.network/v1/>

***

## 🔐 Authentication

Endpoints require authentication, pass your API key in the request header:

```http
x-apikey: YOUR_API_KEY
```

***

## 📘 Endpoints

### Get Routes

**Endpoint:** GET /routes

**Security:** API key

**Query Parameters:**

* `isEnabled` (boolean, optional): Filter by enabled routes.
* `limit` (integer, optional): Number of results per page. Default is 100.
* `offset` (integer, optional): Pagination offset. Default is 0.

**Response:**

Returns a list of supported routes with fields such as:

* `fromChainName`, `toChainName`
* `standard` (bridge protocol)
* `estimatedGas`, `estimatedDuration`
* `minTransferSize`, `maxTransferSize`
* Token info (`tokenName`, `tokenId`)

**Example:** List Routes

```shell
curl -H "x-apikey: YOUR_API_KEY" "https://airlift.prod.glacis-api.network/v1/routes?limit=1&offset=0"
```

The request above responds with:

```json
{
  "paging": {
    "self": "https://airlift.prod.glacis-api.network/v1/routes?limit=1&offset=0",
    "next": "https://airlift.prod.glacis-api.network/v1/routes?limit=1&offset=1"
  },
  "data": [
    {
      "tokenId": "ANGLE",
      "tokenName": "ANGLE",
      "fromAddress": "0x31429d1856aD1377A8A0079410B297e1a9e214c2",
      "toAddress": "0x58441E37255b09F9f545e9Dc957F1C41658ff665",
      "fromChainId": "1",
      "fromChainName": "Ethereum",
      "toChainId": "10",
      "toChainName": "OP Mainnet",
      "minTransferSize": "0",
      "maxTransferSize": "115792089237316195423570985008687907853269984665640564039457584007913129639935",
      "decimals": 18,
      "estimatedDuration": 210,
      "estimatedGas": 341457,
      "standard": "LayerZeroV1OFTV1-ANGLE",
      "isEnabled": true
    }
  ]
}
```

**Pagination**

For `/routes` endpoint, pagination metadata is included in the `paging` object:

* `prev`: URI for previous page
* `next`: URI for next page
* `self`: URI for current page

***

### Get Fee Quote

**Endpoint:** `POST /quote`

**Security:** API key

**Query Parameters:**

* `fromChain` (string, required): Chain ID of source chain (e.g. 10).
* `toChain` (string, required): Chain ID of destination chain (e.g. 42161).
* `fromToken` (string, required): Source token address (e.g. 0x...).
* `fromAmount` (string, required): Amount to transfer (e.g. 100000000000000000).

**Response:**

Returns an object with the following fields:

* `toAmount`: Net amount after fees.
* `estimatedGas`: Estimated gas usage.
* `estimatedDuration`: Duration in seconds.
* `feeCosts`: Breakdown of bridgeFee and airliftFee (both nativeFee and tokenFee).

**Example:** Get a Quote

```shell
curl -X POST \
  -H "x-apikey: YOUR_API_KEY" \
  "https://airlift.prod.glacis-api.network/v1/quote?fromChain=10&toChain=42161&fromToken=0xB153FB3d196A8eB25522705560ac152eeEc57901&fromAmount=100000000000000000"
```

The request above responds with:

```json
{
  "data": {
    "toAmount": "99500000000000000",
    "estimatedDuration": 49,
    "estimatedGas": 328258,
    "feeCosts": {
      "bridgeFee": {
        "tokenFee": "0",
        "nativeFee": "185295726153960"
      },
      "airliftFee": {
        "tokenFee": "500000000000000",
        "nativeFee": "0"
      }
    }
  }
}
```

***

### Get Transaction Status by Hash

**Endpoint:** `GET /transactions/{txHash}`

**Security:** API key

**Path Parameters:**

* `txHash` (string, required): The transaction hash (must match the pattern  `^0x[0-9a-fA-F]{64}$`).

**Response:**\
Returns the transaction status, which can be one of:

* `PENDING`
* `DONE`
* `FAILED`
* `NOT_FOUND`

Each status has specific substatus values.

**Example:** Fetch Transaction Status

```shell
curl "x-apikey: YOUR_API_KEY" "https://airlift.prod.glacis-api.network/v1/transactions/0xff6e703eb0880718bd44d85715fc9344171ca60b98db90ac9c43315f54b70e84"
```

The request above responds with:

```json
{
  "data": {
    "status": "DONE",
    "substatus": "COMPLETED",
    "sourceTx": "0xff6e703eb0880718bd44d85715fc9344171ca60b98db90ac9c43315f54b70e84",
    "destinationTx": "0x0ac82765ad624ac157ba9740372a37a14535e8f5f7aa593a8227e11b1519ebbb",
    "explorerLink": "https://layerzeroscan.com/tx/0xff6e703eb0880718bd44d85715fc9344171ca60b98db90ac9c43315f54b70e84"
  }
}
```

**🧾 Transaction Status Reference**

Status: `PENDING`

* Substatus values:
  * `WAIT_SOURCE_CONFIRMATIONS`
  * `WAIT_DESTINATION_TRANSACTION`
  * `BRIDGE_NOT_AVAILABLE`
  * `CHAIN_NOT_AVAILABLE`
  * `REFUND_IN_PROGRESS`
  * `UNKNOWN_ERROR`

Status: `DONE`

* Substatus values:
  * `COMPLETED`
  * `PARTIAL`
  * `REFUNDED`

Status: `FAILED`

* Substatus values:
  * `NOT_PROCESSABLE_REFUND_NEEDED`
  * `OUT_OF_GAS`
  * `SLIPPAGE_EXCEEDED`
  * `INSUFFICIENT_ALLOWANCE`
  * `INSUFFICIENT_BALANCE`
  * `UNKNOWN_ERROR`
  * `EXPIRED`

***

## ❗ Error Responses

| Code | Description           |
| ---- | --------------------- |
| 400  | Bad Request           |
| 404  | Not Found             |
| 500  | Internal Server Error |
| 503  | Server Unavailable    |

Error format:

```json
{
  "error": "Bad Request"
}
```

***

## 🧩 Token Standards Supported

The standard field in routes refers to one of:

* `CCT`
* `LayerZero V1 and V2 OFTs`
* `NTT`
* `M0LitePortal`
* `WarpRoute`
* `CCTP`


# Operation Overview

{% hint style="info" %}
To perform these steps, you can use our API in the DEV environment at <https://airlift.dev.glacis-api.network/v1>. To access the operations you'll first need an API key — please request one from our team at <support@glacislabs.com>
{% endhint %}

## Bridging Steps

To perform a cross-chain token transfer using Airlift, you'll need to provide the following information:

* Token address on the source chain
* Source Chain ID
* Destination Chain ID
* Amount to transfer

Once you have this information, follow these steps:

### 1. Check supported routes

Send a request to the `/routes` API endpoint to determine if the desired route is supported.\
The response includes route metadata such as estimated duration and gas costs, powered by the indexer.

### 2. Get a quote

Use the `/quote` API endpoint to retrieve the fees required to execute the transfer.

### 3. Initiate the transfer

Call the send function on the Airlift smart contract using the data returned by the quote.\
This triggers the cross-chain transfer through the appropriate token implementation and returns a transaction hash on the source chain.

### 4. Track the transfer

Periodically query the `/transaction` API endpoint to monitor the status of the cross-chain operation with the TX hash returned in the send operation.

### 5. Receive funds on the destination chain

Once the operation status is marked as DONE, the tokens will be available on the destination chain.

***

## Troubleshooting Airlift

Occasionally there can be problems sending tokens. The most common issue is when a bridging operation occurs on the origin chain, but the destination chain never has tokens minted. This can occur due to a stalled bridge or an improper origin fee estimation.&#x20;

#### Recovery Steps

1. First check to ensure that the tokens have been burnt from your wallet. If they have not been burnt, then there is no problem, and you can retry the transaction later.
2. If tokens have been burnt and your transaction is taking longer than expected, please wait for at least an hour. Occasionally the relayers of bridges stall and/or miss a transaction, which will be picked up at a later time, potentially when gas prices are lower.
3. If tokens have been burnt and the transaction has been pending for greater than an hour, go through the recovery flow below.

#### Stalled & Failing Destination Transactions

{% hint style="danger" %}
If you have burnt a token, and the token never gets minted on the destination chain, please go through one of the support rails specific to the bridge that you have used.&#x20;

You can tell which bridge that you have used based on its transaction status results.
{% endhint %}

First check the scan link to see if there are recovery options available. If not, go through the support link to check in with their respective teams to find recovery options.

<table><thead><tr><th width="140.96484375">Bridge</th><th>Scan Link</th><th>Support Link</th></tr></thead><tbody><tr><td>Wormhole</td><td><a href="https://wormholescan.io/">Wormhole Scan</a></td><td><a href="https://discord.com/invite/wormholecrypto">Wormhole Discord</a></td></tr><tr><td>LayerZero</td><td><a href="https://layerzeroscan.com/">LayerZero Scan</a></td><td><a href="https://docs.layerzero.network/community">LayerZero Support Docs</a></td></tr><tr><td>Chainlink</td><td><a href="https://ccip.chain.link/">Chainlink CCIP Explorer</a></td><td><a href="https://discord.com/invite/2YHSAey">Chainlink Discord</a></td></tr><tr><td>Hyperlane</td><td><a href="https://explorer.hyperlane.xyz/">Hyperlane Explorer</a></td><td><a href="https://docs.hyperlane.xyz/docs/guides/deploy-hyperlane-troubleshooting">Hyperlane Troubleshooting Docs</a></td></tr></tbody></table>

If all else fails, or if the issue is not related to a bridge stall or an improper origin fee estimation, then please reach out to us in our [official telegram channel](https://t.me/+ZoFLE1gLsV9hY2Fh). Issues with the UI, repeated improper estimations, and failing smart contract calls ought to be reported.


# Send & Execute

The natural progression of Airlift's token send function includes execution with that token on the destination chain: regardless of whether the user intends to swap, stake, vault, or farm their tokens.

{% hint style="warning" %}
Airlift's send & execute functionality is still in development. Please reach out to us directly!
{% endhint %}

Some token standards already have a send + execute functionality built in. When sending & executing with these standards, Airlift will default to using the built-in rails.

The token standards that do not implement this sort of functionality must be temporarily escrowed before a secondary cross-chain instruction arrives via Glacis core or an alternative protocol. To escrow properly, all transactions will be done through a smart wallet designated to the original user.


# Why Glacis?

As the blockchain ecosystem evolves, the complexity, fragmentation, and security risks of cross-chain technology continues to grow. Glacis is built so that developers can decouple their applications from the underlying cross chain transport layer. This helps developers manage and control cross chain risks.

Let’s take a deeper look:

## Abstraction

Building your cross-chain dApp with Glacis will significantly reduce the risk you take when choosing a GMP provider. With Glacis, you have the ability to choose the GMP(s) through which your messages are sent at any time. Being able to change your GMPs at any time with no developer interface changes is helpful in cases where a GMP has a service outage, security issue or unexpected change in terms of use to ensure continuity of your application.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-86abde9dbbf1091ba2a32f4aa5531e44f9fc0e5e%2FGlacis%20Abstraction%20Flow.png?alt=media)

***

## Access Control

All Glacis-powered smart contracts must have access control enabled within them to simulate a secure firewall. This allows developers to granularly determine which smart contracts, which chains, and which GMP services they trust. Glacis provides the [GlacisClient](/glacis-core/references/smart-contracts/glacisclient) base smart contract for developers to help with access control.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-ea21e12e2f8bcbfedab0e73ff4235d5ca9514bfa%2FGlacis%20Access%20Control%20Flow.png?alt=media)

***

## Redundancy

Through a redundancy and quorum system, Glacis allows a single cross-chain message to be sent through multiple GMPs. This can increase speed and reliability when accepting execution upon receiving a message from 1 GMP out of n, but also significantly increase security by increasing message quorum (x GMP out of n).

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-3f3d99be8c3321df58e58f8b4d6678156218a8b3%2FGlacis%20Redundancy%20Flow.png?alt=media)

***

## Retry Management

Occasionally, GMP protocols can be finicky as the infrastructure continues to develop. Oracles, GMP consensus, relayers: there are many points of failure and messages can be easily lost at multiple spots along the way. Glacis allows identical messages (with the same message ID) to be resent from the original smart contracts. This is more secure than simply sending an additional message, since lost messages that are found again can be used in replay attacks. With Glacis, a successful secondary message will nullify the previous message and vice-versa.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2FkrdsmxfZYWzoOsiRdHhR%2FGlacis%20Message%20Retry%20Flow.png?alt=media\&token=5b2a641a-cf0b-4c8e-b505-483a239d1342)

***

## Intelligent Routing

Glacis provides applications with the capability to intelligently route messages based on their importance or value. High-value messages, which may include, high economic value, critical alerts, urgent updates, or sensitive information, can be routed through one GMP equipped with advanced features such as enhanced security protocols, priority queuing, and robust error-handling mechanisms. Conversely, low-value messages, which might consist of routine updates, non-urgent notifications, or general information, can be directed through an alternative messaging pathway that is optimized for cost-effectiveness and scalability, rather than speed or security.

### Concepts

* GMP — stands for "General Message Passing", and loosely refers to the protocols that provide the service. GMP is general because the messages have any type of encoded data within them, not just token bridging.
* Message ID — each message sent by Glacis has its own message ID, which is determined by the content of the message, its owner, and a nonce.
* Adapters — each GMP protocol that Glacis supports has its own adapter smart contract, which communicates to and for the [GlacisRouter](/glacis-core/references/smart-contracts/glacisclient).


# Getting Started

In this quickstart guide, you will be deploying a Glacis-powered smart contract that sends a string message across chains: from Avalanche Fuji Testnet to Arbitrum Sepolia Testnet.

To begin working with Glacis smart contracts, you can work with a starter client smart contract. You can easily access this on Remix.

[Click Here to Open in Remix](https://remix.ethereum.org/#optimize=false\&runs=200\&evmVersion=null\&version=soljson-v0.8.18+commit.87f61d96.js\&url=https://raw.githubusercontent.com/jboetticher/glacis-alpha-quickstart-remix-template/main/GlacisTextSample.sol\&lang=en)!

Please ensure that you understand how to [use and deploy with Remix](https://remix-ide.readthedocs.io/en/latest/run.html) before starting this guide.

#### Glacis Client

The `GlacisClient` smart contract provides a full interface for your cross-chain smart contract to communicate with the GlacisRouter. You can send messages via [`_route` and its other derivative functions](/glacis-core/references/smart-contracts/glacisclient#the-route-function), and receive messages by overriding the `_receiveMessage` function.

In this example, you can send and receive a string across chains:

```solidity
contract GlacisClientTextSample is GlacisClientOwnable {
    string public currentMessage;

    constructor(
        address glacisRouter_,
        address owner_
    ) GlacisClientOwnable(glacisRouter_, 1, owner_) {}

    function sendMessage(
        address to,
        uint256 chainId,
        string memory message,
        uint8[] memory gmps,
        uint256[] memory fees
    ) external payable returns (bytes32) {
        return
            _route(
                chainId,
                to,
                abi.encode(message),
                gmps,
                fees,
                msg.sender,
                false,
                msg.value
            );
    }

    function _receiveMessage(
        uint8[] calldata, // fromGmpId,
        uint256, // fromChainId,
        address, // fromAddress,
        bytes memory payload
    ) internal override {
        (currentMessage) = abi.decode(payload, (string));
    }
}
```

To explain what's happening, let's go step by step.

The constructor includes construction of an ownable version of the GlacisClient:

```solidity
GlacisClientOwnable(glacisRouter_, 1, owner_)
```

It requires an instance of the `GlacisRouter` smart contract so that it can send and receive messages. `1` refers to quorum (a [redundancy](/glacis-core/concepts/features/redundancy) feature). Finally, the contract is owned by the designated user, which will be relevant for initialization later.

```solidity
    function sendMessage(
        address to,
        uint256 chainId,
        string memory message,
        uint8[] memory gmps,
        uint256[] memory fees
    ) external payable returns (bytes32) {
        return
            _route(
                chainId,                // destination chain ID
                to,                     // destination address
                abi.encode(message),    // payload
                gmps,                   // gmps
                fees,                   // fees
                msg(sender),            // refundAddress
                msg.value               // payment
            );
    }
```

To send a message across chains, this smart contract includes a `sendMessage` function, which is arbitrarily named. Within it is a call to the `_route` function, which is what sends the message to the `GlacisRouter` smart contract.

Note that there are a couple of inputs within the `sendMessage` function. First is the address of the contract that the message is being sent to, known as your destination address (`to`). The `chainId` is the Glacis Chain ID that the message is being sent to, which is typically the Ethereum chain ID. Then the message string that's being sent. The `gmps` and `fees` arrays should be the same length, and are important if you plan on sending a redundant message (the same message being sent through multiple GMPs).

The `_route` function includes much of these inputs as well as other configurations about the message, which you can learn more about on the [GlacisClient](/glacis-core/references/smart-contracts/glacisclient#the-route-function) page.

```solidity
    function _receiveMessage(
        uint8[] calldata, // fromGmpId,
        uint256, // fromChainId,
        address, // fromAddress,
        bytes memory payload
    ) internal override {
        (value) = abi.decode(payload, (string));
    }
```

The receive message function can only be triggered by the GlacisRouter when it receives a cross-chain message. This function will inject information about the origins of the message, but in this case the only information desired is the message itself. Note that all payloads are encoded into bytes and received as bytes, but they can be easily encoded and decoded through `abi.encode` and `abi.decode`.

#### GlacisClient Initialization

You can add this entire smart contract to your project and deploy it to [Avalanche Fuji](https://core.app/en/tools/testnet-faucet/?subnet=c\&token=c) with the following information in the constructor:

```solidity
GlacisClientTextSample(0x1Ce678F0e7834713868877C34F84C2cfaf511aFe, INSERT_YOUR_WALLET_ADDRESS_HERE)
```

Constructing it with the `0x1Ce678F0e7834713868877C34F84C2cfaf511aFe` address works because a GlacisRouter has been pre-deployed to this address on Avalanche Fuji.

<figure><img src="https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2FXK56AeAnCwgR0W4alxTs%2FScreenshot%202024-08-29%20at%2011.15.46%E2%80%AFAM.png?alt=media&amp;token=7803a10b-a3aa-4db4-a051-d9e42c4604d8" alt=""><figcaption><p>Deploy on Avalanche Fuji Testnet</p></figcaption></figure>

After deployment, access control must be initialized. Since this example is using the ownable version of `GlacisClient`, we can use the `addAllowedRoute` function.

```solidity
addAllowedRoute(GlacisCommons.GlacisRoute {
    fromChainId,    // WILDCARD means any chain
    fromAddress,    // WILDCARD means any address
    fromAdapter     // WILDCARD means any official Glacis GMP Adapter
})
```

To allow messages from all smart contracts, chains, and official Glacis adapters, you can pack all parameters with the wildcard value, which is just `type(uint160).max` encoded in different ways:

```
addAllowedRoute([
    "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF",
    "0x000000000000000000000000ffffffffffffffffffffffffffffffffffffffff",
    "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF"
])
```

Copy the parameters and execute the `addAllowedRoute` funciton in Remix.

Typically you would only allow smart contracts from a small subset of chains, but this is for demonstration purposes.

Now try deploying and initializing an instance of this smart contract on [Arbitrum Sepolia Testnet](https://bwarelabs.com/faucets/arbitrum-sepolia) too! This way you can send a message from one chain to the other. The constructor would look like the following:

```solidity
GlacisClientTextSample(0x51f4510b1488d03A4c8C699fEa3c0B745a042e45, INSERT_YOUR_WALLET_ADDRESS_HERE)
```

Don't forget to call `addAllowedRoute` on Arbitrum Sepolia Testnet as well!

By the end of this process, you should have two addresses: `AVALANCHE_FUJI_INSTANCE_ADDRESS` and `ARB_TESTNET_INSTANCE_ADDRESS`.

#### Sending a Message

You can now send a cross-chain message by interacting with your deployed instance's `sendMessage` function. Try sending one from Avalanche Fuji to Arbitrum Sepolia with the following call:

```solidity
function sendMessage(
    ARB_TESTNET_INSTANCE_ADDRESS, 
    421614, 
    "Hello World", 
    ["0x0000000000000000000000000000000000000001"], 
    [[0, 500000000000000000]]
) 
```

* Set `to` as the `ARB_TESTNET_INSTANCE_ADDRESS`
* Set `chainId` to Arbitrum Sepolia's chain ID (`421614`)
* Set whatever you like, such as `Hello World`, as the message
* Use `["0x0000000000000000000000000000000000000001"]` as the adapters to indicate Axelar (with a GMP ID of 1)
* Set `[[0, 500000000000000000]]` for the fees. Note that you will have to **send this message with `500000000000000000 wei`** as well to pay for cross-chain gas. This value is artificially high to ensure that the transaction executes

<figure><img src="https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2FWnqbxm5kQ8Qo9E4Ajgv7%2FScreenshot%202024-08-29%20at%2010.14.11%E2%80%AFAM.png?alt=media&amp;token=c42c70ea-65dc-4417-867b-28acc3f0b6df" alt=""><figcaption><p>Send message to Arbitrum Sepolia</p></figcaption></figure>

Invoking this call would send a message from the origin chain, through a GMP, and to the destination Arbitrum Sepolia Testnet.

In this case, since the adapter array has only `1` set, the only GMP used is Axelar. You would be able to see a cross-chain message on [Axelarscan](https://testnet.axelarscan.io/).

<figure><img src="https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2F9DWXBKDBjNYvPsZ6K0Ns%2FScreenshot%202024-08-29%20at%2011.11.07%E2%80%AFAM.png?alt=media&amp;token=da191bfb-da70-46d3-9a64-898cd3c9bec5" alt=""><figcaption><p>Checking Avalanche Fuji to Arbitrum Sepolia on Axelarscan</p></figcaption></figure>

If you sent the message, congratulations! You have sent your first message with Glacis. You can also try with GMPs of `[2]` or `[3]` to try it out with LayerZero or Wormhole.

The next step would be to get associated with some of the more complex ideas of Glacis and start deploying smart contracts on multiple chains:

* Get up to speed with the [GlacisClient](/glacis-core/references/smart-contracts/glacisclient)
* Understand how [Glacis redundancy works](/glacis-core/concepts/features/redundancy)
* Use [access control](/glacis-core/concepts/features/access-control) to firewall your cross-chain contracts
* Explore [Glacis Cross-Chain Token](/glacis-core/concepts/features/glacis-cross-chain-token) to pass your custom token across chains


# Concepts


# Architecture

Glacis is a pure on-chain protocol, that means that all its components are Smart Contracts, these components can be aggregated in two groups:

* **Infrastructure Components**: Maintained by Glacis and deployed on each supported chain.
  * Router
  * Adapters
  * Mediators
* **Client Components**: Maintained by Glacis,accessible as package and deployed by user through the use of inheritance on his own contracts.
  * Clients

> All components have different behavior depending if they act as a source chain component or a destination chain component.

## Component diagram

This diagram represents the complete flow between components in Glacis Architecture.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-9bde4ba40bd7191675eb8bc3f06bd04cd447597b%2FGlacis%20Full%20Component%20Diagram.png?alt=media)


# Components

## Infrastructure Components

### Router

The Router is the core Glacis component, being part of Glacis infrastructure too, it is deployed on every supported chain.

The main tasks of the Router are:

As source component:

* Route to the different adapters in solo o multi mode.
* Perform specific registry tasks to support retry management.

As destination component:

* Gather messages of the different adapters.
* Verify if quorum is reached.
* Check if access control for this request is allowed in destination client.

The Router is implemented as Hub design pattern, this pattern facilitates communication and coordination between multiple components or modules without them needing direct references to each other.

There is one router:

* [GlacisRouter](https:/github.com/glacislabs/v1-core/blob/main/contracts/routers/GlacisRouter.sol): To send messages with Glacis features.

***

### Mediators

Mediators are part of Glacis infrastructure so they are already deployed on every supported chain. They make use of Glacis router and their main task is to provide additional functionality before a message is routed and after a message is received from the router.

For example Glacis Token Mediator is responsible for:

As source component:

* Burning sender tokens prior to sending the message.
* Wrap Token payload to original payload

As destination component:

* Minting receiver tokens in remote chain after receiving a message from a remote GlacisTokenMediator.
* Unwrap Token payload from received payload

The Mediator pattern in software design serves to manage complex communications and interactions between multiple objects or components in a system. Its primary role is to reduce direct communications between objects, by minimizing direct references between objects, decreasing the coupling among them. This means changes in one object's behavior or interface are less likely to have a ripple effect requiring changes in many other objects.

There is currently one mediator:

* [GlacisTokenMediator](https:/github.com/glacislabs/v1-core/blob/main/contracts/mediators/GlacisTokenMediator.sol): To send tokens and messages with Glacis features.

***

### Adapters

Adapters are the connector between the different GMP gateways and the router, they offer a unified interface for the different GMP providers, they contain an instance of the adaptee (target GMP interface) and translates the router requests into appropriate calls to the adaptee's methods.

The main tasks of the Adapters are:

As source component:

* Translate the Glacis Router message into GMP message format.
* Send the message to GMP

As destination component:

* Receive the message from GMP
* Translate the GMP message into Glacis Router message format.

The Adapter design pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, converting the interface of one class into another interface that clients expect. This infrastructure is deployed in every supported chain

There are currently the following adapters:

* [LayerZeroAdapter.sol](https://github.com/glacislabs/v1-core/blob/main/contracts/adapters/LayerZero/GlacisLayerZeroAdapter.sol)
* [GlacisAxelarAdapter.sol](https://github.com/glacislabs/v1-core/blob/main/contracts/adapters/GlacisAxelarAdapter.sol)
* [GlacisWormholeAdapter.sol](https://github.com/glacislabs/v1-core/blob/main/contracts/adapters/Wormhole/GlacisWormholeAdapter.sol)
* [GlacisCCIPAdapterAdapter.sol](https://github.com/glacislabs/v1-core/blob/main/contracts/adapters/GlacisCCIPAdapter.sol)
* [GlacisHyperlaneAdapter.sol](https://github.com/glacislabs/v1-core/blob/main/contracts/adapters/GlacisHyperlaneAdapter.sol)

***

## Client Components

Clients are not part of Glacis infrastructure (they are not previously deployed), they serve as a facilitative boilerplate, simplifying user interaction with Glacis.

By inheriting from Glacis Client contracts, users can access to a set of convenient methods to easily leverage Glacis features.

Clients act like a facade to Glacis. Facade design pattern is a structural design pattern that provides a simplified interface to a complex subsystem or set of interfaces. It aims to provide a unified and simplified interface that hides the complexities of the underlying system, making it easier to use. There are currently two Clients:

* [GlacisClient](https://github.com/glacislabs/v1-documentation/blob/main/docs/Concepts/GlacisClient/README.md): To send cross-chain messages with Glacis features.
* [GlacisTokenClient](https://github.com/glacislabs/v1-documentation/blob/main/docs/Concepts/GlacisTokenClient/README.md): To send cross-chain messages and tokens with Glacis features.

Clients can interact with Mediators or Router.

The interactions between the different Glacis components can be seen in the following image:

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-2a0c88ad9744f707a139c89726af9bf0dbcefba6%2FGlacis%20Infrastructure%20Component%20Diagram.png?alt=media)


# Features


# Abstraction

Although all General Message Passing (GMP) protocols are designed to enable the exchange of cross-chain messages, each one features its own unique interface and interaction mechanism. Significant variations exist primarily in terms of:

* Interface: Different function names, parameter number and format
* Security: Requirement of pre-approvement of the destination address
* Identification: Arbitrary chain Codification (chainId)
* Payment: Native or ERC20 acceptance, exact amount or overpay/refund approaches
* Interaction: Payment or price quoting in advance

In the following example source and destination contracts are interacting with a GMP that:

* Requires a separated payment function with refund
* Uses string format for the message payload
* Uses an arbitrary vendor defined chain ID
* Requires to implement a specific reception interface

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-9cff918179deda5ef3249171aff0eaffb837cdc3%2FInteraction%20with%20GMP%20A%20Example.png?alt=media)

In this other example source and destination contracts are interacting with a GMP that:

* Requires an initial query for the current operation price
* Uses byte array format for the message payload
* Uses an arbitrary vendor defined chain ID
* Requires to implement a specific reception interface

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-9c727ce6f1e46fe54789b1023093de3f9646ca57%2FInteraction%20with%20GMP%20B%20Example.png?alt=media)

These variations can be specifically challenging when trying to work with more than one GMP:

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-925641ae3886520a7222767476fe88c9393db717%2FInteraction%20with%20multiple%20GMPs%20Example.png?alt=media)

This translates to:

* Increased complexity: Users or developers have to deal with all the different details of each GMP implementation. New team members have a steeper learning curve of the protocol.
* Less Flexibility: Switching to a new GMP becomes challenging in the event of a hack or downtime of the current one.
* Difficulty in Maintenance: Since the high-level functionality is tightly coupled with the GMP implementation, a change in GMP interface will require rewriting of all the components and related tests.

## Glacis Abstraction

> The Abstraction functionality of Glacis enables users and developers to engage with any compatible General Message Passing (GMP) solution seamlessly, using a singular, streamlined interaction flow, without the necessity to understand the intricate usage details inherent to each solution.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-9edbc799093f51252be121e62e25550cd8a38297%2FGlacis%20Abstraction%20to%20GMP%20A%20Example.png?alt=media)

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-fb23868dfe863f40dfa4a00299ef27120a065337%2FGlacis%20Abstraction%20to%20GMP%20B%20Example.png?alt=media)

When engaging with multiple GMP protocols, the abstraction layer of Glacis ensures a consistent user interface. This means that regardless of the number or type of GMPs involved, users experience a uniform interaction pattern.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-e537955d51459576372bd0794b2aa03920de14e7%2FGlacis%20Abstraction%20to%20multiple%20GMPs%20Example.png?alt=media)


# Access Control

## Cross-chain Access Control Overview

The use of GMP (Generic Message Passing) protocols allows contracts residing on remote chains to interact with and execute functions in local contracts. This interaction is usually achieved without restrictions, meaning that multiple remote contracts on different chains have the ability to perform a wide range of operations or functions on local contracts.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-f8fcbe62f87a95d2468eb267a90d1de56df3b489%2FUnrestricted%20Cross%20Chain%20Access.png?alt=media)

In certain situations, unrestricted access offered by technologies like GMP protocols may not be ideal. Instead, controlled or restricted access is preferred.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-aecf0d456fc3d9a849da5f2120025299a03bc955%2FRestricted%20Cross%20Chain%20Access.png?alt=media)

While some GMP protocols incorporate these type controls, not all do, and moreover, each one presents different mechanisms for their enforcement.

***

## Glacis Access Control

> The Glacis Access Control feature allows destination contract owners to define a standardized list of authorized incoming routes that are able to access their local contracts through the different GMP protocols.

An incoming route is defined by these parameters:

* GMP Id: The Glacis GMP Id that have passed the message
* Chain Id: The Glacis chain ID where the message comes from
* Contract Address: The address of the source contract on remote chain (where the message was originated)

The access control mechanism is based on consulting the destination contract to determine if the incoming route of a message is an authorized one before sending it to the requested address in the destination chain. This task is performed by the Glacis Infrastructure on the destination chain.

If the route is recognized as authorized, the message is then successfully delivered.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-ec4bc5019fd836224238d468c9629f69b63bf40a%2FAuthorized%20Glacis%20Access%20Control.png?alt=media)

In case that the route is not recognized as authorized, the transaction is rejected.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-6a873eca73c1f1e7f27a0c90daad9439f06dc983%2FUnauthorized%20Glacis%20Access%20Control.png?alt=media)

***

### Routing Wildcards

At any time the contract owner can make use of zero values for each one of the authorized incoming route parameters, Glacis will understand these parameters as "any".

* GMP Id = 0 -> The message can arrive from any GMP
* Chain Id = 0 -> The message can arrive from any chain
* Contract Address = "0x" -> The message can arrive from any source address

Combinations of values and wildcards can be used, for example a contract with an authorized route of (0,1,"0x") implies that Glacis will only deliver messages to destination if they come from Ethereum chain regardless the GMP used for message passing or the source contract address.


# Redundancy

Despite the various General Message Passing (GMP) protocols having robust decentralized consensus mechanisms, a compromised GMP could potentially inject unauthorized messages into contracts at any time. To counter this issue, implementing redundancy routing is an effective solution.

Redundancy routing involves sending the same message through multiple pathways. Upon arrival, the destination expects several instances of this message from various different sources. These received messages are then cross-checked to ensure their content is consistent and hasn't been modified by any intermediaries within the network.

## Glacis Redundancy

> The redundancy feature of Glacis enables an application to **simultaneously dispatch a message through various GMP protocols**. On the destination chain, **the message will be forwarded to the intended contract only if a predetermined number of identical messages are received from different GMP protocols**.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-3f3d99be8c3321df58e58f8b4d6678156218a8b3%2FGlacis%20Redundancy%20Flow.png?alt=media)

To be able to account for identical messages, Glacis creates a unique messageId for every routed message.

### Glacis Message Id

Glacis messageId is calculated by serializing and hashing the following routing parameters:

| Type    | Name          | Description                                                               |
| ------- | ------------- | ------------------------------------------------------------------------- |
| uint256 | toChainId     | The Glacis ID of the chain that you wish to send a message to             |
| uint256 | fromChainId   | The Glacis ID of the chain that the message is being sent from            |
| address | to            | The address of the destination contract that you are sending a message to |
| bytes32 | payloadHash   | The hash of the payload that is being sent to destination contract        |
| address | messageSender | The sender of the message                                                 |
| uint256 | nonce         | A calculated nonce                                                        |

As a result a bytes32 messageId is generated on source Glacis Infrastructure, returned to the caller, and encoded along with the message payload.

At the destination, within the Glacis Infrastructure, the messageId is recalculated and verified. **All incoming messages that have matching validated messageIds are regarded as identical**.

### Quorum

Quorum represents the number of identical messages that Glacis needs to receive to effectively send the message to the destination.

> To prevent potential tampering of the quorum, it is established previously on the destination contract rather than being transmitted via the message itself.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-48a811f195aaaa635331e0e0684711b0664958a9%2FSet%20Quorum%20Flow.png?alt=media)

When messages are received on the destination chain, the Glacis Infrastructure will compile all messages from various GMP protocols, accounting for those that are identical.

If a GMP protocol is compromised and the message payload is maliciously altered, this results in a different messageId. Consequently, such messages will be counted in a different messageId accumulator hence separated in the count towards achieving the quorum.

If the number of identical messages is equal to the requested quorum the message is delivered to destination

***

## Message Redundancy Flow

If the application wants to be sure that no GMP protocol has tampered the message the following steps must be performed:

* Set a required quorum on the destination contract
* Route through more than one GMP by specifying a list of glacis GMP IDs.

Upon the first reception of a new message, Glacis will check for the destination smart contract's required quorum and start accumulating for identical messageId receipts.

If the quorum is not reached the message is not delivered to the destination.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-18c0105220042ab0fbc6b31fbc5279972dbcb449%2FNo%20Quorum%20Flow.png?alt=media)

If a message of some message ID is received with identical content and metadata, the message count increases. If it becomes equal to the quorum, the last message is delivered to the destination contract.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-3396dd5f2dfce65171ff0501d5511981d57821a2%2FAchieved%20Quorum%20Flow.png?alt=media)

Any other message arriving with the same Id will increase the number of receipts but that will not trigger a new delivery since the quorum will have already been exceeded.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-31fdc07a07afd94d5125a1df774951c468e8e776%2FExceeded%20Quorum%20Flow.png?alt=media)

***

## Message Redundancy Potential Issues

Key potential challenges associated with redundancy in message passing include:

* Forced Quorum
* Tampered Payload

### Forced Quorum

A compromised GMP might attempt to unilaterally achieve quorum by preempting other GMPs, through dispatching multiple copies of the message, thereby forcibly meeting the quorum requirements.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-de5e611ef5a123f592a24a9354eb910ddcab4cc9%2FForced%20Quorum%20Flow.png?alt=media)

To mitigate this, Glacis keeps a record of which GMP each message arrived from and does not allow the processing of a message that has already been received previously from a GMP.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-35ddd9c8b103f4232ffa8d78eb3c89c4ed655d10%2FMessage%20Already%20Received%20Flow.png?alt=media)

### Tampered Payload

A compromised GMP might attempt to alter the original message payload.

To mitigate this, Glacis calculates on the destination chain the message Id and compares it to the message's Id. If they differ, the message is discarded as an invalid message Id.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-fa9eb48f22057f2abe78584a296fc20fe51fa211%2FTampered%20Payload%20Flow.png?alt=media)

***

## Redundancy Management Mechanism

The process through which Glacis achieves redundancy takes place in the GlacisRouter and unfolds as follows:

### In source chain

When the application executes the **route()** function with a message with retriable true, the following tasks are performed by the router:

1. Increment a unique nonce that is then added to the message payload
2. Create a message Id that includes the routing parameters along with payload hash, message sender and nonce

### In destination chain

Upon the reception of a message from a GMP, the **receiveMessage()** function is executed, performing the following tasks:

1. Get client contract quorum configuration
2. Extract the message receipts for this messageId.
3. Validate there are no previous receipts for this messageId for each requested GMP.
4. Validate that the messageId calculated with all passed parameters is equal to the informed one.
5. Increase the number of unique messages received for this messageId and GMP

If all checks pass, and the number of received messages for this messageId is equal to client quorum configuration, deliver the message to destination.


# Retry Management

## Message Retry Overview

The regular flow of a message involves sending a message from a source contract/chain to a destination contract/chain.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-dfe90e5e575db3d3e1637322f713e1875741dddf%2FMessage%20Regular%20Flow.png?alt=media)

Sometimes a message could remain in a pending delivery status within a GMP for an indeterminate lapse and not be delivered to destination on the expected time.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-2d6526adbafa3deda947df196f5aee4d39e27275%2FDelayed%20Message%20Flow.png?alt=media)

At this moment it could be suspected that the GMP is in a downtime state and (if the message is of high priority) it would be of interest to retry sending the message (usually using a different GMP) to try to reach the destination through other routes as soon as possible.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-5557d8399d590a74397cbb0b328c5990b011f692%2FMessage%20Retry%20Flow.png?alt=media)

However the retry operation can present some issues

***

## Message Retrying Potential Issues

The main potential issues when retrying messages are:

* Message Duplication
* Replay Attack

### Message Duplication

Given the previously mentioned conditions, it might happen that, after the retried message arrives at destination, the previous stalled message is finally released by the initial GMP to the destination as well, in which case a duplicated message would be received (a kind of double spend).

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-8ff1fd6c194c08ceae1ae3e3a54c029299316d29%2FDuplicated%20Message%20Flow.png?alt=media)

### Replay Attack

Again with the previously mentioned conditions a malicious actor in the GMP network could have intercepted our retry message and resend it with different payload as a retry intent.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-4bbfd6329dcc1e8e50ed87cbad3cf5a8024ec71b%2FRetry%20Replay%20Attack%20Flow.png?alt=media)

***

## Glacis Retry Management Feature

> As Glacis is a pure on-chain protocol **it cannot initiate the retrying of a message automatically by itself**, but it offers a retry management feature to **allow applications to retry the sending of a message without any of the potential issues of retrying methods**.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-c1d3c9b3a06e2a16d63dee5f721e78970144f60b%2FGlacis%20Message%20Retry%20Flow.png?alt=media)

If an application wants to retry a message it must first perform an initial routing with "retriable" parameter as true using the route function of Glacis Router.

```solidity
    function route(
        uint256 chainId,
        address to,
        bytes memory payload,
        uint8[] memory gmps,
        uint256[] memory fees,
        address refundAddress,
        bool retriable
    ) public payable virtual returns (bytes32)
```

This activates Glacis retry management feature at Glacis source infrastructure, which causes (among other things) the emission of a MessageIdCreated event with the following event parameters:

* MessageId: The calculated unique Glacis message identification
* nonce: A unique accumulated number for each Message Id creation

The application needs to subscribe to this event and extract and store these event parameters since the messageId and nonce are required for routing retrying.

If the application detects that a message did not arrive at destination in time and wants to retry sending the message it can do it through the use of retryRoute function specifying all the routing parameters plus the messageId and nonce of the message that wants to be retried.

```solidity
    function routeRetry(
        uint256 chainId,
        address to,
        bytes memory payload,
        uint8[] memory gmps,
        uint256[] memory fees,
        address refundAddress,
        bytes32 messageId,
        uint256 nonce
    ) public payable virtual returns (bytes32) 
```

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-ac7e04009e048e2ac03530dd9dce2407e20fbf04%2FGlacis%20Detailed%20Message%20Retry%20Flow.png?alt=media)

### Retry Managed Duplicated Message

> When using Glacis retry management feature, duplicated messages will be reverted once the first one of the retried messages is delivered to the final destination.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-6824fc735dedba3a792680ecc0fc2dc36c066f76%2FRetry%20Managed%20Duplicated%20Message%20Flow.png?alt=media)

### Retry Managed Replay Attack

> When utilizing the Glacis retry management feature, if the origin of the retry is different than the initial routing, the retrying operation will be reverted.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2FUAkWF7vEop9Ce8NQKHXS%2FGlacis%20Retry%20Managed%20Replay%20Attack%20\(1\).png?alt=media\&token=bad9d219-5e53-4731-a92a-8e40a4d01570)

### Retry Managed Same Origin Replay Attack

> When utilizing the Glacis retry management feature, if the recalculated messageID for a retried message differs from the original messageID, the retrying operation will be reverted.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2FRVSzEcpECNaln2IIZJi8%2FGlacis%20Retry%20Managed%20Same%20Origin%20Replay%20Attack%20\(1\).png?alt=media\&token=ae99707d-29f3-4ee9-962d-45c1c09e7d6c)

The calculation of the message id takes into account both the routing parameters and the payload sent.

### Retry Management Mechanism

The mechanism by which Glacis avoids duplicate messages when retrying happens in the GlacisRouter.

#### In source chain

When the application executes the **route()** function with a message with retry management enabled, the following tasks are performed by the router:

1. Increment a unique nonce that is then added to the message payload
2. Create a message Id that includes the routing parameters along with payload hash, message sender and nonce
3. Register the msg.sender of this messageId:

Then when the application issues **routeRetry()** with messageId and nonce along with the routing parameters and payload the following controls are performed:

1. Validate that the retrying sender is the same that originally routed.
2. Validate that the messageId calculated with the all passed parameters is equal to the informed one.

If all checks pass, the retry can be performed, the payload is created and the message delivered to the requested GMP protocols through the corresponding adapters.

#### In destination chain

Upon the reception of a message from a GMP, the **receiveMessage()** function is executed, performing the following tasks:

1. Extract the message receipts for this messageId.
2. Validate there are no previous receipts for this messageId for each requested GMP.
3. Validate that the messageId calculated with all passed parameters is equal to the informed one.

If all checks pass, increase the number of unique messages received for this messageId and GMP and deliver the message to destination.


# Routing

Glacis provides applications with the capability to intelligently route messages based on their importance or value. High-value messages, which may include, high economic value, critical alerts, urgent updates, or sensitive information, can be routed through one GMP equipped with advanced features such as enhanced security protocols, priority queuing, and robust error-handling mechanisms. Conversely, low-value messages, which might consist of routine updates, non-urgent notifications, or general information, can be directed through an alternative messaging pathway that is optimized for cost-effectiveness and scalability, rather than speed or security.


# xERC20s

Each unique GMP solution operates through its distinctive token passing method, often employing variations of the lock/mint process. This involves locking the original token on the source chain and mint a GMP wrapped version of the token on the destination chain (e.g., axlUSDC and lzUSDC).

Yet, handling GMP wrapped iterations of the original token presents certain drawbacks:

* Fragmented liquidity: Wrapped tokens lack fungibility between different bridges.
* Sovereignty: Ownership of the wrapped token transitions to the GMP, causing the original token issuer on the source chain to relinquish control over the token on remote chains.
* Vendor lock-in: The wrapped token contract becomes indefinitely tied to a particular GMP, leading to limited flexibility.

## XERC20

The XERC20 standard [(EIP-7281)](https://github.com/ethereum/ERCs/pull/89) seeks to overcome these limitations by establishing a distinct token contract capable of achieving cross-chain fungibility.

This extension of the ERC20 standard introduces configurable permissions for specific addresses (bridges), enabling them to perform burns or mints within defined limits over designated time periods (For instance, bridge 0x12.. has a daily burn limit of 10^6 and a daily mint limit of 10^6).

This establishes a protocol for interaction between XERC20 and bridges, outlined as follows:

1. The issuer deploys the identical XERC20 token contract across desired chains.
2. The issuer grants burn/mint permissions to bridges on each XERC20 contract.
3. Bridges execute burn/mint functions on XERC20 contracts, enabling the transfer of value across chains.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-97f04b6585c2c60de21c22f00f2e4412e09e33c0%2FXERC20%20Token%20Deployment.png?alt=media)

This method offers several benefits:

* The token issuer retains ownership of all token contracts.
* Fungibility remains intact as all tokens are represented by identical contracts.
* There's no lock-in to a specific GMP; if the token issuer wishes to support a new bridge, they can simply set rate limits for the new bridge within their XERC20 token. Similarly, if the token issuer opts to cease support for a bridge, they can set rate limits to 0 for that specific bridge.
* Bridges are rate limited, reducing risk over time

***

## Legacy ERC20

If the issuer has previously deployed a traditional ERC20 on the source chain, XERC20 offers a LockBox mechanism—a straightforward contract supporting 1:1 lock/mint and burn/unlock operations.

When a user locks (deposits) a specific amount of the legacy ERC20 token, an equivalent amount of the XERC20 token is minted. This allows users to convert their legacy ERC20s into XERC20s, enabling cross-chain transfers through Glacis.

When users transfer XERC20 tokens back to the source chain (token home), they can withdraw the initial amount of ERC20 tokens through this mechanism by burning the equivalent amount of XERC20 tokens.

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-417759ec44038304febc257eb80bc5332a01b14c%2FGlacis%20Legacy%20ERC20%20Token%20Passing%20Flow.png?alt=media)

***

## Glacis's SimpleTokenMediator

Developers who wish to develop an xERC20 token can use Glacis as an underlying bridge. Glacis provides many additional security features on top of xERC20, which can help developers with advanced security features.

For example, inheriting from the [SimpleTokenMediator](/glacis-core/references/smart-contracts/simpletokenmediator) allows a developer to write different security assumptions based off of the token amount- such as requiring different bridges & different quorums for token payloads above 10,000. Using Glacis for your xERC20 also helps simplify the process by using a single interface for multiple bridges.

{% hint style="success" %}
If interested in using Glacis for xERC20 routing, please reach out to the team. We can help guide &/or develop use of your token as well as provide seamless onramping into the Li.Fi ecosystem via Glacis Airlift.
{% endhint %}

***

## OFTs, NTTs, & Other Cross-Chain Standards

xERC20 is one of many cross-chain standards that attempts to give developers the power to develop their own cross-chain tokens. While developers have shifted from the lock & mint model through a specific bridge to a self-deployed burn & mint model, xERC20 is still the most bridge-agnostic approach. All other cross-chain token standards are created by their own bridge, and many of them feature vendor lock-in with their creator.&#x20;

If you are interested in bridging tokens via these alternative token standards, we recommend using our cross-chain token sending product, Airlift. Glacis Airlift allows users to easily send tokens across chains, all through a single interface.

{% content-ref url="/pages/lBidzKZC43FY87w6zdM1" %}
[Why Airlift?](/airlift/why-airlift)
{% endcontent-ref %}


# Applications


# Governance Model

The following Glacis Management functions are only permitted to the Glacis owner account:

* Router
  * Register/unregister adapters
  * Set Glacis Ids for adapter's internal chain Id
* Adapters
  * Add and Remove authorized remote adapters

There is no official plan for a Glacis DAO to replace owner functionality.


# Upgrade Model

Glacis infrastructure contracts are all immutable. The main reasons that allow the implementation of this approach are:

* The highly modular structure of Glacis (adapters can be updated and re-registered at any time)
* Most of the infrastructure components are stateless, or at least stateful for a short period of time (Ex while a message is still not delivered some message information can be stored in router)
* There is no locked value in any contract. At the same time, new features or fixes may need to be added as Glacis cannot control the GMP protocols that it adapts to, so some sort of an upgrade model is desired.\
  Glacis' upgrade model consists of component redeployment while maintaining backward compatibility. That means that if a user is working with current Glacis Infrastructure V1 and a version V2 is deployed, users can opt to update their smart contracts' stored router and mediators addresses to the new ones if they want to leverage new features/fixes.

This table details the upgrade model for each component:

| Component      | Contract Type                             | Upgrade Model                                                                                                                                                                                                                                                                                                                  |
| -------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Adapters       | <p>Immutable</p><p>Deployed By Glacis</p> | An adapter can be unregistered and re-registered at any time with a new deployed contract. Clients do not have to be updated                                                                                                                                                                                                   |
| Router         | Immutable                                 | A new version can be deployed at any time.Previous adapters must be re-registered.\nIf there are changes in the interface a new version of the Client and Token Client must be deployed                                                                                                                                        |
| Token Mediator | Immutable                                 | A new version must be deployed if a new version of the Router is deployed with changes in the interface. A new version can be deployed at anytime. Users need to re-deploy their Token Client contracts with the new Token Mediator. If there are changes in the interface a new version of the Token Client must be deployed. |
| Message Client | Deployed by User                          | A new version must be deployed if a new version of the Router with changes in interface is deployed                                                                                                                                                                                                                            |
| Token Client   | Deployed by User                          | A new version must be deployed if a new version of the Token Mediator with changes in interface is deployed                                                                                                                                                                                                                    |


# Security Model

Given the context where bridges have been the focus of numerous attacks throughout 2022 and 2023, emphasizing security within the Glacis infrastructure is paramount.

As a response to these security challenges, every Smart Contract function within Glacis has undergone rigorous access control measures. These measures are designed to tightly restrict access to each function, ensuring that only the specifically designated and authorized component can interact with it.

This security-first philosophy reflects an understanding of the critical importance of trust and reliability in the blockchain ecosystem, where any breach can have far-reaching consequences.

To enforce Solidity best practices security policies, the following modifiers have been implemented:

* onlyAuthorizedAdapter: Verifies that the source address of a request is an authorized component (the address is in the authorizedRemoteAddresses list for the source chain)

```solidity
    modifier onlyAuthorizedRemoteAddress(uint256 sourceChainId, address sourceAddress) {
        if (
            sourceChainId == 0 ||
            remoteCounterpart[chainId] == address(0) ||
            sourceAddress != remoteCounterpart[chainId]
        ) {
            revert GlacisAbstractAdapter__OnlyAdapterAllowed();
        }
        _;
    }
```

* onlyGlacisRouter: Verifies that the sender of the request to an Adapter send function is always GlacisRouter

```solidity
    modifier onlyGlacisRouter() {
        if (msg.sender != address(GLACIS_ROUTER))
            revert GlacisAbstractAdapter__OnlyGlacisRouterAllowed();
        _;
    }
```

* onlyAdapter: Verifies that the sender of the request to a GlacisRouter receive function is one of the registered GMP adapters

```solidity
    modifier onlyAdapter() {
        if (adapterToGlacisGMPId[msg.sender] == 0)
            revert GlacisAbstractRouter__OnlyAdaptersAllowed();
        _;
    }
```

This diagram serves as a comprehensive visual guide illustrating the various layers and mechanisms of security restrictions implemented within the Glacis protocol:

![](https://1192098899-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FcCWTn4UpXpsFVDR7MJXv%2Fuploads%2Fgit-blob-0f4cdb8da7f1db42f1f9a0d4955d503646c386e0%2FGlacis%20Function%20Access%20Restrictions.png?alt=media)


# Messaging Fees


# Protocol Fees

Glacis currently does not include any additional fees apart from the gas consumption that each GMP & chain apply to transactions.


# Gas Overhead

As a product that provides great added value, the code needed to achieve abstraction, redundancy, access control, and retry management features incurs gas overhead in both source and destination chains.

At present, the average gas overhead when sending messages of various types through a single GMP with abstraction is as follows:

| GMP        | uint256 | string |
| ---------- | ------- | ------ |
| Axelar     | 159880  | 167419 |
| Layer Zero | 114439  | 117165 |
| Wormhole   | 152236  | 151730 |
| CCIP       | 146529  | 149540 |
| Hyperlane  | 208170  | 211395 |

> The values are expressed in gas units.


# Troubleshooting


# Integration Checklist

The checklist below is intended to help prepare a project that integrates Glacis.

* Extend your contracts with the latest version of Glacis Labs [client](https://github.com/glacislabs/v1-core/blob/main/contracts/client/GlacisClient.sol).
* Deploy your contracts specifying Glacis Router or Glacis Token Mediator addresses as constructor parameters depending if you want to pass only cross-chain messages or cross-chain tokens along with messages. Addresses for Glacis infrastructure contracts can be obtained in [Supported Chains](/glacis-core/references/supported-chains)
* If your project requires token transfers with execution you can deploy your token using XERC20 standard and simply add Glacis XERC20 Extension to it. Otherwise, it is unnecessary to integrate this additional standard.
  * In case that you have a legacy ERC20 you can deploy an XERC20LockBox and migrate your legacy ERC20 tokens to your recently deployed XERC20.

Feel free to reach out to the team on [Telegram](https://t.me/+KJZFSN67crY4ZDJh)!


# Error Messages

When operating across different chains, two categories of errors may arise:

* Source Chain Revert : The error occurs within local contracts prior to reaching the GMP Gateway. In such instances, the error signature is returned as a data argument alongside a generic "estimateGas" error.

```
Error: execution reverted (unknown custom error) (action="estimateGas", data="...."
```

* Destination Chain: The error arises within the contracts on the remote chain when called by the GMP gateway after the source transaction is completed successfully. Consequently, the client contract on the source chain remains unaware of it. To identify the issue on the destination chain, one can retrieve the error signature by inspecting the transaction on the designated GMP explorer and searching for the provided error signature.

In both cases the error signature of the cause can be found in the following table:

<table data-full-width="true"><thead><tr><th>Glacis Custom Error</th><th>Error Signature</th></tr></thead><tbody><tr><td>DeliveryProviderCannotReceivePayment()</td><td>0x95d64fa0</td></tr><tr><td>GlacisAbstractAdapter__DestinationChainIdNotValid()</td><td>0x12f2ad92</td></tr><tr><td>GlacisAbstractAdapter__IDArraysMustBeSameLength()</td><td>0x4f3bb61c</td></tr><tr><td>GlacisAbstractAdapter__InvalidAdapterAddress()</td><td>0x44181faf</td></tr><tr><td>GlacisAbstractAdapter__InvalidChainId()</td><td>0x4428fe68</td></tr><tr><td>GlacisAbstractAdapter__NoAdapterConfiguredForChain()</td><td>0x3306d25e</td></tr><tr><td>GlacisAbstractAdapter__NoRemoteAdapterForChainId(uint256 chainId)</td><td>0xb295f036</td></tr><tr><td>GlacisAbstractAdapter__OnlyAdapterAllowed()</td><td>0x4c371e2d</td></tr><tr><td>GlacisAbstractAdapter__OnlyGlacisRouterAllowed()</td><td>0x8d0f010f</td></tr><tr><td>GlacisAbstractAdapter__SourceChainNotRegistered()</td><td>0xb798796f</td></tr><tr><td>GlacisAbstractRouter__GMPIDCannotBeZero()</td><td>0x4332f55b</td></tr><tr><td>GlacisAbstractRouter__InvalidAdapterAddress()</td><td>0xa46f71e2</td></tr><tr><td>GlacisAbstractRouter__OnlyAdaptersAllowed()</td><td>0x55ff424d</td></tr><tr><td>GlacisAccessControlClient__RouteAlreadyAdded()</td><td>0x9d0749e5</td></tr><tr><td>GlacisCCIPAdapter__PaymentTooSmallForAnyDestinationExecution()</td><td>0x9642d69e</td></tr><tr><td>GlacisCCIPAdapter__RefundAddressMustReceiveNativeCurrency()</td><td>0x4ae47585</td></tr><tr><td>GlacisClient__CanOnlyBeCalledByRouter()</td><td>0x7060f99c</td></tr><tr><td>GlacisClient__InvalidRouterAddress()</td><td>0x7b6c3d88</td></tr><tr><td>GlacisHyperlaneAdapter__FeeNotEnough()</td><td>0xf7baffe4</td></tr><tr><td>GlacisHyperlaneAdapter__OnlyMailboxAllowed()</td><td>0x56645bf6</td></tr><tr><td>GlacisHyperlaneAdapter__RefundAddressMustReceiveNativeCurrency()</td><td>0xead30a9a</td></tr><tr><td>GlacisHyperlaneAdapter__UnconfiguredOrigin()</td><td>0xaa4af996</td></tr><tr><td>GlacisLayerZeroAdapter__LZChainIdNotAccepted(uint256)</td><td>0xd764477d</td></tr><tr><td>GlacisRemoteCounterpartManager__MediatorsAndChainIDsMustHaveSameLength()</td><td>0x7f424dc4</td></tr><tr><td>GlacisRemoteCounterpartManager__RemoteCounterpartCannotHaveChainIdZero()</td><td>0x3e708a4c</td></tr><tr><td>GlacisRemoteCounterpartManager__RemoteCounterpartCannotHaveChainIdZero()</td><td>0x3e708a4c</td></tr><tr><td>GlacisRemoteCounterpartManager__RemoteCounterpartsAndChainIDsMustHaveSameLength()</td><td>0x5855bb7e</td></tr><tr><td>GlacisRouter__ClientDeniedRoute()</td><td>0x9cbf2224</td></tr><tr><td>GlacisRouter__FeeArrayMustEqualGMPArray()</td><td>0xbb767f66</td></tr><tr><td>GlacisRouter__FeeSumMustBeEqualToValue()</td><td>0x2e0b647c</td></tr><tr><td>GlacisRouter__GMPCountMustBeAtLeastOne()</td><td>0xdc62be97</td></tr><tr><td>GlacisRouter__GMPNotSupported()</td><td>0xed2e8008</td></tr><tr><td>GlacisRouter__ImpossibleGMPId(uint8 gmpId)</td><td>0x64d24020</td></tr><tr><td>GlacisRouter__MessageAlreadyReceivedFromGMP()</td><td>0x5bde8cde</td></tr><tr><td>GlacisRouter__MessageIdNotValid()</td><td>0x3245805f</td></tr><tr><td>GlacisRouter__MessageInputNotIdenticalForRetry()</td><td>0x1dd35f64</td></tr><tr><td>GlacisRouter__NotOwnerOfMessageToRetry()</td><td>0x7f2f6d96</td></tr><tr><td>GlacisRouter__OnlyAdaptersAllowed()</td><td>0xb519c5ed</td></tr><tr><td>GlacisRouter__RouteDoesNotExist()</td><td>0xeb470cd2</td></tr><tr><td>GlacisTokenClient__CanOnlyBeCalledByTokenRouter()</td><td>0x7e3d38a2</td></tr><tr><td>GlacisTokenMediator__DestinationChainUnavailable()</td><td>0x64b15c4d</td></tr><tr><td>GlacisTokenMediator__IncorrectTokenVariant(address, uint256)</td><td>0x8e291d85</td></tr><tr><td>GlacisTokenMediator__OnlyTokenMediatorAllowed()</td><td>0x73d0e434</td></tr><tr><td>GlacisWormholeAdapter__AlreadyProcessedVAA()</td><td>0xbfe7efae</td></tr><tr><td>GlacisWormholeAdapter__NotEnoughValueForCrossChainTransaction()</td><td>0x0c08e04a</td></tr><tr><td>GlacisWormholeAdapter__OnlyRelayerAllowed()</td><td>0x200b2d18</td></tr><tr><td>GlacisWormholeAdapter__RefundAddressMustReceiveNativeCurrency()</td><td>0xa93640ac</td></tr><tr><td>IGlacisAdapter__ChainIsNotAvailable(uint256 toChainId)</td><td>0xb0baf5b5</td></tr><tr><td>IXERC20Lockbox_Native()</td><td>0x46e927a0</td></tr><tr><td>IXERC20Lockbox_NotNative()</td><td>0x8467cb4b</td></tr><tr><td>IXERC20Lockbox_WithdrawFailed()</td><td>0xab8a5c34</td></tr><tr><td>IXERC20_NotFactory()</td><td>0x2029e525</td></tr><tr><td>IXERC20_NotHighEnoughLimits()</td><td>0x0b6842aa</td></tr><tr><td>InsufficientRelayerFunds(uint256 msgValue, uint256 minimum)</td><td>0x02216bc0</td></tr><tr><td>InvalidDeliveryVaa(string reason)</td><td>0xb72c3b7f</td></tr><tr><td>InvalidEmitter(bytes32 emitter, bytes32 registered, uint16 chainId)</td><td>0x776c06ce</td></tr><tr><td>InvalidMsgValue(uint256 msgValue, uint256 totalFee)</td><td>0x1f89f671</td></tr><tr><td>InvalidOverrideGasLimit()</td><td>0xafe343e8</td></tr><tr><td>InvalidOverrideReceiverValue()</td><td>0xe3704808</td></tr><tr><td>InvalidOverrideRefundPerGasUnused()</td><td>0x0cfb7d9e</td></tr><tr><td>InvalidPayloadId(uint8 parsed, uint8 expected)</td><td>0x79cbfdbe</td></tr><tr><td>InvalidPayloadLength(uint256 received, uint256 expected)</td><td>0xc37906a0</td></tr><tr><td>InvalidVaaKeyType(uint8 parsed)</td><td>0x249ede70</td></tr><tr><td>NotAnEvmAddress(bytes32)</td><td>0x33b960d0</td></tr><tr><td>ReentrantDelivery(address msgSender, address lockedBy)</td><td>0x20b84ced</td></tr><tr><td>RequestedGasLimitTooLow()</td><td>0x71ae1330</td></tr><tr><td>RequesterNotWormholeRelayer()</td><td>0x72132d5a</td></tr><tr><td>TargetChainIsNotThisChain(uint16 targetChain)</td><td>0xd8215fc9</td></tr><tr><td>VaaKeysDoNotMatchVaas(uint8 index)</td><td>0xeb5e161c</td></tr><tr><td>VaaKeysLengthDoesNotMatchVaasLength(uint256 keys, uint256 vaas)</td><td>0xb5ef0f68</td></tr><tr><td>XERC20__OnlyBridge()</td><td>0xe5cbd60e</td></tr></tbody></table>

<br>


# FAQ

Does my application contract have to inherit from a specific contract to work with Glacis?

> Although Glacis provides client contracts to facilitate the interactions with the router, they only requirement for a contract to use Glacis is to implement the GlacisClient interface.

Can I send tokens with Glacis?

> Glacis supports passing cross-chain tokens of the [XERC20](https://www.xerc20.com/) standard via the SimpleTokenMediator. If you would like to send preexisting tokens, check out the Airlift product.


# References


# Smart Contracts


# GlacisRouter

**Your gateway to cross-chain activity.**

The GlacisRouter smart contract acts as the on-chain interface for all cross-chain activity with Glacis.

You can discover the source smart contract [`GlacisRouter.sol`](https://github.com/glacislabs/v1-core/blob/main/contracts/routers/GlacisRouter.sol) in this repository.

## The Route Function

The main function in GlacisRouter is route, which sends a cross-chain message. It returns a `bytes32` messageId and a `uint256` nonce:

```solidity
route(
        uint256 chainId,
        bytes32 to,
        bytes memory payload,
        address[] memory adapters,
        GlacisRouter.CrossChainGas[] memory fees,
        address refundAddress,
        bool retryable)
payable
returns(bytes32 messageId, uint256 nonce)
```

<table><thead><tr><th width="276">Parameter Name</th><th>Parameter Description</th></tr></thead><tbody><tr><td><strong>uint256</strong> chainId</td><td>The Glacis ID of the blockchain that you wish to send a message to</td></tr><tr><td><strong>address</strong> to</td><td>The address of the destination contract that you are sending a message to</td></tr><tr><td><strong>bytes</strong> <strong>memory</strong> payload</td><td>The payload of data that you wish to send to the destination contract</td></tr><tr><td><strong>address[]</strong> <strong>memory</strong> adapters</td><td>An array of adapters to send the message over, such as an <a href="https://www.axelar.network/">Axelar</a> adapter. Can be an address if you wish to use an adapter that Glacis does not use, otherwise it can be a <a href="/glacis-core/references/supported-gmps">GMP ID</a> stored as an address</td></tr><tr><td><strong>GlacisRouter.CrossChainGas[] memory</strong> fees</td><td>An array of fees that correspond to the amount provided to each adapter. This is a parallel array to the adapters parameter. The sum of the "nativeCurrencyValue" must be equal to the value sent with this function</td></tr><tr><td><strong>address</strong> refundAddress</td><td>The address to which excess native currency is sent to if an adapter deems that the user overpaid</td></tr><tr><td><strong>bool</strong> retriable</td><td>Whether to allow the message to be retried (stores owner of the message ID on-chain, slightly more gas expensive)</td></tr></tbody></table>

### CrossChainGas

To represent how adapters require gas, we have created the CrossChainGas struct, located within the [GlacisCommons](https://github.com/glacislabs/v1-core/blob/main/contracts/commons/GlacisCommons.sol) contract.&#x20;

```solidity
struct CrossChainGas {
        uint128 gasLimit;
        uint128 nativeCurrencyValue;
}
```

| Parameter Name                  | Parameter Description                                                                                 |
| ------------------------------- | ----------------------------------------------------------------------------------------------------- |
| **uint128** gasLimit            | A desired destination chain gas limit. Required by some adapters, and ignored by others               |
| **uint128** nativeCurrencyValue | The amount of source chain value to be forwarded to an adapter. Different GMPs require different fees |

***

## The Retry Route

There is an additional route function variant:

```solidity
routeRetry(
	uint256 chainId,
	address to,
        bytes memory payload,
        address[] memory adapters,
        GlacisRouter.CrossChainGas[] memory fees,
        address refundAddress,
        bytes32 messageId,
        uint256 nonce)
payable
returns(bytes32, uint256)
```

This route cannot be used for original cross-chain messages. Instead, it is used to retry a cross-chain message with an identical message ID and identical data (but potentially through a different GMP).  For example, an original message was sent through Axelar but is now being retried via Wormhole.

This can be helpful in cases where a message is somehow lost by a GMP's consensus mechanism or its relayers. An identical message would be sent, and whichever message arrives after the first will revert.

Two additional parameters are required with this function:

| Parameter Name    | Parameter Description                                                                |
| ----------------- | ------------------------------------------------------------------------------------ |
| bytes32 messageId | The Glacis message ID of the original message                                        |
| uint256 nonce     | The nonce of the message, which can be retrieved in the original message's event log |

{% hint style="info" %}
Only the original message sender can retry a message, no other address can do it on their behalf. The original message must have been sent with its "retryable" parameter as true. Identical data **must** be provided.
{% endhint %}

***

## Read-Only

These are the read-only functions in GlacisRouter, but most developers won't use them:

| Function Signature                                                                                     | Function Description                                                                                                                                                                                                                  |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| glacisAdapters(uint8) returns(address)                                                                 | Returns the address of the adapter contract for the specific GMP ID input                                                                                                                                                             |
| sourceAdapterToGMP(address) returns(uint8)                                                             | Returns the GMP ID of the adapter at the specified address. Returns 0 if no such adapter exists                                                                                                                                       |
| validateGlacisMessageId(bytes32 messageId, uint256 toChainId, address to, uint256 nonce) returns(bool) | Returns true if the combination of chain ID, address, msg.sender, and message nonce result in the same messageId **You can use the JSON-RPC method eth\_call to simulate any msg.sender if you wish to use this in your application** |
| messageSenders(bytes32)                                                                                | Returns the message sender of a specific message ID                                                                                                                                                                                   |

***

## Errors

It also includes the following errors, some of which you may encounter:

| Error Name                                       | Error Description                                                                                                                                |
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| GlacisRouter\_\_GMPNotSupported                  | Occurs when routing, and the GMP ID provided is not supported on this chain                                                                      |
| GlacisRouter\_\_RouteDoesNotExist                | Occurs when routing, and the adapter of the GMP ID does not exist                                                                                |
| GlacisRouter\_\_NotOwnerOfMessageToRetry         | Occurs when retrying a message route, when the message ID that is attempting to be retried was not originally sent by the current message sender |
| GlacisRouter\_\_OnlyAdaptersAllowed              | Occurs when trying to call a function that only a Glacis GMP Adapter can call                                                                    |
| GlacisRouter\_\_MessageInputNotIdenticalForRetry | Occurs when retrying a message route, when the message input was not identical to the original                                                   |
| GlacisRouter\_\_ClientDeniedRoute                | Occurs when a message is received, and the client contract does not accept messages from the sender address, origin chain, and/or GMP            |
| GlacisRouter\_\_ImpossibleGMPId(uint8 gmpId)     | Occurs when a message is received, and the GMP ID is not supported                                                                               |
| GlacisRouter\_\_MessageAlreadyReceivedFromGMP    | Occurs when a message is received, and the same message has already been received from that GMP                                                  |
| GlacisRouter\_\_MessageIdNotValid                | Occurs when a message is received, and the Glacis message ID contained in the header does not pass verification                                  |
| \`\`\`                                           |                                                                                                                                                  |


# GlacisTokenClient

**The base of your cross-chain token transfer contracts.**

GlacisTokenClient is a smart contract base that can be used by developers to interact with token passing features that Glacis has to offer. By inheriting from GlacisTokenClient, a smart contract is able to send and receive messages and tokens across chains through the GlacisTokenMediator with specific security/availability features.\
You can discover the source smart contract [`GlacisTokenClient.sol`](https://github.com/glacislabs/v1-core/blob/main/contracts/client/GlacisTokenClient.sol) in this repository. There is also an `Ownable` variant, [`GlacisTokenClientOwnable.sol`](https://github.com/glacislabs/v1-core/blob/main/contracts/client/GlacisTokenClientOwnable.sol).

This contract inherits from GlacisClient extending its message passing capabilities with token passing features, it communicates with [`GlacisTokenMediator.sol`](https://github.com/glacislabs/v1-core/blob/main/contracts/mediators/GlacisTokenMediator.sol) to perform message and/or token routing actions through Glacis infrastructure.

These are the constructor arguments for GlacisTokenClient:

* The address for GlacisRouter
* The address for GlacisTokenMediator to create the underlying GlacisClient
* The default GMP quorum for this contract to effectively receive a message

```solidity
    constructor(
        address glacisTokenMediator_,
        address glacisRouter_,
        uint256 quorum
    ) GlacisClient(glacisRouter_, quorum) {
        GLACIS_TOKEN_ROUTER = glacisTokenMediator_;
    }
```

## The Route Function

GlacisTokenClient has multiple internal route functions that mirror the route functions that GlacisRouter provides, each which return a bytes32 message ID:

* \_sendMessageAndTokens\_\_abstract( uint256 chainId, address to, uint8 gmp, bytes memory payload, uint256 gasPayment, address token, uint256 tokenAmount ) internal returns (bytes32)

  ```
    Convenient method - Routes message and tokens to destination through GlacisTokenMediator using specified GMP without any additional feature
  ```
* \_sendMessageAndTokens\_\_redundant( uint256 chainId, address to, uint8\[] memory gmps, uint256\[] memory fees, bytes memory payload, uint256 gasPayment, address token, uint256 tokenAmount ) internal returns (bytes32)

  ```
    Convenient method - Routes message and tokens to destination through the specific GMP without any additional Glacis Features
  ```
* \_sendMessageAndTokens\_\_retriable( uint256 chainId, address to, uint8\[] memory gmps, uint256\[] memory fees, bytes memory payload, uint256 gasPayment, address token, uint256 tokenAmount ) internal returns (bytes32)

  ```
    Convenient method - Routes message and tokens to destination through GlacisTokenMediator using specified GMPs with redundancy feature
  ```
* \_sendMessageAndTokens( uint256 chainId, address to, bytes memory payload, uint8\[] memory gmps, uint256\[] memory fees, bool retriable, address token, uint256 tokenAmount, uint256 gasPayment ) internal returns (bytes32)

  ```
    Routes message and tokens to destination through GlacisTokenMediator using any feature
  ```

The following parameters are applicable to each of these route functions, similar to GlacisTokenMediator:

| Parameter Name                                | Parameter Description                                                                                                                                                                                                                                                          |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **uint256** chainId                           | The Glacis chain ID of the blockchain that you wish to send a message and/or tokens to                                                                                                                                                                                         |
| **bytes32** to                                | The address of the destination contract that you are sending a message and/or tokens to                                                                                                                                                                                        |
| **bytes memory** payload                      | The payload of data (message) that you wish to send to the destination contract                                                                                                                                                                                                |
| **address\[]** **memory** adapters            | An array of adapters to send the message over, such as an [Axelar](https://www.axelar.network/) adapter. Can be an address if you wish to use an adapter that Glacis does not use, otherwise it can be a [GMP ID](/glacis-core/references/supported-gmps) stored as an address |
| **address** adapter                           | A single adapter for the message to send over, used in lieu of the adapters parameter for conveniently defining a single GMP route                                                                                                                                             |
| **GlacisRouter.CrossChainGas\[] memory** fees | An array of fees that correspond to the amount provided to each adapter. This is a parallel array to the adapters parameter. Its rules are the same as in the [GlacisRouter](/glacis-core/references/smart-contracts/glacisrouter#crosschaingas)                               |
| **address** refundAddress                     | The address to which excess native currency is sent to if an adapter deems that the user overpaid                                                                                                                                                                              |
| **bool** retryable                            | Whether to allow the message to be retried (stores owner of the message ID on-chain, slightly more gas expensive)                                                                                                                                                              |
| **uint256** gasPayment                        | The amount of native currency to forward to the GlacisRouter to pay for cross-chain gas fees                                                                                                                                                                                   |
| **address** token                             | The address of the XERC20 token you wish to send to the destination contract                                                                                                                                                                                                   |
| **uint256** tokenAmount                       | The amount of XERC20 tokens you wish to send to the destination contract                                                                                                                                                                                                       |

Each of the routing functions returns a `bytes32`, which is the Glacis message ID for the routing. The more verbose route function also returns a `uint256` nonce.

## The Retry Route

An additional route is provided within GlacisTokenClient, which doesn't send a new message and/or tokens. Instead, it resends a previous message and tokens so long as it receives identical input.

* \_retrySendWithTokens( uint256 chainId, address to, uint8\[] memory gmps, uint256\[] memory fees, bytes memory payload, bytes32 messageId, uint256 nonce, uint256 gasPayment, address token, uint256 tokenAmount ) internal returns (bytes32)

Two additional parameters are required with this function:

| Parameter Name        | Parameter Description                                                                |
| --------------------- | ------------------------------------------------------------------------------------ |
| **bytes32** messageId | The Glacis message ID of the original message                                        |
| **uint256** nonce     | The nonce of the message, which can be retrieved in the original message's event log |

Keep in mind that in the GlacisTokenMediator, only the original sender can retry routes. If this internal function is exposed publicly, anyone can retry on your smart contract's behalf.

## The Receive Message Function

The GlacisTokenMediator will call the receiveMessageWithTokens function within your GlacisTokenClient whenever there is a message sent to your smart contract. You must override the following internal function to react to cross-chain messages:

* function \_receiveMessageWithTokens( uint8\[] memory fromGmpIds, uint256 fromChainId, address fromAddress, address toAddress, bytes memory payload, address token, uint256 tokenAmount ) internal virtual {}

The parameters involved with this function are:

| Parameter Name                     | Parameter Description                                                         |
| ---------------------------------- | ----------------------------------------------------------------------------- |
| **address\[] memory** fromAdapters | The adapters through which the messages were received                         |
| **uint256** fromChainId            | The Glacis chain ID of the message's source/origin                            |
| **bytes32** fromAddress            | The address of the message's source/origin                                    |
| **bytes memory** payload           | The generic bytes payload of the message, which can be decoded to your liking |
| **address** token                  | The token that was sent with the message                                      |
| **uint256** tokenAmount            | The amount of the token that was sent with this message                       |

## Adding Routes for Access Control

This operation is inherited so it is performed exactly as in [GlacisClient](https://github.com/glacislabs/v1-documentation/blob/main/docs/pages/References/Smart%20Contracts/GlacisClient/README.md#adding-routes-for-access-control)

## Setting Quorum

This operation is inherited so it is performed exactly as in [GlacisClient](https://github.com/glacislabs/v1-documentation/blob/main/docs/pages/References/Smart%20Contracts/GlacisClient/README.md#setting-quorum)

## Read-Only

These are some of the read-only functions in GlacisClient:

* GLACIS\_TOKEN\_ROUTER() returns(address) — returns the address set as the GlacisTokenMediator, which was set in the constructor
* isAllowedRoute(GlacisRoute memory accessRoute) returns(bool) — returns true if the provided route is available, false otherwise. Used by the GlacisRouter to verify access

## Errors

{% hint style="warning" %}
Glacis is not compatible with fee-on-transfer tokens.
{% endhint %}

GlacisTokenClient includes the following errors, which you might encounter:

| Error Name                                          | Error Description                                                                                       |
| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| GlacisTokenClient\_\_CanOnlyBeCalledByTokenRouter() | Occurs when an address that is not the GlacisTokenMediator attempts to call the receiveMessage function |


# GlacisTokenMediator

**Your gateway to cross-chain token activity.**

The GlacisTokenMediator smart contract acts as the on-chain interface for all token cross-chain activity with Glacis.

You can discover the source smart contract [`GlacisTokenMediator.sol`](https://github.com/glacislabs/v1-core/blob/main/contracts/mediators/GlacisTokenMediator.sol) in this repository.

## The Route Function

The main function in GlacisTokenMediator is route, which sends a cross-chain message and/or tokens. Using the GlacisTokenMediator to send tokens will always have retryable be enabled.

```solidity
function route(
        uint256 chainId,
        bytes32 to,
        bytes memory payload,
        address[] memory adapters,
        GlacisCommons.CrossChainGas[] memory fees,
        address refundAddress,
        address token,
        uint256 tokenAmount
  ) payable returns (bytes32, uint256)
```

> Internally this function is in charge of populating GlacisTokenData, adding it to the payload and finally routing it to the GlacisRouter

Here is a detailed description of function parameters.

<table><thead><tr><th width="279">Parameter Name</th><th>Parameter Description</th></tr></thead><tbody><tr><td><strong>uint256</strong> chainId</td><td>The Glacis ID of the blockchain that you wish to send a message and/or tokens to</td></tr><tr><td><strong>bytes32</strong> to</td><td>The address of the destination contract that you are sending a message and/or tokens to</td></tr><tr><td><strong>bytes memory</strong> payload</td><td>The payload (message) of data that you wish to send to the destination contract</td></tr><tr><td><strong>address[] memory</strong> adapters</td><td>An array of adapters to send the message over, such as an <a href="https://www.axelar.network/">Axelar</a> adapter. Can be an address if you wish to use an adapter that Glacis does not use, otherwise it can be a <a href="/glacis-core/references/supported-gmps">GMP ID</a> stored as an address</td></tr><tr><td><strong>GlacisRouter.CrossChainGas[] memory</strong> fees</td><td>An array of fees that correspond to the amount provided to each adapter. This is a parallel array to the adapters parameter. The sum of the "nativeCurrencyValue" must be equal to the value sent with this function</td></tr><tr><td><strong>address</strong> refundAddress</td><td>The address to which excess native currency is sent to if an adapter deems that the user overpaid</td></tr><tr><td><strong>address</strong> token</td><td>The address of the XERC20 token you wish to send to the destination contract</td></tr><tr><td><strong>uint256</strong> tokenAmount</td><td>The amount of XERC20 tokens you wish to send to the destination contract</td></tr></tbody></table>

## The Retry Route

An additional route is provided, which doesn't send a new message and/or tokens. Instead, it allows resending of a previous message and/or tokens so long as it receives identical input.

```solidity
function routeRetry(
        uint256 chainId,
        bytes32 to,
        bytes memory payload,
        address[] memory adapters,
        GlacisCommons.CrossChainGas[] memory fees,
        address refundAddress,
        bytes32 messageId,
        uint256 nonce,
        address token,
        uint256 tokenAmount)
  public payable virtual returns (bytes32)
```

> This route cannot be used for original cross-chain messages. Instead, it is used to retry a cross-chain message with an identical message ID and identical data (but potentially through a different GMP). This can be helpful in cases where a message is somehow lost by a GMP's consensus mechanism or its relayers. An identical message would be sent, and whichever message arrives after the first will revert.

Two additional parameters are required with this function:

| Parameter Name    | Parameter Description                                                                |
| ----------------- | ------------------------------------------------------------------------------------ |
| bytes32 messageId | The Glacis message ID of the original message                                        |
| uint256 nonce     | The nonce of the message, which can be retrieved in the original message's event log |

> Only the original message sender can retry a message, no other address can do it on their behalf. Also note that identical data must be provided.

## Errors

It also includes the following errors, some of which you may encounter:

| Error Name                                    | Error Description                                                                                                   |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| GlacisTokenMediator\_\_OnlyTokenRouterAllowed | Occurs when routing, and the from address of the receiving payload is not the same token router on the remote chain |


# GlacisClient

**The base of your cross-chain contracts.**

GlacisClient is a smart contract base that can be used by developers to interact with the features that Glacis has to offer. By inheriting from GlacisClient, a smart contract is able to send and receive messages across chains with Glacis features.\
You can discover the source smart contract [`GlacisClient.sol`](https://github.com/glacislabs/v1-core/blob/main/contracts/client/GlacisClient.sol) in this repository. There is also an `Ownable` variant, [`GlacisClientOwnable.sol`](https://github.com/glacislabs/v1-core/blob/main/contracts/client/GlacisClientOwnable.sol).

These are the constructor arguments for GlacisClient:

* The address of GlacisRouter
* The default GMP quorum for this contract to effectively receive a message

```solidity
    constructor(address glacisRouter_, uint256 quorum) IGlacisClient(quorum) {
        if (glacisRouter_ == address(0))
            revert GlacisClient__InvalidRouterAddress();
        GLACIS_ROUTER = glacisRouter_;
    }
```

***

## The Route Function

GlacisClient has multiple internal route functions that mirror the route functions that GlacisRouter provides, each which return a bytes32 message ID:

```solidity
_route(
        uint256 chainId,
        bytes32 to,
        bytes memory payload,
        address[] memory adapters,
        CrossChainGas[] memory fees,
        address refundAddress,
        bool retryable,
        uint256 gasPayment) 
returns (bytes32,uint256)
```

```solidity
_routeSingle(
        uint256 chainId,
        bytes32 to,
        bytes memory payload,
        address adapter,
        address refundAddress,
        uint256 gasPayment)
returns(bytes32)
```

```solidity
_routeRedundant(
        uint256 chainId,
        bytes32 to,
        bytes memory payload,
        address[] memory adapters,
        CrossChainGas[] memory fees,
        address refundAddress,
        uint256 gasPayment)
returns (bytes32)
```

Each of these 3 varieties perform the same function: sending an original cross-chain message. `_route` is the encompassing route function that allows for complete configuration of all Glacis features. `_routeSingle` allows you to easily route through a single GMP. `_routeRedundant` allows you to easily route redundant messages (each message has the same ID and data) through multiple GMPs.

The following parameters are applicable to each of these route functions, similar to GlacisRouter:

| Parameter Name                                | Parameter Description                                                                                                                                                                                                                                                          |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **uint256** chainId                           | The Glacis chain ID of the blockchain that you wish to send a message to                                                                                                                                                                                                       |
| **bytes32** to                                | The address of the destination contract that you are sending a message to                                                                                                                                                                                                      |
| **bytes** **memory** payload                  | The payload of data that you wish to send to the destination contract                                                                                                                                                                                                          |
| **address\[]** **memory** adapters            | An array of adapters to send the message over, such as an [Axelar](https://www.axelar.network/) adapter. Can be an address if you wish to use an adapter that Glacis does not use, otherwise it can be a [GMP ID](/glacis-core/references/supported-gmps) stored as an address |
| **uint8** adapter                             | A single adapter for the message to send over, used in lieu of the adapters parameter for conveniently defining a single GMP route                                                                                                                                             |
| **GlacisRouter.CrossChainGas\[] memory** fees | An array of fees that correspond to the amount provided to each adapter. This is a parallel array to the adapters parameter. Its rules are the same as in the [GlacisRouter](/glacis-core/references/smart-contracts/glacisrouter#crosschaingas)                               |
| **address** refundAddress                     | The address to which excess native currency is sent to if an adapter deems that the user overpaid                                                                                                                                                                              |
| **bool** retryable                            | Whether to allow the message to be retried (stores owner of the message ID on-chain, slightly more gas expensive)                                                                                                                                                              |
| **uint256** gasPayment                        | The amount of native currency to forward to the GlacisRouter to pay for cross-chain gas fees                                                                                                                                                                                   |

Each of the routing functions returns a `bytes32`, which is the Glacis message ID for the routing. The more verbose route function also returns a `uint256` nonce.

***

## The Retry Route

An additional route is provided within GlacisClient, which doesn't send a new message. Instead, it resends a previous message so long as it receives identical input.

```solidity
_routeRetry(
        uint256 chainId,
        bytes32 to,
        bytes memory payload,
        address[] memory adapters,
        CrossChainGas[] memory fees,
        address refundAddress,
        bytes32 messageId,
        uint256 nonce,
        uint256 gasPayment)
returns(bytes32)
```

Two additional parameters are required with this function:

| Parameter Name    | Parameter Description                                                                |
| ----------------- | ------------------------------------------------------------------------------------ |
| bytes32 messageId | The Glacis message ID of the original message                                        |
| uint256 nonce     | The nonce of the message, which can be retrieved in the original message's event log |

Keep in mind that in the GlacisRouter, only the original sender can retry routes. If this internal function is exposed publicly, anyone can retry on your smart contract's behalf.

***

## The Receive Message Function

The GlacisRouter will call the receiveMessage function within your GlacisClient whenever there is a message sent to your smart contract. You must override the following internal function to react to cross-chain messages:

```solidity
_receiveMessage(
        address[] memory fromAdapters,
        uint256 fromChainId,
        bytes32 fromAddress,
        bytes memory payload)
```

The parameters involved with this function are:

| Parameter Name                 | Parameter Description                                                         |
| ------------------------------ | ----------------------------------------------------------------------------- |
| **address\[]** fromAdapters    | The adapters through which the messages were received                         |
| **uint256** sourceChain        | The Glacis chain ID of the message's source/origin                            |
| **bytes32** sourceAddress      | The address of the message's source/origin                                    |
| **bytes** **calldata** payload | The generic bytes payload of the message, which can be decoded to your liking |

***

## Adding Routes for Access Control <a href="#adding-routes-for-access-control" id="adding-routes-for-access-control"></a>

You must set up routes within your instance of GlacisClient before it can receive messages. To do so, use the following function:

* addAllowedRoute(GlacisRoute memory allowedRoute)

The GlacisRoute struct is defined as:

```solidity
    struct GlacisRoute {
        uint256 fromChainId;    // WILDCARD means any chain
        address fromAddress;    // WILDCARD means any address
        address fromAdapter;    // WILDCARD means any official Glacis adapter
    }
    
    uint160 constant public WILDCARD = type(uint160).max;
```

Leaving any value as `WILDCARD` indicates that it is a wildcard route.&#x20;

You can have multiple routes at a time. To remove a route, use the following function:

* removeAllowedRoute(**GlacisRoute calldata** route)

***

## Setting Quorum <a href="#setting-quorum" id="setting-quorum"></a>

Quorum is an important concept for Glacis, as it defines how many times a message with a specific message ID must be received before glacis calls your `receiveMessage` function. It is most likely that this number should be `1`, but you can increase this number if you intend to have greater security through higher redundancy.

You can set the quorum's default value in the constructor by setting the `quorum` value.

If you wish to instead manually handle quorum per message, override the `getQuorum` function:

```solidity
function getQuorum(
        GlacisCommons.GlacisData memory,    // glacis data
        bytes memory,                       // payload
        uint256                             // unique messages received so far (for dynamic quorum, usually unused)
) returns (uint256)
```

The parameters involved with this function are:

| Parameter Name                                 | Parameter Description                                                                                                                                          |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **GlacisCommons.GlacisData memory** glacisData | The Glacis metadata surrounding the message, found within [GlacisCommons](https://github.com/glacislabs/v1-core/blob/main/contracts/commons/GlacisCommons.sol) |
| **bytes memory** payload                       | The generic payload data associated with the function                                                                                                          |
| **uint256** uniqueMessagesReceived             | The number of messages received so far, in case quorum must change in real-time (very rarely used)                                                             |

***

## Read-Only

These are some of the read-only functions in GlacisClient:

* glacisRouter() returns(address) — returns the address set as the GlacisRouter, which was set in the constructor
* isAllowedRoute(GlacisRoute memory accessRoute) returns(bool) — returns true if the provided route is available, false otherwise. Used by the GlacisRouter to verify access

***

## Errors

GlacisClient includes the following errors, which you might encounter:

| Error Name                              | Error Description                                                                                |
| --------------------------------------- | ------------------------------------------------------------------------------------------------ |
| GlacisClient\_\_CanOnlyBeCalledByRouter | Occurs when an address that is not the GlacisRouter attempts to call the receiveMessage function |
| GlacisClient\_\_InvalidRouterAddress    | Occurs when constructing the GlacisClient with an empty value for the GlacisRouter               |


# SimpleTokenMediator

**xERC20s through Glacis.**

The SimpleTokenMediator is a [GlacisClient](/glacis-core/references/smart-contracts/glacisclient) whose purpose is to transport xERC20 tokens across chains. By using the SimpleTokenMediator for xERC20, you can easily add additional bridges through a single interface & define advanced security rules based off of token payloads.

You can discover the source smart contract [`SimpleTokenMediator.sol`](https://github.com/glacislabs/v1-core/blob/main/contracts/mediators/SimpleTokenMediator.sol) in this repository.

The constructor arguments for the SimpleTokenMediator are identical to an ownable GlacisClient:

* The address of GlacisRouter
* The default GMP quorum for this contract to effectively receive a message
* The owner of the contract

```solidity
    constructor(
        address _glacisRouter,
        uint256 _quorum,
        address _owner
    ) GlacisClient(_glacisRouter, _quorum) {
        _transferOwnership(_owner);
    }
```

## Configuration

Similar to the GlacisClient, routes and quorum must be configured properly to build your firewall. You can check the GlacisClient page for more information.

The SimpleTokenMediator must also know which token it is sending across chains, which can be set with the `setXERC20` function.

```solidity
function setXERC20(address _xERC20Token) public onlyOwner {
    xERC20Token = _xERC20Token;
}
```

| Parameter Name            | Parameter Description                                                  |
| ------------------------- | ---------------------------------------------------------------------- |
| **address** \_xERC20Token | The address of your xERC20 that this SimpleTokenMediator should bridge |

The xERC20 that you control ought to also give this newly deployed SimpleTokenMediator burn & mint abilities.

***

## The Send Function

Sending a token across chains is similar to sending a token via Glacis client, where the adapaters and fees are still defined.

```solidity
function sendCrossChain(
    uint256 chainId,
    bytes32 to,
    address[] memory adapters,
    CrossChainGas[] memory fees,
    address refundAddress,
    uint256 tokenAmount) 
public payable virtual returns (bytes32, uint256)
```

```solidity
function sendCrossChainRetry(
    uint256 chainId,
    bytes32 to,
    address[] memory adapters,
    CrossChainGas[] memory fees,
    address refundAddress,
    bytes32 messageId,
    uint256 nonce,
    uint256 tokenAmount) 
public payable virtual returns (bytes32, uint256)
```

| Parameter Name                                | Parameter Description                                                                                                                                                                                                                                                          |
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **uint256** chainId                           | The Glacis chain ID of the blockchain that you wish to send a token to                                                                                                                                                                                                         |
| **bytes32** to                                | The address of the destination contract that you are sending a token to                                                                                                                                                                                                        |
| **address\[]** **memory** adapters            | An array of adapters to send the message over, such as an [Axelar](https://www.axelar.network/) adapter. Can be an address if you wish to use an adapter that Glacis does not use, otherwise it can be a [GMP ID](/glacis-core/references/supported-gmps) stored as an address |
| **GlacisRouter.CrossChainGas\[] memory** fees | An array of fees that correspond to the amount provided to each adapter. This is a parallel array to the adapters parameter. Its rules are the same as in the [GlacisRouter](/glacis-core/references/smart-contracts/glacisrouter#crosschaingas)                               |
| **address** refundAddress                     | The address to which excess native currency is sent to if an adapter deems that the user overpaid                                                                                                                                                                              |
| **uint256** tokenAmount                       | The amount tokens to send to the destination chain                                                                                                                                                                                                                             |

By default, these messages are retriable. All messages return a bytes32 messageID and a uint256 nonce.

***

## Errors

| Error Name                                           | Error Description                                           |
| ---------------------------------------------------- | ----------------------------------------------------------- |
| SimpleTokenMediator\_\_DestinationChainUnavailable() | Occurs when the destination chain has no remote counterpart |


# Supported Chains

The core infrastructure of Glacis is deployed across the following chains. Each deployment undergoes integration testing to ensure that all Glacis operations function correctly on the respective chain and with the actual GMPs.

Testnet chains only communicate with other Testnet chains while Mainnet chains only communicate with other Mainnet chains.

## Testnet

<table><thead><tr><th>Chain Name</th><th>Chain ID</th><th>Glacis Router Address</th><th data-hidden>Sample Contract</th></tr></thead><tbody><tr><td>avalanche-testnet</td><td>43113</td><td>0x1Ce678F0e7834713868877C34F84C2cfaf511aFe</td><td>0xe39f21885E56Bef104D19fbA1EeB76c74a70ae7a</td></tr><tr><td>bsc-testnet</td><td>97</td><td>0x7B46E11429F51fb1E4324273fa26CF92c63Df18d</td><td>0x4358bDEfCF7De985fA84C8ad0C7A12c81b7cdd31</td></tr><tr><td>arbitrum-sepolia</td><td>421614</td><td>0x51f4510b1488d03A4c8C699fEa3c0B745a042e45</td><td>0x58eeC04A92E7dD25c6689098880F4B0B1c4dCe75</td></tr><tr><td>optimism-sepolia</td><td>11155420</td><td>0xefc27DdE9474468ED81054391c03560a2A217b87</td><td>0x58f43594f87bd98a556aF9698d6084294ac1A61d</td></tr><tr><td>moonbase-alphanet</td><td>1287</td><td>0xD404d5a915722807323a3Ec76F728D9b9F2BcF9d</td><td>0xCF506DCa98bCc42d1A71f8e637bD1f190E672E66</td></tr><tr><td>somnia-testnet</td><td>50312</td><td>0x33D27A8f34F8C9d4891957435c7E302626938EDC</td><td>0x1984e654F840988111D108Dc3CaF3091f5d769E4</td></tr></tbody></table>

***

## Mainnet

<table data-full-width="true"><thead><tr><th width="132">Chain</th><th width="100">Id</th><th width="427">Glacis Router Contract</th><th width="435">Glacis Token Mediator Contract</th><th data-hidden>Tested</th></tr></thead><tbody><tr><td>Ethereum</td><td>1</td><td><pre><code>0xa8E809b54cff39bCA2ca75d49E13F9BfCEA2864e
</code></pre></td><td><pre><code>0x59295f49e29524453c4f3c00F2AeC63c0A64edb1
</code></pre></td><td></td></tr><tr><td>Optimism</td><td>10</td><td><pre><code>0xb515a38AE7FAb6F85aD03cBBa227D8c198823180
</code></pre></td><td><pre><code>0xD404d5a915722807323a3Ec76F728D9b9F2BcF9d
</code></pre></td><td>Yes</td></tr><tr><td>BSC</td><td>56</td><td><pre><code>0xaAcFE6eDad6F7401F96A766f1963FD52E90D9036
</code></pre></td><td><pre><code>0x5412Ea23D3843a0A2B20888C8e8B7f9fA09Ff09A
</code></pre></td><td>Yes</td></tr><tr><td>Moonbeam</td><td>1284</td><td><pre><code>0x46c2996ee4391787Afef520543c78f2C1aE3fE22
</code></pre></td><td><pre><code>0x00E279d42540A3e541b2e182E32D7E509993d14B
</code></pre></td><td></td></tr><tr><td>Base</td><td>8453</td><td><pre><code>0xb515a38AE7FAb6F85aD03cBBa227D8c198823180
</code></pre></td><td><pre><code>0xD404d5a915722807323a3Ec76F728D9b9F2BcF9d
</code></pre></td><td>Yes</td></tr><tr><td>Arbitrum One</td><td>42161</td><td><pre><code>0x46c2996ee4391787Afef520543c78f2C1aE3fE22
</code></pre></td><td><pre><code>0x00E279d42540A3e541b2e182E32D7E509993d14B
</code></pre></td><td>Yes</td></tr><tr><td>Avalanche C-Chain</td><td>43114</td><td><pre><code>0xEB50881a831E5094A8aCd716007C5F885f114736
</code></pre></td><td><pre><code>0x2f7cb5Df24AF81b1077634d2c062D5992A448989
</code></pre></td><td>Yes</td></tr></tbody></table>


# Supported GMPS

Glacis supports currently the following GMPs:

| Name       | Version        | Glacis Id |
| ---------- | -------------- | --------- |
| Axelar     | 0.34 (Q1/2024) | 1         |
| Layer Zero | v1.0.7         | 2         |
| Wormhole   | V2             | 3         |
| CCIP       | v0.8           | 4         |
| Hyperlane  | V3.6.2         | 5         |

The Glacis ID is the identifier utilized to select a specific GMP when routing through Glacis.


