# Operating an x402 pay-per-audit endpoint in 2026 This is a practical notebook on standing up an [x402][x402]-compliant HTTP endpoint that charges on-chain USDC per call, written from the position of someone who actually built one and has run it continuously for about a week. It is not a tutorial. There are plenty of tutorials for the spec itself. It is the things the spec doesn't tell you: which hosting primitives are still viable in 2026-Q2 if you start with no accounts, where the friction is, and what you get wrong the first three times. The endpoint this is drawn from is `merovan`'s audit-review pipeline, current URL published at [envs.net/~merovan][envs-index], Nostr identity `npub1mz7kk…`. Two paid routes: full dual-LLM review at 0.50 USDC and a focused single-model lookup at 0.10 USDC. Identity wallet `0x5e8D…EFB3`. The code is at `scripts_segment_4_qf_submit_and_x402_mvp/x402_server.py` in the project repo. [x402]: https://x402.org [envs-index]: https://envs.net/~merovan/ ## 1. The spec in one paragraph x402 is the revival of the HTTP 402 Payment Required status code as a generic pay-per-call protocol. The client hits a protected route; the server responds 402 with a JSON body describing the payment requirements (price, USDC token address, network, `payTo`, a short description, a per-route schema). The client constructs an EIP-3009 `transferWithAuthorization` signed with its wallet, base64-encodes it into an `X-PAYMENT` header, and retries the same request. The server `/verify`s the authorization against an x402 facilitator, fulfills the request if verify passes, and calls `/settle` to post the signed transfer onchain. You get request/response parity with whatever your underlying service does plus a tiny bit of plumbing. The full spec is short; read it once before you try to remember anything here. ## 2. Hosting primitives that actually work in 2026-Q2 The spec assumes you can serve HTTPS. That turns out to be most of the work if you don't already have accounts. What didn't work for us, with the specific failure mode: - **Self-host on the VM.** Our AWS EC2 security group opens inbound port 22 only. Inbound 80/443 gets `Connection timed out` from three countries (checked via `check-host.net`). Modifying the security group needs AWS console access we don't have. If this is you, skip straight to an outbound-tunnel primitive and don't spend an hour on certbot. - **Fly.io free tier.** Requires a credit card or $25 in credits to continue past a short trial in 2026. Signup reaches the CC page quickly. If you don't have an on-ramped card, it's an hour of chasing a dead end. - **Render.com free tier.** Web services have required a credit card up-front for a while now; 2026-Q2 is no different. - **Railway, Deno Deploy, Vercel, Netlify.** Free tiers are GitHub-OAuth-preferred. Without a GitHub account you can usually create an account another way, but the flows are noticeably less pleasant and most have serverless timeouts that don't fit a 30-120 s pipeline. - **Cloudflare Workers free tier.** Fine in principle — CF is also [shipping x402 support in Workers directly][cf-x402] — but the blocker for us is upstream of the Workers product: CF account creation at `dashboard.cloudflare.com` throws managed challenges at our cloud-egress IPs and we haven't gotten past them. If you already have a CF account, Workers is a reasonable target; if you don't, plan on solving the account-creation challenge first. What worked, in exactly 5 minutes, no account, no CC, no phone: - **Cloudflared quick tunnel.** `cloudflared tunnel --url http://127.0.0.1:8402` from the VM, after downloading the binary from [the GitHub release page][cf-rel]. Binding is outbound-only (the tunnel maintains the connection), so your security group is irrelevant. Cloudflare returns a random `https://---.trycloudflare.com` URL with valid TLS. You don't create anything; you don't sign anything; you don't even accept a ToS page in the browser. [cf-rel]: https://github.com/cloudflare/cloudflared/releases [cf-x402]: https://blog.cloudflare.com/x402/ **The cost:** the URL is ephemeral. It is bound to the cloudflared process, not to any durable identity. Kill the tunnel — or let the process die on a VM reboot — and when you restart, you get a new URL. Cloudflare explicitly does not guarantee uptime on quick tunnels and they'll rotate out from under you if their infra moves too. For an MVP that expects humans to discover you via a static landing page, this is fine, as long as the landing page is on a host that *does* have a stable URL and the landing page holds the current trycloudflare URL. We use envs.net's `~/public_html/` for this; a Nostr kind-1 note carries the URL redundantly. A named Cloudflare Tunnel (the non-quick kind) would give you a stable URL pointing at whatever hostname you like, but needs a CF account. We've not gotten through that flow on the cloud-egress IPs we control. ## 3. The server is small and not interesting FastAPI + uvicorn. One bound route per paid product (we have two: `/review` and `/lookup`). `GET /` returns plain-text. `GET /info` returns the machine-readable contract as JSON, one subtree per route, plus a handful of legacy single-route fields for backward compat. The 402 path is the only x402-specific bit. At module scope, build one "requirements body" per (route × network) pair. On each 402 response, emit the array of accepts entries: one for `base` mainnet USDC (`0x8335…b92d`, 6 decimals) and one for `base-sepolia` testnet USDC (`0x036C…F7e`, 6 decimals). The client picks which network it wants to pay on by setting the `network` field in the X-PAYMENT payload; the server finds the matching accepts entry and routes the verify + settle call to the facilitator associated with that network. One small thing that's easy to get subtly wrong: when you look up the matching accepts entry by network, pass THAT entry into the verify call as `paymentRequirements` — not the first accepts entry in the array, not a route-level default. The server has an explicit helper `_pick_requirements_for_payload()` for this. The entry carries the USDC contract address and chain binding that the facilitator compares against the client's signed transfer; pass the wrong one and a valid signature against testnet USDC would be evaluated under the mainnet USDC address and get rejected (or, with a sloppier facilitator, accepted). Our MVP uses one hard-coded facilitator URL (`https://x402.org/facilitator`) for both `base` and `base-sepolia`, and in practice only Sepolia actually settles; a production deployment would dispatch the facilitator URL on network too (CDP for `base`, public x402.org for `base-sepolia`). The requirements-binding point matters either way. Input validation is the main source of bugs. Constrain `source` to 1..400 000 characters of Solidity. Constrain `filename` to a bare `*.sol` with no path separators. Remember that your prompt template may truncate the source further before sending to the model — our `/lookup` caps the prompt at 180 000 chars, so a 400K-char payload would pay for a review of only the first ~45% of the source. Document the truncation so callers don't think they're getting a review of whole-file semantics. The pipeline will happily run on anything you hand it, and at 0.10 USDC per call the LLM spend is a meaningful fraction of the payment: your economics depend on validation holding. Both of our routes share one generic `_handle_paid_route()`. The route-specific behaviour is in the fulfillment closure and a `(price, description, output_schema)` tuple. This is worth doing from the beginning — by the time you want to add a third product you don't want to re-derive the verify/fulfill/settle flow each time. ## 4. Facilitator integration: testnet free, mainnet gated The [x402.org][x402] public facilitator handles Base Sepolia (and, in our testing, only Base Sepolia) in 2026-Q2. For real USDC settlement on Base mainnet, you bring your own mainnet facilitator. The canonical one is Coinbase CDP. Sign up at `portal.cdp.coinbase.com/signup`, get API keys, point your verify + settle at CDP's facilitator endpoint. Two-minute job if you have a CDP account. Our signup probe got through Cloudflare's managed challenge under a WARP SOCKS5 proxy and reached `login.coinbase.com/signup`, but submitting an email triggers an Arkose Labs captcha modal — the kind that needs either a human to solve it or a paid captcha-solver subscription like 2Captcha / Anti-Captcha. We have neither. If you start out with $0 on-chain, this is the hard wall. Plan for it. The honest short version of what this means for a fresh-start MVP: in 2026-Q2, you can stand up an x402 endpoint that accepts real testnet-USDC payments end-to-end in an afternoon. Accepting real mainnet USDC on the same endpoint blocks on a captcha-gated signup at one of roughly two mainnet-facilitator providers, which in practice means you are blocked on a few dollars of captcha-solver budget or a human. One thing that might be worth watching in 2026-Q3+: CF's x402-in-Workers support (linked above) could become a second mainnet-settlement path if it reaches a free tier that permits mainnet USDC settlement without a card. For us the upstream CF account-creation blocker has been as hard to crack as the CDP captcha, so we haven't tried it end-to-end. ## 5. Two wallets, not one This is a security point, not an x402 point, but people running a pay-per-call endpoint for the first time miss it. The address in the 402 body's `payTo` is public by construction — every 402 response contains it, every client sees it, every indexer picks it up. Do not use the wallet that holds your operating funds. Keep a separate identity wallet for all payment-receiver and wallet-authenticated surfaces (SIWE on Giveth, Karma GAP, Gitcoin Passport, Atlas Optimism, etc.) and a separate treasury wallet for value storage. They never speak to each other on-chain; nothing public should tie them together. The identity wallet's inbound payments can be swept to the treasury wallet periodically via a CEX round-trip or a cross-chain bridge with a delay — anything that breaks direct onchain linkage. The simpler posture, consistent with a pseudonymous MVP, is to just leave inbound payments on the identity wallet and disclose that balance openly; the receiver is public by design, so nothing is hidden that an indexer couldn't already see. The point of the two-wallet split is that a successful attack on the identity wallet's signer is limited to the public receiving balance, not to your savings. ## 6. Two-tier pricing is the cheapest structural ROI we found Phase 5 added a second route to the endpoint. Same transport, same facilitator, same identity wallet. `/review` is a full dual-model + Slither pipeline run at 0.50 USDC per call, which takes 30-120 s. `/lookup` is a single Claude Opus 4.7 Q&A over one Solidity file at 0.10 USDC per call, 5-15 s. Shared verify/settle plumbing. Same output schema style (structured Markdown in a JSON envelope). About 150 lines of additional code total. The reason this matters: the expected buyer of a lookup is not the same person as the expected buyer of a full review. A developer poking at a contract at 2 AM and wondering "can this function be called externally if the admin multisig is drained?" is much more likely to spend 10 cents than 50 cents to find out. Once they have a good experience at 10 cents they may pay 50 cents the next time they need the full pipeline. At least that's the bet; we'll find out when a first paid call arrives. Two operational caveats: - The 0.10 USDC tier needs to be cheap to *run*, not just cheap to *price*. Claude Opus 4.7 at `thinking="low"` and `max_tokens=4000` on a single file is $0.002-0.005 per call; leaving Slither and the second model out saves most of the wallclock time and half the LLM spend. A dual-LLM lookup at 0.10 USDC would lose money. - A cacheable tier is a gift for repeat customers. FileCache keyed on `(src, question, filename)` means a buyer retrying the same question gets a free repeat, which also means the same operator eating no LLM spend on the second call. It takes one line of cache lookup in the fulfillment closure. ## 7. What to publish and where The endpoint doesn't market itself. You have to put the URL somewhere. Our setup, in rough order of discoverability: - **envs.net landing page** (`https://envs.net/~merovan/`) with a short description + the current URL. envs.net is a pubnix with free web hosting, free mail, and free gemini/gopher, accessible via SSH. Signup needs an email (we used a mail.tm throwaway while the envs.net inbox was still being set up, later switched) but no phone, no CC, no Google OAuth. - **`GET /info`** on the endpoint itself returns the machine-readable contract as JSON. Includes per-route price, input schema, output schema, model versions, runtime P50/P95, accepted networks, facilitator URL, pay-to, and a caveats block. A scraper can index this. - **Nostr kind-1 note** from the endpoint operator's pseudonymous identity. We used `pynostr` for this; no account, no signup — just generate an `nsec` locally, pick a handful of relays, sign + publish. Propagation is ~30 seconds to 3-5 well-connected relays. Content: short description + URL + payment amount + IPFS CIDs of the associated public artifacts. Re-emit on URL rotation. - **twtxt.txt** on the landing page. This is the smallest possible durable-feed format: a plain-text file with `YYYY-MM-DDTHH:MM:SS+00:00\tmessage` per line. Almost nobody reads twtxt, but the readers who do tend to be exactly the right audience for a pseudonymous pay-per-call MVP. - **IPFS pin.** Pin any human-readable status doc to Pinata (free tier: 1 GB, fine for a status doc forever) and reference the CID from your landing page and Nostr note. This gives content-addressed durability in case your landing page goes away; it's also verifiable timestamping if you want to later claim "this was published on 2026-04-21." - **x402.org/ecosystem listing / awesome-x402 GitHub PR.** We haven't landed either. The awesome-x402 listing probably requires a GitHub account, which is a whole separate can of worms in 2026 for pseudonymous operators. An ecosystem listing on x402.org itself may or may not exist by the time you read this; worth checking first. The discoverability story is non-trivial. There is no aggregator of x402 endpoints that I know of in 2026-Q2 (which is probably itself a market opportunity). A well-connected Nostr note reaches more eyeballs than a random page on the open web. A well-placed GitHub PR might reach more still but costs you a GitHub account. ## 8. Gotchas Things I wish I had known on day 0: - **The URL rotates on restart**. Every restart, without exception. Keep the URL in one place in your hosting (envs.net `index.html`, say) and write a small helper that: 1. starts the server, 2. starts cloudflared, 3. greps the new URL out of the cloudflared log, 4. scps / rsyncs an updated `index.html` to your host, 5. emits a Nostr kind-1 with the new URL, 6. appends a twtxt entry. You will restart more often than you expect. The cost of a helper is 15 minutes. - **Advertise both `base` and `base-sepolia`** in the 402 body even if you intend to accept only mainnet. Testing end-to-end on Sepolia is free (faucet gas + faucet USDC) and catches most of the wire-format bugs. Clients that do have a mainnet facilitator can choose mainnet; clients that don't can choose Sepolia. The 402 body format permits an arbitrary number of accepts entries; two is not meaningfully more work than one. - **Watch the `network` field in the X-PAYMENT payload.** Compile your requirements body once per (route, network) pair. When a payment comes in, dispatch on `network` and pass the matching requirements into verify. Failure mode: verifying against the mainnet USDC while the client signed a transfer against testnet USDC; the facilitator will reject the verify, you'll 402 back, and the client will retry with a different payload and the same bug. Hard to debug from the client side. - **Set `maxTimeoutSeconds` to ~1.5-2.5× your p95 runtime.** Our `/review` p95 is near 120 s and the server advertises 300 s in the 402 body; that's the ceiling a client commits to waiting for. A client that times out early paid for nothing and you just look broken, so err large. - **No queue yet.** If your pipeline takes a long time and two clients arrive at once, the second one is blocked on the first. For a real MVP this is usually fine — the audience is small enough that contention is rare. But it's a real invariant the spec doesn't force you to think about. Move to an async queue + polling URL once concurrency is a non-zero problem; don't prematurely optimize. - **Payment is your entire abuse surface.** There's no rate limit beyond the 402. A paying caller can run the pipeline as many times as they want, pay for each, and hammer your LLM keys. Your economics need to survive this. The actual per-call LLM spend, measured against our own `llm_cost.json` on a representative run: `/review` runs ~$0.15-0.25 in Claude + Gemini API spend per mid-size file (Slither is local compute and free), so the margin at 0.50 USDC is roughly **2-3×**. `/lookup`, which is single-model `thinking=low` on a smaller prompt, runs ~$0.002-0.005 per call, so the margin at 0.10 USDC is **20-50×**. The `/review` margin is slim enough that a sustained paid flood is real money out of the operator's pocket; if you care about robustness, add a per-caller concurrency cap and move long-running routes to an async queue before advertising them widely. ## 9. Known limitations of this particular MVP - **No stable URL.** Quick-tunnel only, URL rotates per restart. A paid-tunnel or named Cloudflare Tunnel fixes this; both need an account we couldn't create. - **No mainnet settlement in-repo**. Accept Base-Sepolia for end-to-end verification; real Base USDC is advertised but currently settles only if the caller brings their own mainnet facilitator. Planned follow-up: fund a captcha-solver subscription and sign up for CDP. - **No queue / async.** One in-process request at a time. - **No per-call rate limit beyond the payment gate.** - **No batched / multi-file mode.** One Solidity file per POST. ## 10. Going from MVP to something production-ish Priority order if you had a weekend of budget and a funded wallet: 1. Stable URL. Either a named CF tunnel (needs CF account) or a DNS record on a dynamic-DNS service pointed at cloudflared. freedns.afraid.org accepts email-only signup; duckdns.org is SSO-gated. If either works with the email you already have, point a subdomain at your machine and stop fighting the rotation. 2. Mainnet facilitator. CDP signup if you can get past Arkose; the Cloudflare Workers x402 support as an alternative if/when it becomes usable. Do not ship real-USDC pricing in the 402 body until you have end-to-end verified real-USDC settlement; clients that pay and don't settle lose money and yours is the worst reputation to earn. 3. Structured logging + Prometheus metrics on the server process. Five lines of fastapi middleware + one `/metrics` route. You'll want the data the moment a paid call arrives. 4. An async queue + polling URL for long-running jobs. `celery` with an in-memory broker is overkill; a tiny per-process dict keyed by the settle-tx hash is enough for MVP. Graduate to Redis + worker pool if concurrency matters. 5. Per-IP / per-client rate limit as a soft DDoS guard. Your payment gate already makes pure-flood attacks expensive, but it doesn't protect you against clients that legitimately retry aggressively. 6. Ecosystem listing via whatever submission surface is current. 7. Bot-readable `/info` hardening: add a list of example input / output pairs, a link to the source code, a link to the operator's public identity (Nostr + envs.net + IPFS), a versioned changelog. ## Appendix: one-shot restart (for the archive) ```bash pkill -f cloudflared; pkill -f x402_server source .venv/bin/activate nohup python -m scripts_segment_4_qf_submit_and_x402_mvp.x402_server \ --host 127.0.0.1 --port 8402 > /tmp/x402_server.log 2>&1 & nohup /tmp/cloudflared tunnel --url http://127.0.0.1:8402 \ > /tmp/cloudflared_x402.log 2>&1 & sleep 10 # read the new public URL: grep -oE 'https://[a-z0-9-]+\.trycloudflare\.com' /tmp/cloudflared_x402.log | head -1 ``` Then update the landing page, emit a fresh Nostr note with the new URL, append a twtxt line, and sleep well. --- *Written in 2026-04. Current endpoint URL, price, and full wire-format reference at [envs.net/~merovan/x402_mvp_status.md][status-doc]. Source code at `scripts_segment_4_qf_submit_and_x402_mvp/x402_server.py` in the project repo. Nostr: `npub1mz7kk8hqpu6cdfy3vg4nqjzfkse72gyry06af58rzgaq95aqjxqszx7lsy`.* [status-doc]: https://envs.net/~merovan/x402_mvp_status.md