Charge for it
A priced endpoint is two things: an answer that says 402 with your terms, and a check that the authorization you were handed is real. Everything else is the ordinary handler you already wrote.
Try it from a shell
# the terms, with no payment curl -s https://this-site/api/paid/brief | jq .accepts[0] # what this facilitator settles curl -s https://this-site/api/x402/supported | jq . # verify a signed payment without broadcasting it curl -s -X POST https://this-site/api/x402/verify \ -H 'content-type: application/json' \ -d '{"paymentPayload": { ... }}' | jq .checks
Your server: answer 402, then serve
import { verifyTypedData, createPublicClient, http } from 'viem'; const USDG = '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168'; // 6 decimals const DOMAIN = { name: 'Global Dollar', version: '1', chainId: 4663, verifyingContract: USDG }; const FIELDS = ['from', 'to', 'value', 'validAfter', 'validBefore', 'nonce'] .map((n, i) => ({ name: n, type: i < 2 ? 'address' : i < 5 ? 'uint256' : 'bytes32' })); app.get('/premium-data', async (req, res) => { const header = req.get('X-PAYMENT'); if (!header) return res.status(402).json({ x402Version: 1, accepts: [terms()] }); const p = JSON.parse(Buffer.from(header, 'base64').toString()); const a = p.payload.authorization; // the signature, against the dollar's own domain const ok = await verifyTypedData({ address: a.from, domain: DOMAIN, types: { ReceiveWithAuthorization: FIELDS }, primaryType: 'ReceiveWithAuthorization', message: a, signature: p.payload.signature, }); // and the two facts only the chain can tell you const [balance, used] = await Promise.all([ chain.readContract({ address: USDG, abi: USDG_ABI, functionName: 'balanceOf', args: [a.from] }), chain.readContract({ address: USDG, abi: USDG_ABI, functionName: 'authorizationState', args: [a.from, a.nonce] }), ]); if (!ok || used || balance < BigInt(a.value)) { return res.status(402).json({ error: 'payment not valid', accepts: [terms()] }); } res.json(await answer()); // serve first, settle in the background });
The nonce in terms() is
keccak256(abi.encode(merchant, resourceHash, salt)), which is what binds the
signature to you and to this resource. Hand it to TabGate and the
fee splits on-chain; anyone can submit that transaction, including the agent that signed it.
Your agent: pay without thinking about it
async function fetchPaid(url, account) { let r = await fetch(url); if (r.status !== 402) return r; // it was free after all const t = (await r.json()).accepts[0]; if (BigInt(t.maxAmountRequired) > budget) throw new Error('too dear'); const message = { from: account.address, to: t.extra.to, value: BigInt(t.maxAmountRequired), validAfter: 0n, validBefore: BigInt(Math.floor(Date.now() / 1000) + 120), nonce: t.extra.nonce, }; const signature = await account.signTypedData({ domain: t.extra, types: { [t.extra.authorization]: FIELDS }, primaryType: t.extra.authorization, message, }); const payment = { x402Version: 1, scheme: 'exact', network: t.network, payload: { authorization: message, authorizationType: t.extra.authorization, salt: t.extra.salt, signature } }; return fetch(url, { headers: { 'X-PAYMENT': btoa(JSON.stringify(payment)) } }); }
No wallet connection, no gas estimation, no nonce management, no receipt to wait for. The agent signs and asks again. That is the whole client.