agentscore-commerce 1.3.2__tar.gz → 1.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/CLAUDE.md +1 -1
  2. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/PKG-INFO +17 -1
  3. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/README.md +16 -0
  4. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/discovery/probe.py +5 -1
  5. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/__init__.py +8 -0
  6. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/x402_server.py +65 -0
  7. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/x402_settle.py +51 -6
  8. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/examples/api_provider.py +10 -2
  9. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/examples/multi_rail_merchant.py +11 -17
  10. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/pyproject.toml +1 -1
  11. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_discovery.py +5 -1
  12. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_lifted_helpers.py +214 -0
  13. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_payment_servers.py +50 -0
  14. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/uv.lock +1 -1
  15. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/.github/dependabot.yml +0 -0
  19. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/.github/workflows/ci.yml +0 -0
  20. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/.github/workflows/publish.yml +0 -0
  21. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/.github/workflows/security.yml +0 -0
  22. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/.gitignore +0 -0
  23. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/CODE_OF_CONDUCT.md +0 -0
  24. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/CONTRIBUTING.md +0 -0
  25. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/LICENSE +0 -0
  26. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/SECURITY.md +0 -0
  27. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/__init__.py +0 -0
  28. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/api/__init__.py +0 -0
  29. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/__init__.py +0 -0
  30. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/accepted_methods.py +0 -0
  31. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/agent_instructions.py +0 -0
  32. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/agent_memory.py +0 -0
  33. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/body.py +0 -0
  34. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/how_to_pay.py +0 -0
  35. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/identity.py +0 -0
  36. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/order_receipt.py +0 -0
  37. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/pricing.py +0 -0
  38. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/respond_402.py +0 -0
  39. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/challenge/validation_error.py +0 -0
  40. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/discovery/__init__.py +0 -0
  41. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/discovery/bazaar.py +0 -0
  42. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/discovery/llms_txt.py +0 -0
  43. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/discovery/openapi.py +0 -0
  44. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/discovery/robots_tag.py +0 -0
  45. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/discovery/skill_md.py +0 -0
  46. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/discovery/well_known_mpp.py +0 -0
  47. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/discovery/well_known_x402.py +0 -0
  48. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/__init__.py +0 -0
  49. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/_denial.py +0 -0
  50. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/_response.py +0 -0
  51. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/a2a.py +0 -0
  52. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/address.py +0 -0
  53. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/aiohttp.py +0 -0
  54. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/cache.py +0 -0
  55. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/client.py +0 -0
  56. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/django.py +0 -0
  57. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/fastapi.py +0 -0
  58. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/flask.py +0 -0
  59. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/middleware.py +0 -0
  60. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/policy.py +0 -0
  61. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/py.typed +0 -0
  62. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/sanic.py +0 -0
  63. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/sessions.py +0 -0
  64. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/signer.py +0 -0
  65. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/types.py +0 -0
  66. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/identity/ucp.py +0 -0
  67. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/directive.py +0 -0
  68. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/dispatch.py +0 -0
  69. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/headers.py +0 -0
  70. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/idempotency.py +0 -0
  71. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/mppx_server.py +0 -0
  72. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/networks.py +0 -0
  73. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/rails.py +0 -0
  74. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/settlement_override.py +0 -0
  75. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/signer.py +0 -0
  76. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/usdc.py +0 -0
  77. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/wwwauthenticate.py +0 -0
  78. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/x402.py +0 -0
  79. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/payment/x402_validation.py +0 -0
  80. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/stripe_multichain/__init__.py +0 -0
  81. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/stripe_multichain/mppx_stripe.py +0 -0
  82. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/stripe_multichain/payment_intent.py +0 -0
  83. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/stripe_multichain/pi_cache.py +0 -0
  84. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/agentscore_commerce/stripe_multichain/simulate_deposit.py +0 -0
  85. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/examples/README.md +0 -0
  86. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/examples/compliance_merchant.py +0 -0
  87. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/examples/identity_only.py +0 -0
  88. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/examples/per_product_policy_merchant.py +0 -0
  89. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/examples/stripe_multichain_merchant.py +0 -0
  90. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/examples/variable_cost_merchant.py +0 -0
  91. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/lefthook.yml +0 -0
  92. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/ruff.toml +0 -0
  93. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/__init__.py +0 -0
  94. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/conftest.py +0 -0
  95. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_a2a.py +0 -0
  96. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_address.py +0 -0
  97. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_agent_memory_emitter.py +0 -0
  98. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_aiohttp.py +0 -0
  99. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_api_reexport.py +0 -0
  100. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_cache.py +0 -0
  101. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_challenge.py +0 -0
  102. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_client.py +0 -0
  103. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_coverage_fillers.py +0 -0
  104. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_denial.py +0 -0
  105. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_django.py +0 -0
  106. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_fastapi.py +0 -0
  107. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_flask.py +0 -0
  108. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_gate_quota_info.py +0 -0
  109. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_idempotency_helper.py +0 -0
  110. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_integration.py +0 -0
  111. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_middleware.py +0 -0
  112. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_payment_directive.py +0 -0
  113. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_payment_dispatch.py +0 -0
  114. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_payment_headers.py +0 -0
  115. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_payment_misc.py +0 -0
  116. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_payment_signer.py +0 -0
  117. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_policy.py +0 -0
  118. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_pricing.py +0 -0
  119. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_response.py +0 -0
  120. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_robots_tag.py +0 -0
  121. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_sanic.py +0 -0
  122. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_sessions.py +0 -0
  123. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_signer_match.py +0 -0
  124. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_skill_md.py +0 -0
  125. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_stripe_multichain.py +0 -0
  126. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_ucp.py +0 -0
  127. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/tests/test_validation_error.py +0 -0
  128. {agentscore_commerce-1.3.2 → agentscore_commerce-1.3.4}/vulture_whitelist.py +0 -0
