Whitechain escrow
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. Gas is paid in test WBT from the Whitechain faucet.
The settlement token is ITC (Inferit Test Credit): 6 decimals, so one base unit is exactly one µUSD and 1 ITC counts as one test dollar in prices. Its faucet() mints test ITC at most once per 24h per address, and faucetTo(address) (v2) lets anyone pay the gas of that mint for another address. It has no monetary value.
Live deployment
Read from the contract itself, so this always matches what is deployed. The deployment record lives in the repository at chains/whitechain/deployments/1874.json (contracts) and 1874.admin.json (the Safe).
What the API reports (GET /v1/rails)
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.
- Operator (the API's hot key): can call
settlewithin the rules below and, in v0.3, credit a front-run x402 authorization to its signer out of the escrow's unattributed balance (below); nothing else. A new operator takes effect only after a timelock (24h 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
depositWithAuthorizationand, on testnet, mints sponsored ITC withfaucetTo. 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
// 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)The spending cap
The cap 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 buy flow 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 Pay per request with x402; 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:
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. - 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 outstandingsetSpendingCapBySigsignature 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
transferWithAuthorizationdirectly, the tokens still land in the escrow, because the signature fixes the recipient. They are just 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'sAuthorizationUsedcame 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 isunattributedBalance(). 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-2612permit. USDC.e implements the same functions, so the same escrow works with it on mainnet.
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:
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 nonceThis 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, which ITC v2 and USDC.e both have. The website does not use it yet.
The facilitator key
The hosted API submits x402 payments, and testnet faucet claims for agents with no gas (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 micro-units (µITC) 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 (the hosted testnet demo uses 2 minutes so reviewers see settlement quickly; the default and the mainnet plan is 1 hour), and at once when a withdrawal, a lower cap or the cap's expiry is near. Each batch is one settle transaction:
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 batchEffects: 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 on your dashboard; market-wide totals are on On-chain metrics.
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:
approveanddepositAndSetCap, and withdrawals. - Sellers:
registerSellerandwithdrawEarnings. - The operator: every
settlebatch. Buyers do not pay gas per request. - The facilitator key (v0.3): every x402 payment (
depositWithAuthorization) and every sponsored testnet faucet claim. An agent paying with x402 needs no WBT at all until it wants to withdraw. A relayeddepositWithPermitis 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.
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 for what this does and does not protect against.