Skip to content

fix(x402): coerce dict payload + serialize Pydantic SettleResponse (1.3.3)#10

Merged
vvillait88 merged 1 commit intomainfrom
fix/x402-coerce-payment-payload
May 5, 2026
Merged

fix(x402): coerce dict payload + serialize Pydantic SettleResponse (1.3.3)#10
vvillait88 merged 1 commit intomainfrom
fix/x402-coerce-payment-payload

Conversation

@vvillait88
Copy link
Copy Markdown
Contributor

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 — 1PaymentPayloadV1 (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

….3.3)

Two sibling bugs in process_x402_settle's verify→settle path, both
unmasked once 1.3.2 fixed the Coinbase facilitator wiring.

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 with:
       AttributeError("'dict' object has no attribute 'get_scheme'")
   New _coerce_payment_payload() routes by x402Version field:
       1 → PaymentPayloadV1, anything else → PaymentPayload (v2 nested).
   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.

2) Pydantic SettleResponse not JSON-serializable for the response header
   x402 2.9's settle_payment returns a SettleResponse Pydantic model;
   plain json.dumps(settle_result) raises TypeError. Caught by the
   surrounding except Exception, the helper would have reported
   settle_failed AFTER the on-chain settle already succeeded.
   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 in camelCase) and falls through to json.dumps for plain
   dicts (older x402 / test stubs).

Verified locally against the real Coinbase CDP facilitator: dict payload
coerce now reaches verify_payment as typed PaymentPayload (no AttributeError);
CDP receives the request and processes it through schema validation.

Tests added: 4 new
  - coerces_dict_payload_v2_to_typed_payment_payload
  - passes_typed_payment_payload_unchanged
  - serializes_pydantic_settle_result_to_payment_response_header
  - serializes_dict_settle_result_for_legacy_stubs

Full suite: 723 passed, 95.30% coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vvillait88 vvillait88 merged commit 69eaddb into main May 5, 2026
7 checks passed
@vvillait88 vvillait88 deleted the fix/x402-coerce-payment-payload branch May 5, 2026 14:36
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