@@ -9,7 +9,7 @@ Every helper is extracted from a real consumer, not speculated.
9
9
  | Submodule | What it is |
10
10
  |---|---|
11
11
  | `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware (KYC, age, sanctions, jurisdiction) |
12
- | `agentscore_commerce.payment` | Networks/USDC/rails registries, paymentauth.org directive builders, `create_x402_server` (wraps official `x402[evm]>=2.9` peer dep with v1+v2 dual-register + bazaar extension; for `facilitator="coinbase"` mints per-endpoint CDP JWTs via `cdp-sdk` (install with `coinbase` extra) and points HTTPFacilitatorClient at `api.cdp.coinbase.com/platform/v2/x402` bare `x402Facilitator()` is empty and the CDP docs' env-var-only snippet does NOT auto-auth), `process_x402_settle` (single-call verify+settle wrapper around `x402ResourceServer`'s real 2.9 API: `build_payment_requirements` → `verify_payment` → `settle_payment`; auto-coerces dict `resource_config` with camelCase keys → typed `ResourceConfig`), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6` peer dep with Tempo charge/session + Stripe SPT helpers), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header |
12
+ | `agentscore_commerce.payment` | Networks/USDC/rails registries, paymentauth.org directive builders, `create_x402_server` (wraps `x402[evm]>=2.9` + `cdp-sdk` for `facilitator="coinbase"`; install via the `coinbase` extra), `build_x402_accepts_for_402` (build the 402's `accepts[]` from the registered scheme derives the right `extra.name` per network), `process_x402_settle` (verify+settle in one call), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6`), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header |
13
13
  | `agentscore_commerce.discovery` | Discovery probe, Bazaar wrapper, `/.well-known/mpp.json`, `llms.txt` builder, `skill.md` builder (Claude-Skill-compatible agent-discovery manifest), OpenAPI snippets, `NoindexNonDiscoveryMiddleware` ASGI middleware |
14
14
  | `agentscore_commerce.challenge` | 402-body builders: accepted_methods, identity_metadata, how_to_pay, agent_instructions, build_402_body, `build_validation_error` (4xx body builder) |
15
15
  | `agentscore_commerce.stripe_multichain` | Multichain PaymentIntent helper, deposit-address lookup, testnet simulator, mppx Stripe wrapper |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentscore-commerce
3
- Version: 1.3.2
3
+ Version: 1.3.4
4
4
  Summary: Agent commerce SDK for Python — identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce.
5
5
  Project-URL: Homepage, https://agentscore.sh
6
6
  Project-URL: Repository, https://github.com/agentscore/python-commerce
@@ -291,6 +291,22 @@ await simulate_deposit_if_test_mode(SimulateDepositIfTestModeInput(
291
291
  ))
292
292
  ```
293
293
 
294
+ ## Build the x402 accepts entry for the 402 challenge
295
+
296
+ ```python
297
+ from agentscore_commerce.payment import build_x402_accepts_for_402
298
+
299
+ x402_accepts = build_x402_accepts_for_402(
300
+ x402_server,
301
+ network=X402_BASE,
302
+ price=f"${total_usd}",
303
+ pay_to=os.environ["TREASURY_BASE_RECIPIENT"],
304
+ max_timeout_seconds=300,
305
+ )
306
+ ```
307
+
308
+ Returns a list of plain dicts ready for the 402 body's `accepts[]`. `extra.name` is derived from the registered scheme metadata so the EIP-712 domain matches the on-chain USDC contract.
309
+
294
310
  ## Drop-in 402 + settle (x402)
295
311
 
296
312
  ```python
@@ -247,6 +247,22 @@ await simulate_deposit_if_test_mode(SimulateDepositIfTestModeInput(
247
247
  ))
248
248
  ```
249
249
 
250
+ ## Build the x402 accepts entry for the 402 challenge
251
+
252
+ ```python
253
+ from agentscore_commerce.payment import build_x402_accepts_for_402
254
+
255
+ x402_accepts = build_x402_accepts_for_402(
256
+ x402_server,
257
+ network=X402_BASE,
258
+ price=f"${total_usd}",
259
+ pay_to=os.environ["TREASURY_BASE_RECIPIENT"],
260
+ max_timeout_seconds=300,
261
+ )
262
+ ```
263
+
264
+ Returns a list of plain dicts ready for the 402 body's `accepts[]`. `extra.name` is derived from the registered scheme metadata so the EIP-712 domain matches the on-chain USDC contract.
265
+
250
266
  ## Drop-in 402 + settle (x402)
251
267
 
252
268
  ```python
@@ -39,7 +39,11 @@ def sample_x402_accept_for_network(caip2: str, amount_atomic: str = "1000000") -
39
39
  "asset": USDC.base.mainnet.address,
40
40
  "payTo": _ZERO_EVM_PAYTO,
41
41
  "maxTimeoutSeconds": 300,
42
- "extra": {"name": "USDC", "version": "2"},
42
+ # ``extra.name`` mirrors the on-chain USDC contract's ``name()`` return value
43
+ # because EIP-712 domain hashes include this string. Wrong name → every
44
+ # signed payload fails facilitator verify with ``invalid_exact_evm_payload_signature``.
45
+ # Base mainnet USDC returns "USD Coin"; base sepolia USDC returns "USDC".
46
+ "extra": {"name": "USD Coin", "version": "2"},
43
47
  }
