Skip to content

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.

  1. Client calls your endpoint without payment.
  2. You return 402 Payment Required with the price and your recipient address.
  3. Client signs an EIP-3009 authorization (or queues it through their wallet UX).
  4. Client retries with the signed payload — either inline in the request or via the facilitator.
  5. 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.

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 Fuji
const 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}`));

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.

  • 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 validBefore short. Recommend clients use ~5-minute windows so a stale auth doesn’t burn nonce slots.
  • 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.
  • 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.