I wanted to sell two small PDFs for crypto without a payment processor, a database, or a wallet connection on the server. The result is one Cloudflare Worker (free plan) that only receives USDC on Base to a public address. Here is how it works, including the parts that are weak.
Live example: Setrov Store.
The core problem: which payment belongs to which order?
A plain transfer to an address carries no order ID. If two people buy the same $9 file, two identical 9.000000 USDC transfers arrive and you can't tell them apart.
Fix: exact-amount tagging. USDC has 6 decimals, so each order gets a random tag in the low digits:
base price 9.000000
order tag 0.004217 (random 4-digit tag, 1000..9999 millionths)
pay exactly 9.004217 USDC
The buyer pays that exact amount. A transfer of any other amount simply doesn't match that order.
Stateless orders with HMAC
There's no database. The order is a token: product | exactAmount | createdAt, signed with HMAC-SHA256 using a Worker secret. The order page URL contains the token, so the Worker can rebuild the order from the URL and check the signature. Rotating the secret invalidates every open order.
const sig = await crypto.subtle.sign('HMAC', key, enc.encode(payload));
const token = b64url(payload) + '.' + b64url(sig);
Verifying the transaction hash
The buyer pastes the tx hash. The Worker calls a public Base RPC (eth_getTransactionReceipt), then checks:
- receipt
statusis success, and it has at least 2 confirmations - a USDC
Transferlog (USDC contract address, topic0 = Transfer) to the store address - log value == the order's exact amount
- the block timestamp is after the order was created and less than 24h old
If all of that passes, it returns a signed download link that expires in 2 hours. No private key is involved anywhere: the server only reads public chain data.
Machine buyers: x402
The same Worker exposes /x402/buy/<slug>. An unpaid request gets HTTP 402 with an accepts[] block (scheme exact, network base, amount, payTo, USDC asset). An x402 client signs an EIP-3009 authorization and retries with an X-PAYMENT header. The Worker forwards it to a facilitator (I use PayAI) for /verify, then /settle, and returns the download link. The facilitator submits the transaction, so the server still holds no keys.
curl -i https://setrov-store.elayosetrov78.workers.dev/x402/buy/ai-agent-prompt-pack
# HTTP/2 402 { "x402Version":1, "accepts":[{ "maxAmountRequired":"9000000", ... }] }
Honest limits
- Wrong amount = manual handling. If someone pays 9.00 instead of 9.004217, the check rejects it and you refund by hand.
- Replay guard is best-effort. Without a database, reuse of a tx across orders is blocked with the Cache API, which is per-datacenter. The random tag makes accidental matches unlikely (about 1 in 9000), but it's not a ledger.
- Links can be shared during their 2-hour window, like any download link.
- Public RPC rate limits can delay verification ("try again in a minute").
- Size: files are embedded in the Worker bundle, so this only works for small files (under ~3 MB total on the free plan). Bigger files need R2 or another host.
- Taxes and accounting are still your job. No KYC on the server doesn't remove your own obligations.
When this makes sense
Small digital goods, a crypto-native audience, and no wish to run a database. For anything bigger (subscriptions, refunds at scale, files larger than a few MB) use a real payment stack.
If you want to see it working, the store sells an AI Agent Prompt Pack for 9 USDC; it's also on Payhip for card payments.
Top comments (0)