fix(x402): coerce dict payload + serialize Pydantic SettleResponse (1.3.3)#10
Merged
vvillait88 merged 1 commit intomainfrom May 5, 2026
Merged
Conversation
….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>
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
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
payloadnot coerced to typedPaymentPayloadverify_x402_requestreturnspayloadas a plain dict (the result ofjson.loads(base64.b64decode(X-Payment))), but x402 2.9'sverify_payment/settle_paymentcallpayload.get_scheme()and other Pydantic-model methods on it. Without coercion, prod store crashed at the verify leg with:New
_coerce_payment_payload()routes byx402Versionfield —1→PaymentPayloadV1(flat top-level shape); anything else →PaymentPayload(v2 nested underaccepted). 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
SettleResponsenot JSON-serializable for the response headerx402 2.9's
settle_paymentreturns aSettleResponsePydantic model. The helper was doing plainjson.dumps(settle_result)to base64-encode the X-Payment-Response header — that raisesTypeError("Object of type SettleResponse is not JSON serializable"). Caught by the surroundingexcept Exception, the helper would mark the settle assettle_failedafter the on-chain settle had already succeeded — payment taken, order shows as failed.New
_settle_result_to_json_bytes()usesmodel_dump_json(by_alias=True)for Pydantic models (so emitted keys match the wire shape —errorReason/errorMessagecamelCase) and falls through tojson.dumpsfor plain dicts (older x402 / test stubs).Verification
Live integration test against the real Coinbase CDP facilitator (with prod CDP creds, mainnet) confirmed:
PaymentPayloadhappens (get_scheme()resolves on the model).verify_paymentis reached with the typed model.AttributeErroranywhere.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
coerces_dict_payload_v2_to_typed_payment_payload— asserts both verify + settle legs see the typedPaymentPayloadandget_scheme()resolves.passes_typed_payment_payload_unchanged— caller-typed instance flows through.serializes_pydantic_settle_result_to_payment_response_header— PydanticSettleResponseround-trips to base64'd JSON witherrorReason(camelCase wire key).serializes_dict_settle_result_for_legacy_stubs— plain-dict settle results still serialize.Test plan
v1.3.3, push tag → trigger PyPI publish.core/store/uv.lockto 1.3.3 + fix store'stx_hashextraction (companion bug — store readssettle_obj.get("transaction")only when isinstance dict; with 1.3.3 the innersettle_resultwill be a Pydantic model, so the dict branch is skipped and tx_hash silently staysNone).200 + orderwith non-nulltx_hash.🤖 Generated with Claude Code