Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
e63626d
feat(x402-settle): wrap verify-stage facilitator throws as 'facilitat…
vvillait88 May 4, 2026
d39814d
feat(x402-settle): add classifyX402SettleResult helper
vvillait88 May 4, 2026
20e15ba
docs(x402-settle): drop 'string-sniffing' phrasing from /payment table
vvillait88 May 4, 2026
01010df
feat(discovery): x402scan Layer 1 builders (well-known/x402, x-paymen…
vvillait88 May 4, 2026
32ea663
feat(solana)!: kill x402 Solana, add MPP solana/charge as the canonic…
vvillait88 May 4, 2026
7df77db
docs(commerce): clean up x402-Solana remnants in README + example met…
vvillait88 May 5, 2026
4055a6e
feat(commerce): targeted Solana-network rejection hint + extraWarning…
vvillait88 May 5, 2026
3e4924d
chore(commerce): scrub internal disclosures from public-package source
vvillait88 May 5, 2026
1a697c9
chore(commerce): scrub remaining customer-domain example in _denial d…
vvillait88 May 5, 2026
df16fb5
chore(commerce): genericize Martin Estate references in tests
vvillait88 May 5, 2026
bf562d4
chore(commerce): final scrub of customer + threat-model leaks
vvillait88 May 5, 2026
ff50bdd
chore(commerce): drop "malicious merchant" rationale from tests + scr…
vvillait88 May 5, 2026
e8329e3
chore: bump version to 1.3.0
vvillait88 May 5, 2026
c91e9ad
fix(commerce): case-insensitive Solana CAIP-2 detection in verifyX402…
vvillait88 May 5, 2026
d12db62
feat(signer): match Solana DIDs (did:pkh:solana:...) in extractPaymen…
vvillait88 May 5, 2026
a0c6e8a
feat(signer): decode Solana credential payload to recover signer (pul…
vvillait88 May 5, 2026
76364a0
fix(signer): defensive bounds-check on Solana lookup-table indices + …
vvillait88 May 5, 2026
d433b5b
test(signer): cover decode-fallback edge cases (kit-missing, lookup-t…
vvillait88 May 5, 2026
2f93357
chore(deps): bun update — mppx 0.6.14 → 0.6.15, typescript-eslint 8.5…
vvillait88 May 5, 2026
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
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,15 @@ Single TypeScript package, tsup-built CJS + ESM with subpath exports. Per-framew
| `examples/` | Runnable single-file Hono apps for each common scenario |
| `tests/` | Vitest, one file per surface, ~360+ tests |

Peer-dep pattern: payment/x402/mppx/stripe modules `dynamic import` at runtime — vendors install only what they use (`@x402/core`, `@x402/evm`, `@x402/svm`, `@coinbase/x402`, `mppx`, `stripe`). Missing peer dep throws a guiding error with the install command.
Peer-dep pattern: payment/x402/mppx/stripe modules `dynamic import` at runtime — vendors install only what they use (`@x402/core`, `@x402/evm`, `@coinbase/x402`, `mppx`, `@solana/mpp`, `@solana/kit`, `stripe`). Missing peer dep throws a guiding error with the install command. x402 in this SDK is EVM-only; Solana SPL payments go through MPP `solana/charge` (`@solana/mpp/server`).

## Examples

`examples/` contains full single-file Hono apps for the most common merchant scenarios — copy-paste templates, not frameworks:

| Example | Scenario |
|---|---|
| `api-provider.ts` | Per-call API billing on multiple rails: Tempo MPP + x402 (Base + Solana); no compliance gate |
| `api-provider.ts` | Per-call API billing on multiple rails: Tempo MPP + x402 Base + Solana MPP; no compliance gate |
| `identity-only.ts` | Compliance gate without payment (vendor handles their own) |
| `multi-rail-merchant.ts` | Full agent-commerce: identity + Tempo MPP + x402 + Stripe SPT |
| `stripe-multichain-merchant.ts` | Stripe-anchored multichain (PaymentIntent → tempo/base/solana deposit addresses) |
Expand All @@ -58,7 +58,7 @@ Denial reason codes: `missing_identity`, `identity_verification_required`, `toke

Captured wallets: `captureWallet(ctx, { walletAddress, network, idempotencyKey })` is fire-and-forget — reads `operator_token` stashed during gating and POSTs to `/v1/credentials/wallets`. No-ops for wallet-authenticated requests.

Wallet-signer-match: `verifyWalletSignerMatch(ctx, { signer, network })` makes a single `/v1/assess` call with `resolve_signer` set; the API resolves both wallets and emits a `signer_match` verdict in the same response — collapses the legacy 2 follow-up assess calls into one round trip. Repeat lookups for the same `(claimed, signer)` pair hit a per-cache-entry `signerMatchBySigner` sub-map and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety).
Wallet-signer-match: `verifyWalletSignerMatch(ctx, { signer, network })` makes a single `/v1/assess` call with `resolve_signer` set; the API resolves both wallets and emits a `signer_match` verdict in the same response — collapses the legacy 2 follow-up assess calls into one round trip. Repeat lookups for the same `(claimed, signer)` pair hit a per-cache-entry `signerMatchBySigner` sub-map and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety). Signer recovery covers x402 EIP-3009 (EVM `from` address), Tempo MPP (`did:pkh:eip155` source), and Solana MPP `solana/charge` (via `did:pkh:solana` source when set, otherwise by decoding the credential's signed-tx payload to read the SPL `TransferChecked` authority — pull mode only, requires the `@solana/kit` optional peer).

### Fail-open (opt-in)

Expand Down
37 changes: 26 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ bun add @agent-score/commerce
Framework + protocol packages are optional peer deps — install only what you use:

```bash
npm install hono mppx @x402/core @x402/evm @x402/svm stripe # whatever your stack needs
npm install hono mppx @x402/core @x402/evm @solana/mpp @solana/kit stripe # whatever your stack needs
```

## What's in the package

| Subpath | What it provides |
|---|---|
| `/identity/{hono,express,fastify,nextjs,web}` | Trust gate middleware: KYC, sanctions, age, jurisdiction. `agentscoreGate(...)`, `getAgentScoreData(c)`, `captureWallet(...)`, `verifyWalletSignerMatch(...)`. Plus shared denial helpers: `denialReasonStatus`, `denialReasonToBody`, `buildSignerMismatchBody`, `buildContactSupportNextSteps`, `verificationAgentInstructions`, `isFixableDenial`, `FIXABLE_DENIAL_REASONS`. |
| `/payment` | `networks`, `USDC`, `rails` registries; `paymentDirective`, `buildPaymentDirective`, `wwwAuthenticateHeader`, `paymentRequiredHeader`, `aliasAmountFields` (v1↔v2 amount field shim — emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlementOverrideHeader`, `dispatchSettlementByNetwork`, `extractPaymentSigner` (returns `{address, network}`); `createX402Server`, `createMppxServer`; drop-in x402 helpers: `validateX402NetworkConfig` (boot-time guard), `verifyX402Request` (parse + validate inbound X-Payment), `processX402Settle` (verify-then-settle with one call). |
| `/payment` | `networks`, `USDC`, `rails` registries; `paymentDirective`, `buildPaymentDirective`, `wwwAuthenticateHeader`, `paymentRequiredHeader`, `aliasAmountFields` (v1↔v2 amount field shim — emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlementOverrideHeader`, `dispatchSettlementByNetwork`, `extractPaymentSigner` (returns `{address, network}`); `createX402Server`, `createMppxServer`; drop-in x402 helpers: `validateX402NetworkConfig` (boot-time guard), `verifyX402Request` (parse + validate inbound X-Payment), `processX402Settle` (verify-then-settle with one call), `classifyX402SettleResult` (maps the tagged settle result to a recommended HTTP status / code / nextSteps so merchants get a controlled envelope without coupling to facilitator-specific error text). |
| `/discovery` | `isDiscoveryProbeRequest`, `buildDiscoveryProbeResponse` (with optional `x402Sample` for x402-aware crawlers — `awal x402 details` etc.), `sampleX402AcceptForNetwork` (USDC sample-accept builder for known CAIP-2 networks), `buildWellKnownMpp`, `buildLlmsTxt` + `llmsTxtIdentitySection` + `llmsTxtPaymentSection` (compact + verbose modes), `buildSkillMd` (Claude-Skill-compatible `/skill.md` agent-discovery manifest — strictly agent-facing data only, no internal posture), `agentscoreOpenApiSnippets`, `createBazaarDiscovery`, `noindexNonDiscoveryPaths` (Hono middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces — defaults cover `/openapi.json`, `/llms.txt`, `/skill.md`, `/.well-known/{mpp.json,agent-card.json,ucp}`, `/favicon.{png,ico}`; pure helpers `isDiscoveryPath` + `defaultDiscoveryPaths` for non-Hono frameworks). |
| `/challenge` | `build402Body`, `buildAcceptedMethods`, `buildIdentityMetadata`, `buildHowToPay`, `buildAgentInstructions` (auto-emits per-rail `compatible_clients` — smoke-verified CLIs the agent should use; vendor override supported), `buildPricingBlock`, `firstEncounterAgentMemory`, `OrderReceipt`; `respond402` — drop-in 402 emit that preserves mppx's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `buildValidationError` — structured 4xx body builder (`{error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}`) so vendors compose body shapes by name instead of inlining at every validation site. |
| `/stripe-multichain` | `createMultichainPaymentIntent`, `getDepositAddress`, `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`. |
Expand Down Expand Up @@ -89,13 +89,16 @@ import {

// Build paymentauth.org directives by symbolic rail name (decimals + currency from registry)
const directives = [
buildPaymentDirective({ rail: "tempo-mainnet", id: "chg_t", realm: "ex.com", recipient: TEMPO_ADDR, amountUsd: 0.01 }),
buildPaymentDirective({ rail: "x402-base-mainnet", id: "chg_b", realm: "ex.com", recipient: BASE_ADDR, amountUsd: 0.01 }),
buildPaymentDirective({ rail: "x402-solana-mainnet", id: "chg_s", realm: "ex.com", recipient: SOL_ADDR, amountUsd: 0.01 }),
buildPaymentDirective({ rail: "tempo-mainnet", id: "chg_t", realm: "ex.com", recipient: TEMPO_ADDR, amountUsd: 0.01 }),
buildPaymentDirective({ rail: "x402-base-mainnet", id: "chg_b", realm: "ex.com", recipient: BASE_ADDR, amountUsd: 0.01 }),
buildPaymentDirective({ rail: "mpp-solana-mainnet", id: "chg_s", realm: "ex.com", recipient: SOL_ADDR, amountUsd: 0.01 }),
];
const wwwAuth = wwwAuthenticateHeader(directives);

// Recover the on-chain signer from the inbound credential — returns {address, network}
// Recover the on-chain signer from the inbound credential — returns {address, network}.
// Covers x402 EIP-3009 (EVM `from` address), Tempo MPP (`did:pkh:eip155` source),
// and Solana MPP `solana/charge` (via `did:pkh:solana` source when set, else by
// decoding the credential's signed-tx payload — `@solana/kit` optional peer).
const signer = await extractPaymentSigner(req, req.headers.get("x-payment") ?? undefined);
```

Expand All @@ -106,12 +109,17 @@ import { createX402Server, createMppxServer } from "@agent-score/commerce/paymen

const x402 = await createX402Server({
facilitator: "coinbase", // or "http", or pass a custom facilitator instance
rails: ["x402-base-mainnet", "x402-solana-mainnet", "x402-base-mainnet-upto"],
rails: ["x402-base-mainnet", "x402-base-mainnet-upto"],
});

const mppx = await createMppxServer({
rails: {
tempo: { recipient: process.env.TEMPO_RECIPIENT! },
solana: {
recipient: process.env.SOLANA_RECIPIENT!,
// Optional fee sponsor — pass any `TransactionPartialSigner` from `@solana/kit`.
// signer: solanaFeePayerSigner,
},
stripe: { profileId: process.env.STRIPE_PROFILE_ID!, secretKey: process.env.STRIPE_SECRET_KEY! },
},
secretKey: process.env.MPP_SECRET_KEY!,
Expand All @@ -132,7 +140,7 @@ import {
const acceptedMethods = buildAcceptedMethods({
tempo: { recipient: TEMPO_ADDR },
x402_base: { recipient: BASE_ADDR },
x402_solana: { recipient: SOL_ADDR },
solana_mpp: { recipient: SOL_ADDR, feePayerKey: SOLANA_FEE_PAYER },
stripe: { profileId: STRIPE_PROFILE_ID },
});

Expand Down Expand Up @@ -236,22 +244,23 @@ await simulateDepositIfTestMode({

```typescript
import {
classifyX402SettleResult,
processX402Settle,
validateX402NetworkConfig,
verifyX402Request,
} from "@agent-score/commerce/payment";
import { respond402 } from "@agent-score/commerce/challenge";

// Boot-time guard — raises if a configured network isn't supported.
validateX402NetworkConfig({ baseNetwork: X402_BASE, svmNetwork: X402_SVM });
validateX402NetworkConfig({ baseNetwork: X402_BASE });

app.post("/purchase", async (c) => {
// Path A — agent presented an x402 X-Payment header
if (c.req.header("payment-signature") || c.req.header("x-payment")) {
const verified = await verifyX402Request({
request: c.req.raw,
isCachedAddress: piCache.hasAddress,
acceptedNetworks: { base: X402_BASE, svm: X402_SVM },
acceptedNetwork: X402_BASE,
});
if (!verified.ok) return c.json(verified.body, verified.status);

Expand All @@ -261,7 +270,13 @@ app.post("/purchase", async (c) => {
resourceConfig: { scheme: "exact", network: verified.signedNetwork, price: `$${total}`, payTo: verified.signedPayTo, maxTimeoutSeconds: 300 },
resourceMeta: { url: c.req.url, mimeType: "application/json" },
});
if (!settle.success) return c.json({ error: { code: "payment_proof_invalid", phase: settle.phase } }, 400);
const classified = classifyX402SettleResult(settle);
if (classified) {
// Log raw `settle` server-side; return controlled phase-based response to the agent.
console.error("[x402-settle]", { phase: settle.success ? null : settle.phase, raw: settle });
return c.json({ error: { code: classified.code, message: classified.message }, next_steps: classified.nextSteps }, classified.status);
}
if (!settle.success) throw new Error("unreachable: classified covers every non-success phase");

const headers: Record<string, string> = {};
if (settle.paymentResponseHeader) headers["payment-response"] = settle.paymentResponseHeader;
Expand Down
Loading
Loading