fix(x402): call real x402 2.9 API in process_x402_settle + auto-coerce dict resource_config#8
Merged
vvillait88 merged 4 commits intomainfrom May 5, 2026
Conversation
Two bugs in process_x402_settle uncovered by store's first real x402-base
mainnet smoke today:
1. ``server.process_payment_request(payload, resource_config, resource_meta,
extensions)`` is a fictional method. ``x402.x402ResourceServer`` actually
exposes ``verify_payment(payload, requirements) -> VerifyResponse``. The
call only worked against the in-tree ``_FakeServer`` stub which
coincidentally implemented the made-up name. Real consumers got
``AttributeError("'x402ResourceServer' object has no attribute
'process_payment_request'")`` — surfaced as a generic facilitator_error
with no useful detail.
2. ``server.build_payment_requirements(config)`` accesses ``config.network``
etc. as attributes. Consumers ported from the JS / Hono stack pass plain
dicts with camelCase keys (``payTo``, ``maxTimeoutSeconds``); the dict
raised ``AttributeError("'dict' object has no attribute 'network'")`` at
the build step, before the verify ever ran.
Fix:
- Auto-coerce dict resource_config → ``x402.schemas.config.ResourceConfig``
with camelCase → snake_case alias mapping (``payTo`` → ``pay_to``,
``maxTimeoutSeconds`` → ``max_timeout_seconds``). Falls back to the input
unchanged when the x402 peer dep is missing or validation fails.
- Replace the fictional ``server.process_payment_request(...)`` with the
real ``server.verify_payment(payload, matched_requirement)``.
- Accept either ``is_valid`` (x402 2.9's VerifyResponse field) or ``success``
(older / stub shape) as the verify-result success signal.
- Rename the ``step`` literal ``"process_payment_request"`` →
``"verify_payment"`` to match the real API.
- Re-run ``build_payment_requirements`` after enrich_extensions to fold the
enriched extension list into the build (x402 2.9 takes extensions at build
time, not as a separate verify input).
- Drop the un-awaited sync calls' ``await`` — ``build_payment_requirements``
and ``enrich_extensions`` are sync on x402 2.9.
Tests:
- ``_FakeServer`` updated to match the real surface (sync build/enrich,
async verify_payment/settle_payment with the 2-arg signatures).
- The wrap-test was renamed and its step assertion updated.
- All 35 ``test_lifted_helpers.py`` tests pass; total suite 715 passed with
coverage 95.41% above the 95% bar.
Verified against the locally-installed ``x402==2.9.0`` via runtime
``inspect.signature(...)`` to confirm method names + signatures match.
Bumps to 1.3.1.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two tests for the new auto-coerce path: - camelCase dict (payTo, maxTimeoutSeconds) coerced into typed ResourceConfig - already-typed ResourceConfig passed through unchanged (`is`, not `==`) Locks the consumer contract so a future regression in _coerce_resource_config fails the test suite, not store-prod's first base settle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ment + dict-coerce Quick CLAUDE.md update so future readers don't have to re-derive the API surface of process_x402_settle (real method names + the camelCase dict coercion). 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
## Summary `create_x402_server(facilitator="coinbase")` was passing a bare `x402Facilitator()` (an empty in-process facilitator with no schemes) to `x402ResourceServer`. Pre-existing bug — only surfaced after #8 fixed the dict→`ResourceConfig` coerce in `process_x402_settle`, which then unblocked the path through `build_payment_requirements` where the missing facilitator wiring raised: ``` SchemeNotFoundError("No scheme 'exact' registered for network 'eip155:8453'") ``` The Coinbase x402 facilitator at `api.cdp.coinbase.com` requires a **per-endpoint JWT** signed with the CDP API secret over `(method, host, path)`. The TS sibling `@coinbase/x402` ships that JWT minter (`createCdpAuthHeaders` → `generateJwt` from `@coinbase/cdp-sdk/auth`). There is no Python `coinbase-x402` package on PyPI. The official `docs.cdp.coinbase.com/x402/quickstart-for-sellers#python` snippet implies `HTTPFacilitatorClient(FacilitatorConfig(url=...))` auto-picks up `CDP_API_KEY_ID`/`SECRET` from env — empirically it does not (returns 401 Unauthorized). ## What this PR does - New `coinbase` install extra → pulls in `cdp-sdk>=1.0,<2`. - `create_x402_server(facilitator="coinbase")` now builds an `HTTPFacilitatorClient` pointed at `https://api.cdp.coinbase.com/platform/v2/x402` with a `CreateHeadersAuthProvider` that mints per-endpoint Bearer JWTs via `cdp.auth.utils.jwt.generate_jwt`. Mirrors the `@coinbase/x402` (TS) shape. - Reads `CDP_API_KEY_ID` / `CDP_API_KEY_SECRET` from env, or accepts explicit `cdp_api_key_id` / `cdp_api_key_secret` args. Raises a clear `ValueError` when missing; raises a guiding `ImportError` when `cdp-sdk` isn't installed. - Also fixes the `http` branch to use `HTTPFacilitatorClient` (was also passing a bare `x402Facilitator()`), so the public `x402.org` testnet facilitator actually populates `_supported_responses`. - Updates doc strings to call out that `x402[evm]>=2.9` is the required peer. ## Verification (live, against prod CDP) ``` $ CDP_API_KEY_ID=... CDP_API_KEY_SECRET=... uv run python -c "..." init OK _supported_responses: ['eip155:1', 'polygon', 'arbitrum', 'world', ..., 'eip155:8453', ...] eip155:8453: ['exact', 'upto'] build OK, requirements len: 1 asset: 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 amount: 100000 # $0.10 USDC at 6 decimals ``` ## Tests - New: `test_create_x402_server_coinbase_facilitator_wires_cdp_jwt` (asserts HTTPFacilitatorClient at the CDP URL with auth_provider attached) - New: `test_create_x402_server_coinbase_without_creds_raises` (asserts the missing-creds ValueError) - Full suite: **719 passed, 3 skipped, 95.33% coverage** (above 95% bar). ## 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 stale `server.process_payment_request(request)` reference (replaced by `process_x402_settle(...)` in 1.3.1) and adds `[coinbase]` to the peer-dep install line. ## Test plan - [ ] CI green. - [ ] Tag `v1.3.2`, push tag → trigger PyPI publish via trusted publisher. - [ ] Bump `core/store/uv.lock` to 1.3.2 + add `[coinbase]` extra to `core/store/pyproject.toml`. - [ ] Deploy store prod. - [ ] Re-run T4 base mainnet smoke against agents.agentscore.sh — expect a **200 + order**, not a 503 SchemeNotFoundError. 🤖 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
`process_x402_settle` had two bugs that only surfaced when store-prod attempted its first real `x402-base-mainnet` settle today:
Fictional method name — `server.process_payment_request(payload, resource_config, resource_meta, extensions)` doesn't exist on `x402.x402ResourceServer`. The real API is `verify_payment(payload, requirements) -> VerifyResponse`. The call was only ever exercised against the in-tree `_FakeServer` stub which happened to implement the made-up method.
Plain dict for `resource_config` — `build_payment_requirements` does `config.network` attribute access. JS-ported consumers pass dicts (`payTo`, `maxTimeoutSeconds` camelCase). The dict raised `AttributeError("'dict' object has no attribute 'network'")` at the build step.
Both surfaced today as a generic `phase=facilitator_error` with no underlying detail until store landed extended logging.
Fix
Methods audited against locally-installed `x402==2.9.0`
```
build_payment_requirements: (config: 'Any', extensions: 'list[str] | None' = None) -> 'list[PaymentRequirements]' # sync
enrich_extensions: (declared: 'dict[str, Any]', transport_context: 'Any') -> 'dict[str, Any]' # sync
verify_payment: (payload, requirements, payload_bytes=None, requirements_bytes=None) -> VerifyResponse # async
settle_payment: (payload, requirements, payload_bytes=None, requirements_bytes=None) -> SettleResponse # async
```
All four methods exist and are called with the correct signatures + sync/async disposition.
Test plan
🤖 Generated with Claude Code