feat(checkout): seamless merchant helpers (adapters, hooks, discovery) - #46
Merged
Conversation
Collapses ~190 lines of per-merchant boilerplate into spec-rooted SDK helpers: * Checkout framework adapters: handle_aiohttp / handle_sanic / handle_flask / handle_django (parity with the FastAPI adapter; sync frameworks bridge via asgiref.async_to_sync). validation_envelope + per-framework validation_response_* wrappers for the canonical 4xx body. * Lazy server helpers: lazy_x402_server / lazy_mppx_server return memoized asyncio.Lock-guarded async getters; Checkout auto-derives both from flat cdp_api_key_id / cdp_api_key_secret / mppx_secret_key kwargs. * make_mppx_compose_hook factory: the canonical compose_mppx hook for pympp, including DID-based signer extraction. * pre_validate hook + ctx.state pattern so a module-level Checkout supports per-request validation without surrendering encapsulation. * CheckoutGateConfig wires the AgentScore gate into Checkout; per-request policy is resolved via per_request_policy(ctx). * SettleOutcome normalized (rail_key, signer_address, signer_network, tx_hash). CheckoutContext.identity_status property. * zero_settle_carve_out handles the EIP-3009 value=0 and pympp 'proof' carve-outs. * extract_signer_for_precheck reads x402 OR MPP credentials. * Checkout.accepted_rails + accepted_method_names accessors. * Signed discovery surfaces: build_signed_ucp_response, build_signed_jwks_response, well_known_cors_preflight_headers, bootstrap_ucp_signing_key, default_a2a_services. UCP section 6 cache, CORS, and X-Request-ID handling are folded in. * agentscore_content (PURCHASE_MODE_NOTES, build_agentscore_onboarding_steps, standard_endpoint_descriptions, build_order_success_next_steps) plus build_redemption_skill_md template for the canonical agent-facing copy. * format_usd_cents helper. * RailSpec auto-derivation (X402BaseRailSpec network -> chain_id/token, TempoRailSpec testnet -> network/chain_id/token). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three internal-only references with zero callers, kept alive only by keep-import-name tricks: * ``__all_sdk_timeout__ = SdkTimeoutError`` in identity/core.py: never imported. * ``_ = asdict`` in identity/_denial.py: marked as "re-exported for vendors" but not in __all__ and not assigned to a public name. * ``_ = networks`` in payment/rail_spec.py: import was only kept alive by this line; the module's actual values come from USDC constants. Plus: * Inline ``TokenDeniedError.next_steps`` at its sole internal caller; the attribute was labelled "Legacy accessor" with no external use. * Rename the misleading "legacy stubs" test in test_lifted_helpers.py and reword the matching comment in payment/x402_settle.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the work shipped alongside the consumer migrations: - `Checkout` gate hook extensions (`per_request_policy`, `on_denied`, `run_gate`) and `ctx.capture_wallet` closure for clean settle-time wallet capture without framework-bound helpers. - `Checkout.discovery_extensions` plumbed through `build_402_body` so per-route bazaar (and other x402 body extensions) merge into the challenge body. - `well_known_preflight_response` / `WellKnownPreflightResponse` for the 204 CORS preflight every UCP-publishing merchant otherwise hand-rolls. - `build_merchant_index_json` canonical `/` discovery body builder. - `x_service_info_extension` + `x_payment_info_from_checkout` openapi helpers. - `x_payment_info_extension` emits `authMode: "payment"` + `description`. - `load_solana_fee_payer` accepting base58 (64-byte or 32-byte seed) and 128-char hex; merchants drop the per-merchant Solana keypair parsing boilerplate. - Auto-CDP-toggle in `create_x402_server`: when `CDP_API_KEY_ID` + `CDP_API_KEY_SECRET` env vars are set, defaults to `facilitator="coinbase"`. - `body.extensions` passthrough on `X402PaymentRequired`. - Top-level `agentscore_commerce` re-exports of frequently-used SDK symbols (`AgentScoreCore`, denial helpers, rail-spec types, signer helpers, ...) for flat imports. Tests: 1132 passed, 4 skipped. Includes coverage for `solana.load_solana_fee_payer`, the new `x402_server` paths (auto-CDP toggle, custom schemes, upto rails, dict requirements, JWT minting via cdp-sdk), and the new openapi/discovery helpers. Tightens the test_seamless_helpers.py:400 assertion so CodeQL no longer matches `py/incomplete-url-substring-sanitization` on a markdown content check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
… openapi rails) - pi_cache.py Redis-backed paths via sys.modules['redis.asyncio'] stub + eviction loop via asyncio.sleep monkeypatch. pi_cache.py 71 to 100 percent. - build_signed_ucp_response happy path + multi-rail (solana/stripe/tempo_session) + 503 misconfigured branch. - build_signed_jwks_response with X-Request-ID echo. - bootstrap_ucp_signing_key malformed-env throw + valid-env success. - default_a2a_services + well_known_cors_preflight_headers. - x_payment_info_from_checkout across all four rail types (stripe, base, solana with currency, tempo). openapi.py 86 to 100 percent. Drops an unused _FacilitatorStub ellipsis-body class from the bazaar test (code-quality bot flagged the no-op statement); facilitator is now opaque object(). Tests: 1146 passed, 4 skipped. Total coverage: 91.19 to 93.02 percent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…5% coverage Adds tests covering the previously-uncovered Checkout flows: - gate run_gate escape hatch (allow / deny / invalid-shape branches) - gate per_request_policy returning None (skip-gate path) - gate on_denied callback reshaping the canonical denial body - gate happy path attaches ctx.capture_wallet closure; the closure itself is exercised end-to-end with AgentScoreCore.acapture_wallet mocked - Checkout.accepted_rails dedupes tempo + tempo_session to a single slug - Checkout.accepted_method_names emits each protocol method - zero-settle MPP carve-out at zero (compose_mppx skipped, signer lifted) Also tightens the _spec_rail_key / _spec_method_name return types from RailKey | None / str | None to non-nullable (every code path returns a value; the | None was dead and the key is None checks in the accepted_rails / accepted_method_names accessors were unreachable). Mirrors the same fix landed in node-commerce. Tests: 1156 passed, 4 skipped. Total coverage: 93.02 to 95.00 percent. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vvillait88
added a commit
to agentscore/node-commerce
that referenced
this pull request
May 15, 2026
## Summary Cross-language port of python-commerce #46 ([seamless merchant helpers](agentscore/python-commerce#46)). Collapses ~190 lines of per-merchant boilerplate into spec-rooted SDK helpers. After this PR a goods-merchant `/purchase` route can be a one-liner that calls `Checkout.handleHono(c)`, and a UCP-publishing merchant's `/.well-known/{ucp,jwks.json}` handlers are ~10 lines instead of ~150. **Seamless helpers:** - Checkout framework adapters: `handleHono` / `handleExpress` / `handleFastify` / `handleNextjs` / `handleWeb`. - `validationEnvelope` + per-framework `validationResponse*` wrappers for the canonical 4xx body. - Lazy server helpers: `lazyX402Server` / `lazyMppxServer` return memoized async getters; `Checkout` auto-derives both from flat `cdpApiKeyId` / `cdpApiKeySecret` / `mppxSecretKey` kwargs. - `makeMppxComposeHook` factory: the canonical `composeMppx` hook for mppx. - `preValidate` hook + `ctx.state` pattern so a module-level Checkout supports per-request validation without surrendering encapsulation. - `CheckoutGateConfig` wires the AgentScore gate into Checkout; per-request policy via `perRequestPolicy(ctx)`. - `SettleOutcome` normalized (`railKey`, `signerAddress`, `signerNetwork`, `txHash`). `CheckoutContext.identityStatus`. - `zeroSettleCarveOut` handles the EIP-3009 `value=0` and mppx `proof` carve-outs. - `extractSignerForPrecheck` reads x402 OR MPP credentials. - `Checkout.acceptedRails` + `acceptedMethodNames` accessors. - Signed discovery surfaces: `buildSignedUcpResponse`, `buildSignedJwksResponse`, `wellKnownCorsPreflightHeaders`, `wellKnownPreflightResponse`, `bootstrapUcpSigningKey`, `defaultA2aServices`. UCP section 6 cache, CORS, and `X-Request-ID` handling folded in. - `agentscore_content`: `PURCHASE_MODE_NOTES`, `buildAgentscoreOnboardingSteps`, `standardEndpointDescriptions`, `buildSuccessNextSteps`, `buildMerchantIndexJson`, `buildRedemptionSkillMd`. - `formatUsdCents` helper. **Tier 1+2+3 lifts + integration fixes:** - Gate-hook extensions on `Checkout` (`perRequestPolicy`, `onDenied`, `runGate`) and `ctx.captureWallet` closure for clean settle-time wallet capture without framework-bound helpers. - `Checkout.discoveryExtensions` plumbed through `build402Body` so per-route bazaar merges into the challenge body. - `xServiceInfoExtension` + `xPaymentInfoFromCheckout` openapi helpers. - `xPaymentInfoExtension` emits `authMode: "payment"` + `description`. - `loadSolanaFeePayer` accepting base58 (64-byte or 32-byte seed) and 128-char hex. - Auto-CDP-toggle in `createX402Server`: when `CDP_API_KEY_ID` + `CDP_API_KEY_SECRET` env vars are set, defaults to `facilitator="coinbase"`. - `body.extensions` passthrough on x402 PaymentRequired body. - `processX402Settle` calls `verifyPayment` (was a non-existent method; both verify + settle phases now exercise the right x402/core v2 method). ## Verification - `bun run lint`, `bun run typecheck` clean. - `bun run test` 1055 passed, 4 skipped. - Smoke-tested live against the migrated martin-estate and AgentScore Store: real orders end-to-end on tempo + x402-base + Solana MPP. - Coverage threshold (95/90/95/95) still failing on some pre-existing files and the `buildSignedUcpResponse` happy path (~50 uncovered lines in `well_known.ts`); follow-up. ## Test plan - [x] Local vitest + typecheck green - [x] Two consumers (`martin-estate` + `core/store`) already migrated and smoke-clean 🤖 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
Collapses ~190 lines of per-merchant boilerplate into spec-rooted SDK helpers. After this PR a goods-merchant
/purchaseroute can be a one-liner that callsCheckout.handle_fastapi(request), and a UCP-publishing merchant's/.well-known/{ucp,jwks.json}handlers are ~10 lines instead of ~150.Seamless helpers (commit 1):
handle_aiohttp/handle_sanic/handle_flask/handle_django(parity with the FastAPI adapter; sync frameworks bridge viaasgiref.async_to_sync).validation_envelope+ per-frameworkvalidation_response_*wrappers for the canonical 4xx body.lazy_x402_server/lazy_mppx_serverreturn memoized asyncio.Lock-guarded async getters;Checkoutauto-derives both from flatcdp_api_key_id/cdp_api_key_secret/mppx_secret_keykwargs.make_mppx_compose_hookfactory: the canonicalcompose_mppxhook for pympp, including DID-based signer extraction.pre_validatehook +ctx.statepattern so a module-level Checkout supports per-request validation without surrendering encapsulation.CheckoutGateConfigwires the AgentScore gate into Checkout; per-request policy is resolved viaper_request_policy(ctx).SettleOutcomenormalized (rail_key,signer_address,signer_network,tx_hash).CheckoutContext.identity_statusproperty.zero_settle_carve_outhandles the EIP-3009value=0and pymppproofcarve-outs.extract_signer_for_precheckreads x402 OR MPP credentials.Checkout.accepted_rails+accepted_method_namesaccessors.build_signed_ucp_response,build_signed_jwks_response,well_known_cors_preflight_headers,bootstrap_ucp_signing_key,default_a2a_services. UCP section 6 cache, CORS, andX-Request-IDhandling are folded in.agentscore_content(PURCHASE_MODE_NOTES,build_agentscore_onboarding_steps,standard_endpoint_descriptions,build_success_next_steps) plusbuild_redemption_skill_mdtemplate for canonical agent-facing copy.format_usd_centshelper.X402BaseRailSpecnetwork -> chain_id/token,TempoRailSpectestnet -> network/chain_id/token).Cleanup (commit 2): Drops three internal-only re-export shims (
__all_sdk_timeout__,_ = asdict,_ = networks) and inlines the lone-callerTokenDeniedError.next_stepsaccessor.Tier 1+2+3 lifts + integration fixes (commit 3):
Checkout(per_request_policy,on_denied,run_gate) andctx.capture_walletclosure for clean settle-time wallet capture without framework-bound helpers.Checkout.discovery_extensionsplumbed throughbuild_402_bodyso per-route bazaar (and other x402 body extensions) merge into the challenge body.well_known_preflight_response/WellKnownPreflightResponsefor the 204 CORS preflight every UCP-publishing merchant otherwise hand-rolls.build_merchant_index_jsoncanonical/discovery body builder.x_service_info_extension+x_payment_info_from_checkoutopenapi helpers.x_payment_info_extensionemitsauthMode: "payment"+description.load_solana_fee_payeraccepting base58 (64-byte or 32-byte seed) and 128-char hex.create_x402_server: whenCDP_API_KEY_ID+CDP_API_KEY_SECRETenv vars are set, defaults tofacilitator="coinbase".body.extensionspassthrough onX402PaymentRequired.agentscore_commercere-exports of frequently-used SDK symbols.Smoke-tested live against the migrated AgentScore Store (
core/store) and martin-estate: real orders end-to-end on tempo, x402-base, and Solana, plus signed UCP + JWKS,GET /orders/{id}gate, and every 4xx error envelope.Verification
uv run ruff check,uv run ty check,uv run vultureall pass.uv run pytest1132 passed, 4 skipped.pi_cache.py, parts ofcheckout.pygate-hook paths,well_known.pysigned-profile branches). Follow-up.Test plan
core/store+martin-estate) migrated and smoke-clean against dev🤖 Generated with Claude Code