Guide · updated 2026-10-07 · about 12 minutes

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

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 settlementReceipt check (this guide)
Who can payx402 clients that sign paymentsAnyone who can send USDC on Base
SetupFacilitator URL and, for some, API keysPublic RPCs and D1
Main riskFacilitator availabilityReplay 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:

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:

  1. POST /order creates a random 128-bit orderId and 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.
  2. The buyer sends exactly that amount.
  3. POST /claim needs the orderId and 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

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