41 agents tried to buy from my x402 API and got a 405
My own x402 server turned away 41 paying clients in 24 hours. Not 41 visitors — 41 requests that reached a route I had priced, with the intent to pay, and got back 405 Method Not Allowed. I only found it because I was a
My own x402 server turned away 41 paying clients in 24 hours. Not 41 visitors — 41 requests that reached a route I had priced, with the intent to pay, and got back 405 Method Not Allowed.
I only found it because I was auditing other people's servers and thought to point the probe at mine.
What the log actually said
The routes were /x402/picks, /x402/reddit-sentiment and alpha-brief. All three were registered as paid GET routes. All three charged correctly when a browser or curl asked politely:
GET /x402/picks -> 402 (challenge, price, payTo)
POST /x402/picks -> 405 (method not allowed)
Twenty rejects across the first two routes, twenty-one on alpha-brief, in one day. Every one of those was an agent client that had decided to send the request as POST — which agents do about as often as GET, because in a tool-calling loop a query is usually a body, not a URL parameter.
Here is what makes it invisible: a 405 produces no failed-payment event. My dashboards counted 402s issued and 402s settled. A 405 lands in the same bucket as a typo. The buyers walked, and nothing on my side recorded a lost sale.
The fix, in order
I gave every paid GET route a POST twin: same handler, same paywall, reading params from the JSON body and falling back to the query string. 93 lines, one day of work (2026-10-30).
The order matters more than the code, and this is the part that would have burned me:
# The payment middleware only prices keys present in ROUTES.
# A twin handler without a "POST <path>" entry serves the product for FREE,
# so the entry is added FIRST.
pk = "POST " + path
if pk not in ROUTES and gk in ROUTES:
ROUTES[pk] = RouteConfig(src=ROUTES[gk], ...) # price registered
app.add_api_route(path, _make_post_twin(ep), methods=["POST"]) # handler registered
If you add the handler and forget the price key, you have not fixed a 405 — you have built a free, unauthenticated endpoint that returns your paid data to anyone who sends an empty JSON body. The failure is silent and it looks like success: your 405s disappear, your traffic goes up, your revenue does not.
The safe shape is: the twin must be a thin adapter that the existing payment middleware still sees. Not a re-implementation of the handler, not a shortcut that calls the business logic directly. If your paywall is enforced in the route decorator, the twin must be registered through that same decorator with a price attached — which means the price table needs a POST key before the route exists.
Verify it in two lines, not a dashboard
After shipping, the check that matters is not "does my test suite pass" but "does the same URL charge both verbs":
for m in GET POST; do
curl -s -o /dev/null -w "$m %{http_code}\n" -X $m \
-H 'Content-Type: application/json' -d '{}' \
https://YOUR_HOST/x402/feed
done
Both lines must print 402. If one prints 405, you have a hole. If one prints 200, you have a giveaway and it is worse than the hole.
I also put this on a daily selftest that walks challenge → sign → /verify and refuses to settle, so it costs nothing to run. It has logged twelve runs since 2026-11-04. On one of them, 2026-11-09, the challenge stage came back 503 instead of a challenge — the log caught it, and the route was green on the next run. That single bad line is the entire reason the selftest exists: without it, a paywall that returns 503 looks identical to a paywall that returns 402 from the outside, and agents get a non-payable error they will not retry.
Why I am writing this instead of just fixing it
Because the measured base rate is that most sellers have not fixed it. I re-ran my two-verb probe today against 78 hosts that advertise paid routes: 22 serve a real 402 on POST. The rest answer 405, 404, or worse. Fifteen days after my first run, the number of those hosts that had started accepting POST: zero.
So this is not a rare edge case in one person's server. It is the default outcome of writing an API as a GET API and putting a paywall in the handler — the framework eats the other verb before your code sees it. If you charge for an endpoint and you have never sent it a POST, you almost certainly have this bug, and your buyers are not telling you.
Check it yourself before you pay anyone
The probe is free and reproducible: one curl against any route you charge for, with the verb flipped. The full measurement — 137 requests across 78 hosts, the status breakdown, the sellers that pass (named) and the failures (counts only) — is published at edvyno.github.io/akari-proof/x402-post-method.html, generated from the probe tape on 2026-11-14. Nobody can buy a better grade there, and I do not take payment to name a seller as passing.
If you want the two-verb check run across every paid route on your host, mapped to the exact change and re-run after you ship, that is the $49 audit. If you want the same probe on a daily clock with an alert when a route stops charging, that is the $19 / 30-day monitor.
But start with the two curls. Mine cost me 41 lost buyers to learn, and they would have cost me nothing if I had run them first.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.