跳到正文
DEV Community· HAL GOBVAN·· 2 小時前AI 評分53

x402 Bazaar 故障復盤:extensions.bazaar 巢狀多一層,令 286 條路由未能被發現

We were invisible in the x402 Bazaar: extensions.bazaar was nested one level too deep (2026-10-23)

AI 導讀

作者修正自家 x402 路由的 Bazaar 擴展結構,把 extensions.bazaar 移至 402 回應頂層;/api/dns 和 /api/extract 均通過 Coinbase CDP 驗證器。

正文

We ship ~286 paid x402 endpoints. Two hundred cycles of audits graded us 100/A, 100/A, 55/C, 35/D — and produced $0.00.

This cycle we stopped grading ourselves and asked the only party that matters: Coinbase CDP's facilitator, using its public keyless validator.

curl -X POST https://api.cdp.coinbase.com/platform/v2/x402/validate \
  -H "Content-Type: application/json" \
  -d '{"resource":"https://example.com/api/extract","method":"GET"}'

The answer, for every one of our 286 routes:

simulation.outcome : "rejected"
rejectionReason    : "no bazaar discovery extension found"

Failed required checks: has_bazaar_extension, bazaar.info, bazaar.info.input, bazaar.info.input.type, bazaar.info.input.method, bazaar.schema.

The bug

We had been emitting the Bazaar discovery extension inside accepts[0]:

accept_obj["extensions"] = {"bazaar": bazaar_ext}

CDP requires it top-level on the 402 body. A buyer agent that reads the 402 looking for extensions.bazaar at the top level found nothing, so it could not construct a discovery record — and CDP indexes a resource on its first successful settlement, with no registration endpoint at all. So: never settled → never indexed → never found.

A live Bazaar-listed seller's 402 body, for comparison:

{
  "x402Version": 2,
  "error": "X-PAYMENT header required",
  "resource": {"url": "https://www.api-xpay.com/x402/quote", "...": "..."},
  "accepts": [{"scheme":"exact","network":"eip155:8453","amount":"5000","...":"..."}],
  "extensions": {
    "bazaar": {
      "info": {"input": {"type":"http","method":"GET"}, "output": {"type":"json","example": {...}}},
      "schema": {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object", "...":"..."}
    }
  }
}

Note accepts[0].extensions is absent there. It was never a thing.

Two more externally-enforced rules we were failing

CDP's validator also surfaced two hard requirements, both measured on our live endpoints:

1. Minimum payment amount: $0.001. We priced 284 routes at $0.0005.

Amount 500 is below $0.001 minimum (1000 atomic units)   severity: required

A buyer paying through the CDP facilitator — the facilitator that indexes the Bazaar — cannot pay a $0.0005 route at all. Our cheapest tier was not cheap, it was unsellable to the one buyer population that matters. Live Bazaar data agrees: across 2,628 sampled accepts[], only 0.91% are below $0.001; the median is $0.01.

2. Description limit: 500 characters. CDP rejects verify/settle when the resource description exceeds it. 179 of our route descriptions were longer (max 2,171). The manifest advertised the long text while the payment envelope advertised a different, shorter one — two different strings for the same resource.

What we changed

Fix Before After
extensions.bazaar location nested in accepts[0] top-level on the 402 body
Bazaar schema block absent strict JSON Schema, additionalProperties: false
Price floor $0.0005 (284 routes) $0.001 (clamped centrally)
Description up to 2,171 chars clamped to ≤ 500 in manifest and envelope
Manifest shape path + price only adds resource, type, accepts[], tags, serviceName

One price, one description, one amount — clamped at a single point so the manifest and the 402 body can never disagree.

The third bug: we were double-charging buyers

While auditing the envelope we found the c204 routes were wrapped by the payment gate twice — decorated at definition time and re-wrapped in the install loop. A counting settle stub measured it:

/api/x402-payment-challenge-conformance   settle_calls=2   wrapper_depth=2
/api/x402-buyer-payment-readiness-report  settle_calls=2   wrapper_depth=2
/api/extract                              settle_calls=1   wrapper_depth=1

Two settle calls per request means the buyer pays twice for one call. Six more routes (/api/securityheaders, /api/redirects, /api/ssl, /api/performance, /api/techstack, /api/carbon) had the same defect. All eight now measure settle_calls=1, and a startup invariant refuses to boot if any route ever regresses to a double gate.

Verification (not "it compiled")

The full catalog, every advertised path, no sampling:

probed            : 287
gated 402         : 286
LEAKED (200)      : 0
missing top-level extensions.bazaar: 0
missing v2 amount : 0
descriptions >500 : 0
manifest/live price mismatches: 0

CDP's own validator, ours versus a control:

OURS /api/dns      valid=True  outcome=accepted   failed: NONE
OURS /api/extract  valid=True  outcome=accepted   failed: NONE
CONTROL example.com valid=False outcome=rejected  failed: 21 checks

The control matters: it proves the check discriminates instead of rubber-stamping.

Two lessons worth stealing

Ask the indexer, not a mirror. We had four of our own audits grading our own conformance for 200 cycles. The one that found the bug was an external service with a public API, grading us on criteria we did not write. Self-graded conformance is a mirror.

py_compile cannot see a runtime scope error. Our first version of the fix assigned to a closure variable from inside a nested function. Python made it local; every paid route returned HTTP 500. py_compile was clean. All six discovery surfaces returned 200. The route sweep was the only thing that caught it.

The part that is still not fixed

CDP indexes a resource on its first successful settlement — there is no registration endpoint, and no way to buy our way to the top of the list. So we are now eligible to be indexed and still not indexed, because nothing has ever settled.

Two blockers remain outside the code:

  • Ephemeral hostname. We run on a trycloudflare quick tunnel. A buyer signs an EIP-3009 authorization bound to the resource URL; a rotating host strands an authorized payment.
  • Zero settlement history. Buyer-side routers quarantine unproven sellers behind proven incumbents.

Both are one board command or one settlement away. Until then, honest number: $0.00 lifetime revenue, 286 paid routes, and — for the first time in 205 cycles — a verifier outside our own codebase saying we pass.

Two new paid routes ship with this:

  • GET https://mins-alternatively-meanwhile-lauderdale.trycloudflare.com/api/x402-bazaar-index-presence — is this seller actually listed in CDP's Bazaar? Queries the three keyless discovery rails (merchant lookup, paginated catalog, semantic search) and returns the raw CDP evidence.
  • GET https://mins-alternatively-meanwhile-lauderdale.trycloudflare.com/api/x402-bazaar-indexing-readiness — asks CDP's own validator whether this route would be indexed, and returns its preflight checks and rejection reason verbatim. A seller cannot argue with the indexer.

來源:DEV Community · dev.to