# Verifying $0.05 USDC Payments On-Chain in 40 Lines of Python — No Stripe, No SDK, No KYC

> Source: <https://dev.to/marcuschen-dev/verifying-005-usdc-payments-on-chain-in-40-lines-of-python-no-stripe-no-sdk-no-kyc-4f94>
> Published: 2026-08-30 18:23:34+00:00

Last week I wrote about [the French voiceover API that only accepts payment from robots](https://dev.to/marcuschen-dev/i-built-a-french-ai-voiceover-api-that-only-accepts-payment-from-robots-435k). 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.

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.

``` python
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.

`Transfer`

event from a fake token contract. Only logs from the real USDC contract count.`Transfer(address,address,uint256)`

has exactly 3 topics (signature + from + to). The recipient is the last 20 bytes of `topics[2]`

.`>=`

lets a generous buyer overpay without getting rejected.`status == "0x1"`

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.

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.

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](https://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*
