Skip to content

fix(x402): call real x402 2.9 API in process_x402_settle + auto-coerce dict resource_config#8

Merged
vvillait88 merged 4 commits intomainfrom
fix/process-x402-settle-real-x402-api
May 5, 2026
Merged

fix(x402): call real x402 2.9 API in process_x402_settle + auto-coerce dict resource_config#8
vvillait88 merged 4 commits intomainfrom
fix/process-x402-settle-real-x402-api

Conversation

@vvillait88
Copy link
Copy Markdown
Contributor

Summary

`process_x402_settle` had two bugs that only surfaced when store-prod attempted its first real `x402-base-mainnet` settle today:

  1. 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.

  2. 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

  • Auto-coerce dict `resource_config` → `x402.schemas.config.ResourceConfig` with camelCase alias mapping. Falls back to the input on missing peer dep / validation failure.
  • Replace `server.process_payment_request(...)` with the real `server.verify_payment(payload, matched_requirement)`.
  • Accept `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"`.
  • Re-run `build_payment_requirements` after `enrich_extensions` (x402 2.9 takes the extension list at build time, not as a separate verify input).
  • Drop unnecessary `await` on sync calls (`build_payment_requirements`, `enrich_extensions` are sync on x402 2.9).

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

  • `uv run ruff check` + `ruff format` + `ty check` clean
  • `uv run pytest` 715 passed, 3 skipped, coverage 95.41% above 95% bar
  • `_FakeServer` updated to mirror the real surface; step-name assertion updated
  • After merge + tag `v1.3.1` + PyPI publish, bump `core/store/uv.lock` to pick it up; re-run base mainnet smoke

🤖 Generated with Claude Code

vvillait88 and others added 4 commits May 5, 2026 06:08
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>
@vvillait88 vvillait88 merged commit 0acbe39 into main May 5, 2026
7 checks passed
@vvillait88 vvillait88 deleted the fix/process-x402-settle-real-x402-api branch May 5, 2026 13:19
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>
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