44
48
  if caip2 == networks.base.sepolia.caip2:
45
49
  return {
@@ -52,6 +52,7 @@ from agentscore_commerce.payment.x402_server import (
52
52
  CustomScheme,
53
53
  X402FacilitatorChoice,
54
54
  X402SymbolicRail,
55
+ build_x402_accepts_for_402,
55
56
  create_x402_server,
56
57
  )
57
58
  from agentscore_commerce.payment.x402_settle import (
@@ -61,7 +62,10 @@ from agentscore_commerce.payment.x402_settle import (
61
62
  ProcessX402SettleResult,
62
63
  ProcessX402SettleSuccess,
63
64
  classify_x402_settle_result,
65
+ coerce_payment_payload,
66
+ coerce_resource_config,
64
67
  process_x402_settle,
68
+ settle_result_to_json_bytes,
65
69
  )
66
70
  from agentscore_commerce.payment.x402_validation import (
67
71
  X402_SUPPORTED_BASE_NETWORKS,
@@ -114,7 +118,10 @@ __all__ = [
114
118
  "build_payment_directive",
115
119
  "build_payment_headers",
116
120
  "build_payment_request_blob",
121
+ "build_x402_accepts_for_402",
117
122
  "classify_x402_settle_result",
123
+ "coerce_payment_payload",
124
+ "coerce_resource_config",
118
125
  "create_mppx_server",
119
126
  "create_x402_server",
120
127
  "dispatch_settlement_by_network",
@@ -130,6 +137,7 @@ __all__ = [
130
137
  "rails",
131
138
  "read_x402_payment_header",
132
139
  "register_x402_schemes_v1_v2",
140
+ "settle_result_to_json_bytes",
133
141
  "settlement_override_header",
134
142
  "validate_x402_network_config",
135
143
  "verify_x402_request",
@@ -266,10 +266,75 @@ async def create_x402_server(
266
266
  return server
267
267
 
268
268
 
269
+ def build_x402_accepts_for_402(
270
+ server: Any,
271
+ *,
272
+ network: str,
273
+ price: str,
274
+ pay_to: str,
275
+ scheme: str = "exact",
276
+ max_timeout_seconds: int = 300,
277
+ extensions: list[str] | None = None,
278
+ ) -> list[dict[str, Any]]:
279
+ """Build x402 ``accepts[]`` entries for a 402 challenge body.
280
+
281
+ Wraps ``server.build_payment_requirements(...)`` so merchants don't have to:
282
+
283
+ 1. Import ``x402.schemas.config.ResourceConfig`` themselves
284
+ 2. Remember to call ``model_dump(by_alias=True, mode="json")`` on each Pydantic
285
+ requirement so the surrounding JSON response can serialize it
286
+ 3. Hardcode ``extra`` (which differs by the actual on-chain contract: base mainnet
287
+ USDC has ``name="USD Coin"``, base sepolia USDC has ``name="USDC"`` — EIP-712
288
+ domain hashes differ, so getting this wrong silently breaks every signature
289
+ verify at the facilitator)
290
+
291
+ Returns a list of plain dicts in the shape that x402 expects on the wire — drop
292
+ them straight into the ``accepts`` field of the 402 challenge body.
293
+
294
+ Raises ``Exception`` if the underlying ``build_payment_requirements`` raises;
295
+ callers should wrap with ``try/except`` and either omit x402 from the 402 or
296
+ surface a 5xx (depending on whether other rails are advertised).
297
+ """
298
+ config_cls_module = _import_optional("x402.schemas.config")
299
+ config_cls = getattr(config_cls_module, "ResourceConfig", None) if config_cls_module else None
300
+ if config_cls is None:
301
+ msg = "x402 not installed — run `pip install 'x402[evm,fastapi]>=2.9,<3'` to use build_x402_accepts_for_402."
302
+ raise ImportError(msg)
303
+ config = config_cls(
304
+ scheme=scheme,
305
+ network=network,
306
+ price=price,
307
+ pay_to=pay_to,
308
+ max_timeout_seconds=max_timeout_seconds,
309
+ )
310
+ requirements = (
311
+ server.build_payment_requirements(config, extensions)
312
+ if extensions
313
+ else server.build_payment_requirements(config)
314
+ )
315
+ # Pydantic ``PaymentRequirements`` is the live shape under x402 2.9+. Older
316
+ # versions (and test stubs) return plain dicts that already match the wire form.
317
+ out: list[dict[str, Any]] = []
318
+ for req in requirements:
319
+ model_dump = getattr(req, "model_dump", None)
320
+ if callable(model_dump):
321
+ out.append(model_dump(by_alias=True, mode="json"))
322
+ elif isinstance(req, dict):
323
+ out.append(dict(req))
324
+ else:
325
+ msg = (
326
+ f"build_payment_requirements returned {type(req).__name__}; expected a "
327
+ "Pydantic PaymentRequirements or a dict."
328
+ )
329
+ raise TypeError(msg)
330
+ return out
331
+
332
+
269
333
  __all__ = [
270
334
  "CreateX402ServerOptions",
271
335
  "CustomScheme",
272
336
  "X402FacilitatorChoice",
273
337
  "X402SymbolicRail",
338
+ "build_x402_accepts_for_402",
274
339
  "create_x402_server",
275
340
  ]
@@ -189,7 +189,7 @@ def classify_x402_settle_result(result: ProcessX402SettleResult) -> ClassifiedX4
189
189
  return None
190
190
 
191
191
 
192
- def _coerce_resource_config(config: Any) -> Any:
192
+ def coerce_resource_config(config: Any) -> Any:
193
193
  """Best-effort dict → x402 ``ResourceConfig`` coercion.
194
194
 
195
195
  Consumers ported from the JS / Hono stack often pass a plain dict with the JS-style
@@ -220,10 +220,40 @@ def _coerce_resource_config(config: Any) -> Any:
220
220
  return config
221
221
 
222
222
 
223
+ def coerce_payment_payload(payload: Any) -> Any:
224
+ """Best-effort dict → x402 ``PaymentPayload`` (v1 or v2) coercion.
225
+
226
+ ``verify_x402_request`` returns ``payload`` as a plain dict (the result of
227
+ ``json.loads(base64.b64decode(X-Payment))``), but x402 2.9's
228
+ ``server.verify_payment`` / ``server.settle_payment`` call ``payload.get_scheme()``
229
+ and other typed-model methods on it. Without coercion, the dict raises
230
+ ``AttributeError("'dict' object has no attribute 'get_scheme'")`` on the verify leg.
231
+
232
+ Routes by the ``x402Version`` field: ``1`` → ``PaymentPayloadV1`` (flat shape with
233
+ top-level ``scheme`` / ``network``); anything else → ``PaymentPayload`` (v2 shape
234
+ nested under ``accepted``). Falls back to the original dict on any failure so callers
235
+ that already pass typed instances or unusual shapes still flow through unchanged.
236
+ """
237
+ if not isinstance(payload, dict):
238
+ return payload
239
+ try:
240
+ from x402.schemas import PaymentPayload
241
+ from x402.schemas.v1 import PaymentPayloadV1
242
+ except ImportError:
243
+ return payload
244
+ version = payload.get("x402Version")
245
+ model = PaymentPayloadV1 if version == 1 else PaymentPayload
246
+ try:
247
+ return model.model_validate(payload)
248
+ except Exception:
249
+ return payload
250
+
251
+
223
252
  async def process_x402_settle(input: ProcessX402SettleInput) -> ProcessX402SettleResult:
224
253
  """Run the x402 verify→settle flow and return a tagged outcome."""
225
254
  server = input.x402_server
226
- resource_config = _coerce_resource_config(input.resource_config)
255
+ resource_config = coerce_resource_config(input.resource_config)
256
+ payload = coerce_payment_payload(input.payload)
227
257
 
228
258
  try:
229
259
  built_requirements = server.build_payment_requirements(resource_config)
@@ -267,7 +297,7 @@ async def process_x402_settle(input: ProcessX402SettleInput) -> ProcessX402Settl
267
297
  # — not ``process_payment_request`` (a fictional method that earlier versions of this
268
298
  # helper called and only ever worked against test stubs).
269
299
  try:
270
- verify_result = await server.verify_payment(input.payload, matched_requirement)
300
+ verify_result = await server.verify_payment(payload, matched_requirement)
271
301
  except Exception as err:
272
302
  return ProcessX402SettleFailure(phase="facilitator_error", step="verify_payment", error=err)
273
303
 
@@ -288,11 +318,10 @@ async def process_x402_settle(input: ProcessX402SettleInput) -> ProcessX402Settl
288
318
  return ProcessX402SettleFailure(phase="verify_failed", verify_result=verify_result)
289
319
 
290
320
  try:
291
- settle_result = await server.settle_payment(input.payload, matched_requirement)
321
+ settle_result = await server.settle_payment(payload, matched_requirement)
292
322
  payment_response_header: str | None = None
293
323
  if settle_result is not None:
294
- payload_bytes = json.dumps(settle_result, separators=(",", ":")).encode()
295
- payment_response_header = base64.b64encode(payload_bytes).decode()
324
+ payment_response_header = base64.b64encode(settle_result_to_json_bytes(settle_result)).decode()
296
325
  return ProcessX402SettleSuccess(
297
326
  matched_requirement=matched_requirement,
298
327
  settle_result=settle_result,
@@ -301,3 +330,19 @@ async def process_x402_settle(input: ProcessX402SettleInput) -> ProcessX402Settl
301
330
  )
302
331
  except Exception as err:
303
332
  return ProcessX402SettleFailure(phase="settle_failed", error=err, matched_requirement=matched_requirement)
333
+
334
+
335
+ def settle_result_to_json_bytes(settle_result: Any) -> bytes:
336
+ """Serialize the settle result to a base64-friendly JSON byte string.
337
+
338
+ x402 2.9's ``settle_payment`` returns a Pydantic ``SettleResponse`` model that
339
+ ``json.dumps`` rejects with ``TypeError: Object of type SettleResponse is not
340
+ JSON serializable``. Use ``model_dump_json(by_alias=True)`` for Pydantic models
341
+ (so emitted keys match the wire shape — ``errorReason`` / ``errorMessage`` rather
342
+ than the snake_case attrs) and fall through to ``json.dumps`` for plain dicts
343
+ (used by older x402 / test stubs).
344
+ """
345
+ model_dump_json = getattr(settle_result, "model_dump_json", None)
346
+ if callable(model_dump_json):
347
+ return model_dump_json(by_alias=True).encode()
348
+ return json.dumps(settle_result, separators=(",", ":")).encode()
@@ -128,8 +128,16 @@ async def search(request: Request):
128
128
  "payTo": os.environ["X402_BASE_RECIPIENT"],
129
129
  "maxTimeoutSeconds": 300,
130
130
  # EIP-712 domain required by every x402 EVM client to sign
131
- # EIP-3009 TransferWithAuthorization.
132
- "extra": {"name": "USDC", "version": "2"},
131
+ # EIP-3009 TransferWithAuthorization. ``name`` MUST match the
132
+ # on-chain USDC contract's ``name()`` — base mainnet returns
133
+ # "USD Coin", base sepolia returns "USDC". Wrong value silently
134
+ # breaks signature verify at the facilitator. Production code
135
+ # should use ``build_x402_accepts_for_402(server, ...)`` which
136
+ # derives ``extra`` from the registered scheme metadata.
137
+ "extra": {
138
+ "name": "USD Coin" if X402_BASE_NETWORK.split(":")[-1] == "8453" else "USDC",
139
+ "version": "2",
140
+ },
133
141
  },
134
142
  ]
135
143
  return JSONResponse(
@@ -69,6 +69,7 @@ from agentscore_commerce.payment import (
69
69
  ProcessX402SettleInput,
70
70
  ValidateX402NetworkConfigInput,
71
71
  VerifyX402RequestInput,
72
+ build_x402_accepts_for_402,
72
73
  networks,
73
74
  process_x402_settle,
74
75
  validate_x402_network_config,
@@ -250,24 +251,17 @@ async def purchase(request: Request, assess: dict = Depends(get_assess_data)):
250
251
  ),
251
252
  x402=PaymentRequiredHeaderInput(
252
253
  x402_version=2,
254
+ # Base accept comes from the registered x402 scheme — `extra` (incl. the
255
+ # network-correct USDC `name`) is filled in automatically. Solana goes
256
+ # through MPP `solana/charge` not x402's exact scheme, so it stays inline.
253
257
  accepts=[
254
- {
255
- "scheme": "exact",
256
- "network": X402_BASE_NETWORK,
257
- "amount": str(round(float(total_usd) * 1_000_000)),
258
- # Asset must match the configured network — sepolia + mainnet
259
- # use different USDC contracts.
260
- "asset": (
261
- USDC.base.sepolia.address
262
- if networks.base.sepolia.caip2 == X402_BASE_NETWORK
263
- else USDC.base.mainnet.address
264
- ),
265
- "payTo": deposit_addresses["base"],
266
- "maxTimeoutSeconds": 300,
267
- # EIP-712 domain — required by every x402 EVM client to
268
- # sign EIP-3009 TransferWithAuthorization.
269
- "extra": {"name": "USDC", "version": "2"},
270
- },
258
+ *build_x402_accepts_for_402(
259
+ x402_server,
260
+ network=X402_BASE_NETWORK,
261
+ price=f"${total_usd}",
262
+ pay_to=deposit_addresses["base"],
263
+ max_timeout_seconds=300,
264
+ ),
271
265
  {
272
266
  "scheme": "exact",
273
267
  "network": SOLANA_NETWORK_CAIP2,
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentscore-commerce"
7
- version = "1.3.2"
7
+ version = "1.3.4"
8
8
  description = "Agent commerce SDK for Python — identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -176,7 +176,9 @@ def test_sample_accept_base_mainnet() -> None:
176
176
  assert e is not None
177
177
  assert e["network"] == "eip155:8453"
178
178
  assert e["scheme"] == "exact"
179
- assert e["extra"] == {"name": "USDC", "version": "2"}
179
+ # Base mainnet USDC's contract returns ``name() == "USD Coin"``. ``extra.name``
180
+ # mirrors that for EIP-712 domain-hash parity with what the facilitator validates.
181
+ assert e["extra"] == {"name": "USD Coin", "version": "2"}
180
182
 
181
183
 
182
184
  def test_sample_accept_base_sepolia() -> None:
@@ -185,6 +187,8 @@ def test_sample_accept_base_sepolia() -> None:
185
187
  e = sample_x402_accept_for_network("eip155:84532")
186
188
  assert e is not None
187
189
  assert e["network"] == "eip155:84532"
190
+ # Base sepolia USDC contract returns ``name() == "USDC"`` (differs from mainnet).
191
+ assert e["extra"] == {"name": "USDC", "version": "2"}
188
192
 
189
193
 
190
194
  def test_sample_accept_solana_mainnet() -> None:
@@ -534,6 +534,220 @@ async def test_process_x402_settle_passes_typed_resource_config_unchanged():
534
534
  assert captured["cfg"] is typed
535
535
 
536
536
 
537
+ @pytest.mark.asyncio
538
+ async def test_process_x402_settle_coerces_dict_payload_v2_to_typed_payment_payload():
539
+ """Same fix as resource_config: ``verify_x402_request`` returns ``payload`` as a
540
+ plain dict (the result of ``json.loads(base64.b64decode(X-Payment))``); x402 2.9's
541
+ ``server.verify_payment`` calls ``payload.get_scheme()`` and other typed-model methods
542
+ on it. Coerce dicts → ``PaymentPayload`` (or ``PaymentPayloadV1`` when ``x402Version=1``)
543
+ so the helper doesn't ``AttributeError`` at the verify leg.
544
+ """
545
+ captured: dict = {}
546
+
547
+ class _CapturingServer:
548
+ def build_payment_requirements(self, _cfg: object, _ext: object = None) -> list:
549
+ return [{"id": "req1"}]
550
+
551
+ async def verify_payment(self, payload: object, _req: object) -> dict:
552
+ captured["verify_payload"] = payload
553
+ return {"is_valid": True}
554
+
555
+ async def settle_payment(self, payload: object, _req: object) -> dict:
556
+ captured["settle_payload"] = payload
557
+ return {"tx_hash": "0xabc"}
558
+
559
+ payload_dict = {
560
+ "x402Version": 2,
561
+ "accepted": {
562
+ "scheme": "exact",
563
+ "network": "eip155:8453",
564
+ "amount": "100000",
565
+ "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
566
+ "payTo": "0x000000000000000000000000000000000000dEaD",
567
+ "maxTimeoutSeconds": 300,
568
+ },
569
+ "payload": {
570
+ "authorization": {
571
+ "from": "0xeb2Ca790F72787c7e61bC6c861353a1e4ACDFCa5",
572
+ "to": "0x000000000000000000000000000000000000dEaD",
573
+ "value": "100000",
574
+ "validAfter": 0,
575
+ "validBefore": 9999999999,
576
+ "nonce": "0x" + "aa" * 32,
577
+ },
578
+ "signature": "0x" + "cc" * 65,
579
+ },
580
+ }
581
+ res = await process_x402_settle(
582
+ ProcessX402SettleInput(
583
+ x402_server=_CapturingServer(),
584
+ payload=payload_dict,
585
+ resource_config={
586
+ "scheme": "exact",
587
+ "network": "eip155:8453",
588
+ "price": "$0.10",
589
+ "payTo": "0x" + "00" * 19 + "dE" + "aD",
590
+ "maxTimeoutSeconds": 300,
591
+ },
592
+ resource_meta=_RESOURCE_META,
593
+ )
594
+ )
595
+ assert isinstance(res, ProcessX402SettleSuccess)
596
+ # Both verify and settle legs received the typed Pydantic model, not the raw dict.
597
+ verify_payload = captured["verify_payload"]
598
+ settle_payload = captured["settle_payload"]
599
+ assert type(verify_payload).__name__ == "PaymentPayload"
600
+ assert type(settle_payload).__name__ == "PaymentPayload"
601
+ # The typed model exposes the get_scheme() method that x402's facilitator calls.
602
+ assert hasattr(verify_payload, "get_scheme")
603
+ assert verify_payload.get_scheme() == "exact"
604
+
605
+
606
+ @pytest.mark.asyncio
607
+ async def test_process_x402_settle_serializes_pydantic_settle_result_to_payment_response_header():
608
+ """x402 2.9's ``settle_payment`` returns a Pydantic ``SettleResponse`` model;
609
+ plain ``json.dumps`` rejects it with ``TypeError``. The helper must call
610
+ ``model_dump_json(by_alias=True)`` so the X-Payment-Response header stays
611
+ base64'd JSON with the right wire keys (``errorReason`` not ``error_reason``).
612
+ """
613
+ from x402.schemas import SettleResponse
614
+
615
+ pydantic_settle = SettleResponse(
616
+ success=True,
617
+ transaction="0xabc",
618
+ network="eip155:8453",
619
+ payer="0x000000000000000000000000000000000000dEaD",
620
+ amount=None,
621
+ )
622
+
623
+ class _PydanticSettleServer:
624
+ def build_payment_requirements(self, _cfg: object, _ext: object = None) -> list:
625
+ return [{"id": "req1"}]
626
+
627
+ async def verify_payment(self, _payload: object, _req: object) -> dict:
628
+ return {"is_valid": True}
629
+
630
+ async def settle_payment(self, _payload: object, _req: object) -> SettleResponse:
631
+ return pydantic_settle
632
+
633
+ res = await process_x402_settle(
634
+ ProcessX402SettleInput(
635
+ x402_server=_PydanticSettleServer(),
636
+ payload={},
637
+ resource_config={
638
+ "scheme": "exact",
639
+ "network": "eip155:8453",
640
+ "price": "$0.10",
641
+ "payTo": "0x" + "00" * 19 + "dE" + "aD",
642
+ "maxTimeoutSeconds": 300,
643
+ },
644
+ resource_meta=_RESOURCE_META,
645
+ )
646
+ )
647
+ assert isinstance(res, ProcessX402SettleSuccess)
648
+ assert res.payment_response_header is not None
649
+ decoded = json.loads(base64.b64decode(res.payment_response_header).decode())
650
+ assert decoded["success"] is True
651
+ assert decoded["transaction"] == "0xabc"
652
+ assert decoded["network"] == "eip155:8453"
653
+ # by_alias=True: wire shape uses errorReason / errorMessage (camelCase) — not snake_case.
654
+ assert "errorReason" in decoded
655
+
656
+
657
+ @pytest.mark.asyncio
658
+ async def test_process_x402_settle_serializes_dict_settle_result_for_legacy_stubs():
659
+ """Plain-dict settle results (from older x402 / test stubs) still serialize."""
660
+
661
+ class _DictSettleServer:
662
+ def build_payment_requirements(self, _cfg: object, _ext: object = None) -> list:
663
+ return [{"id": "req1"}]
664
+
665
+ async def verify_payment(self, _payload: object, _req: object) -> dict:
666
+ return {"is_valid": True}
667
+
668
+ async def settle_payment(self, _payload: object, _req: object) -> dict:
669
+ return {"success": True, "transaction": "0xdef"}
670
+
671
+ res = await process_x402_settle(
672
+ ProcessX402SettleInput(
673
+ x402_server=_DictSettleServer(),
674
+ payload={},
675
+ resource_config={
676
+ "scheme": "exact",
677
+ "network": "eip155:8453",
678
+ "price": "$0.10",
679
+ "payTo": "0x" + "00" * 19 + "dE" + "aD",
680
+ "maxTimeoutSeconds": 300,
681
+ },
682
+ resource_meta=_RESOURCE_META,
683
+ )
684
+ )
685
+ assert isinstance(res, ProcessX402SettleSuccess)
686
+ assert res.payment_response_header is not None
687
+ decoded = json.loads(base64.b64decode(res.payment_response_header).decode())
688
+ assert decoded == {"success": True, "transaction": "0xdef"}
689
+
690
+
691
+ @pytest.mark.asyncio
692
+ async def test_process_x402_settle_passes_typed_payment_payload_unchanged():
693
+ """If the caller already provides a typed PaymentPayload, pass it through unchanged."""
694
+ from x402.schemas import PaymentPayload
695
+
696
+ typed = PaymentPayload.model_validate(
697
+ {
698
+ "x402Version": 2,
699
+ "accepted": {
700
+ "scheme": "exact",
701
+ "network": "eip155:8453",
702
+ "amount": "100000",
703
+ "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
704
+ "payTo": "0x000000000000000000000000000000000000dEaD",
705
+ "maxTimeoutSeconds": 300,
706
+ },
707
+ "payload": {
708
+ "authorization": {
709
+ "from": "0xeb2Ca790F72787c7e61bC6c861353a1e4ACDFCa5",
710
+ "to": "0x000000000000000000000000000000000000dEaD",
711
+ "value": "100000",
712
+ "validAfter": 0,
713
+ "validBefore": 9999999999,
714
+ "nonce": "0x" + "aa" * 32,
715
+ },
716
+ "signature": "0x" + "cc" * 65,
717
+ },
718
+ }
719
+ )
720
+ captured: dict = {}
721
+
722
+ class _CapturingServer:
723
+ def build_payment_requirements(self, _cfg: object, _ext: object = None) -> list:
724
+ return [{"id": "req1"}]
725
+
726
+ async def verify_payment(self, payload: object, _req: object) -> dict:
727
+ captured["payload"] = payload
728
+ return {"is_valid": True}
729
+
730
+ async def settle_payment(self, _payload: object, _req: object) -> dict:
731
+ return {"tx_hash": "0xabc"}
732
+
733
+ res = await process_x402_settle(
734
+ ProcessX402SettleInput(
735
+ x402_server=_CapturingServer(),
736
+ payload=typed,
737
+ resource_config={
738
+ "scheme": "exact",
739
+ "network": "eip155:8453",
740
+ "price": "$0.10",
741
+ "payTo": "0x" + "00" * 19 + "dE" + "aD",
742
+ "maxTimeoutSeconds": 300,
743
+ },
744
+ resource_meta=_RESOURCE_META,
745
+ )
746
+ )
747
+ assert isinstance(res, ProcessX402SettleSuccess)
748
+ assert captured["payload"] is typed
749
+
750
+
537
751
  # ─────────────────────────────────────────────────────────────────────────────
538
752
  # process_x402_settle: facilitator_error wrap
539
753
  # ─────────────────────────────────────────────────────────────────────────────
@@ -91,6 +91,56 @@ async def test_create_x402_server_coinbase_without_creds_raises(monkeypatch: pyt
91
91
  await create_x402_server(facilitator="coinbase", rails=["x402-base-mainnet"], initialize=False)
92
92
 
93
93
 
94
+ @pytest.mark.skipif(not _X402_INSTALLED, reason="x402 peer dep not installed")
95
+ def test_build_x402_accepts_for_402_returns_dicts_from_typed_requirements() -> None:
96
+ """``build_x402_accepts_for_402`` wraps ``server.build_payment_requirements`` and
97
+ returns the requirements as wire-shape dicts (via ``model_dump(by_alias=True, mode="json")``)
98
+ so merchants can drop them straight into the 402 response body without importing
99
+ Pydantic types or remembering to serialize.
100
+ """
101
+ from x402.schemas import PaymentRequirements
102
+
103
+ from agentscore_commerce.payment import build_x402_accepts_for_402
104
+
105
+ captured: dict = {}
106
+
107
+ class _CapturingServer:
108
+ def build_payment_requirements(self, config, _ext=None):
109
+ captured["config"] = config
110
+ return [
111
+ PaymentRequirements(
112
+ scheme="exact",
113
+ network="eip155:8453",
114
+ amount="100000",
115
+ asset="0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
116
+ pay_to="0x000000000000000000000000000000000000dEaD",
117
+ max_timeout_seconds=300,
118
+ extra={"name": "USD Coin", "version": "2"},
119
+ )
120
+ ]
121
+
122
+ accepts = build_x402_accepts_for_402(
123
+ _CapturingServer(),
124
+ network="eip155:8453",
125
+ price="$0.10",
126
+ pay_to="0x000000000000000000000000000000000000dEaD",
127
+ )
128
+ # Caller-side: a typed ResourceConfig is passed to build_payment_requirements.
129
+ cfg = captured["config"]
130
+ assert cfg.network == "eip155:8453"
131
+ assert cfg.pay_to == "0x000000000000000000000000000000000000dEaD"
132
+ assert cfg.max_timeout_seconds == 300
133
+ # Returned shape: wire-form dicts, not Pydantic models.
134
+ assert isinstance(accepts, list)
135
+ assert len(accepts) == 1
136
+ assert isinstance(accepts[0], dict)
137
+ assert accepts[0]["network"] == "eip155:8453"
138
+ # Camel-case keys (by_alias=True) — facilitator + clients expect this shape.
139
+ assert accepts[0]["payTo"] == "0x000000000000000000000000000000000000dEaD"
140
+ assert accepts[0]["maxTimeoutSeconds"] == 300
141
+ assert accepts[0]["extra"] == {"name": "USD Coin", "version": "2"}
142
+
143
+
94
144
  @pytest.mark.skipif(not _MPPX_INSTALLED or not _TEMPO_INSTALLED, reason="pympp[tempo] not installed")
95
145
  @pytest.mark.asyncio
96
146
  async def test_create_mppx_server_tempo_returns_mpp_instance() -> None:
@@ -10,7 +10,7 @@ resolution-markers = [
10
10
 
11
11
  [[package]]
12
12
  name = "agentscore-commerce"
13
- version = "1.3.2"
13
+ version = "1.3.4"
14
14
  source = { editable = "." }
15
15
  dependencies = [
16
16
  { name = "agentscore-py" },