Paying x402 endpoints
There are two ways to pay an x402 endpoint:
- Use the Agent SDK — recommended for autonomous bots. The platform holds the key, you just call
client.x402.pay(). - Sign with your own wallet, then submit to the facilitator — recommended when you want full control of the signing wallet.
This page covers option 2.
The flow
Section titled “The flow”1. Client calls a paid endpoint: GET /premium-data2. Server returns 402 + requirements: 402 Payment Required { network, asset, recipient, value }3. Client signs an EIP-3009 authorization (off-chain).4. Client POSTs the signed payload to: https://x402.0xgasless.com/settle5. Facilitator submits on-chain, returns tx hash, facilitator pays gas.6. Client retries the original request with X-Payment header containing the settled transaction hash (or proof).7. Server verifies the payment and serves the resource.What you sign
Section titled “What you sign”For an x402 payment on Avalanche or Base USDC, you sign an EIP-3009 TransferWithAuthorization typed message against the token’s own EIP-712 domain. Not against the relayer contract — directly against the token.
EIP-712 typed data shape
Section titled “EIP-712 typed data shape”{ "domain": { "name": "USD Coin", // from USDC's `name()` "version": "2", // from USDC's `version()` "chainId": 43113, // current chain "verifyingContract": "0x5425890298aed601595a70AB815c96711a31Bc65" // USDC address on this chain }, "types": { "TransferWithAuthorization": [ { "name": "from", "type": "address" }, { "name": "to", "type": "address" }, { "name": "value", "type": "uint256" }, { "name": "validAfter", "type": "uint256" }, { "name": "validBefore", "type": "uint256" }, { "name": "nonce", "type": "bytes32" } ] }, "primaryType": "TransferWithAuthorization", "message": { "from": "0xYourAddress...", "to": "0xRecipient...", "value": "500000", // atomic units of the token "validAfter": 1781000040, "validBefore": 1781000340, "nonce": "0x..." // 32 bytes, random per authorization }}Example — sign with ethers v6
Section titled “Example — sign with ethers v6”import { Wallet, JsonRpcProvider } from 'ethers';
const provider = new JsonRpcProvider('https://api.avax-test.network/ext/bc/C/rpc');const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
const now = Math.floor(Date.now() / 1000);const nonce = '0x' + crypto.randomBytes(32).toString('hex');
const authorization = { from: wallet.address, to: '0xMerchant...', value: '500000', // 0.5 USDC validAfter: now - 60, validBefore: now + 300, nonce,};
const signature = await wallet.signTypedData( { name: 'USD Coin', version: '2', chainId: 43113, verifyingContract: '0x5425890298aed601595a70AB815c96711a31Bc65', }, { TransferWithAuthorization: [ { name: 'from', type: 'address' }, { name: 'to', type: 'address' }, { name: 'value', type: 'uint256' }, { name: 'validAfter', type: 'uint256' }, { name: 'validBefore', type: 'uint256' }, { name: 'nonce', type: 'bytes32' }, ], }, authorization,);
const paymentPayload = { token: '0x5425890298aed601595a70AB815c96711a31Bc65', payload: { authorization, signature },};const paymentRequirements = { chainId: 43113 };Example — submit to the facilitator
Section titled “Example — submit to the facilitator”const res = await fetch('https://x402.0xgasless.com/settle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paymentPayload, paymentRequirements }),});const result = await res.json();
if (result.success) { console.log('Settled:', result.transaction);} else { console.error('Failed:', result.errorReason);}Verify-first pattern
Section titled “Verify-first pattern”If you want to validate the signature before incurring any on-chain work — for example to gate access to a paid resource before settling — call /verify first:
const v = await fetch('https://x402.0xgasless.com/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paymentPayload, paymentRequirements }),});const { isValid, payer, invalidReason } = await v.json();if (!isValid) throw new Error(invalidReason);
// signature is valid, nonce isn't used, window is current — safe to settleawait fetch('https://x402.0xgasless.com/settle', { /* ... */ });This is cheap (no gas) and gives you a clean error path.
Building an x402-aware HTTP client
Section titled “Building an x402-aware HTTP client”For convenience, here’s a tiny wrapper that does the 402 handshake automatically:
async function x402Fetch(url: string, init: RequestInit = {}) { let res = await fetch(url, init); if (res.status !== 402) return res;
const requirements = await res.json(); // requirements = { network, asset, recipient, maxAmountRequired, ... }
// Build + sign payload (see above) const paymentPayload = await buildPaymentPayload(requirements); const paymentRequirements = { chainId: requirements.chainId };
// Settle via the facilitator const settle = await fetch('https://x402.0xgasless.com/settle', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paymentPayload, paymentRequirements }), }).then((r) => r.json());
if (!settle.success) throw new Error(settle.errorReason);
// Retry with the payment proof return fetch(url, { ...init, headers: { ...(init.headers || {}), 'X-Payment': settle.transaction, }, });}Common pitfalls
Section titled “Common pitfalls”- Wrong EIP-712 domain. For canonical x402, sign against the token’s own domain (
name: 'USD Coin', version: '2', verifyingContract: USDC_address). Signing against the relayer’s domain works for the legacy A402 scheme but is not how new x402 deployments expect it. - Reusing nonces. Each authorization needs a fresh random 32-byte nonce per
(from, token)pair. If you reuse one, the on-chain submit reverts with “Nonce already used.” - Long
validBeforewindows. The facilitator caps it at +1 hour from now. Beyond that →400. - No USDC balance. Verify passes (signature checks out) but settle reverts. Top up the payer first.
See also
Section titled “See also”- Facilitator API — full REST reference.
- Supported chains — chain IDs and USDC addresses.
- Agent SDK Quickstart — much less code if you let the platform hold the key.