Skip to content

feat: 2.1.1 — typed errors, bootstrap session helper, stripe $0.50 auto-drop - #55

Merged
vvillait88 merged 7 commits into
mainfrom
feat/2.1.1-typed-errors-bootstrap-helper
May 20, 2026
Merged

feat: 2.1.1 — typed errors, bootstrap session helper, stripe $0.50 auto-drop#55
vvillait88 merged 7 commits into
mainfrom
feat/2.1.1-typed-errors-bootstrap-helper

Conversation

@vvillait88

@vvillait88 vvillait88 commented May 19, 2026

Copy link
Copy Markdown
Contributor

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: 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 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. 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.

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. 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 often want the bare-denial path).

5. Knip dead-code check wired into CI

Added bun run knip to the CI job between typecheck and test. To make it pass clean, this PR also addresses the three real findings: ComputeFirstMppContext + ComputeFirstSettledContext added to public exports (legitimate hook param types); RateLimitDecision export dropped (file-local in middleware/_core.ts); @solana/kit + @solana/mpp removed from ignoreDependencies (real runtime deps now).

6. 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 amountUsd < 0.50:

  • buildMppxComposeRails in src/payment/compose_rails.ts — drops the stripe/charge intent at mppx compose time with a one-time console.warn.
  • Checkout.emit_402 in src/checkout.ts AND computeFirstCheckout._emit_402 in src/checkout_compute_first.ts — strip the stripe slot from the rails dict before buildAcceptedMethods / buildHowToPay run, so the 402 body's accepted_methods + agent_instructions.how_to_pay stay consistent with what mppx will actually accept.

Pass includeStripe: false to 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

test npm script now runs vitest run --coverage so local runs enforce the same global threshold as CI (90% branches / 96% statements). The CI workflow no longer passes -- --coverage since it's baked in. Caught a missing test on the new auto-drop emit-402 branch.

8. In-range deps bumps

@types/node 25.8.0 → 25.9.0, typescript-eslint 8.59.3 → 8.59.4. bun update only; no major bumps.

Version

  • 2.1.02.1.1

Test plan

  • bun run typecheck
  • bun run lint
  • bun run knip — clean
  • bun run test — 1395 pass, 96.45/90.02/97.94/97.52 coverage
  • bun run build — clean
  • Live end-to-end smoke against people-data-labs (real PDL data via Tempo / x402 Base / Solana MPP settles) and martin-estate (Stripe auto-drop verified: stripe absent at test-wine $0.11, present at rose-2022 $51.72)

🤖 Generated with Claude Code

vvillait88 and others added 2 commits May 18, 2026 21:37
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>
vvillait88 and others added 4 commits May 18, 2026 22:50
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>
@vvillait88 vvillait88 changed the title feat: 2.1.1 — typed errors + bootstrap session-mint helper feat: 2.1.1 — typed errors, bootstrap session helper, stripe $0.50 auto-drop May 19, 2026
…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
vvillait88 merged commit fa7414a into main May 20, 2026
6 checks passed
@vvillait88
vvillait88 deleted the feat/2.1.1-typed-errors-bootstrap-helper branch May 20, 2026 00:39
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>
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