How to sell a digital file for USDC with x402 on Cloudflare Workers
Quick answer: serve your file from a Worker route that returns 402 with x402 v2 payment terms. Give each buyer an order with a secret code and a unique exact USDC amount. When they send it and paste the tx hash, fetch the receipt from Base and accept only one USDC Transfer log to your address for exactly that amount, mined after the order. Record the hash in D1 so it works once.
1. What you need
- A Cloudflare account (the free plan is enough) and
wrangler. - A Base address to receive USDC. A receive-only address is fine, so the Worker never holds a private key.
- The file. Under ~5 MB you can embed it as base64; above that, use R2.
- A D1 database for orders and claims.
2. Return a real 402
x402 v2 puts the payment terms in the body and, base64-encoded, in a PAYMENT-REQUIRED header. Directories and agents read these to index and price your resource.
const terms = {
x402Version: 2,
error: 'PAYMENT-SIGNATURE header is required',
resource: { url: origin + '/download', description: 'My file', mimeType: 'application/zip' },
accepts: [{ scheme: 'exact', network: 'eip155:8453', amount: '29000000',
asset: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', payTo: PAY_TO,
maxTimeoutSeconds: 300, extra: { name: 'USDC', version: '2' } }],
};
return new Response(JSON.stringify(terms), { status: 402, headers: {
'Content-Type': 'application/json',
'PAYMENT-REQUIRED': btoa(JSON.stringify(terms)),
'Access-Control-Expose-Headers': 'PAYMENT-REQUIRED' } });
USDC has 6 decimals, so 29000000 means 29 USDC.
3. Pick how you’ll confirm payment
| Facilitator settlement | Receipt check (this guide) | |
|---|---|---|
| Who can pay | x402 clients that sign payments | Anyone who can send USDC on Base |
| Setup | Facilitator URL and, for some, API keys | Public RPCs and D1 |
| Main risk | Facilitator availability | Replay and front-running, which you must handle |
They aren’t exclusive. You can add a facilitator later and keep the receipt flow for humans.
4. Verify a USDC transfer from the receipt
Call eth_getTransactionReceipt. Accept the payment only if the receipt status is 0x1 and one log meets all four conditions:
- it was emitted by the USDC contract
- topic0 is the ERC-20
Transferevent - topic2 is your address
- the value equals what you expect
const TRANSFER = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
const to = '0x' + PAY_TO.toLowerCase().slice(2).padStart(64, '0');
const hit = receipt.status === '0x1' && receipt.logs.find((l) =>
l.address.toLowerCase() === USDC.toLowerCase() &&
l.topics.length === 3 && l.topics[0] === TRANSFER &&
l.topics[2].toLowerCase() === to &&
BigInt(l.data) === BigInt(order.amount_atomic));
Use several RPC endpoints in order (for example mainnet.base.org, base.gateway.tenderly.co and 1rpc.io/base). Free public endpoints rate-limit shared Cloudflare egress, so list several. Set a User-Agent header: it’s a cheap precaution with public endpoints. If every RPC fails, tell the buyer to retry. Never report “not paid” when you simply couldn’t check.
5. Make each payment work once
CREATE TABLE claims (tx_hash TEXT PRIMARY KEY, order_id TEXT, amount_atomic TEXT NOT NULL,
claimed_at TEXT DEFAULT (datetime('now')));
-- in the Worker:
const r = await env.DB.prepare(
'INSERT INTO claims (tx_hash, order_id, amount_atomic) VALUES (?1, ?2, ?3) ON CONFLICT(tx_hash) DO NOTHING'
).bind(txHash, orderId, amount).run();
if (r.meta.changes !== 1) return new Response('already used', { status: 409 });
Checking “does it exist?” and then inserting leaves a race. Checking that the insert itself changed one row closes it.
6. Stop front-running with order codes
A hash is public the moment it’s mined. If a claim needs only the hash, anyone watching your address can claim first. The fix:
POST /ordercreates a random 128-bitorderIdand a unique amount, e.g. base + 1–9,999 micro-USDC (29.000001–29.009999). Don’t hand out an amount while any earlier order using it could still receive a payment.- The buyer sends exactly that amount.
POST /claimneeds theorderIdand the hash. The transfer must match the order’s amount exactly, and the block timestamp must fall inside the order’s window.
An attacker who sees the transaction still doesn’t have the order code, and can’t create an order with the same amount while the buyer’s window is open. The trade-off: buyers must send an exact amount. Tell them to make sure the amount received is exact.
7. Rate-limit cheaply
A per-IP, per-minute counter in D1 (INSERT … ON CONFLICT DO UPDATE SET n = n + 1 RETURNING n) works on the free plan. Add the Workers rate-limiting binding if you have it. Limit order creation too, so nobody can tie up your amount slots.
8. Get discovered
- Add Bazaar discovery metadata to the 402 body and serve
/llms.txt,/sitemap.xmlandrobots.txt. Disallow/orderand/claim. - Register with x402 directories. In October 2026, x402dash, 402 Index and agent-tools.cloud accepted free listings. Some directories charge a small fee for
workers.devhosts. - Ping IndexNow after each deploy so Bing picks up new pages.
9. Test before you share the link
curl -s -o /dev/null -w '%{http_code}\n' $STORE/download # 402
curl -s -X POST $STORE/order # 201 + orderId + amount
curl -s -X POST $STORE/claim -H 'content-type: application/json' \
-d '{"orderId":"<id>","txHash":"0xabab…ab"}' # 402 not_found
curl -s -X POST $STORE/claim -H 'content-type: application/json' \
-d '{"txHash":"0xabab…ab"}' # 402 order_required
FAQ
Do I need a facilitator to sell with x402?
Only if you want x402 clients to pay automatically with a signed payment header. To sell to humans, or to agents that can send a normal USDC transfer, you can verify the on-chain transfer yourself from the transaction receipt.
Can I just ask buyers to paste a transaction hash?
Not safely. Hashes are public on Basescan, so someone watching your address can submit a buyer’s hash first. Bind each payment to a secret order code and a unique amount.
How big a file can a Worker serve?
Embedding a base64 file in the script works up to a few MB because of Worker script size limits. For larger files, store them in R2 and stream them after a successful claim.
Which token address is USDC on Base?
Native USDC on Base mainnet (chain 8453) is 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913. Only count Transfer logs emitted by that contract.
Want this already built and tested? The x402 Seller Kit packages every step above as a Worker template with a threat model and a directory list. The README is free to read.
Get the kit · $29