Answer an unfundable payment with a 402 + reason instead of a 502 - #7
Open
sudzikcoin wants to merge 3 commits into
Open
Answer an unfundable payment with a 402 + reason instead of a 502#7sudzikcoin wants to merge 3 commits into
sudzikcoin wants to merge 3 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Coinbase CDP answers an unfundable
/verifywith HTTP 400 carrying the normal verdict body ({"isValid":false,"invalidReason":"invalid_payload","invalidMessage":"contract call failed: … execution reverted"}).httpJsonturned every non-2xx into aProviderErrorbefore the adapter could read it, the facilitator answered 502, and the proxy middleware (which only knew the flaterrorCode/errorMessageshape, not our nestederror:{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.htmlWhat
84945e6 — CDP 4xx verdict is a verdict, not a provider failure
provider-sdkhttpJson: opt-inacceptStatus(status)returns a JSON body for the chosen statuses instead of throwing (non-JSON bodies still throw). Default behaviour unchanged.adapter-coinbase-cdpverify(): accepts 4xx; a body withisValid:falseis returned asvalid:falsewith 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 toinsufficient_fundswhen the message shows the transfer simulation reverted (mapCdpVerifyRejection).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}}.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_requeststays 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 failingapps/proxytests (carrier-risk-verdict ×4, road-conditions ×1) fail identically on the base commit — pre-existing.error:"insufficient_funds"in one facilitator call (was 502 after three); real $0.001 settle from the test wallet → 200, tx0x7863bb54e156116c77d1f35d7000c78e72b2f7fa5e693d4710642d54de1f7c72. Details: https://api.suverse.io/launchloop/cdp-verify-fix-20260907.htmlNote 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 againstfix/discovery-probe-405and 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