Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 13 additions & 13 deletions CLAUDE.md

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ npm install hono mppx @x402/core @x402/evm @solana/mpp @solana/kit stripe # wh
| `/stripe-multichain` | `createMultichainPaymentIntent` (returns `{ paymentIntentId, depositAddresses }`, read `depositAddresses[network]` directly), `createPayToAddressFromStripePI({request, amountCents, stripe, piCache, networks?, staticRecipients?, metadata?, orderId?, preferredNetwork?})`, per-order payTo resolver: on the settle leg, reuses the buyer's signed-against payTo from the MPP credential (after `piCache.hasAddress` check OR a `staticRecipients` match, the static address is always-accepted because the merchant owns it); on the discovery leg, mints a fresh PI for the rails NOT covered by `staticRecipients`, caches the merged map, registers static addresses with `piCache.cacheAddress` so verify-leg lookups pass. `mintMultichainRecipients({...same opts})`, structured variant that returns `{ recipients: Record<string,string>, paymentIntentId?, reusedFromCredential }`; prefer this when your `mintRecipients` hook needs the full per-rail map (typical multi-rail merchant). Solana MPP requires a static recipient: pass `staticRecipients: { solana: '<wallet>' }` and pre-fund its USDC ATA once from any external wallet. Rotating (per-order, Stripe-minted) Solana recipients do not settle, since the settlement transaction does not create the recipient's token account; the SDK logs a warning when one is minted. The static recipient is reused forever, each settle pays only the ~$0.001 per-tx fee, and this also sidesteps the per-call ATA rent (~$0.50 against MPP spec §13.6). `simulateCryptoDeposit`, `createMppxStripe`; `createPiCache` (TTL'd PI / deposit-address cache, Redis-backed when `redisUrl` set, in-memory otherwise), `simulateDepositIfTestMode` (gates on `sk_test_` and looks up the PI for you), `STRIPE_TEST_TX_HASH_SUCCESS` / `STRIPE_TEST_TX_HASH_FAILED` constants. Peer dep on `stripe`. |
| `/api` | Everything from `@agent-score/sdk` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `isAgentScoreTestAddress`. **Don't add `@agent-score/sdk` as a separate dep**; the two can drift versions and cause subtle type mismatches. |
| `/middleware/{hono,express,fastify,nextjs,web}` | Framework-specific rate-limit middleware. Hono / Express / Fastify expose middleware factories (`rateLimitHono`, `rateLimitExpress`, `rateLimitFastify`); Next.js exposes `withRateLimit(opts, handler)`; Web Fetch exposes `createRateLimit(opts) => guard(req)`. Shared options: `windowSeconds` (default 60), `maxRequests` (default 60), `keyResolver` (default first hop of `x-forwarded-for`), `redisUrl` (lazy-imports `ioredis` when set, in-memory `Map` fallback otherwise), `keyPrefix`. `ioredis` is an optional peer dep, install it only if you want distributed limiting. |
| `@agent-score/commerce` (top-level) | `Checkout` orchestrator + `CheckoutContext` + `CheckoutGateConfig` + `CheckoutValidationError` + `DiscoveryProbeConfig` + `MountUcpRoutesOptions` + `SettleOutcome` + `MppxComposeOutcome` + `PricingResult` (the 2.0 high-level surface: one config object, hooks for preValidate/computePricing/onSettled/mintRecipients/composeMppx, auto-derived x402+mppx servers, per-framework adapters `handleHono`/`handleExpress`/`handleFastify`/`handleNextjs`/`handleWeb`, signed UCP routes via `mountUcpRoutes{Hono,Express,Fastify}`); `pricingResult` (factory: cents-denominated → typed `PricingResult` with embedded `PricingBlock`); `validationResponse{Hono,Express,Fastify,Nextjs,Web}` (per-framework 4xx envelope wrappers); `loadUCPSigningKeyFromEnv` (cached env-driven loader for the UCP signing key, reads `UCP_SIGNING_KEY_JWK_PRIVATE` JSON JWK, detects alg from shape, falls back to ephemeral when unset, sanitizes errors so key bytes never reach logs, concurrent-safe via Promise-pinned cache); `hashOperatorToken` (sha256 hex of plaintext `opc_...`, for merchants persisting `operator_token_id` to their own DB without ever storing the plaintext); `extractOwnerScope(headers) → { walletAddress?, operatorTokenHash? }` (canonical owner-identity extractor for caller-scoped resource queries, reads `X-Wallet-Address` / `X-Operator-Token`, hashes the token so plaintext never leaves the request); `hasPaymentHeader` / `hasX402Header` / `hasMppxHeader` (request discriminators, any-credential vs x402 vs MPP); `malformedPaymentCredential` (wire-shape gate: payment headers that are not base64 JSON and not token-shaped are rejected before merchant hooks run; `Checkout` applies it by default, opt out with `credentialPreCheck: false`); `createResultCache` (neutral keyed JSON-value cache with a stable body-hash key builder; cache a probe-leg result, e.g. a paid upstream call made in `preValidate`, and replay it on the settle leg) plus the compute-first-shaped `createQuoteCache` built on it; `defaultReadOnlyOnDenied(reason)` (canonical `onDenied` for read-only resource gates: 401 + `Cache-Control: no-store` while still spreading `denialReasonToBody`); plus the cross-vendor publishers `buildA2AAgentCard` + `buildUCPProfile` (return unsigned payloads: vendor signs + publishes). |
| `@agent-score/commerce` (top-level) | `Checkout` orchestrator + `CheckoutContext` + `CheckoutGateConfig` + `CheckoutValidationError` + `DiscoveryProbeConfig` + `MountUcpRoutesOptions` + `SettleOutcome` + `MppxComposeOutcome` + `PricingResult` (one config object, hooks for preValidate/computePricing/onSettled/mintRecipients/composeMppx, auto-derived x402+mppx servers, per-framework adapters `handleHono`/`handleExpress`/`handleFastify`/`handleNextjs`/`handleWeb`, signed UCP routes via `mountUcpRoutes{Hono,Express,Fastify}`); `pricingResult` (factory: cents-denominated → typed `PricingResult` with embedded `PricingBlock`); `validationResponse{Hono,Express,Fastify,Nextjs,Web}` (per-framework 4xx envelope wrappers); `loadUCPSigningKeyFromEnv` (cached env-driven loader for the UCP signing key, reads `UCP_SIGNING_KEY_JWK_PRIVATE` JSON JWK, detects alg from shape, falls back to ephemeral when unset, sanitizes errors so key bytes never reach logs, concurrent-safe via Promise-pinned cache); `hashOperatorToken` (sha256 hex of plaintext `opc_...`, for merchants persisting `operator_token_id` to their own DB without ever storing the plaintext); `extractOwnerScope(headers) → { walletAddress?, operatorTokenHash? }` (canonical owner-identity extractor for caller-scoped resource queries, reads `X-Wallet-Address` / `X-Operator-Token`, hashes the token so plaintext never leaves the request); `hasPaymentHeader` / `hasX402Header` / `hasMppxHeader` (request discriminators, any-credential vs x402 vs MPP); `malformedPaymentCredential` (wire-shape gate: payment headers that are not base64 JSON and not token-shaped are rejected before merchant hooks run; `Checkout` applies it by default, opt out with `credentialPreCheck: false`); `createResultCache` (neutral keyed JSON-value cache with a stable body-hash key builder; cache a probe-leg result, e.g. a paid upstream call made in `preValidate`, and replay it on the settle leg) plus the compute-first-shaped `createQuoteCache` built on it; `defaultReadOnlyOnDenied(reason)` (canonical `onDenied` for read-only resource gates: 401 + `Cache-Control: no-store` while still spreading `denialReasonToBody`); plus the cross-vendor publishers `buildA2AAgentCard` + `buildUCPProfile` (return unsigned payloads: vendor signs + publishes). |

## Quick start

Expand Down Expand Up @@ -96,7 +96,7 @@ app.post("/purchase", async (c) => {
});
```

### Checkout orchestrator (the 2.0 high-level surface)
### Checkout orchestrator

`Checkout` is the canonical merchant surface for fixed-price one-shot endpoints: one config object, hooks for the merchant-specific pieces, and the SDK handles 402 emit, identity gating, x402 verify+settle, mppx compose, $0 carve-out, and the per-framework adapter. Most fixed-price merchants reach for `Checkout` first.

Expand Down Expand Up @@ -279,12 +279,12 @@ return new Response(JSON.stringify(responseBody), { status: 402, headers });
```typescript
import { buildA2AAgentCard, buildUCPProfile, ucpA2AExtension } from "@agent-score/commerce";

// Google A2A v1.0 Signed Agent Card; publish at /.well-known/agent-card.json.
// A2A v1.0 Agent Card; publish at /.well-known/agent-card.json.
// Per UCP §A2A binding, the card MUST declare the canonical UCP extension URI in
// `capabilities.extensions[]`; pass `ucpA2AExtension()` with empty capabilities
// until you bind formal UCP capabilities (dev.ucp.shopping.checkout, etc.).
// Skills are top-level AgentSkill objects; identity claims live in a separate
// AgentCardSignature (RFC 7515 JWS) wrapping the serialized card.
// Skills are top-level AgentSkill objects. The card is returned unsigned: to sign it,
// compute RFC 7515 JWS entries over the card body and pass them as `signatures`.
const card = buildA2AAgentCard({
name,
description,
Expand All @@ -297,7 +297,7 @@ const card = buildA2AAgentCard({
// Google Universal Commerce Protocol; publish at /.well-known/ucp
// Output shape: { ucp: { version, services, capabilities, payment_handlers,
// name?, supported_versions? }, keys: [...], signature?: "..." }
//, services / capabilities / payment_handlers are MAPS keyed by reverse-DNS
// services / capabilities / payment_handlers are MAPS keyed by reverse-DNS
// service / capability / handler name (UCP spec §3 + §6).
const profile = buildUCPProfile({
name,
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ We will acknowledge receipt within 48 hours and aim to release a fix within 7 da

| Version | Supported |
|---------|-----------|
| 2.x | ✅ |
| < 2.0 | ❌ |
| 3.x | ✅ |
| < 3.0 | ❌ |
6 changes: 3 additions & 3 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ Runnable, copy-pasteable example integrations covering the most common merchant
| [`api-provider.ts`](./api-provider.ts) | API provider (Exa-style) | Per-call billing on multiple rails: Tempo MPP + x402 Base + Solana MPP, all driven by `Checkout`. No identity gate, no compliance; pay-or-fail. Demos `Checkout(discoveryProbe: ...)` for x402-crawler auto-routing, `buildMerchantIndexJson` + `standardEndpointDescriptions({ kind: 'api' })` for `GET /` discovery, and `buildRedemptionSkillMd` with the trial-credit body shape on `GET /redemption.md`. |
| [`identity-only.ts`](./identity-only.ts) | Compliance gate without payment | Wraps any endpoint with KYC + age + jurisdiction checks; vendor handles their own payment |
| [`multi-rail-merchant.ts`](./multi-rail-merchant.ts) | Full agent-commerce merchant | Identity gate + Tempo MPP + x402 Base + Solana MPP + Stripe SPT, all rails accepted via `Checkout`. Demos `pricingResult` (cents → typed PricingResult), `Receipt` + `buildSuccessNextSteps` in `onSettled`, per-order Stripe-multichain deposit minting via `mintRecipients`, and `simulateDepositIfTestMode` for Stripe testnet round-trips. |
| [`stripe-multichain-merchant.ts`](./stripe-multichain-merchant.ts) | Stripe-anchored multi-chain | Stripe PaymentIntent with deposit_options for tempo/base/solana; crypto deposits flow through Stripe. Read `result.depositAddresses[network]` directly. For low-margin endpoints (sub-dollar APIs), use `createPayToAddressFromStripePI` / `mintMultichainRecipients` with `staticRecipients: { solana }` — see the `/stripe-multichain` row in the main `README.md` for the full pattern and economics. |
| [`compute-first-merchant.ts`](./compute-first-merchant.ts) | Pay-per-result variable cost (LLM, transcode, per-token/byte) | The probe leg runs the work server-side, caches the result by body content-hash, and emits a 402 at the **exact** computed price; the retry pays that price and gets the cached result. Exact-mode rails only (x402-exact Base, plus Tempo/Solana/Stripe SPT via a `composeMppx` callback) — deliberately scoped out of x402-upto (Permit2) and Settlement-Overrides. Uses `computeFirstCheckout` + `createQuoteCache`; pairs with `rateLimitHono` since the probe leg runs work pre-payment. |
| [`stripe-multichain-merchant.ts`](./stripe-multichain-merchant.ts) | Stripe-anchored multi-chain | Stripe PaymentIntent with deposit_options for tempo/base/solana; crypto deposits flow through Stripe. Read `result.depositAddresses[network]` directly. For low-margin endpoints (sub-dollar APIs), use `createPayToAddressFromStripePI` / `mintMultichainRecipients` with `staticRecipients: { solana }`: see the `/stripe-multichain` row in the main `README.md` for the full pattern and economics. |
| [`compute-first-merchant.ts`](./compute-first-merchant.ts) | Pay-per-result variable cost (LLM, transcode, per-token/byte) | The probe leg runs the work server-side, caches the result by body content-hash, and emits a 402 at the **exact** computed price; the retry pays that price and gets the cached result. Exact-mode rails only (x402-exact Base, plus Tempo/Solana/Stripe SPT via a `composeMppx` callback): deliberately scoped out of x402-upto (Permit2) and Settlement-Overrides. Uses `computeFirstCheckout` (its cache is `createQuoteCache`); pairs with `rateLimitHono` since the probe leg runs work pre-payment. |
| [`compliance-merchant.ts`](./compliance-merchant.ts) | Regulated-goods merchant (wine, cannabis, etc.) | Full compliance gate (KYC + sanctions + age + jurisdiction) + custom `onDenied` composing commerce helpers: `verificationAgentInstructions`, `isFixableDenial`, `buildContactSupportNextSteps`, `denialReasonToBody`/`denialReasonStatus`, `buildSignerMismatchBody`. Shows how vendors write only the business-specific branches and let commerce handle the rest. |
| [`per-product-policy-merchant.ts`](./per-product-policy-merchant.ts) | Multi-product merchant with mixed compliance needs | One product hard-gates KYC + 21 + US-state allowlist (wine), one is anonymous (merch, ships anywhere), a third uses `enforcement: 'soft'` to request KYC as a fraud signal but accept anonymous sales (stamps `identity_status: 'unverified'` on the order). Uses `PolicyBlock`, `buildGateFromPolicy`, `runGateWithEnforcement`, and the one-call `validateShippingAgainstPolicy`. |
| [`signed-ucp-merchant.ts`](./signed-ucp-merchant.ts) | Signed UCP profile + JWKS endpoint | One-call mount via `checkout.mountUcpRoutesHono(app, ...)` registers `/.well-known/ucp` + `/.well-known/jwks.json` + the OPTIONS preflights. AgentScore's `agentscore-profile+jws` is a vendor extension on top of UCP for trust-mode verifiers (regulated-commerce, AP2-aware) that opt into auditable profiles; UCP §6 itself does NOT mandate signing — production UCP merchants commonly ship unsigned. This example shows ephemeral-for-dev / env-JWK-for-prod and `bootstrapUcpSigningKey` lifespan-hook usage. |
| [`signed-ucp-merchant.ts`](./signed-ucp-merchant.ts) | Signed UCP profile + JWKS endpoint | One-call mount via `checkout.mountUcpRoutesHono(app, ...)` registers `/.well-known/ucp` + `/.well-known/jwks.json` + the OPTIONS preflights. AgentScore's `agentscore-profile+jws` is a vendor extension on top of UCP for trust-mode verifiers (regulated-commerce, AP2-aware) that opt into auditable profiles; UCP §6 itself does NOT mandate signing: production UCP merchants commonly ship unsigned. This example shows ephemeral-for-dev / env-JWK-for-prod and `bootstrapUcpSigningKey` lifespan-hook usage. |

## How to use

Expand Down
2 changes: 1 addition & 1 deletion examples/api-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*
* Peer deps:
* bun add @agent-score/commerce hono mppx @x402/core @x402/evm @solana/mpp @solana/kit
* # @coinbase/x402 optional — only if you want the Coinbase CDP facilitator
* # @coinbase/x402 optional: only if you want the Coinbase CDP facilitator
*
* Env vars:
* TEMPO_RECIPIENT your Tempo wallet for receiving USDC.e
Expand Down
2 changes: 1 addition & 1 deletion examples/compliance-merchant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
* bun add @agent-score/commerce hono
*
* Env vars:
* AGENTSCORE_API_KEY — your AgentScore API key
* AGENTSCORE_API_KEY: your AgentScore API key
*
* Run: bun run examples/compliance-merchant.ts
*/
Expand Down
8 changes: 4 additions & 4 deletions examples/compute-first-merchant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* Why this exists (vs x402 upto / Permit2):
* - upto's facilitator support is still limited (Coinbase CDP testnet rejects
* upto-mode settles today; only mainnet claims support).
* - Permit2 is Ethereum-only — no Solana, no Tempo non-EIP-3009, no Stripe.
* - Permit2 is Ethereum-only: no Solana, no Tempo non-EIP-3009, no Stripe.
* - Compute-first works on every exact-mode rail in the ecosystem with no
* buyer setup and no facilitator extensions.
*
Expand All @@ -21,11 +21,11 @@
*
* This example wires the x402-exact rail on Base only. To add MPP rails
* (Tempo, Solana, Stripe SPT), pass a `composeMppx` callback that builds
* mppx intents at the exact cached price — see
* mppx intents at the exact cached price: see
* `examples/multi-rail-merchant.ts` for the fixed-price MPP compose pattern;
* the compute-first variant is structurally identical except the helper
* passes the cached price + recipients into your callback. Stripe SPT
* requires the computed price to be at least $0.50 USD — below that
* requires the computed price to be at least $0.50 USD: below that
* Stripe's fixed ~$0.30 fee makes the charge unprofitable, so
* `buildMppxComposeRails` auto-drops the stripe rail and sub-50-cent
* pay-per-result APIs ship Tempo + x402 + Solana only.
Expand Down Expand Up @@ -71,7 +71,7 @@ const x402Server = await createX402Server({
const searchHandler = computeFirstCheckout({
name: 'search',
url: `${APP_URL}/search`,
// $0.01 per result. Use `0.0001` for sub-cent / per-token pricing — the
// $0.01 per result. Use `0.0001` for sub-cent / per-token pricing: the
// helper auto-derives decimal precision from the unit price.
unitPriceCents: 1,
rails: {
Expand Down
6 changes: 3 additions & 3 deletions examples/identity-only.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* bun add @agent-score/commerce hono
*
* Env vars:
* AGENTSCORE_API_KEY — your AgentScore API key
* AGENTSCORE_API_KEY: your AgentScore API key
*
* Run: bun run examples/identity-only.ts
*/
Expand Down Expand Up @@ -51,7 +51,7 @@ app.post('/restricted', async (c) => {
const assess = getAgentScoreData(c);
// assess includes: { decision, operator, kyc_verified, age_bracket, jurisdiction, ... }

// Run your own business logic here — buy something via your existing Stripe flow,
// Run your own business logic here: buy something via your existing Stripe flow,
// grant access to gated content, write to your DB, whatever. AgentScore's job ends
// at "this agent is verified, here's their operator id."

Expand All @@ -74,6 +74,6 @@ app.post('/restricted/capture-wallet-example', async (c) => {
});

// ── Public routes (no gate) ────────────────────────────────────────────────
app.get('/public-info', (c) => c.json({ message: 'open access — no identity required' }));
app.get('/public-info', (c) => c.json({ message: 'open access: no identity required' }));

export default app;
4 changes: 2 additions & 2 deletions examples/multi-rail-merchant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async function _computePricing(ctx: CheckoutContext): Promise<PricingResult> {

async function _mintRecipients(ctx: CheckoutContext): Promise<Record<string, string>> {
const totalCents = Math.round((ctx.pricing?.amountUsd ?? 0) * 100);
// `mintMultichainRecipients` returns the full per-rail map in one call —
// `mintMultichainRecipients` returns the full per-rail map in one call:
// preferred over the single-string `createPayToAddressFromStripePI` for
// multi-rail merchants because there's no second pi-cache lookup to glue
// the other rails' addresses back together. On the settle leg it short-
Expand All @@ -106,7 +106,7 @@ async function _mintRecipients(ctx: CheckoutContext): Promise<Record<string, str
//
// For low-margin endpoints (sub-dollar per call), pass
// `staticRecipients: { solana: process.env.MERCHANT_SOLANA_RECIPIENT! }` to
// skip Stripe minting on Solana — at $0.01/call MPP spec §13.6's ~$0.50 per-PI
// skip Stripe minting on Solana: at $0.01/call MPP spec §13.6's ~$0.50 per-PI
// ATA rent dominates revenue. With a stable merchant-owned recipient + one-
// time external pre-funding of its USDC ATA, every settle pays only the
// per-tx fee.
Expand Down
2 changes: 1 addition & 1 deletion examples/per-product-policy-merchant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
* bun add @agent-score/commerce hono
*
* Env vars:
* AGENTSCORE_API_KEY — your AgentScore API key
* AGENTSCORE_API_KEY: your AgentScore API key
*
* Run: bun run examples/per-product-policy-merchant.ts
*/
Expand Down
Loading
Loading