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

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

### The three layouts

* **`compact`** — one card: a **You pay** pane and a **You receive** pane with a swap button on the seam between them, a **Compliance** and a **Quote** button, and a rate line that opens onto the network fee and the estimated time.
* **`wide`** — the From / To tiles and the amount on a 360 px card, beside a 280 px **You receive** card that carries the received amount, the rate (open), and the Compliance and Quote rows with their verdicts. It folds to one column below a 660 px container.
* **`pill`** — a single button, *Exchange with ZeroDelta · 100 USDC*, that opens the compact card in a modal or a drawer. See [The pill](#the-pill).

Each pane prints a dollar line under its amount when the gateway's price snapshot covers the asset (read through your proxy's `prices` path — see The Keyless Proxy). The dollar figure is display only: it is never an input to a quote or a signature, and an asset the snapshot does not price simply shows no dollar line.

From **v4.8.0** amounts read the way the ZeroDelta app prints them. The amount field groups with thousands separators while it is typed (`1,234,567.5`), and the received amount is shown to seven significant digits, grouped, with the exact figure as a tooltip when the label is rounded. A comma is only ever the separator: the value the card holds, and reports through `onInputChange`, is the bare number (`1234567.5`), and a comma typed by hand is ignored rather than read as a decimal point. A long amount shrinks to fit its slot instead of being cut off.

From **v4.10.0** a quote left on screen for two minutes without any input on the page — no pointer, key, wheel, scroll or touch — is paused, on the same rule the ZeroDelta app uses. The rate line says *paused due to inactivity* and the button reads **Refresh quote**. The card will not sign a paused quote, nor one priced before the payer came back: their next input re-prices it once, and the button reads *Refreshing quote…* until the new quote lands. A transaction already being signed is never interrupted. This applies to every card and to the pill. The widget does not re-quote on a timer, so a card left alone makes no requests.

### Component props

#### `StableGenieExchange` / `StableGenieRwaExchange`

