fix(x402): wire Coinbase facilitator JWT auth (1.3.2)#9
Merged
vvillait88 merged 1 commit intomainfrom May 5, 2026
Merged
Conversation
…1.3.2)
Pre-existing bug unmasked by 1.3.1: facilitator='coinbase' was passing a
bare x402Facilitator() — an empty in-process facilitator with no schemes —
to x402ResourceServer. Worked through dict→ResourceConfig coercion in 1.3.1
but build_payment_requirements then raised SchemeNotFoundError("No scheme
'exact' registered for network 'eip155:8453'") because _supported_responses
was never populated.
The Coinbase x402 facilitator at api.cdp.coinbase.com requires a per-endpoint
JWT bearer signed with the CDP API secret over (method, host, path). The TS
sibling @coinbase/x402 ships that JWT minter; there is no Python equivalent
package. The docs.cdp.coinbase.com Python snippet implies HTTPFacilitatorClient
auto-picks up CDP_API_KEY_ID/SECRET — it does not (returns 401 Unauthorized).
Fix: use cdp-sdk's generate_jwt to mint per-endpoint Bearer tokens via the
existing CreateHeadersAuthProvider hook on HTTPFacilitatorClient. New
`coinbase` extra pulls in cdp-sdk; create_x402_server reads CDP creds from
env (or new cdp_api_key_id/secret args) and raises a guiding ValueError if
absent. Verified live against prod CDP — eip155:8453 listed under supported
with [exact, upto], $0.10 requirement built correctly.
Also fixes the http branch to use HTTPFacilitatorClient (not bare
x402Facilitator) so the public x402.org testnet facilitator actually
populates _supported_responses for testnet rails.
Tests:
- 6 new x402 server tests (3 already passing + 2 for coinbase wiring + 1 for missing-creds error path)
- Full suite: 719 passed, 95.33% coverage (above the 95% bar)
Doc updates: python-commerce README + CLAUDE.md + Mintlify python-commerce.mdx
+ variable_cost_merchant.py example. Removes stale process_payment_request
reference (replaced by process_x402_settle in 1.3.1).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
vvillait88
added a commit
that referenced
this pull request
May 5, 2026
….3.3) (#10) ## Summary Two sibling bugs in `process_x402_settle`'s verify → settle path, both surfaced once #9 (1.3.2) cleared the facilitator-wiring layer. ### 1) Dict `payload` not coerced to typed `PaymentPayload` `verify_x402_request` returns `payload` as a plain dict (the result of `json.loads(base64.b64decode(X-Payment))`), but x402 2.9's `verify_payment` / `settle_payment` call `payload.get_scheme()` and other Pydantic-model methods on it. Without coercion, prod store crashed at the verify leg with: ``` AttributeError("'dict' object has no attribute 'get_scheme'") ``` New `_coerce_payment_payload()` routes by `x402Version` field — `1` → `PaymentPayloadV1` (flat top-level shape); anything else → `PaymentPayload` (v2 nested under `accepted`). Falls back to the original input on any failure so caller-typed instances pass through unchanged. Both verify **and** settle legs now receive the coerced payload (so the post-verify settle leg also sees a typed model). ### 2) Pydantic `SettleResponse` not JSON-serializable for the response header x402 2.9's `settle_payment` returns a `SettleResponse` Pydantic model. The helper was doing plain `json.dumps(settle_result)` to base64-encode the X-Payment-Response header — that raises `TypeError("Object of type SettleResponse is not JSON serializable")`. Caught by the surrounding `except Exception`, the helper would mark the settle as `settle_failed` **after the on-chain settle had already succeeded** — payment taken, order shows as failed. New `_settle_result_to_json_bytes()` uses `model_dump_json(by_alias=True)` for Pydantic models (so emitted keys match the wire shape — `errorReason` / `errorMessage` camelCase) and falls through to `json.dumps` for plain dicts (older x402 / test stubs). ## Verification Live integration test against the real Coinbase CDP facilitator (with prod CDP creds, mainnet) confirmed: 1. Dict → typed `PaymentPayload` happens (`get_scheme()` resolves on the model). 2. `verify_payment` is reached with the typed model. 3. CDP receives the request, validates through schema, returns a structured 400 (rejected the synth signature) — **no `AttributeError` anywhere**. The CDP rejection at the schema layer (not the type layer) is the strongest signal short of an actual settle. Real `agentscore-pay`-signed payloads conform to the x402V2 schema and will pass. ## Tests - New: `coerces_dict_payload_v2_to_typed_payment_payload` — asserts both verify + settle legs see the typed `PaymentPayload` and `get_scheme()` resolves. - New: `passes_typed_payment_payload_unchanged` — caller-typed instance flows through. - New: `serializes_pydantic_settle_result_to_payment_response_header` — Pydantic `SettleResponse` round-trips to base64'd JSON with `errorReason` (camelCase wire key). - New: `serializes_dict_settle_result_for_legacy_stubs` — plain-dict settle results still serialize. - Full suite: **723 passed, 3 skipped, 95.30% coverage**. ## Test plan - [ ] CI green. - [ ] Tag `v1.3.3`, push tag → trigger PyPI publish. - [ ] Bump `core/store/uv.lock` to 1.3.3 + fix store's `tx_hash` extraction (companion bug — store reads `settle_obj.get("transaction")` only when isinstance dict; with 1.3.3 the inner `settle_result` will be a Pydantic model, so the dict branch is skipped and tx_hash silently stays `None`). - [ ] Deploy store prod. - [ ] T4 base mainnet smoke against agents.agentscore.sh — expect `200 + order` with non-null `tx_hash`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
create_x402_server(facilitator="coinbase")was passing a barex402Facilitator()(an empty in-process facilitator with no schemes) tox402ResourceServer. Pre-existing bug — only surfaced after #8 fixed the dict→ResourceConfigcoerce inprocess_x402_settle, which then unblocked the path throughbuild_payment_requirementswhere the missing facilitator wiring raised:The Coinbase x402 facilitator at
api.cdp.coinbase.comrequires a per-endpoint JWT signed with the CDP API secret over(method, host, path). The TS sibling@coinbase/x402ships that JWT minter (createCdpAuthHeaders→generateJwtfrom@coinbase/cdp-sdk/auth). There is no Pythoncoinbase-x402package on PyPI. The officialdocs.cdp.coinbase.com/x402/quickstart-for-sellers#pythonsnippet impliesHTTPFacilitatorClient(FacilitatorConfig(url=...))auto-picks upCDP_API_KEY_ID/SECRETfrom env — empirically it does not (returns 401 Unauthorized).What this PR does
coinbaseinstall extra → pulls incdp-sdk>=1.0,<2.create_x402_server(facilitator="coinbase")now builds anHTTPFacilitatorClientpointed athttps://api.cdp.coinbase.com/platform/v2/x402with aCreateHeadersAuthProviderthat mints per-endpoint Bearer JWTs viacdp.auth.utils.jwt.generate_jwt. Mirrors the@coinbase/x402(TS) shape.CDP_API_KEY_ID/CDP_API_KEY_SECRETfrom env, or accepts explicitcdp_api_key_id/cdp_api_key_secretargs. Raises a clearValueErrorwhen missing; raises a guidingImportErrorwhencdp-sdkisn't installed.httpbranch to useHTTPFacilitatorClient(was also passing a barex402Facilitator()), so the publicx402.orgtestnet facilitator actually populates_supported_responses.x402[evm]>=2.9is the required peer.Verification (live, against prod CDP)
Tests
test_create_x402_server_coinbase_facilitator_wires_cdp_jwt(asserts HTTPFacilitatorClient at the CDP URL with auth_provider attached)test_create_x402_server_coinbase_without_creds_raises(asserts the missing-creds ValueError)Doc updates
README.md+CLAUDE.md: add[coinbase]extra install snippet + CDP env var note.core/docs/integrations/python-commerce.mdx(Mintlify): same.examples/variable_cost_merchant.py: corrects staleserver.process_payment_request(request)reference (replaced byprocess_x402_settle(...)in 1.3.1) and adds[coinbase]to the peer-dep install line.Test plan
v1.3.2, push tag → trigger PyPI publish via trusted publisher.core/store/uv.lockto 1.3.2 + add[coinbase]extra tocore/store/pyproject.toml.🤖 Generated with Claude Code