Last week I wrote about the French voiceover API that only accepts payment from robots. Today: the part people actually asked me about — how do you verify a $0.05 payment on-chain with zero payment processor, zero SDK, and zero KYC?
The answer: one Python function, ~40 lines, stdlib only. Here's the real production code.
The setup
My endpoint sells French neural TTS voiceovers for $0.03–0.05 USDC. At that price, Stripe is a non-starter (their floor is ~$0.50 per charge) and any processor's KYC kills the "robots welcome" model. So payments go through the x402 pattern: client pays USDC on Base, sends me the transaction hash, I verify it myself against a public RPC before delivering.
The verification function
import json, os, urllib.request
WALLET_BASE = "0x3f97...D074" # where I receive
USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" # USDC on Base
BASE_RPC = "https://mainnet.base.org"
TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
def rpc(method, params):
req = urllib.request.Request(
BASE_RPC,
data=json.dumps({"jsonrpc": "2.0", "id": 1,
"method": method, "params": params}).encode(),
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req, timeout=30) as r:
return json.load(r).get("result")
def verify_payment(tx_hash, min_usdc):
# 1. format sanity
if not tx_hash.startswith("0x") or len(tx_hash) != 66:
return False, "bad hash format"
# 2. anti-replay: one hash = one delivery
if tx_hash.lower() in load_used_txs():
return False, "tx already used (replay)"
# 3. fetch the receipt
receipt = rpc("eth_getTransactionReceipt", [tx_hash])
if not receipt:
return False, "tx not found on Base"
if receipt.get("status") != "0x1":
return False, "tx failed on-chain"
# 4. scan logs for a USDC Transfer TO my wallet
want_to = WALLET_BASE.lower().replace("0x", "")
for log in receipt.get("logs", []):
if log.get("address", "").lower() != USDC_BASE.lower():
continue # not the USDC contract
topics = log.get("topics", [])
if len(topics) != 3 or topics[0].lower() != TRANSFER_TOPIC:
continue # not a Transfer event
to = topics[2][-40:].lower() # last 20 bytes of topic[2]
amount = int(log["data"], 16) / 1e6 # USDC has 6 decimals
if to == want_to and amount >= min_usdc:
mark_used(tx_hash)
return True, f"{amount} USDC received"
return False, "no sufficient USDC transfer to me in this tx"
That's the whole payment gateway. No web3.py, no API key, no account anywhere.
Why each check exists
-
Contract address filter — anyone can emit a
Transferevent from a fake token contract. Only logs from the real USDC contract count. -
Topic structure — ERC-20
Transfer(address,address,uint256)has exactly 3 topics (signature + from + to). The recipient is the last 20 bytes oftopics[2]. -
Amount in the log data — parsed from hex, divided by 10^6 (USDC decimals). Comparing
>=lets a generous buyer overpay without getting rejected. -
status == "0x1"— a reverted transaction still has a receipt. Skipping this check would accept failed payments. - Anti-replay file — without it, one $0.05 payment could be reused for unlimited generations. A plain JSON set is enough at this scale.
The 402 gate around it
If there's no X-Payment-Proof header, the server answers with HTTP 402 (yes, the "Payment Required" status code that's been reserved since 1999 and almost never used) plus everything a machine needs to pay:
{
"error": "Payment Required",
"amount": 0.05, "currency": "USDC", "network": "BASE",
"address": "0x3f97...D074",
"proof": "send the Base tx hash in X-Payment-Proof"
}
An AI agent reading that response has all it needs: chain, token, amount, destination. Pay, retry with the hash, get the MP3. Total round-trip: two HTTP calls.
What this costs to run
- RPC: public Base endpoint, free
- Verification: ~1 RPC call per purchase
- Gas: paid by the buyer, not me — at $0.05 price points this matters
- Chargebacks: don't exist
- KYC: don't exist (receiving crypto needs no identity)
The obvious trade-off: this only works for buyers who already hold USDC on Base — which today means mostly other agents and crypto-natives. That's a feature for now, not a bug.
Try it
Live endpoint (humans get the service card, agents get the 402 dance):
curl http://187.77.111.249.sslip.io:8402/
curl -X POST http://187.77.111.249.sslip.io:8402/generate \
-H 'Content-Type: application/json' \
-d '{"product":"ivr","text":"Bonjour et bienvenue"}'
# → 402 with payment instructions
Human portfolio with free audio samples: voixoff-fr.netlify.app
Building in public, week 2. Scoreboard so far: 0 sales, 1 working payment rail, ~0 lines of payment-processor code. Previous posts: robot-only API · $0 avatar pipeline
Top comments (0)