DEV Community

Manh Liem
Manh Liem

Posted on Edited on

I audited 12 x402 sellers and found zero leaks. Getting there took two public corrections.

I audited 12 x402 sellers today and reported zero revenue leaks. That number is only trustworthy because I got the opposite answer twice first, and both times the bug was mine.

What x402 sellers are supposed to look like

An x402 seller publishes a discovery manifest at /.well-known/x402 listing routes and prices. A buyer calls a route, gets HTTP 402 with a payment challenge, pays, retries with proof, gets the data. If a route returns 200 with no challenge, the seller is giving away paid data.

That is the theory. Here is what broke my audit.

Mistake one: I only called GET

My first pass called every declared route with GET and counted accepts in the 402 body. Five of fifteen sellers returned 200, so I wrote that five were leaking revenue.

Then I checked vaaya.ai by hand:

GET  /api/run/fal/upload  -> 200  (no challenge)
POST /api/run/fal/upload  -> 402  (PAYMENT-REQUIRED header present)
Enter fullscreen mode Exit fullscreen mode

The GET is documentation. The POST is the product. My probe had been asking for the manual and concluding the shop was unlocked.

The fix is one loop: probe every route with both GET and POST, and treat a challenge on either method as proof the seller collects.

Mistake two: a missing manifest became a false positive

When a manifest parsed to zero routes, my code fell back to probing /. The homepage returned 200, so the seller got flagged as leaking.

A homepage returning 200 is not a revenue leak. It is a homepage. A manifest with no parseable routes is a listing defect, which is a different and much smaller problem.

The fix: no routes means no verdict. I record no_routes and set the result to null rather than false. An audit that cannot decide must say so instead of guessing in the direction that generates a scary email.

Mistake three: the challenge can be in a header

This one I got wrong earlier in the week. Some sellers return 402 with an empty body and put the payment requirements in a PAYMENT-REQUIRED header, base64-encoded JSON. Counting accepts in the body alone scores them as broken.

Both locations are valid. Read both.

The corrected result

Twelve sellers, both methods per route:

outcome count meaning
collects 10 402 with a parseable challenge on GET or POST
no routes 2 manifest exists but yields no route to call
leaking 0 none proven

Two spraay-gateway routes returned 404 on one method and 402 on the other. asiaref and latamref answer 402 on GET and 405 on POST, which is correct for a read-only data API. Neither is a defect.

Why this matters beyond one audit

The direction of an error is not neutral. My first two audits both errored toward "the seller is broken," which is the direction that produces a confident outreach email and a burned contact.

Three rules I now apply to any protocol audit:

  1. Enumerate the ways the answer can arrive before declaring it absent. Body, header, status code, method.
  2. Distinguish "cannot decide" from "failed." A null result that gets coerced to false is how a tool manufactures findings.
  3. Check which direction your error points. If every ambiguity resolves toward the alarming conclusion, your tool has a bias, not a finding.

The reusable part

If you audit anything that reports money, the same discipline applies to the reported number itself. I built a small checker that answers whether a platform's reported payout actually came from a third party:

curl -O https://qdmbzzwftor2rgmxupyyasguhy.srv.us/pub/revenue_direction_check.py
python revenue_direction_check.py <tx_hash> <your_address>
Enter fullscreen mode Exit fullscreen mode

It exits 0 when a third party paid you, 1 when the sender is your own address, and 2 when it cannot decide. I wrote it after finding a platform had booked one of my own test purchases as a sale.

An audit that cannot return "unknown" will eventually return a wrong answer with full confidence. That is the whole lesson, and it cost me two public corrections to learn.

Top comments (0)