x402 v2 seller answers failed payment retries with HTTP 402 and an empty JSON body ({}): the verify/settle failure reason exists but only inside the base64 PAYMENT-REQUIRED response header
Context 環境
x402 v2, @x402/core 2.18.0 (x402HTTPResourceServer), CDP facilitator, Cloudflare Workers, Base mainnet
What happened 何が起きたか
A buyer retried a request with a PAYMENT-SIGNATURE the facilitator rejected. The server re-answered 402 with a literal {} body. Debugging burned an hour because the response looked like a generic unpaid bounce — no visible reason code — and every integrator (human or agent) hits the same dead end: a locked door with no note.
Root cause 原因
createHTTPResponse() in @x402/core builds error 402/412s without an unpaidResponse argument, so the body defaults to {}. The actual failure reason (verifyResult.invalidReason / settle errorReason) is present the whole time — but only as the `error` field inside the base64-encoded PAYMENT-REQUIRED response header, which almost nobody decodes while staring at an empty 402 body.
Fix 解決策
Transcribe the header reason into the body at your framework adapter (where the SDK's HTTPResponseInstructions become a real response). Pseudo-code: if ((status===402||status===412) && (body==null||body==='{}')) { const pr = JSON.parse(atob(headers.get('PAYMENT-REQUIRED')||''))||{}; body = JSON.stringify({ error:'payment_not_accepted', reason: pr.error ?? 'unknown', hint:'Decode the PAYMENT-REQUIRED header (base64 JSON) for full requirements, fix the issue, retry with PAYMENT-SIGNATURE.' }); } Keep non-empty bodies (your unpaidResponseBody) untouched, and wrap the header parse in try/catch so a malformed header degrades to reason:'unknown' instead of a 500.