Skip to content

Answer an unfundable payment with a 402 + reason instead of a 502 - #7

Open
sudzikcoin wants to merge 3 commits into
fix/discovery-probe-405from
fix/cdp-verify-invalid-verdict
Open

Answer an unfundable payment with a 402 + reason instead of a 502#7
sudzikcoin wants to merge 3 commits into
fix/discovery-probe-405from
fix/cdp-verify-invalid-verdict

Conversation

@sudzikcoin

Copy link
Copy Markdown
Owner

Why

Coinbase CDP answers an unfundable /verify with HTTP 400 carrying the normal verdict body ({"isValid":false,"invalidReason":"invalid_payload","invalidMessage":"contract call failed: … execution reverted"}). httpJson turned every non-2xx into a ProviderError before the adapter could read it, the facilitator answered 502, and the proxy middleware (which only knew the flat errorCode/errorMessage shape, not our nested error:{code,message}) treated the 5xx as transient, retried 3× and handed the buyer a bare "facilitator verify returned HTTP 502".

Measured impact: payer 0x8a1A… (0.000008 USDC) got that 502 every ~4 h from 2026-08-14 (143 attempts), and the Base swap-quote QA wallet failed 105 of 112 verifies the same way. Root-cause write-up: https://api.suverse.io/launchloop/funnel-report-20260907.html

What

84945e6 — CDP 4xx verdict is a verdict, not a provider failure

  • provider-sdk httpJson: opt-in acceptStatus(status) returns a JSON body for the chosen statuses instead of throwing (non-JSON bodies still throw). Default behaviour unchanged.
  • adapter-coinbase-cdp verify(): accepts 4xx; a body with isValid:false is returned as valid:false with the mapped code and CDP's text; any other 4xx body re-throws with the same code/message as before.
  • error-map: invalid_payload → invalid_authorization, refined to insufficient_funds when the message shows the transfer simulation reverted (mapCdpVerifyRejection).
  • Settle path untouched.

f474c8c — middleware reads nested errors, logs their text, never retries a verdict

  • readFacilitatorError() accepts both the flat spec shape and our facilitator's {error:{code,message,details}}.
  • Transient / exhausted / rejected log lines carry the (truncated) provider message.
  • Payment-verdict codes (insufficient_funds, invalid_signature, invalid_authorization, invalid_payload, nonce_already_used, expired_authorization, insufficient_grant, unsupported_scheme, route_unsupported) are never retried and always produce a 402 with the reason, whatever HTTP status wrapped them. invalid_request stays retryable on purpose (Jun-2026 CDP outage class).

Verification

  • pnpm turbo run build --force: 25/25. Changed packages + api: x402-server 52/52, provider-sdk 42/42, adapter-coinbase-cdp 67/67, api 83/83 (13 tests added). The 5 failing apps/proxy tests (carrier-risk-verdict ×4, road-conditions ×1) fail identically on the base commit — pre-existing.
  • Old dist vs new dist with identical mocked CDP replies for a valid payment: adapter output, outbound verify+settle envelopes and the accepted receipt are byte-identical.
  • Deployed 2026-09-07 06:06 UTC. Live: empty wallet → HTTP 402 error:"insufficient_funds" in one facilitator call (was 502 after three); real $0.001 settle from the test wallet → 200, tx 0x7863bb54e156116c77d1f35d7000c78e72b2f7fa5e693d4710642d54de1f7c72. Details: https://api.suverse.io/launchloop/cdp-verify-fix-20260907.html

Note on the base

Stacked on feat/no-wallet-hint (628ba93, live on the proxy since 2026-09-07 07:10 CET) which is not on the remote, so this PR is opened against fix/discovery-probe-405 and shows that one extra commit (apps/proxy/src/handler.ts, extensions.no_wallet) alongside the two commits above. Retarget once that branch is pushed.

🤖 Generated with Claude Code

https://claude.ai/code/session_0155WVV9rzxoxDrGJNWM9BmK

Wallet-less agents hitting a proxy 402 now find extensions.no_wallet —
a literal JSON block pointing at a wallet installer. Purely additive:
accepts, input_schema and the bazaar extension keep their exact shape
and position, the payment-required header mirrors the same body, and
nothing is fetched from or sent to stipend.sh at runtime.
Coinbase CDP answers an unfundable /verify with HTTP 400 and the normal
VerifyResponse body ({"isValid":false,"invalidReason":"invalid_payload",
"invalidMessage":"contract call failed: ... execution reverted"}).
httpJson turned every non-2xx into a ProviderError before the adapter
could read the body, so the facilitator answered 502 and the buyer never
learned the real reason. Payer 0x8a1A… (0.000008 USDC) was answered
that way every four hours from 2026-08-14, and the Base swap-quote QA
wallet failed 105 of 112 verifies the same way.

- provider-sdk httpJson: opt-in `acceptStatus` returns a JSON body for
  the chosen statuses instead of throwing; non-JSON bodies still throw.
- coinbase-cdp verify(): accept 4xx, and when the body is a verdict with
  isValid:false, return valid:false with the mapped code and CDP's text.
  Any other 4xx body re-throws with the same code/message as before.
- error-map: `invalid_payload` → invalid_authorization, refined to
  insufficient_funds when the message shows the transfer simulation
  reverted (mapCdpVerifyRejection).

Settle is untouched.
The middleware only looked for top-level errorCode/errorMessage in a
facilitator error body. The SuVerse facilitator nests them under
`error:{code,message,details}`, so every provider failure reached the
logs and the buyer as a bare "facilitator verify returned HTTP 502" and
was retried three times as if transient.

- readFacilitatorError(): accept both the flat spec shape and the nested
  shape; append the providerId when present.
- Log lines for transient, exhausted and rejected facilitator calls now
  carry the (truncated) message text.
- Payment verdict codes (insufficient_funds, invalid_signature, ...) are
  never retried and always produce a 402 with the reason, whatever HTTP
  status the facilitator wrapped them in.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant