Accepting x402 payments
If you run an API or serve any HTTP resource, you can charge per call using x402. Clients pay; you receive USDC directly to your wallet; the facilitator handles the on-chain settlement and pays the gas.
The server flow
Section titled “The server flow”- Client calls your endpoint without payment.
- You return
402 Payment Requiredwith the price and your recipient address. - Client signs an EIP-3009 authorization (or queues it through their wallet UX).
- Client retries with the signed payload — either inline in the request or via the facilitator.
- You verify with
https://x402.0xgasless.com/verify, optionally settle, then serve the resource.
You don’t need to deploy any contracts. You just need a wallet address where you want USDC to arrive.
Minimal Express handler
Section titled “Minimal Express handler”import express from 'express';const app = express();const PORT = 3000;app.use(express.json());
const RECIPIENT = '0xYourMerchantAddress...';const PRICE_USDC = '100000'; // 0.1 USDC (6 decimals)const TOKEN = '0x5425890298aed601595a70AB815c96711a31Bc65'; // USDC on Fujiconst CHAIN_ID = 43113;const FACILITATOR = 'https://x402.0xgasless.com';
app.get('/premium-data', async (req, res) => { const xPayment = req.header('X-Payment');
if (!xPayment) { // No payment yet — respond with 402 and the requirements. return res.status(402).json({ x402Version: 1, scheme: 'exact', network: 'fuji', chainId: CHAIN_ID, asset: TOKEN, payTo: RECIPIENT, maxAmountRequired: PRICE_USDC, maxTimeoutSeconds: 300, description: 'Premium data feed', resource: req.originalUrl, }); }
// The client sent us a settled tx hash — verify it on-chain or trust the facilitator. // For a stricter check, parse the X-Payment header as the signed payload and verify yourself.
// Naive path: trust the X-Payment as a tx hash and check it on-chain. // Better path: have the client send the signed payload and you settle. const isValid = await verifyPayment(xPayment); if (!isValid) { return res.status(402).json({ error: 'Invalid payment' }); }
// Payment is good — return the premium data. res.json({ secret: 'the answer is 42' });});
app.listen(PORT, () => console.log(`Listening on ${PORT}`));Two patterns for “settle”
Section titled “Two patterns for “settle””You can decide who settles the on-chain transaction:
Pattern A: Client settles, sends you the tx hash
Section titled “Pattern A: Client settles, sends you the tx hash”Simpler for your server. The client takes the signed authorization and submits it to the facilitator, then sends you the resulting tx hash as X-Payment. You verify the tx exists, was sent to your address, for the expected amount.
async function verifyPayment(txHash: string): Promise<boolean> { // Pseudo-code — use ethers / viem to actually do this. const receipt = await provider.getTransactionReceipt(txHash); if (!receipt || receipt.status !== 1) return false;
// Parse the TransferWithAuthorization event from logs. const transfer = parseUSDCTransferFromReceipt(receipt); return ( transfer.to.toLowerCase() === RECIPIENT.toLowerCase() && transfer.value === PRICE_USDC );}Pattern B: You settle directly via the facilitator
Section titled “Pattern B: You settle directly via the facilitator”Tighter integration. The client sends you the signed payload as X-Payment; you submit it to the facilitator from your server.
async function verifyAndSettle(signedPayload: string) { const parsed = JSON.parse(Buffer.from(signedPayload, 'base64').toString());
// Step 1: verify (cheap, no on-chain work) const v = await fetch(`${FACILITATOR}/verify`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paymentPayload: parsed, paymentRequirements: { chainId: CHAIN_ID }, }), }).then((r) => r.json());
if (!v.isValid) throw new Error(v.invalidReason);
// Step 2: confirm amount + recipient match what we expect const auth = parsed.payload.authorization; if (auth.to.toLowerCase() !== RECIPIENT.toLowerCase()) throw new Error('wrong recipient'); if (BigInt(auth.value) < BigInt(PRICE_USDC)) throw new Error('underpaid');
// Step 3: settle on-chain (facilitator pays gas) const s = await fetch(`${FACILITATOR}/settle`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paymentPayload: parsed, paymentRequirements: { chainId: CHAIN_ID }, }), }).then((r) => r.json());
if (!s.success) throw new Error(s.errorReason); return s.transaction; // tx hash}Pattern B is more bulletproof — you control the verify step and ensure the amount and recipient match before settling.
Pricing tips
Section titled “Pricing tips”- Charge in atomic units of the token. USDC has 6 decimals, so
100000= $0.10. - One nonce per call. The signature is one-time use; clients can’t replay it.
- Reuse
validBeforeshort. Recommend clients use ~5-minute windows so a stale auth doesn’t burn nonce slots.
What you don’t need
Section titled “What you don’t need”- No smart contracts.
- No on-chain pre-approval — USDC’s EIP-3009 means you can be paid directly without the payer first calling
approve(). - No relayer contract on Avalanche or Base — the facilitator submits to the token directly using canonical x402.
- No subscription billing infrastructure.
Common pitfalls
Section titled “Common pitfalls”- Trusting the client’s tx hash without checking the chain. Always verify the tx exists on-chain and the amount + recipient match. Don’t just trust the hash they sent you.
- Re-settling the same payload twice. If the nonce was already used, the second settle reverts. Track served nonces in your DB if you want strong idempotency.
- Forgetting to scope by token. A nonce is only single-use per
(payer, token). If you accept multiple tokens, dedupe nonces with both as keys.
See also
Section titled “See also”- Facilitator API —
/verifyand/settlereference. - Pay endpoints — what the client side looks like.
- Supported chains — chain IDs, USDC addresses.