# Whitechain escrow

> How the MarketEscrow contract on Whitechain Sepolia bounds what the operator can settle from a buyer, how x402 payments and gasless deposits credit the signer, who pays gas, and the live parameters.

Inferit settles on Whitechain. Buyer funds sit in the MarketEscrow contract; the operator settles usage in batches, but only inside limits the buyer set and the contract is designed to enforce. The contracts are unaudited testnet software.

## Network

Whitechain Sepolia is an OP Stack L2 testnet: chain id `1874`, RPC `https://rpc.testnet.whitechain.io`, explorer [explorer.testnet.whitechain.io](https://explorer.testnet.whitechain.io). Gas is paid in test WBT, Whitechain's native token, from the [Whitechain faucet](https://faucet.testnet.whitechain.io). You need WBT only for transactions you send yourself: deposits, seller registration and withdrawals.

The settlement token is **ITC (Inferit Test Credit)**, the test money you pay for inference with. It has 6 decimals, so one base unit is exactly one µ (1 µ = 0.000001 ITC), and 1 ITC counts as one test dollar in prices. It has no monetary value. Inferit's ITC faucet gives 1,000 ITC per address per 24 h, free, and the token enforces the limit. The API's sponsored faucet ([`POST /v1/faucet`](https://inferit.cc/docs/api-reference.md#faucet)) mints with `faucetTo(address)` and pays the gas, so the claim needs no WBT. The token's own `faucet()` also works, sent from your wallet, so it uses a little WBT.

- **Network name**: Whitechain Sepolia
- **Chain ID**: 1874
- **RPC URL**: https://rpc.testnet.whitechain.io
- **Currency symbol (gas)**: WBT
- **Block explorer**: https://explorer.testnet.whitechain.io
- **ITC token (6 decimals)**: 0x2E672dFE33EA977FD064E01aDe7d8c73B3Be7fBB

[Open the Buy workspace](https://inferit.app/buy)

### Live deployment

Read from the contract itself, so this always matches what is deployed. Every address below links to Blockscout, where the escrow's verified source and every transaction can be checked.

Whitechain Sepolia, chain id 1874, as deployed on 2026-10-04 (the page reads the live values from the contract):

| Contract or role | Address |
| --- | --- |
| MarketEscrow v0.3 | [0x705dD9eCffa888FbcF2849eAE2D3b136eb78B7e0](https://explorer.testnet.whitechain.io/address/0x705dD9eCffa888FbcF2849eAE2D3b136eb78B7e0) |
| ITC (Inferit Test Credit, TestCredit v2, 6 decimals) | [0x2E672dFE33EA977FD064E01aDe7d8c73B3Be7fBB](https://explorer.testnet.whitechain.io/address/0x2E672dFE33EA977FD064E01aDe7d8c73B3Be7fBB) |
| Owner and fee recipient (2-of-3 Safe) | [0x69ADd11c8c12C26bbB5be93a330853C4eac7F18b](https://explorer.testnet.whitechain.io/address/0x69ADd11c8c12C26bbB5be93a330853C4eac7F18b) |
| Guardian (can only pause) | [0x4fF520e5fA18dcfC67E2c14cf81172c0CE2F1Bf0](https://explorer.testnet.whitechain.io/address/0x4fF520e5fA18dcfC67E2c14cf81172c0CE2F1Bf0) |
| Settlement operator (the API's hot key; can only settle) | [0x639C007216D1020d6448d60E695F199a0d4404Ab](https://explorer.testnet.whitechain.io/address/0x639C007216D1020d6448d60E695F199a0d4404Ab) |
| x402 facilitator key (pays gas only; no role in the escrow) | [0xc8c8E79E2547ba1D81FAC12c84867B1DbD83e602](https://explorer.testnet.whitechain.io/address/0xc8c8E79E2547ba1D81FAC12c84867B1DbD83e602) |

| Parameter | Value |
| --- | --- |
| `operatorDelay` | 24 h |
| `settleLimit` | 1,000 ITC per 3,600 s |
| `withdrawDelay` | 1 h |
| `maxFeeBps` | 1000 (10%, immutable) |
| `AUTH_CREDIT_TTL` | 30 days |

## Roles and limits

-   **Owner: a 2-of-3 Safe.** Sets the fee recipient, the guardian and the settlement limit, proposes operator changes, and unpauses. It cannot move buyer funds or change the fee cap or the withdrawal delay. Ownership changes are two-step. On testnet one team member holds all three signer keys.
-   **Operator** (the API's hot key): can call `settle` within the rules below and, in v0.3, credit a front-run x402 authorization to its signer out of the escrow's unattributed balance ([below](https://inferit.cc/docs/payments/whitechain.md#x402)); nothing else. A new operator takes effect only after a timelock (24 h on the hosted deployment), so a compromised admin key cannot swap it instantly.
-   **Guardian**: can pause deposits and settlement in an emergency, nothing else. Only the owner can unpause.
-   **Circuit breaker**: the total the operator can settle per window, across all buyers, is capped by `settleLimit`. A leaked operator key can therefore settle at most each buyer's remaining cap, and no more than the window limit in total, before the owner pauses it.
-   **Sellers**: register and change their payout from their own wallet only. No API key can redirect earnings.
-   **Facilitator key** (v0.3, the API's second hot key, separate from the operator): submits x402 payments with `depositWithAuthorization` and, on testnet, mints sponsored ITC with `faucetTo`. Anyone may make those two calls, so the key has no role in the escrow: it pays gas and nothing else. It cannot settle, and it cannot send a payment anywhere but the signer's own escrow balance.

## Calls

_MarketEscrow / TestCredit_

```
// buyer
TestCredit.approve(escrow, amount)                   // ITC, 6 decimals
MarketEscrow.depositAndSetCap(amount, cap, expiry)   // or deposit() + setSpendingCap()
MarketEscrow.depositWithPermit(buyer, amount, cap, expiry, deadline, v, r, s, termsSignature)  // v0.3: gasless, relayed (EIP-2612)
MarketEscrow.depositWithAuthorization(from, value, validAfter, validBefore, nonce, v, r, s)   // v0.3: x402 (EIP-3009)
MarketEscrow.requestWithdraw(amount)                 // starts the delay
MarketEscrow.executeWithdraw()                       // after withdrawDelay; cancelWithdraw() also available

// seller
MarketEscrow.registerSeller(payout)                  // from the seller wallet; payout = msg.sender if zero
MarketEscrow.withdrawEarnings()                      // pays the registered payout

// views
buyerState(buyer)   -> (deposit, cap, spent, expiry, pendingWithdraw, withdrawReadyAt)
sellerState(seller) -> (registered, payout, earnings)
```

## Deposits and the spending cap

A buyer deposits with two transactions from its own wallet, so both cost a little WBT: `approve` on the ITC token, then `depositAndSetCap(amount, cap, expiry)`. The deposit stays the buyer's until it is settled or withdrawn.

The cap (the ceiling) is the total the operator may ever settle from you, counted cumulatively from `spent`. Setting it again replaces the cap but keeps `spent`, so raising a 100 ITC cap to 150 after spending 40 leaves 110 of headroom. After `expiry`, nothing more can be settled from you until you set a new cap, so the API stops accepting new usage a settlement margin (at least five minutes) before the expiry. The app suggests a cap equal to your deposit, at most 25 ITC, expiring in 7 days. A relayed, gasless variant (`setSpendingCapBySig`, EIP-712) exists for wallets that cannot send transactions.

## x402 and gasless deposits (v0.3)

`MarketEscrow` v0.3 adds two ways to deposit without the buyer sending a transaction. Both are relayed: someone else submits the buyer's signature and pays the gas. Both are blocked while the escrow is paused, like every deposit. Agents should start at [Agents](https://inferit.cc/agents) or [agents.md](https://inferit.cc/agents.md); this section is the contract view.

### depositWithAuthorization (x402)

The buyer signs an EIP-3009 `TransferWithAuthorization` for the token, naming the escrow as the recipient. This is exactly what a standard x402 client signs for the `exact` scheme. The API's facilitator submits it:

_MarketEscrow.depositWithAuthorization (v0.3)_

```
depositWithAuthorization(from, value, validAfter, validBefore, nonce, v, r, s)   // blocked while paused

require  (from, nonce) not credited before             // authorizationCredited(from, nonce)

if token.authorizationState(from, nonce) is unused:    // the normal path: anyone may call (the facilitator does)
    token.transferWithAuthorization(from, escrow, value, validAfter, validBefore, nonce, v, r, s)
    // the token checks the signature, the validity window and the nonce;
    // a signature that names any other recipient fails
else:                                                  // recovery: the token already executed it
    require  caller is the operator or the owner
    require  the signature recovers to "from" over TransferWithAuthorization(from, to = escrow, ...)
             under token.DOMAIN_SEPARATOR()
    require  unattributedBalance() >= value            // balance - (totalDeposits + totalEarnings + feeAccrued)
    // recovered = true

credit "from":   deposit += value
                 cap += value;  expiry = max(expiry, now + AUTH_CREDIT_TTL)
                 // if the old cap had expired: cap = spent + value, expiry = now + AUTH_CREDIT_TTL
emit Deposited(from, value), SpendingCapSet(from, cap, expiry),
     DepositedWithAuthorization(from, value, nonce, recovered)
```

-   **Credited to the signer, for spending.** The payment raises both the deposit and the cap by `value`, so the operator can settle the request it paid for. It also pushes the cap's expiry to at least now + `AUTH_CREDIT_TTL`, an immutable constructor parameter (30 days by default). Money paid through x402 is meant to be spent, so it is never stranded behind an expired cap. Whatever the request does not use stays as the buyer's balance, withdrawable like any deposit.
-   **A prepaid meter, not a payment per µ.** x402 amounts are in token base units, so a payment of 1 µ is expressible. But each payment is an on-chain transaction that costs gas, so the API asks for at least 0.01 ITC (10 000 µ) per payment. The payment tops up the payer's escrow balance; each request is charged its exact µ cost against it, and settlement to sellers is batched.
-   **If the old cap had expired,** the payment starts fresh terms: the cap becomes `spent + value`, so headroom the buyer let lapse is not revived. Like any cap change it uses up the buyer's escrow nonce, so an outstanding `setSpendingCapBySig` signature stops being valid.
-   **Front-running cannot steal a payment.** An authorization is public once it is sent. If someone submits it to the token's `transferWithAuthorization` directly, the tokens still land in the escrow, because the signature fixes the recipient. They are not attributed to anyone yet, and nobody can withdraw them. The **operator or the owner** then credits them to the signer with the same call (`recovered = true`). The escrow checks the signature itself, that the same `(from, nonce)` was never credited, and that the unattributed balance covers the amount. Recovery is limited to those two roles because the token cannot tell a used authorization from a cancelled one; they first check that the token's `AuthorizationUsed` came with a transfer to the escrow in the same transaction. For a payment the API is serving, it runs that check and sends the recovery from the operator key itself, then serves the request.
-   **Solvency.** v0.3 holds `token.balanceOf(escrow) ≥ totalDeposits + totalEarnings + feeAccrued`, and all three are readable on chain. The difference is `unattributedBalance()`. It grows only when tokens are sent to the escrow directly, such as a front-run authorization, and only a recovered credit can attribute it.
-   **Token.** ITC v2 (TestCredit) implements EIP-3009 like Circle's FiatToken v2: `transferWithAuthorization`, `receiveWithAuthorization`, `cancelAuthorization`, `authorizationState`, with one EIP-712 domain (name "Inferit Test Credit", version "1") shared with EIP-2612 `permit`.
-   **Mainnet money (planned).** On mainnet, buyers pay in USDC (Portal-bridged USDC.e). On Whitechain Sepolia, USDC.e is Circle's FiatToken v2.2 with EIP-3009, so x402 works with it as it does with ITC; the mainnet token is expected to match. USDT has no EIP-3009 functions (USDT.e on Whitechain has EIP-2612 `permit` only), so x402's default `exact` path cannot take it. USDT is planned through x402's Permit2 transfer method, which takes any ERC-20: it needs the x402 Permit2 contracts deployed on Whitechain and support in Inferit's escrow and facilitator. Until then the API accepts EIP-3009 payments only. Nothing on mainnet is deployed: on testnet everything is paid in ITC, which has no monetary value.

A prepaid balance charged by the µ: a worked example at example prices

| Line | What | Amount |
| --- | --- | --- |
| Prepaid | One x402 payment, the minimum | 10 000 µ |
| Request | One request · 1,000 in · 500 out | minus 168 µ |
| Balance | Stays yours: spend it or withdraw it | 9 832 µ |

Settled. The seller is paid its share of the 168 µ in the next batch on Whitechain Sepolia: one transaction, one line per seller.

Example: a seller asking 80 000 µ / 160 000 µ per 1M tokens before the fee. 1,000 input and 500 output tokens cost 160 µ plus the 5% fee, 8 µ: 168 µ. Testnet demo: 1 µ is a millionth of an ITC, a test token with no monetary value.

### depositWithPermit (gasless deposit)

The buyer signs two messages and sends no transaction: an EIP-2612 `permit` that lets the escrow pull the amount, and the deposit terms (amount, cap, expiry) under the escrow's own EIP-712 domain. A relayer submits both and pays the gas:

_MarketEscrow.depositWithPermit (v0.3)_

```
depositWithPermit(buyer, amount, cap, expiry, deadline, v, r, s, termsSignature)   // blocked while paused

if caller != buyer:                                    // relayed: the buyer also signs the terms
    require  termsSignature is buyer's EIP-712 signature (the escrow's domain) over
             DepositWithPermit(buyer, amount, cap, expiry, nonce = nonces(buyer), deadline)
token.permit(buyer, escrow, amount, deadline, v, r, s) // EIP-2612; a permit someone already submitted is tolerated
require  token.allowance(buyer, escrow) >= amount
pull amount from buyer, credit it as buyer's deposit
set buyer's cap and expiry, as depositAndSetCap does   // consumes buyer's escrow nonce
```

This is the gasless version of `depositAndSetCap`. The cap and expiry are the ones the buyer signed, so a relayer or a front-runner cannot choose them, and the usual rules for raising and lowering a cap apply. It needs a token with `permit`: ITC v2 has it. The Buy workspace deposits with `depositAndSetCap`.

### The facilitator key

The hosted API submits x402 payments, and testnet faucet mints for agents that hold no WBT (`faucetTo`), from `WHITECHAIN_FACILITATOR_KEY`. It is a hot key separate from the settlement operator, funded with WBT for gas only. It has no role in the escrow, so a leak costs at most its WBT. Every payment it submits is still credited to the wallet that signed it. It cannot recover a front-run authorization; that takes the operator or the owner. The operator's bounds (caps, registered sellers, the fee cap, the circuit breaker) are unchanged.

## Settlement

The API meters each request in µ (a micro-ITC on the testnet) and groups unsettled charges into batches. A buyer is settled once their unsettled usage reaches 0.10 ITC or their oldest charge reaches the maximum age, whichever comes first (by default 1 hour, also the planned mainnet setting; the cadence a server runs is in `GET /metrics`, `settlement.cadence`), and at once when a withdrawal, a lower cap or the cap's expiry is near. Each batch is one `settle` transaction:

_checks the contract performs_

```
settle(bytes32 batchId, Line[] lines)   // operator only
Line { buyer, seller, sellerAmount, fee }

require  seller is registered
require  fee * 10_000 <= sellerAmount * maxFeeBps     // maxFeeBps immutable, <= 1000 (10%)
require  block.timestamp <= buyer.expiry
require  buyer.spent + sellerAmount + fee <= buyer.cap
require  buyer.deposit >= total for the buyer
require  settled in this window + batch total <= settleLimit   // circuit breaker
require  batchId not used before                       // idempotent
// any failing line reverts the whole batch
```

Effects: your deposit is debited, the seller's earnings credited and the fee accrued, with `LineSettled` and `BatchSettled` events. Your own escrow events, with explorer links, are in the [Buy workspace](https://inferit.app/buy); market-wide totals are on [On-chain metrics](https://inferit.app/analytics).

## Finality

Whitechain is an OP Stack L2. A settlement is first included by the sequencer (unsafe, a soft confirmation), becomes safe once its batch is posted to Ethereum (typically within about 30 minutes), and finalized once that Ethereum block is final (about 13 minutes later). Only finalized is irreversible; "pending finality" means included but not yet finalized.

The site shows each settlement as **pending finality** until its block is at or below the chain's finalized head, then as **finalized**.

## Who pays gas

-   Buyers: `approve` and `depositAndSetCap`, an ITC `faucet()` claim sent from their own wallet, and withdrawals.
-   Sellers: `registerSeller` and `withdrawEarnings`.
-   The operator: every `settle` batch. Buyers do not pay gas per request.
-   The facilitator key (v0.3): every x402 payment (`depositWithAuthorization`) and every sponsored testnet faucet mint. An agent paying with x402 needs no WBT at all until it wants to withdraw. A relayed `depositWithPermit` is paid for by whoever relays it.

Gas on an OP Stack chain includes a small L1 data fee. On testnet it is all test WBT.

## What is public

Everything in the escrow is public chain data. `LineSettled` names the buyer and seller addresses of every line with its amounts, `buyerState` and `sellerState` show any account's deposit, cap, spent and earnings, and `SellerRegistered` links a seller to its payout address. Anyone can list sellers and their volume, and a buyer can match its own requests to the seller that served them. See [On-chain visibility](https://inferit.cc/docs/security-trust.md#on-chain).

x402 payments are public too: each one is a `DepositedWithAuthorization` event naming the paying wallet, and its settlement lines are ordinary `LineSettled` events.

## Withdrawals

`requestWithdraw` starts a fixed delay (immutable per deployment; see the live value above). Usage you already consumed can still settle during the delay, which is what lets the API serve requests before they settle. After the delay, `executeWithdraw` returns the funds. The contract is designed so withdrawals keep working while deposits and settlement are paused.

See [Security & trust](https://inferit.cc/docs/security-trust.md) for what this does and does not protect against.

---

Canonical page: https://inferit.cc/docs/payments/whitechain
All pages as markdown: https://inferit.cc/llms.txt
Testnet demo: test tokens, no monetary value.
