feat: 2.1.1 — typed errors, bootstrap session helper, stripe $0.50 auto-drop - #55
Merged
Merged
Conversation
Lifts three patterns from the merchant-side template into the SDK so every
gated endpoint gets the bootstrap UX out of the box.
1. Typed errors in stripe-multichain + dispatch helpers
- `pay_to_address.ts`: malformed `Authorization: Payment`, cache-miss
recipient, missing recipient field → CheckoutValidationError(401,
`invalid_credential`, action=`retry_without_credential`).
`pay_to_address` fallback when no preferred/base/tempo recipient lands
in the minted PI → 503 `payment_provider_unavailable`.
- `payment_intent.ts`: Stripe returns an empty `deposit_addresses` map
(account not enrolled in the Stablecoins and Crypto preview) → 503
`payment_provider_unavailable`.
- `payment/dispatch.ts`: unregistered EVM/Solana handler or unrecognized
network family → 503 `payment_provider_unavailable`.
Previously these all surfaced as bare `Error` → 500 to the agent.
2. CheckoutValidationError extraction to its own module
`src/errors.ts` is the new canonical home for the class. checkout.ts,
pay_to_address.ts, payment_intent.ts, payment/dispatch.ts,
identity/policy.ts import directly. `index.ts` re-exports for the
public surface. No re-export through checkout (avoids the cycle).
In Checkout.handle, the wrap around the first
resolveRecipientsForCtx call uses an inline
`err.name === 'CheckoutValidationError'` check rather than `instanceof`
— tsup's per-entry bundles produce separate class identities under
`splitting: false`, so an error thrown from stripe-multichain would
miss an `instanceof` check in checkout.
3. buildVerificationRequiredBody(reason, opts?) helper
Collapses the per-merchant identity_verification_required body mapping
into one call. Returns the canonical 4xx envelope with verify_url /
session_id / poll_secret / poll_url / agent_instructions spread in,
with merchant overrides for message, agentInstructions, and arbitrary
extra fields (goods merchants use the latter for order_id).
4. Checkout auto-defaults createSessionOnMissing
Mirrors python-commerce's behavior. When the merchant doesn't supply
createSessionOnMissing in the gate config, Checkout builds one from
gate.apiKey + gate.baseUrl + gate.context + gate.merchantName.
Merchants that need custom session context or onBeforeSession side
effects still supply their own to override. Standalone agentscoreGate
adapters stay opt-in (read-only gates and similar primitives often
want the bare-denial path).
CLAUDE.md updated to describe the dual-path session auto-mint, the
auto-default, and the new helper. Compliance-merchant example updated
to use the helper. Tests cover the new throws + the predicate behavior.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… export
Adds `bun run knip` to the CI job between typecheck and test. To make
it pass clean, this commit also addresses the three real findings:
- `ComputeFirstMppContext` + `ComputeFirstSettledContext`: legitimate
public hook parameter types (the `composeMppx` + `onSettled` hooks
on `computeFirstCheckout({...})` need consumers to be able to import
them). Added to the public exports from `./checkout_compute_first`.
- `RateLimitDecision`: file-local to `src/middleware/_core.ts` — no
external imports. Drop the `export` modifier so it's not part of the
surface area.
Also drops `@solana/kit` + `@solana/mpp` from `ignoreDependencies` in
knip.json — both are real runtime deps now (used via the Solana MPP
path), not optional.
Matches the same `bun run knip` CI wiring just added to martin / sayer /
people-data-labs.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
CI was passing `bun run test -- --coverage` while local `bun run test`
omitted the flag, so coverage thresholds were never enforced locally.
That's how the 2.1.1 work landed with branches coverage just under 90%
and was only caught on the first CI run.
Two fixes:
- package.json `"test"` → `"vitest run --coverage"` so local + CI
both run with coverage instrumentation + threshold check.
- .github/workflows/ci.yml drops the `-- --coverage` suffix since the
npm script now carries it.
Also adds tests covering the new `buildVerificationRequiredBody` helper
+ the cross-bundle CheckoutValidationError catch in resolveRecipients
so the branches bar (90%) is genuinely held — currently 90.05%.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
buildMppxComposeRails now drops the stripe/charge intent (with a one-time console.warn) when amountUsd < 0.50. Stripe's fixed ~$0.30 fee makes sub-50-cent charges unprofitable - a $0.11 PI nets -$0.19 after fees; many accounts also reject PI creation under the floor with amount_too_small. Callers can pass includeStripe: false explicitly to silence the warning. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The compose-time auto-drop in buildMppxComposeRails landed last commit but the 402 body's accepted_methods + how_to_pay still came from the static buildDefaultCheckoutRails config — so agents saw stripe offered even though there was no matching WWW-Authenticate challenge for it. Move STRIPE_MIN_CHARGE_USD into payment/constants.ts and consume it from BOTH layers: - buildMppxComposeRails (already did): drops the stripe intent from the compose array. - Checkout.emit_402 + computeFirstCheckout._emit_402 (this commit): strip stripe from emit_rails before buildAcceptedMethods runs, so accepted_methods + how_to_pay never advertise a rail mppx won't accept. For variable-price merchants like martin (where one product is below $0.50 and others above), each cart now gets a consistent 402 - the rail appears/disappears with the cart total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- @types/node 25.8.0 -> 25.9.0 - typescript-eslint 8.59.3 -> 8.59.4 bun update only; no major bumps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…fy reason When an mppx rail's verify() throws a non-PaymentError (e.g. a viem RpcRequestError from a Tempo eth_sendRawTransactionSync rejection), mppx replaces the original with a bare VerificationFailedError before emitting payment.failed or returning the 402 — so the agent's response loses the inner reason and gets the generic `payment_proof_invalid: regenerate` body. Capture the inner reason via a console.error interceptor scoped per compose call with AsyncLocalStorage (mppx logs the original error via console.error before swallowing). New helpers: - `runWithMppxFailureCapture(fn)`: wraps a compose call in an async context and returns the captured reason alongside the result. Checkout.handleMppx uses it; no per-merchant compose hook changes. - `classifyMppxFailure(reason)`: pattern-matches known reasons to a typed envelope. First entry: Tempo `keychain validation failed` / `KeyNotFound` -> 401 `tempo_key_not_registered` with recovery hints (run `tempo wallet login` or switch rail). handleMppx now returns the typed envelope when classified; falls back to the existing `payment_proof_invalid` when unrecognized. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vvillait88
added a commit
to agentscore/python-commerce
that referenced
this pull request
May 20, 2026
…to-drop (#53) ## Summary Python parity for the node-commerce 2.1.1 release ([agentscore/node-commerce#55](agentscore/node-commerce#55)). ### 1. Typed errors in stripe-multichain + dispatch helpers - `pay_to_address.py`: malformed `Authorization: Payment`, cache-miss recipient, missing recipient field → `CheckoutValidationError(401, invalid_credential, action=retry_without_credential)`. Fallback when none of preferred/base/tempo recipients land → 503 `payment_provider_unavailable`. - `payment_intent.py`: Stripe returns an empty `deposit_addresses` map → 503 `payment_provider_unavailable`. - `payment/dispatch.py`: unregistered EVM/Solana handler or unrecognized network family → 503 `payment_provider_unavailable`. Previously these all surfaced as bare `ValueError` / `RuntimeError` → 500 to the agent. ### 2. `CheckoutValidationError` extraction to its own module `agentscore_commerce/errors.py` is the new canonical home. `checkout.py`, `checkout_compute_first.py`, `identity/policy.py`, `stripe_multichain/{pay_to_address,payment_intent}.py`, `payment/dispatch.py` import directly. Top-level `__init__.py` re-exports for the public surface. This breaks the `identity.policy` → `checkout` cycle that previously required lazy/local imports. ### 3. `build_verification_required_body(reason, message=?, agent_instructions=?, extra=?)` helper Collapses the per-merchant `identity_verification_required` body mapping into one call. Same shape as the node helper. ### 4. (Already existing — preserved) `Checkout` auto-defaults `create_session_on_missing` From `gate.api_key` + `gate.base_url` + `gate.context` + `gate.merchant_name` when not supplied. ### 5. Stripe `$0.50` USD auto-drop (compose + discovery) Stripe's fixed ~$0.30 fee makes sub-50-cent card charges unprofitable (a $0.11 PI nets -$0.19 after fees); many accounts also reject PI creation under the floor with `amount_too_small`. The SDK now drops the `stripe/charge` rail from BOTH layers when `amount_usd < 0.50`: - `build_mppx_compose_rails` in `agentscore_commerce/payment/compose_rails.py` — drops the `stripe/charge` intent at mppx compose time with a one-time `logging.warning`. Warn-once state lives on a `_WarnedFlags` class (module-level class attribute, lint-clean). - `Checkout._emit_402` in `agentscore_commerce/checkout.py` AND `compute_first_checkout._emit_402` in `agentscore_commerce/checkout_compute_first.py` — strip the `stripe` slot from the rails dict before `build_accepted_methods` / `build_how_to_pay` run, so the 402 body's `accepted_methods` + `agent_instructions.how_to_pay` stay consistent with what pympp will actually accept. Pass `include_stripe=False` to suppress the warning when the merchant knows their pricing tier is permanently sub-50-cent. Shared constant: `agentscore_commerce/payment/constants.py:STRIPE_MIN_CHARGE_USD`. ### 6. In-range deps bumps `lefthook` 2.1.6 → 2.1.8. `uv sync --upgrade --all-extras --all-groups`; no major bumps. ## Version - `2.1.0` → `2.1.1` ## Test plan - [x] `uv run ruff check .` - [x] `uv run ty check agentscore_commerce/` - [x] `uv run pytest tests/` — 1385 pass + 4 skipped, 95.05% coverage (clears 95% bar) - [x] Cross-language parity verified against node-commerce 2.1.1 (same auto-drop behavior at $0.50 boundary, same envelope shape) 🤖 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
Lifts patterns from the merchant-side template into the SDK so every gated endpoint gets the bootstrap UX out of the box, plus prevents an unprofitable Stripe rail from being advertised below the $0.50 USD floor.
1. Typed errors in stripe-multichain + dispatch helpers
pay_to_address.ts: malformedAuthorization: Payment, cache-miss recipient, missing recipient field →CheckoutValidationError(401, invalid_credential, action=retry_without_credential). Fallback when none of preferred/base/tempo recipients land in the minted PI → 503payment_provider_unavailable.payment_intent.ts: Stripe returns an emptydeposit_addressesmap (account not enrolled in the Stablecoins-and-Crypto preview) → 503payment_provider_unavailable.payment/dispatch.ts: unregistered EVM/Solana handler or unrecognized network family → 503payment_provider_unavailable.Previously these all surfaced as bare
Error→ 500 to the agent.2.
CheckoutValidationErrorextraction to its own modulesrc/errors.tsis the new canonical home.checkout.ts,pay_to_address.ts,payment_intent.ts,payment/dispatch.ts,identity/policy.tsimport directly.index.tsre-exports for the public surface — no re-export throughcheckout(avoids the cycle).In
Checkout.handle, the wrap around the firstresolveRecipientsForCtxcall uses an inlineerr.name === 'CheckoutValidationError'check rather thaninstanceof— tsup's per-entry bundles produce separate class identities undersplitting: false.3.
buildVerificationRequiredBody(reason, opts?)helperCollapses the per-merchant
identity_verification_requiredbody mapping into one call. Returns the canonical 4xx envelope withverify_url/session_id/poll_secret/poll_url/agent_instructionsspread in. Merchant overrides formessage,agentInstructions, and arbitraryextrafields (goods merchants use the latter fororder_id).4.
Checkoutauto-defaultscreateSessionOnMissingMirrors python-commerce's behavior. When the merchant doesn't supply
createSessionOnMissingin the gate config,Checkoutbuilds one fromgate.apiKey+gate.baseUrl+gate.context+gate.merchantName. Merchants that need custom session context oronBeforeSessionside effects still supply their own to override. StandaloneagentscoreGateadapters stay opt-in (read-only gates often want the bare-denial path).5. Knip dead-code check wired into CI
Added
bun run knipto the CI job between typecheck and test. To make it pass clean, this PR also addresses the three real findings:ComputeFirstMppContext+ComputeFirstSettledContextadded to public exports (legitimate hook param types);RateLimitDecisionexport dropped (file-local inmiddleware/_core.ts);@solana/kit+@solana/mppremoved fromignoreDependencies(real runtime deps now).6. Stripe
$0.50USD auto-drop (compose + discovery)Stripe's fixed ~$0.30 fee makes sub-50-cent card charges unprofitable (a $0.11 PI nets -$0.19 after fees); many accounts also reject PI creation under the floor with
amount_too_small. The SDK now drops thestripe/chargerail from BOTH layers whenamountUsd < 0.50:buildMppxComposeRailsinsrc/payment/compose_rails.ts— drops thestripe/chargeintent at mppx compose time with a one-timeconsole.warn.Checkout.emit_402insrc/checkout.tsANDcomputeFirstCheckout._emit_402insrc/checkout_compute_first.ts— strip the stripe slot from the rails dict beforebuildAcceptedMethods/buildHowToPayrun, so the 402 body'saccepted_methods+agent_instructions.how_to_paystay consistent with what mppx will actually accept.Pass
includeStripe: falseto suppress the warning when the merchant knows their pricing tier is permanently sub-50-cent. Shared constant:src/payment/constants.ts:STRIPE_MIN_CHARGE_USD.7. Test coverage parity
testnpm script now runsvitest run --coverageso local runs enforce the same global threshold as CI (90% branches / 96% statements). The CI workflow no longer passes-- --coveragesince it's baked in. Caught a missing test on the new auto-drop emit-402 branch.8. In-range deps bumps
@types/node25.8.0 → 25.9.0,typescript-eslint8.59.3 → 8.59.4.bun updateonly; no major bumps.Version
2.1.0→2.1.1Test plan
bun run typecheckbun run lintbun run knip— cleanbun run test— 1395 pass, 96.45/90.02/97.94/97.52 coveragebun run build— clean🤖 Generated with Claude Code