Testnet
What x402 is
x402 is an open protocol that puts payment into HTTP. A server answers 402 Payment Required with what it wants to be paid (token, amount, recipient, network) in a PAYMENT-REQUIRED header. The client signs a payment and repeats the request with a PAYMENT-SIGNATURE header, and the server settles it on chain before it answers. Both headers are base64-encoded JSON. A facilitator verifies and settles the payment.
Inferit speaks standard x402 v2 with the exact scheme on EVM, so unmodified clients work: for example @x402/fetch with ExactEvmScheme registered for eip155:*. The payment is an EIP-3009 TransferWithAuthorization of the settlement token, signed off chain. The agent never sends a transaction and needs no WBT.
Where the money goes. The authorization names the MarketEscrow contract as the recipient. The escrow pulls the tokens with depositWithAuthorization and credits them to the signer: its deposit and its spending cap both rise by the amount paid. The request is then charged against that balance exactly like an API-key request, under the same on-chain rules (registered sellers only, fee capped, settlement within the cap). x402 does not create a second, looser payment path.
Who runs the facilitator
No x402 facilitator existed for Whitechain, so Inferit runs its own inside the API. To our knowledge it is the first x402 facilitator on Whitechain. It is self-hosted and operated by Inferit; no third party sees or settles the payment.
Separately, we have open-sourced a standalone x402 facilitator that any Whitechain merchant can use (Apache-2.0, whitechain-x402-facilitator). This API does not use it: it keeps its own facilitator, with its own key.
- It submits each payment from a dedicated facilitator key, separate from the settlement operator. That key only pays gas. It holds no role in the escrow: anyone may submit a signed payment to
depositWithAuthorization, and the credit always goes to the wallet that signed. - A signature cannot be redirected: the authorization fixes the recipient to the escrow, and the token rejects any other recipient. If someone submits your authorization straight to the token instead, the tokens still land in the escrow, unattributed. The escrow's operator or owner then credits them to you, and only to you (a recovered payment). While your paid request is in flight the API does this itself, from the operator key, and still serves it.
- Every payment is public on Whitechain as a
DepositedWithAuthorization(from, value, nonce, recovered)event. The on-chain metrics count them separately, and each one links to the explorer.
Quickstart: zero WBT to a paid request
- Make a key. Any EVM private key works, including a brand-new one with no balance at all.
- Get test ITC.
POST /v1/faucet { address }asks the API to mint 1,000 ITC to your address; Inferit pays the gas. It is offered on testnets only, at most once per 24 hours per address, and is rate-limited per IP. - Pay and call. Wrap
fetchwith the x402 client and call/v1/chat/completionswithout a key. The wrapper handles the 402, signs, and retries with the same body.
// npm i @x402/fetch @x402/evm viem then: npx tsx agent.mts (Node 18+, @x402 2.28 or newer)
import { decodePaymentResponseHeader, wrapFetchWithPayment, x402Client } from '@x402/fetch';
import { ExactEvmScheme } from '@x402/evm/exact/client';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
const API = 'https://api-production-c74b9.up.railway.app';
// Any key works, including a brand-new one with zero WBT: the agent never sends a transaction itself.
const account = privateKeyToAccount((process.env.AGENT_KEY as `0x${string}` | undefined) ?? generatePrivateKey());
// 1. Testnet only: the sponsored faucet mints 1,000 test ITC to this address (Inferit pays the gas).
const faucet = await fetch(`${API}/v1/faucet`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ address: account.address }),
});
console.log('faucet', faucet.status, await faucet.text());
// 2. The standard x402 v2 client, unmodified. It only pays in tokens it was told about, so name ITC,
// and cap one payment at 1 ITC (1000000 base units, 6 decimals), whatever the server quotes.
// Pin these two values in real code instead of asking the server that is being paid.
const doc = await (await fetch(`${API}/.well-known/x402`)).json();
const { network, asset } = doc.accepts?.[0] ?? doc;
const ITC = { network, asset };
const client = new x402Client()
.register('eip155:*', new ExactEvmScheme(account))
.setSpendControls({ allowedAssets: [{ ...ITC, maxAmountPerPayment: '1000000' }] });
const fetchWithPayment = wrapFetchWithPayment(fetch, client);
// 3. Call the API with no key: 402 -> sign -> paid retry, all inside fetchWithPayment.
const res = await fetchWithPayment(`${API}/v1/chat/completions`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
model: 'qwen/qwen-2.5-14b-instruct',
messages: [{ role: 'user', content: 'Say hello in five words.' }],
max_tokens: 64, // the quote is the worst case for this body, so a small max_tokens keeps it small
}),
});
const body = await res.json();
console.log(res.status, body.choices?.[0]?.message?.content ?? body);
// The settlement receipt: the escrow deposit transaction on Whitechain, and who paid.
const receipt = res.headers.get('PAYMENT-RESPONSE');
if (receipt) console.log(decodePaymentResponseHeader(receipt));
console.log('cost', res.headers.get('x-inferit-buyer-cost-micro'), 'µITC; the rest stays in your escrow balance');Allow the token, and cap it. The x402 client only pays in tokens it was told about. ITC is not on its built-in list, so a client with default settings refuses the payment ("rejected by spendControls"). The TypeScript snippet adds one allowedAssets entry for ITC on Whitechain Sepolia with maxAmountPerPayment set to 1 ITC. That is configuration of the standard client, not a patch, and it is also your safety limit: the client signs whatever the 402 quotes up to that cap, and nothing in another token.
Python. The Python tab has no x402 dependency: it does the v2 exchange by hand with eth-account and requests, so you can see every field, and it asserts the same token and cap. Any x402 SDK that speaks v2 (the PAYMENT-REQUIRED and PAYMENT-SIGNATURE headers) and the exact scheme on eip155 networks works against the same endpoint. The paid retry must send the same JSON body as the first request, because the quote is pinned to it.
The exchange on the wire:
POST /v1/chat/completions (no Authorization header)
<- 402 PAYMENT-REQUIRED: base64({ x402Version: 2, resource, accepts: [{
scheme: "exact", network: "eip155:1874", amount: "<quote>", asset: <ITC>,
payTo: <MarketEscrow>, maxTimeoutSeconds: 120,
extra: { name: "Inferit Test Credit", version: "1" } }] })
client signs EIP-3009 TransferWithAuthorization(from, to = escrow, value = quote,
validAfter = 0, validBefore = now + 120 s, nonce)
POST /v1/chat/completions (same body) PAYMENT-SIGNATURE: base64(payload)
API verifies -> escrow.depositWithAuthorization(...) from the facilitator key -> receipt
-> routes and serves the request like any API-key request (streaming included)
<- 200 PAYMENT-RESPONSE: base64({ success: true, transaction, network, payer })
x-inferit-buyer-cost-micro, x-inferit-rail, x-request-id, ...The API-key alternative
x402 costs one extra round trip and one signature per request. For steady traffic, use the balance the payments built up with an ordinary API key. The same wallet signs in (EIP-4361, an off-chain signature, no gas) and mints a key; requests with Authorization: Bearer ik_… then spend the escrow balance directly, with no 402.
// Same wallet as the x402 payer: sign in (EIP-4361, no gas) and mint an API key.
const post = (path: string, body: unknown, token?: string) =>
fetch(`https://api-production-c74b9.up.railway.app${path}`, {
method: 'POST',
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
body: JSON.stringify(body),
}).then((r) => r.json());
const { message } = await post('/v1/auth/evm/challenge', { address: account.address });
const { token } = await post('/v1/auth/evm/verify', { message, signature: await account.signMessage({ message }) });
const { key } = await post('/v1/keys', { name: 'agent' }, token); // shown once; ik_…
// From here on: Authorization: Bearer <key>, spending what is left of your escrow balance. No 402 round trip.A person with a browser wallet can do the same on Start here, which also covers funding the escrow directly. The quickstart and the API reference have the details.
Unused payment stays yours
The 402 quote is the worst case for that exact request body: the input estimate plus your output limit (max_tokens, or 1,024 tokens when you send none), all-in, at the top-ranked offer (normally the cheapest healthy one), and never less than 0.01 ITC. You are charged only the metered cost, shown in x-inferit-buyer-cost-micro.
- The remainder stays in the escrow as your balance. Spend it later with an API key (above), or withdraw it:
requestWithdraw, thenexecuteWithdrawafter the withdrawal delay. Withdrawals are on-chain transactions from your wallet, so they need a little WBT, and nobody can pause them. - Each x402 credit extends your cap's expiry to at least 30 days from the payment (
AUTH_CREDIT_TTL), so money paid through x402 stays spendable. - If inference fails after the payment settled, nothing is lost. The credit is already in your escrow balance; the error says so. Retry with an API key, or withdraw.
Limits
| What | Default | Why |
|---|---|---|
| Minimum payment | 0.01 ITC (X402_MIN_PAYMENT_MICRO = 10000) | A deposit costs gas; dust payments are not worth settling |
| Signature validity | 120 s (maxTimeoutSeconds, X402_MAX_TIMEOUT_S) | The client sets validBefore = now + this; the API refuses authorizations that are close to expiry |
| Quote pin | 120 s, per request (path, routing headers and JSON body) | The paid retry verifies against the quote it was shown; a changed request gets a new 402 |
| Signer | A plain key (EOA), 65-byte signature, x402 version 2 | Smart-wallet signatures (ERC-1271, ERC-6492) and the Permit2 transfer method are not accepted |
| One payment per authorization | Each nonce is accepted once | A replayed payment is refused, even though the signature is public once it is on chain |
| Rate limits | 30 paid attempts a minute per paying address, 60 per IP | On top of the API's general per-IP limit |
| Sponsored faucet | 1,000 ITC per address per 24 h (enforced by the token), and 10 calls an hour per IP; testnets only | The API refuses to offer it on any other chain |
| Escrow paused | No new payments | Pausing blocks deposits, including x402; withdrawals keep working |
Streaming works: the payment settles before the first byte, and PAYMENT-RESPONSE arrives with the response headers. The /min{N}/v1/chat/completions discount prefix works too. The API never logs signatures.
Discovery
https://api-production-c74b9.up.railway.app/.well-known/x402: the paid resources and the requirement a client must sign for (scheme, network, asset, payTo and the token's EIP-712 name and version). The panel on this page reads it live.https://api-production-c74b9.up.railway.app/llms.txt: a summary for coding agents with both paths: x402, and the API key (sign-in challenge and verify, thenPOST /v1/keys). This site serves the same file at /llms.txt.
On mainnet
Circle's USDC (bridged to Whitechain as USDC.e) implements the same EIP-3009 functions as ITC, so the same escrow and the same facilitator path work unchanged with it. The EIP-712 domain name and version come from the token itself, and the discovery document reports them. ITC never goes to mainnet; the sponsored faucet is testnet-only.
Contract details: Whitechain escrow: x402 and gasless deposits.