Skip to content

Paying x402 endpoints

There are two ways to pay an x402 endpoint:

  1. Use the Agent SDK — recommended for autonomous bots. The platform holds the key, you just call client.x402.pay().
  2. 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.

1. Client calls a paid endpoint: GET /premium-data
2. 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/settle
5. 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.

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.

{
"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
}
}
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 };
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);
}

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 settle
await fetch('https://x402.0xgasless.com/settle', { /* ... */ });

This is cheap (no gas) and gives you a clean error path.

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,
},
});
}
  • 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 validBefore windows. 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.