| Prop                                      | Type                                  | Default                     |
| ----------------------------------------- | ------------------------------------- | --------------------------- |
| `mode`                                    | `"exchange" \| "onramp" \| "offramp"` | `"exchange"`                |
| `product`                                 | `"stable" \| "rwa"`                   | `"stable"`                  |
| `variant`                                 | `"compact" \| "wide" \| "pill"`       | `"compact"`                 |
| `openAs`                                  | `"modal" \| "drawer"`                 | `"modal"` — pill only       |
| `heightPx`                                | `number`                              | fit-content                 |
| `assets`                                  | `SupportedAsset[]`                    | the product's full universe |
| `defaultFromChainId` / `defaultToChainId` | `number`                              | —                           |
| `defaultFromAsset` / `defaultToAsset`     | `SupportedAsset`                      | —                           |
| `defaultAmount`                           | `string`                              | —                           |
| `onInputChange`                           | `(input: ExchangeInput) => void`      | — see [Events](#events)     |
| `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                    | a *Pay with ZeroDelta* button — see [The pill](#the-pill) |
| `openAs`          | `"modal" \| "drawer"`    | `"modal"`                    | pill only                                                 |
| `moneyDisplay`    | `string`                 | `amount` and `toAsset`       | pill face, e.g. `"$99.00"`                                |
| `defaultExpanded` | `boolean`                | `false`                      | pill only — start with the card already open              |
| `settlement`      | `"stablecoin" \| "bank"` | `"stablecoin"`               | how **you** are settled, never a payer choice             |
| `bankAccount`     | `BankPayoutAccount`      | —                            | display-only; a masked tail, never an account number      |

The card shows the payer one **You pay** pane — the funding token they picked, its amount and their balance — a **Compliance** row with the verdict, the network fee and the estimated time, and the pay button. Before a wallet is connected it asks for one and shows nothing else.

From **v4.11.0** the **Pay with** list offers every asset and chain the payment can be funded from (within `fundingAssets`): the ones the payer holds first, the empty ones after them, greyed as *No balance on this chain*. The card prices the most promising options as it opens, and shows a loading state while it reads the payer's balances and prices them; an option it did not price up front is priced when the payer picks it.

From **v4.12.0** the payer picks the token first and the network second (see [Choosing a token](#choosing-a-token)). Every token is listed, including one the payer holds on no network; a network with no balance is greyed on that token's network list as *No balance on this chain*.

> **`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. A `"bank"` pill keeps the ZeroDelta Link island (below), the one surface that renders that panel.

#### `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 island: a collapsed button that morphs into a one-click card. `variant="pill"` on `StableGenieSwap` renders it, and so does a `StableGeniePay` pill with `settlement: "bank"`; you can also mount it directly. `StableGenieExchange` and every other `StableGeniePay` pill use [the pill launcher](#the-pill) instead.

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

### The pill

`variant="pill"` on `StableGenieExchange` (and `StableGenieRwaExchange`) and on `StableGeniePay` renders one button — *Exchange with ZeroDelta · 100 USDC*, or *Pay with ZeroDelta · $20.00* — that opens the ordinary compact card over your page.

```tsx
<StableGenieExchange variant="pill" openAs="drawer" defaultFromChainId={1} defaultFromAsset="USDC" defaultAmount="100" />
```

| `openAs`   | Opens as                                                                        |
| ---------- | ------------------------------------------------------------------------------- |
| `"modal"`  | the card centred over a dimmed page (default)                                   |
| `"drawer"` | a full-height panel docked to the right edge, the pay button pinned to its foot |

* **Nothing runs behind a closed pill.** The card — and with it every catalogue read, quote and balance read — mounts only when the payer opens it. It is the same card with the same guarantees, not a second implementation.
* **It closes the way a dialog should.** Escape, the close button in the card's head, or a click on the dimmed page close it, and keyboard focus returns to the pill.
* **The dim layer is `position: fixed`.** It covers the viewport — or the nearest ancestor that establishes a containing block for fixed elements (one with a `transform`, `filter` or `contain: paint`). Give a region of your page one of those if you want the dialog confined to it; the widget playground does exactly that with its preview frame.

The pill's face uses `defaultAmount` and `defaultFromAsset` (exchange) or `moneyDisplay`, falling back to `amount` and `toAsset` (pay).

### 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                   |
| `onInputChange` | `ExchangeInput`                          | the exchange card's pair or amount changed (v4.8.0)    |

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

> **The `default*` props only seed the exchange card; `onInputChange` is how you follow it.** After the first render the card owns its pair and amount, so a token picked, a direction flipped or an amount typed inside it does not change your props. If you show the same trade anywhere else — a summary, a configurator, a URL — listen to `onInputChange`, which reports `{ fromChainId, fromAsset, toChainId, toAsset, amount }` on every change and once on mount. `amount` is the bare number as typed; a leg is `null` until the card has one. It is on `StableGenieExchange` and `StableGenieRwaExchange` only.

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

From **v4.11.0** a picker never offers what cannot be traded: a token the gateway cannot deliver for the pair — a measured refusal, a lane the operator has closed, a deployment it cannot reach — is not listed, and it is offered again by itself as soon as the live data says it delivers.

#### Choosing a token

From **v4.12.0** every picker — **Exchange from**, **Exchange to**, the RWA exchange and the deposit card's **Pay with** — works the way the ZeroDelta app's does: the payer chooses the **token** first and its **network** second.

* **One row per token**, with the logos of the networks it is on. On the **Pay with** side the row shows the payer's total across those networks, in dollars over the token amount.
* A token on **one network** is picked in one click. A token on **several** opens the list of its networks, each with that network's own balance.
* **Search reads token and network together.** `usdc base` keeps USDC narrowed to Base, so it is one click. A contract address works too.
* Reopening the picker shows the current choice in a **Selected** row at the top.
* A network the route cannot deliver to is not shown at all. A network the payer holds no balance on is listed, greyed, on the token's network list.

### 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. The card's **Quote** details list both orders.

See [Integration Guide](/zero-delta/integration-guide.md) 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. The exchange and deposit cards carry a **Compliance** button whose shield turns red on a block; where there is room — the wide card's side panel, the drawer, the deposit card — it also shows the verdict as a pill (`Passed`, `Allowed`, `Review`, `Remediate`, `Blocked`, `Not screened`). It opens the controls that actually ran — which policies applied, which jurisdiction, which wallets were screened. The swap card and the ZeroDelta Link island show the same disclosure as a one-line chip. You do not configure this; it is on.

The jurisdiction shown is the gateway's policy region (`EU`). From **v4.9.0** the panel names the visitor's country beside it (`EU · Spain (ES)`) when the decision carries `sourceCountry` — which your proxy adds, since the gateway returns the region only. See The Keyless Proxy → Naming the visitor's country. Without it the panel shows the region alone.

Beside it, a **Quote** button opens the quote's own facts: the route (`Direct`, or `Via` the connector of a chained order), the provider, what is sent and received, the rate, the fee, the network fee and the estimated time.

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.
