x402 Doctor
· Live status ·
Diagnoses why an x402-payable endpoint's payment flow is broken, without needing a funded wallet.
Paste a URL (web app) or run x402-doctor <url> (CLI, CI) and get back exactly which check failed, why, and how
to fix it. Every check traces back to a real bug hit while shipping
PlainText and
Ichimoku Signal.
▶ Watch the 70-second explainer (with voice and captions): diagnosing a broken endpoint, the pre-payment check for agents, and the daily Trust Index.
▶ New: the paid fix in 70 seconds: for $0.05 the Doctor hands you the medicine, the exact code that fixes each problem for your stack, in the browser or as JSON for agents.
What it checks
Statuses: pass, warn (works, but something is off), fail (payments fail or it is not valid x402), info.
Web app
npm install
npm start # http://localhost:3001
Reports are shareable: https://<host>/?url=<endpoint>&method=GET runs the diagnosis on load (&method=POST&body=<json> for a POST with an example body).
API: POST /api/diagnose with { "url": "...", "method": "GET" | "POST", "body": { ... } } (method and body optional)
returns { url, method, overall, checks[], challenge, share_url }. body is an example request body (JSON, up to
16 KB) for a POST that checks its input before the 402; it implies POST, and the report says which body reached the 402
(body, and body_source when it came from the seller's /openapi.json). share_url is the page link above, to paste into an issue or a chat. Rate-limited to 10 diagnoses per minute per IP.
Paid API for agents (x402)
GET /api/v1/diagnose?url=<endpoint>&method=GET|POST returns the same report, $0.01 USDC per call on Base or
Solana via x402, with no rate limit. It is meant for agents and CI pipelines; the web page stays free.
- Unpaid requests get the x402 challenge (
PAYMENT-REQUIREDheader, mirrored in the body) with a Bazaar input and output schema. body=<URL-encoded JSON>(optional, also on preflight, deep and fix, and asbodyin the MCP tools): an example request body for a POST route that checks its input before the 402. It implies POST; withmethod=GET, or when it is not JSON, the answer is a 400 before payment. The batch takes no body.- Invalid input (bad URL, non-http(s), unknown method) gets a 400 before payment. The bare route without
urlanswers 402, so indexers such as x402scan can register it. Paying without aurlreturns a 400. - The x402 middleware settles only after a 2xx response, so a diagnosis that errors out is never charged.
- Discovery:
/openapi.json(withx-payment-info) and/.well-known/x402. - Browsers opening the route get a wallet paywall (Base first: MetaMask, Coinbase Wallet) instead of the bare 402.
- Bazaar: a facilitator lists a route after the first payment it settles for it. With the CDP keys set, one paid call (e.g. from the browser paywall) puts the route in the CDP Bazaar that agents search.
# pay-per-call from Node with @x402/fetch
const paidFetch = wrapFetchWithPayment(fetch, client); // client with an EVM or SVM signer
const report = await (await paidFetch("https://<host>/api/v1/diagnose?url=https://api.example.com/paid")).json();
Pre-payment check for buyers: GET /api/v1/preflight
GET /api/v1/preflight?url=<endpoint>&max_usd=0.05&network=<caip2>&method=GET|POST, $0.001 USDC per call:
call it before your agent pays an x402 endpoint it has not used before. It never pays the endpoint. Add
client=gateway when your agent pays with Circle's Gateway client from a Gateway balance: Circle Gateway-only
endpoints (gateway_only, otherwise a caution) are then a go, and a Gateway option is recommended first.
{
"verdict": "go", // go | caution | no_go
"safe_to_pay": true,
"summary": "OK to pay: $0.02 on Solana.",
"recommended_option": 1, // index into the endpoint's accepts[]
"options": [{ "network": "eip155:8453", "asset_symbol": "USDC", "usd": 0.02, "payable": true, "problems": [] }, …],
"signals": { "https": true, "advertised_price_usd": 0.02, "listed_in_cdp_bazaar": true },
"reasons": [], // [{ level: no_go | caution | info, code, message }]
"cached": false
}
BNB Chain (eip155:56) has no Circle USDC. Sellers price in 18-decimal dollar tokens: Binance-Peg USDC, USDT,
USD1 and United Stables (U), so $0.01 is "10000000000000000". Doctor prices those options like USDC. USDC and USDT
there have no EIP-3009, so an option without assetTransferMethod: "permit2" fails; a value standard x402 clients
don't know is a warning, because @x402/evm then signs EIP-3009 instead. "permit2-exact" and "permit2-upto" are the
methods of Binance's B402 facilitator, so only its own clients pay those options.
Bait check (TypeSafe's Jev, when TYPESAFE_API_KEY is set): for an endpoint that could be paid, Jev judges
what the protocol checks can't. Does it claim to be, or to be official from or endorsed by, a brand that doesn't own
its domain (brand_impersonation; reselling or proxying a brand's product under the seller's own name, such as an
LLM gateway naming the models it serves, is not), lure with free tokens, airdrops or guaranteed returns (lure_description), or
promise something its declared output doesn't deliver (output_mismatch)? A clear yes is a caution, an
in-between answer info; it never makes a verdict no_go and never turns one into go. The probabilities are
in signals.jev_lure. On 300 real CDP Bazaar listings it raised no caution for brand or lure wording. When a bait
question changes (BAIT_VERSION in lib/jev-lure.js), the Trust Index's next run asks again, first, every service
the old wording flagged or was unsure about; clear answers carry over.
Price check (same key): is the price far above what comparable services ask? Doctor takes up to 24 paid APIs
of other sellers from the x402 Trust Index whose descriptions share rare words with this one (at most two per
seller), and Jev judges per candidate whether a buyer would be served just as well by it. With at least 5 such
services, their median price is the comparison: 5x or more (and at least $0.05 more) is info, 20x or more (and
at least $0.25 more) a caution, both price_high and naming up to three cheaper alternatives. The comparison is
in signals.price_check (comparable_median_usd, comparables, ratio, cheaper[]). It never makes a verdict
no_go: the price is the seller's choice, the agent decides whether it is worth it.
Not being listed in the CDP Bazaar is reported as info only. Results are cached for 10 minutes per URL, budget
and network (cached: true), so checking before every payment stays fast.
Several endpoints at once: GET /api/v1/preflight/batch
GET /api/v1/preflight/batch?url=<a>&url=<b>&max_usd=0.05, $0.005 USDC per call for up to 10 endpoints (one
url parameter each; duplicates count once). For marketplaces, directories and agents that weigh several services.
Each endpoint gets the same verdict as a single preflight; one that errors or does not answer within 20 seconds is
unknown and does not hold up the rest.
{
"count": 2,
"counts": { "go": 1, "caution": 0, "no_go": 1, "unknown": 0 },
"results": [
{ "url": "https://a.example/paid", "verdict": "go", "safe_to_pay": true, "summary": "OK to pay: $0.02 on Base.", "recommended_option": 0, "options": […], "reasons": [] },
{ "url": "https://b.example/paid", "verdict": "no_go", "safe_to_pay": false, "summary": "Do not pay: …", … }
]
}
Before an expensive call: GET /api/v1/preflight/deep
GET /api/v1/preflight/deep?url=<endpoint>&max_usd=0.5, $0.01 USDC per call: everything the preflight answers,
plus
diagnosis: the full diagnosis (overall, every check, and the failing or warning ones asproblems);history: the daily scans of the last 30 days, one{ date, status }per day;seller: the seller's other scanned endpoints on the same origin (how many, payable today, average payable ratio, how many were unreliable). A seller whose endpoints were payable on less than half of the scans turnsgointocaution(unreliable_seller);domain: the domain-move checks (well-known,bazaar-listing), e.g. a Bazaar listing only under an old domain.
Paid and delivered? GET /api/v1/delivery-check
GET /api/v1/delivery-check?url=<endpoint>&method=GET|POST, $0.25 USDC per check: x402 Doctor buys the endpoint
once, with the example input its own listing declares, and publishes what came back. For sellers who want proof that
their endpoint delivers, and for buyers before a large order.
- Checked before you pay: the endpoint must be https, not a fizzl.eu route, get a
gofrom the pre-payment check, be payable in USDC on Base and cost at most $0.10. Anything else answers 400 (or 503 when the day's 20 checks are taken), and nothing is charged. A request with a payment always waits for the whole check before anything settles. A probe without a payment wants the 402: it gets one unless the request itself is malformed (not one https URL, a fizzl.eu route, a bad method), also for an endpoint that can't be bought now, since uptime monitors fillurlwith a placeholder such ashttps://example.com, and within 5 seconds (the CDP Bazaar's validator and health probe give up after 10). To get the answer before paying, sendX-Delivery-Precheck: 1; the homepage does. - The answer comes at once, the purchase shortly after: the paid call answers with a job
idand astatus_url.delivery-check.ymltakes the queue (GET /api/v1/delivery-check/queue, free) and buys with the dedicated spot-buy wallet (SPOT_BUY_KEY; no wallet key on the server). Only a job whose payment settled is released to the queue. The server starts the workflow as soon as a payment settles (DELIVERY_DISPATCH_TOKEN: a fine-grained GitHub token for this repository with only Actions read and write;lib/delivery-dispatch.js); its schedule, every 10 minutes, is the fallback, since GitHub runs scheduled workflows late or not at all. - The same steps as the spot-buy (
lib/spot-buy.js): a fresh preflight with the OFAC payout screen right before paying, the seller's declared example input, the answer checked against the declared output schema, and the charge measured on chain. - Public result:
GET /api/v1/delivery-check/<id>(free) showsqueued, thendonewith the result (delivered,off_schema,empty,charged_not_delivered,payment_rejected,error_not_charged, or the step that stopped it before paying) and the transaction. Every result is on thedelivery-databranch. - Caching: for a delivered GET answer,
cachesays whether a shared cache (a CDN) may store the paid answer and serve it to callers who did not pay (shared_cache:yes,noorunspecified, with theCache-Controlit sent). The x402 payment travels in its own header, not in , so only or keeps it out (, attack III).
Jobs are kept in the credits Redis (CREDITS_REDIS_URL); without it the route answers 503 before payment.
Prepaid credits: GET /api/v1/credits/1000
Agents that check often can prepay: one x402 payment of $0.80 for 1000 credits or $7.00 for 10,000 (20% and 30% off), valid for a year. A credit is $0.001: a preflight costs 1, a batch 5, a deep preflight or a diagnosis 10, the fix 50. The pack answers with a credit_key; send it as the x-credit-key header with any paid call instead of paying. GET /api/v1/credits (free) shows the prices and, with the header, your balance. A call that fails gives its credits back; with no key or too few credits the normal 402 applies. Needs CREDITS_REDIS_URL (a persistent Redis such as Upstash; the presign-guard instance is fine, the keys are apart); without it, no packs are sold.
The fix, as code: GET /api/v1/fix
GET /api/v1/fix?url=<endpoint>&method=GET|POST&stack=<optional>, $0.05 USDC per call: diagnoses the endpoint,
then returns, per failed or warning check, the concrete change that fixes it, as code for your stack and filled in
with your own values (payTo, amount, network, route).
{
"stack": { "id": "express", "name": "Express (@x402/express)", "detected_from": "x-powered-by: Express" },
"summary": "3 fixes (1 blocking), for Express (@x402/express).",
"fixes": [
{
"recipe": "amount",
"severity": "warn",
"checks": ["accepts[0]-amount"],
"title": "Amount \"0.01\" → \"10000\" ($0.01)",
"why": "x402 amounts are integer strings in the token's smallest unit; USDC has 6 decimals.",
"steps": ["Easiest: let the SDK convert it by using price: \"$0.01\" instead of an amount."],
"code": [{ "language": "javascript", "stack": "node", "label": "Route config", "snippet": "accepts: [{ scheme: \"exact\", price: \"$0.01\", ... }]" }],
"verify": "Run x402 Doctor again on https://…; accepts[0]-amount should pass."
}
],
"unfixed": []
}
The stack is read from the response headers (x-powered-by, server, Vercel/Render/Cloudflare headers) and the
challenge: express, next, hono, node (an @x402 SDK behind another framework), python or generic (raw
HTTP, for any language). Pass stack= when the guess is wrong. Node snippets use the @x402 v2 route config, which is
the same for @x402/express, /next and /hono; lines that only apply to Express say so.
Recipes (lib/recipes.js, one test each in test/recipes.test.js): no 402 at all (200 before the paywall, auth or
validation first, 404/405, unreachable), v1 or invalid challenge, wrong header name, no body mirror, no accepts,
scheme, legacy network names, invalid payTo (spots quotes, spaces, the wrong chain), USDC asset, decimal amounts,
Solana fee payer, EIP-712 domain, http resource URL behind a proxy, resource URL and metadata, Bazaar declaration and
example, Solana payout token account, Phantom on PayAI, testnet paywall on mainnet, and openapi.json. Anything
without a recipe is returned under unfixed with the diagnosis hint.
Learning from outcomes: POST /api/v1/outcome
After a paid preflight, a buyer can report (free) what happened when it then paid the endpoint: paid_ok, paid_failed (402 again) or paid_error. x402-safe-fetch does it with shareOutcomes: true.
{ "outcome": "paid_failed", "status": 402, "preflight": { …the signed preflight answer… }, "query": { "url": "…", "method": "GET", "max_usd": "0.05", "network": "eip155:8453" } }
- A report counts only with a real preflight that Doctor signed for exactly that query, once per preflight (
receipt.request_id), so a false report costs a paid preflight. - Doctor counts reports per endpoint over 30 days. When most reported payments failed, from at least three different paying wallets, later preflights add
payments_fail_after_preflight(caution);signals.agent_outcomesshows the counts. - Reports land in the usage log (endpoint, outcome, status, verdict, payer from the receipt); nothing else about the buyer.
Signed verdicts
Every paid answer (diagnose, preflight, batch, deep and fix; HTTP and MCP) carries a receipt signed by Doctor, so an agent can later prove which verdict it got for which endpoint, for example why it did or didn't pay:
"receipt": {
"request_id": "…", "route": "GET /api/v1/preflight", "input_sha256": "…",
"payment": { "network": "eip155:8453", "amount": "1000", "payer": "0x…", "nonce": "0x…", "proof": "eip3009" },
"signed_at": "2026-09-27T08:00:00.000Z", "signer": "0x…",
"algorithm": "eip191-canonical-json-v1", "signature": "0x…"
}
- Signed: the whole answer without
receipt.signature, as canonical JSON (profilejs-json-stringify-sorted-utf16-ascii-v1: keys sorted by UTF-16 code units, compact, every code unit from U+007F up as lowercase\uXXXX, numbers as JavaScript'sJSON.stringifywrites them —1.0→1,0.000001→0.000001— then UTF-8 bytes; Python'sjson.dumpsmatches only for ASCII keys and integers, so useexamples/canonical.py, which also hasverify_receipt(answer)), with EIP-191personal_sign. Flipping the verdict or moving it to another request breaks it. payment: the payment that bought this answer, from your x402 payment payload. On Base the payer and the EIP-3009 nonce, so anyone can find the settlement on-chain as the USDC contract'sAuthorizationUsed(payer, nonce)event; on Solana the payer and a SHA-256 of the signed transaction you sent.input_sha256: SHA-256 of the canonical JSON of{"route", "input"}, whereinputis your query parameters as strings (HTTP) or the tool arguments (MCP, routemcp <tool>).- Signer:
/.well-known/x402-doctor-signer.json, with retired signers listed so old receipts keep verifying. - Free check:
POST /api/v1/verifywith{"response": …, "route": …, "input": …}returnsvalid,signer,known_signerandinput_matches. Over MCP: the free toolverify_receiptwith the same arguments (routelikemcp x402_preflight,inputthe tool arguments).
Key rotation without client updates: the payout wallet (0x6B0F4651eD42893ab58139938175E4a69f175F25) authorises each signing key with a personal_sign over fizzl receipt signer / service: x402-doctor / signer: <address> / valid_from: <YYYY-MM-DD>. Set it as RECEIPT_SIGNER_CERT (YYYY-MM-DD:0x<signature>, produced by /sign-receipt-key in the wallet's browser). It is checked at startup and carried in every receipt as receipt.cert, so a client that pins only the payout wallet verifies a new key offline.
Same format as presign-guard, so one verifier works for both. The key comes from RECEIPT_SIGNER_SECRET (holds no funds); without it answers are unsigned.
x402 Trust Index
Once a day, trust-scan.yml runs the pre-payment check against every resource in
the CDP Bazaar, the MPP directory, the GoPlausible facilitator's discovery list (x402 on Algorand, about 2,500 endpoints the Bazaar doesn't list) and Circle's Agent Marketplace catalog (about 2,800 endpoints, many paid through Circle Gateway) (read-only: it stops at the 402 challenge, at most two requests per host at a time, one
/openapi.json per origin, a User-Agent that links to /trust). The results roll into a 30-day history per resource,
one letter per day (g go, c caution, n no-go, x unreachable, - not scanned), published as
index.json + summary.json on the trust-data branch.
The server doesn't load that index (32 MB, ~70,000 resources): after the scan, scripts/trust-views.js
(lib/trust-views.js) writes it as small precomputed files on the same branch, from the same code (views/meta.json
with the summary, views/atlas.json, views/templates.json, views/sellers.json, the price check's word index under
views/similar/, one views/networks/<key>.json per network, and the resources in 256 buckets by key under keys/).
lib/trust-reader.js reads only what a request needs (one ~130 kB bucket per lookup, at most 32 kept) with the same
answers as the full index, and falls back to the full index while the views aren't published. Measured on the 20 Oct
index: 116 MB peak instead of 285 MB, 37 MB of heap instead of ~100 MB.
After each scan, hf-sync.yml publishes it as an open dataset on Hugging Face,
x402Fizzl/x402-trust-index (CC BY 4.0): data/endpoints.jsonl (every endpoint in the latest scan with its
verdict, reasons, price, networks, category and history), data/daily.csv (go / caution / no_go per day),
data/spot_buys.jsonl (every purchase of the weekly spot-buy, from the spot-data branch), data/onchain_payments.csv
(signed USDC payments on Base per payout address and day, from the payments-data branch) and the dataset card
(). It also keeps the dataset in the Hugging Face collection "x402 agent payments" when the token
may write collections. It needs the repository secret (a Hugging Face write token); the variable
overrides the dataset name.
publishes the
the same way
(): a dataset with every run and its transcript (from x402-examples'
branch), the published tables as , and a static leaderboard Space of the same name. It
runs when the script changes, every Monday and by hand; the variable overrides the name.
publishes as the Space
: a Gradio app that is also an MCP server
(, or added under Hugging Face's MCP settings), with four
free tools over the public API: (), (),
() and (). It never pays or holds a key; the variable overrides the name.
Hugging Face hosts Gradio Spaces only for PRO accounts: without PRO the workflow warns and publishes nothing, and the
Space waits in . Doctor's own MCP server is .
- The pre-payment check adds
signals.track_recordand acaution(unreliable_history) when a seller was payable on fewer than half of at least 3 scanned days. - Spot-buy sample: does "go" mean paid and delivered? The scan only reads the 402 challenge. Once a week
spot-buy.ymlbuys a small random sample of "go" endpoints (lib/spot-buy.js): x402exactin USDC on Base only, one endpoint per host, never a fizzl.eu or test host, the seller's own declared example as the request (when it reaches the paywall), a fresh preflight with the OFAC payout screen right before paying, and hard caps (default 30 endpoints, $0.05 per call, $2 per run; at most 50, $0.10 and $5). Whether money left the wallet is measured on chain (its USDC balance before and after), and the seller'sPAYMENT-RESPONSEis checked against the chain. Each result isdelivered,off_schema(an answer that doesn't match the declared output schema),empty,charged_not_delivered,payment_rejected,error_not_chargedorpayment_failed; the run is appended to thespot-databranch (latest.json,results/<date>.json, a second run that day asresults/<date>-2.json). It pays only with the secretSPOT_BUY_KEY, a dedicated wallet holding a few USDC; without it every run is a dry run that pays nothing. The latest run is on/trust("Paid and delivered?") and atGET /api/trust/spot(summary and every purchase, no wallet details);GET /api/trust?url=…addsspot_buywhen that endpoint was bought in it (lib/spot-results.js). The paid pre-payment check adds the same assignals.spot_buy, and acaution(charged_without_answer) when the endpoint took the payment for the example request its own listing declares and answered with an error or not within 90 seconds. Each purchase also records its answer time (answer_s, from the first request to the paid answer): the summary counts answers slower than 30 seconds (slow_answers, anddelivered_within_30s_pct, what an agent that waits 30 seconds gets), and a slow one addsslow_after_payment(info) to the pre-payment check. The same wallet makes one other kind of payment, started by hand: pays our own fizzl.eu routes (), because the CDP Bazaar lists a route only after a payment for it settles and drops it after 30 days without one. (the default) pays each of our 16 routes whose last settled payment in the Bazaar is 21 days old or more (read from its public search), each at most its own price (); pays one route once. The money goes to our own payout wallets. It publishes nothing, so the sample stays one of other sellers. Each run is also a self-audit of those routes: the same payment header is sent once more and must get a 402 ("Five Attacks on x402", attack II; a route that serves it again fails the run), and the settlement transaction's ERC-8021 suffix shows whether our Base Builder Code was written into it. Other sellers' endpoints are never replayed.
Weekly x402 health report
Every Monday, weekly-report.yml turns the last 7 days of the Trust Index into a
report: how many Bazaar resources are clean, payable with a caution or not payable, the most common reasons with the fix
for each, which networks payable resources accept, and what changed this week (broke, fixed, flaky, new). It names no
seller or host. The report, its numbers and drafts for X, Discord and Reddit (for a person to review and post) go to
reports/<date>.md, .json and -posts.md on the weekly-reports branch. Locally:
node scripts/weekly-report.js --out reports [--index trust-data/index.json].
The same run writes a digest per network, Algorand and the XRP Ledger (network-digest.js):
how many endpoints could be paid there, from how many sellers, what changed since last week (new endpoints, ones that
stopped being payable, new sellers), how many accept only that network, the median price and the biggest sellers. It
names payable sellers only; problems are counted. Files: reports/<date>-<network>.md and -posts.md (X and community
drafts), plus reports/networks/<network>-latest.json, the snapshot next week compares with. Locally:
node scripts/network-digest.js --out reports [--index trust-data/index.json] [--previous reports/networks].
Agent skills
Two Agent Skills in skills/ teach a coding agent (Claude Code, Codex, Cursor and others that read SKILL.md) when and how to use Doctor:
x402-preflight: before the agent pays any x402 or MPP endpoint (awal x402 pay, x402-fetch, Privy's x402 client, mppx…), it looks up the free track record, then buys the $0.001 preflight and follows go / caution / no_go, and reports the outcome afterwards.x402-seller-check: after building or changing a paid endpoint, it runs the free check until it passes, and buys the $0.05 fix when a problem isn't obvious.
npx skills add Fizzl13/x402-doctor
Claude Code plugin
In Claude Code, the same skills come as a plugin together with the MCP server below, and presign-guard's skill and MCP server as a second plugin. The Fizzl Agent Wallet and Ichimoku Signal have a plugin each, from Fizzl13/fizzl-skills, the overview of every Fizzl skill:
/plugin marketplace add Fizzl13/x402-doctor
/plugin install x402-doctor@fizzl
/plugin install presign-guard@fizzl
/plugin install fizzl-agent-wallet@fizzl
/plugin install ichimoku-signal@fizzl
The marketplace is .claude-plugin/marketplace.json. The paid tools of both MCP servers need an MCP client that pays x402; without one, the skills pay over HTTP with your own x402 client, as they describe.
MCP server
https://x402-doctor.fizzl.eu/mcp is an MCP server (Streamable HTTP, stateless) for Claude, Cursor and agent frameworks, listed in the official MCP registry as io.github.Fizzl13/x402-doctor. Directories can read its tools without connecting from the server card at /.well-known/mcp/server-card.json (the same list as tools/list).
https://x402-doctor.fizzl.eu/mcp/free is the same server with only the free tools (x402_quick_check, check_payment_proof, x402_trust_record, x402_trust_add), for directories whose clients don't pay, such as Claude's connectors directory. Claude calls connectors from Anthropic's outbound range (160.79.104.0/21), so calls from there share one pool of 300 free calls an hour instead of 10 per address, on both endpoints.
The paid tools are paid inside the MCP call with the x402 MCP transport (_meta["x402/payment"]), on Base or Solana, at the same prices and to the same payout wallets as the HTTP routes. Invalid input is refused before payment, and a failed call is not charged. A delivery check ordered over MCP is queued only once its payment settled, like the HTTP route.
Feedback
Found a bug, or missing something? Send it with POST /feedback (free, no payment) or the MCP tool feedback:
curl -X POST https://x402-doctor.fizzl.eu/feedback \
-H 'content-type: application/json' \
-d '{"type": "feature", "message": "Also check the facilitator /supported list", "endpoint": "/api/v1/diagnose"}'
type is bug, feature or other; message is required (up to 2000 characters); endpoint and contact are optional. The answer is 202 with an id. At most 10 reports per hour per caller. Reports go to the usage log (shown on /admin/usage), and a person reads every one; nothing in a report is run or changed automatically. GET /feedback shows the schema. A diagnosis never calls another server's feedback tool: tools that call themselves free are skipped.
CLI
npx github:Fizzl13/x402-doctor https://api.example.com/paid
npx github:Fizzl13/x402-doctor https://api.example.com/paid --method POST --json
npx github:Fizzl13/x402-doctor https://api.example.com/v1/score --body '{"text":"hello"}'
Exit code 0 on pass/warn, 1 when a check fails (or on warnings with --strict), 2 on usage errors.
The CLI can diagnose localhost, so you can run it against a dev server.
GitHub Action
Check your x402 endpoints on every push. The step fails on a broken 402, each problem shows up as an annotation on the run and the PR, and the job summary lists what is wrong with the fix next to it. On the GitHub Marketplace as x402 Doctor check; a public endpoint that passes also gets a badge snippet.
- uses: Fizzl13/x402-doctor-action@v1
with:
urls: |
https://your-api.example.com/paid
https://your-api.example.com/other
Outputs: overall (pass, warn or fail, the worst across the URLs) and
report (the full reports as JSON). The Action uses the runner's own Node
(20.18 or newer, as on GitHub-hosted runners) and does not change the job's
Node version. Read-only: no wallet, no payment.
Or the CLI in a plain run: step:
- run: npx -y github:Fizzl13/x402-doctor https://your-api.example.com/paid
Configuration
| USAGE_LOG_TOKEN | Fine-grained GitHub token with Contents read/write on the usage-log repo only. Every call is logged there (see below) |
| USAGE_LOG_REPO | The private usage-log repo (default Fizzl13/usage-log) |
| ADMIN_PASSWORD | Password for /admin/usage (any user name). Without it the admin pages do not exist |
Without either payout wallet the paid route answers 503 and the rest of the app works as before.
Usage dashboard
/admin/usage shows every call to the Fizzl services (x402 Doctor, PlainText, Ichimoku Signal, presign-guard):
totals, revenue, paying wallets, calls per day per service, the most asked inputs and the latest calls with what
was filled in, the result and the payment (with a Basescan/Solscan link). Each service appends one JSON line per
call to events/<service>/<YYYY-MM-DD>.jsonl in the private usage-log repo (lib/usage-log.js, copied into
each service); the dashboard reads it back (lib/usage-reader.js). 402 challenges, health checks and static
files are not counted, and inputs are cut to 300 characters. Logging never delays or fails a request.
Payment security (Five Attacks on x402)
Five Attacks on x402 (Li, Wang and Wang, 2026) describes five ways an x402 payment can go wrong between the HTTP request and the chain, and maps each to a mitigation. How this service handles them:
Doctor checks other sellers for two of them as well: the paid delivery check records whether a CDN may serve the paid answer for free (III), and the pre-payment check covers IV for every endpoint a buyer is about to pay.
Safety
The web app fetches user-submitted URLs server-side:
- Every connection is checked against private and reserved ranges (RFC 1918, loopback, link-local and cloud metadata, CGNAT, IPv6 ULA/link-local, IPv4-mapped IPv6 in any notation) at connect time, so DNS rebinding cannot slip past a separate lookup. Redirects are followed manually and each hop is re-checked.
- Responses are capped at 2 MB and every request times out after 8 s.
/api/diagnose(free) is rate-limited per IP;/api/v1/diagnoseis paid per call and uses the same guards.- Everything taken from the diagnosed endpoint is rendered as text, never as HTML.
It never pays the endpoint it diagnoses: no wallet, no signatures, zero financial risk. A paid probe (sign and settle one minimal payment to trace verify/settle failures) is deliberately out of scope here.
Project structure
server.js Express app: /api/diagnose (free), paid API, /openapi.json, /.well-known/x402, static frontend
lib/paid-api.js GET /api/v1/diagnose, /preflight (+ /batch, /deep) and /fix behind x402 (Base + Solana USDC)
lib/recipes.js The fix per failed check, as code for the detected stack
lib/stack.js Which stack runs an endpoint, from its response headers
lib/preflight.js Pre-payment check: verdict, recommended option, reasons (cached)
lib/preflight-plus.js Batch (up to 10 endpoints) and deep pre-payment checks
lib/bazaar-index.js Cached CDP Bazaar index (listing signal for preflight)
lib/trust-scan.js Trust Index scan: catalog, polite fetch, 30-day history
lib/trust-index.js Reads the published index for preflight and /api/trust
scripts/trust-scan.js Daily scan entry point (trust-scan.yml)
public/trust.html /trust page
lib/media.js Serves the explainer and paid-fix videos from their branches (/media/explainer.mp4, /media/fix.mp4)
media/explainer/ Explainer video pipeline (script, voice, recording, encoding)
lib/diagnose.js The checks
lib/request-body.js Example POST body: the caller's, or the requestBody example from the seller's /openapi.json
lib/site-scan.js MetaMask's site scanner verdict for the domain
lib/mcp-check.js x402 over MCP: tools/list, unpaid tools/call, the payment requirement in the tool result
lib/networks.js Known networks, USDC (or BNB Chain's dollar tokens) per network, address validation
lib/safe-fetch.js SSRF-safe fetch (connect-time IP check, redirects, size cap, timeout)
bin/x402-doctor.js CLI
public/index.html Frontend
skills/ Agent Skills: x402-preflight (buyers) and x402-seller-check (sellers)
.claude-plugin/ Claude Code plugin marketplace: the skills plus the MCP servers of Doctor and presign-guard
test/ node:test suites (fixture x402 servers, SSRF, CLI, API)
Tests
npm test
The suites run the doctor against local fixture servers: a healthy v2 service (Base + Solana, Bazaar, OpenAPI, mainnet paywall) and a broken one that reproduces the real bugs above, plus SSRF, size-cap, API and CLI tests.
License
Copyright (C) 2026 Frits Zwager. Licensed under the GNU Affero General Public License v3.0 only (AGPL-3.0-only); see LICENSE. If you run a modified version as a network service, you must offer its source code to the users of that service.

