Skip to content

feat(checkout): seamless merchant helpers + Tier 1+2+3 SDK lifts - #49

Merged
vvillait88 merged 11 commits into
mainfrom
seamless-checkout-helpers
May 15, 2026
Merged

feat(checkout): seamless merchant helpers + Tier 1+2+3 SDK lifts#49
vvillait88 merged 11 commits into
mainfrom
seamless-checkout-helpers

Conversation

@vvillait88

Copy link
Copy Markdown
Contributor

Summary

Cross-language port of python-commerce #46 (seamless merchant helpers).

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

  • Local vitest + typecheck green
  • Two consumers (martin-estate + core/store) already migrated and smoke-clean

🤖 Generated with Claude Code

Cross-language port of python-commerce #46.

**Seamless helpers:**

- Checkout framework adapters: `handleHono` / `handleExpress` / `handleFastify`
  / `handleNextjs` / `handleWeb` (parity with python's per-framework adapters).
- `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,
  including DID-based signer extraction.
- `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 is resolved via `perRequestPolicy(ctx)`.
- `SettleOutcome` normalized (`railKey`, `signerAddress`, `signerNetwork`,
  `txHash`). `CheckoutContext.identityStatus` property.
- `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 are 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 (#94: fixes martin captureWallet integration).
- `Checkout.discoveryExtensions` plumbed through `build402Body` so per-route
  bazaar (and other x402 body extensions) merge 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; merchants drop the per-merchant Solana keypair parsing boilerplate.
- 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` (#95: was the non-existent
  `processPaymentRequest`; both verify + settle phases now exercise the right
  x402/core v2 method).

Tests: 1055 passed, 4 skipped. Includes coverage for `loadSolanaFeePayer`,
`buildSignedUcpResponse` misconfigured branch + X-Request-ID echo,
`bootstrapUcpSigningKey` malformed-env throw, the new openapi/agentscore_content
helpers, and `wellKnownPreflightResponse`.

Smoke-tested live against the migrated martin-estate and AgentScore Store:
real orders end-to-end on tempo + x402-base + Solana MPP.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread src/checkout.ts Fixed
Comment thread src/checkout.ts Fixed
vvillait88 and others added 10 commits May 14, 2026 22:20
… coverage

Addresses inline review on PR #49:

- `specRailKey` and `specMethodName` return types narrowed from `RailKey | null` /
  `string | null` to non-nullable. Every code path in both helpers returns a value;
  the `| null` was dead and triggered `Comparison between inconvertible types` static
  analysis on `key === null` / `name === null` checks in `Checkout.acceptedRails`
  + `Checkout.acceptedMethodNames` (which were therefore unreachable).
- Adds tests covering `buildSignedUcpResponse` happy path (single + multi-rail +
  empty-string sentinel recipient), `buildSignedJwksResponse`, `bootstrapUcpSigningKey`
  with a real `UCP_SIGNING_KEY_JWK_PRIVATE` env, plus extras for `loadSolanaFeePayer`
  + `wellKnownPreflightResponse` Headers-instance input.

Tests: 1063 passed, 4 skipped. Coverage: 89.62/81.61/94.84/90.71 (up from
88.13/80.54/93.63/89.34). Threshold (95/90/95/95) still failing on `checkout.ts`
gate-hook paths; follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssor/zero-settle coverage

Two behaviors users hit during gate-config testing:

- `gate.runGate` returning `undefined` (the common case for a no-op pass-through)
  previously left `denial.status` undefined and the handler crashed with
  `Cannot read properties of undefined (reading 'status')`. Now normalize:
  `undefined` or `null` → allow; any other shape that isn't an object with a
  numeric `status` throws TypeError (parity with python's `_coerce_run_gate_result`).

New tests cover:

- `Checkout.acceptedRails` dedupes tempo + tempo_session → single `tempo_mpp`.
- `Checkout.acceptedMethodNames` emits each protocol slug.
- `gate.runGate` allow / deny / invalid-shape branches.
- `gate.perRequestPolicy` returning null skips the gate.
- `zeroSettleCarveOut=true` at $0 MPP path: skips compose, lifts signer.

Coverage: 89.62/81.61/94.84/90.71 → 91.21/83.49/95.15/92.00 (functions
threshold now met; statements/branches/lines pending).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ettle coverage

Covers Checkout settle paths that were previously only exercised by
processX402Settle unit tests:

- handleX402 verify_failed branch (verifyPayment returns success:false)
- handleX402 facilitator error → 503 payment_provider_unavailable
- handleMppx 200 path: composeMppx returns success, signer + txHash propagate to onSettled

Coverage: 91.21/83.49/95.15/92.00 → 91.93/84.42/95.45/92.78.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- preValidate populating ctx.state, throwing CheckoutValidationError, and
  rethrowing non-validation errors.
- handleHono / handleNextjs / handleWeb / handleExpress / handleFastify
  emitting 200 with the on_settled body on the settle leg (only the
  402 discovery leg was previously tested).

Coverage: 91.93/84.42/95.45/92.78 → 92.74/84.89/96.06/93.56.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- SDK gate allow → ctx.captureWallet wired with operator-token; closure call
  reaches AgentScoreCore.captureWallet through the gate-installed closure.
- SDK gate deny → 403/401/503 with denialReasonToBody envelope; reason code
  surfaces in error.code on the body.
- SDK gate onDenied callback can reshape the canonical body and return a
  custom { status, body, headers } envelope.

Coverage: 92.74/84.89/96.06/93.56 → 93.88/86.71/96.96/94.82.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…requires_wallet_signing)

The post-allow signer-match enforcement branch in the SDK gate converts non-pass
signer_match verdicts into wallet_signer_mismatch (for cross-operator) and
wallet_auth_requires_wallet_signing (for non-signing wallet auth) denials.

Coverage: 93.88/86.71/96.96/94.82 → 94.18/87.26/96.96/95.17 (lines green,
statements + branches still pending).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…2 server

- Checkout auto-derives composeMppx from mppxSecretKey (lazy.lazyMppxServer +
  makeMppxComposeHook wiring).
- 402 emit with x402Server configured: buildPaymentRequirements happy path
  + the catch-fallback when scheme registration fails.
- getIdentityStatus: anonymous/verified/unverified branches.

Coverage: 94.18/87.26/96.96/95.17 → 95.11/88.19/97.57/96.14. Statements,
lines, and functions thresholds now met. Branches still at 88.19 vs 90.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arding-steps variations

- makeMppxComposeHook with did:pkh:solana credential → signerNetwork=solana
- non-did credential.source → signer null (compose still returns 200)
- receipt.reference unset → falls back to receipt.transaction for txHash
- buildAgentscoreOnboardingSteps api+stripe / goods+stripe / api-no-stripe branches
- buildSuccessNextSteps omits order_status_url + fulfillment_eta when unset
- buildSignedJwksResponse with Headers instance request (vs plain object)

Coverage: 95.11/88.19/97.57/96.14 → 95.33/88.74/97.57/96.34.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…recipient rail

Brings all four coverage thresholds green (95/90/95/95):

- zero-settle x402-base carve-out: $0 amount with valid x402 header lifts signer
  from EIP-3009 from-field; invalid base64 still completes the carve-out.
- handleX402 fallback: when verified.payload lacks .authorization.from, signer
  falls back to settleResult.payer (no lowercase conversion).
- discoveryExtensions on 402 body: bazaar extension propagates verbatim;
  empty discoveryExtensions object skips the extensions field entirely.
- Express + Fastify adapters with array-valued headers (multi-value normalization)
  and 400 invalid_body branch.
- mintRecipients drops rails with empty-string recipients (per-order-mint pattern).
- CheckoutValidationError defaults (status=400, action=fix_request) when unset.

Coverage: 95.33/88.74/97.57/96.34 → 96.13/90.01/97.57/97.07 (all four
thresholds passing).

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
vvillait88 merged commit 0146938 into main May 15, 2026
6 checks passed
@vvillait88
vvillait88 deleted the seamless-checkout-helpers branch May 15, 2026 13:18
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