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
8 changes: 7 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Every helper is extracted from a real consumer, not speculated.

## Architecture

Single TypeScript package, tsup-built CJS + ESM with subpath exports. Per-framework identity adapters expose the same surface — `agentscoreGate`, `captureWallet`, `getAgentScoreData`, `verifyWalletSignerMatch` — with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim).
Single TypeScript package, tsup-built CJS + ESM with subpath exports. Per-framework identity adapters expose the same surface — `agentscoreGate`, `captureWallet`, `getAgentScoreData`, `verifyWalletSignerMatch`, `getGateDegradedState`, `getGateQuotaInfo` — with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim).

| Directory | Contents |
|---|---|
Expand Down Expand Up @@ -58,6 +58,12 @@ 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).

### Fail-open (opt-in)

`failOpen: true` on `agentscoreGate({...})` flips infra-failure handling: 429 / 5xx / network-timeout return `{ kind: 'allow', degraded: true, infraReason: 'quota_exceeded' | 'api_error' | 'network_timeout' }` instead of throwing. Per-adapter `getGateDegradedState(c)` exposes the flag for merchant logging/alerting; `withAgentScoreGate` (Next.js / Web Fetch) propagates `degraded` + `infraReason` directly on the handler's `gate` arg. Default stays `failOpen: false` — regulated commerce should keep it. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of the flag.

### Mount posture: gate-first vs gate-conditional

`agentscoreGate(...)` returns a vanilla framework middleware. Mount it directly when the route is AgentScore-only (`app.use('/purchase', gate)` in Hono / Express, `dependencies=[Depends(gate)]` in FastAPI, etc.) — every request runs identity + policy. To support **anonymous discovery by any spec-compliant x402 wallet** (Coinbase awal, Phantom, Solflare, …), wrap the gate so it fires only when a payment credential is attached:
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,33 @@ app.post("/purchase", async (c) => {
});
```

## Fail-open behavior

By default AgentScore Gate fails closed: any AgentScore-side infrastructure failure (HTTP 429, 5xx, network timeout) returns 503 to the buyer. Set `failOpen: true` on `agentscoreGate({...})` to opt in to graceful degradation:

```ts
import { agentscoreGate, getGateDegradedState } from '@agent-score/commerce/identity/hono';

const gate = agentscoreGate({ apiKey: process.env.AGENTSCORE_API_KEY!, failOpen: true });

app.use('/purchase', gate);

app.post('/purchase', async (c) => {
const { degraded, infraReason } = getGateDegradedState(c);
if (degraded) {
// Compliance was NOT enforced this request — log/alert/refund-async/etc.
console.warn(`[gate] degraded: ${infraReason}`);
}
// ...rest of handler
});
```

When `failOpen: true` AND the failure is infra-shape, the gate carries `degraded: true` + `infraReason: 'quota_exceeded' | 'api_error' | 'network_timeout'` so merchants can log/alert without parsing console output. **Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of `failOpen`** — `failOpen` only covers "AgentScore couldn't tell us," never "AgentScore said no."

For regulated commerce (alcohol, age-gated, sanctioned-jurisdiction-relevant) keep the default `failOpen: false` — outage is the correct posture; bypassing compliance on infra failure is a compliance gap. For low-stakes commerce or high-uptime SLAs, opt in and use the `degraded` flag as the audit trail.

The `getGateDegradedState` helper is exported by every framework adapter (Hono, Express, Fastify, Next.js, Web Fetch). For `withAgentScoreGate` (Next.js / Web Fetch), the `degraded` + `infraReason` fields land directly on the `gate` object passed to your handler.

## Examples

The [examples/](./examples) directory has 7 runnable single-file Hono apps covering common merchant scenarios — copy-paste templates, not frameworks. See [examples/README.md](./examples/README.md) for the full table.
Expand Down
8 changes: 4 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ All seven examples follow the same rough shape:
2. **Discovery routes:** `/llms.txt` + `/.well-known/mpp.json` + `/openapi.json` (where applicable) using commerce/discovery helpers
3. **Per-request:** identity gate → validate body → 402 challenge (built via commerce/challenge helpers) → settle payment → return result

The commerce SDK keeps every step ~5-15 lines instead of ~50-150 lines. Vendors compose; commerce wraps the protocol-correctness boilerplate.
AgentScore Commerce keeps every step ~5–15 lines instead of ~50–150 lines. Vendors compose; the SDK wraps the protocol-correctness boilerplate.

## What stays vendor-specific

Expand All @@ -41,4 +41,4 @@ These examples are intentionally thin on domain logic. Vendors plug in their own
- Tax / shipping calculators
- Frontend UI (none of these examples include one — they're agent-only APIs)

Commerce handles the agent commerce protocol layer; everything else is your business.
AgentScore Commerce handles the agent commerce protocol layer; everything else is your business.
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agent-score/commerce",
"version": "1.0.3",
"version": "1.1.0",
"description": "Agent commerce SDK — identity middleware (Hono, Express, Fastify, Next.js, Web Fetch) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce.",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
Expand Down Expand Up @@ -120,7 +120,7 @@
"node": ">=20"
},
"dependencies": {
"@agent-score/sdk": "^2.1.1"
"@agent-score/sdk": "^2.2.0"
},
"overrides": {
"axios": "^1.15.0"
Expand Down Expand Up @@ -163,7 +163,7 @@
"fastify": "^5.8.5",
"hono": "^4.12.16",
"lefthook": "^2.1.6",
"mppx": "^0.6.8",
"mppx": "^0.6.13",
"tsup": "^8.5.1",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.1",
Expand Down
41 changes: 30 additions & 11 deletions src/_response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,13 @@ import type { DenialCode, DenialReason } from './core.js';
* - `payment_required` — gate never stamps; merchant tier misconfig, contact-merchant action
* - `identity_verification_required` — fallback when API didn't return next_steps
* - `token_expired` — fallback when API didn't return next_steps
* - `api_error` — `retry_with_backoff` envelope; sole retry channel (no separate
* next_steps block emitted)
*
* Codes already stamped explicitly upstream in core.ts (`missing_identity`,
* `invalid_credential`) and codes that don't go through DenialReason
* (`wallet_signer_mismatch`, `wallet_auth_requires_wallet_signing` — handled by
* `verifyWalletSignerMatch` result type) are not in this map. `api_error` has
* its own `next_steps: {action: retry}` fallback below.
* `verifyWalletSignerMatch` result type) are not in this map.
*/
const WALLET_NOT_TRUSTED_INSTRUCTIONS = JSON.stringify({
action: 'contact_support',
Expand All @@ -44,11 +45,11 @@ const WALLET_NOT_TRUSTED_INSTRUCTIONS = JSON.stringify({
const PAYMENT_REQUIRED_INSTRUCTIONS = JSON.stringify({
action: 'contact_merchant',
steps: [
'The merchant\'s AgentScore tier does not include the assess feature, so agent identity cannot be evaluated. This is a merchant-side configuration gap — there is no agent-side recovery.',
'Contact the merchant (their support channel — typically listed in /llms.txt or the OpenAPI servers metadata) and request they upgrade their AgentScore plan.',
'The merchant\'s AgentScore account does not have the assess endpoint enabled, so agent identity cannot be evaluated. This is a merchant-side configuration gap — there is no agent-side recovery.',
'Contact the merchant (their support channel — typically listed in /llms.txt or the OpenAPI servers metadata) so they can resolve the configuration on their side.',
],
user_message:
'This merchant\'s identity gate is misconfigured (AgentScore tier doesn\'t support assess). Contact the merchant — there\'s nothing to fix on the agent side.',
'This merchant\'s identity gate is misconfigured. Contact the merchant — there\'s nothing to fix on the agent side.',
});

const IDENTITY_VERIFICATION_REQUIRED_FALLBACK_INSTRUCTIONS = JSON.stringify({
Expand All @@ -62,6 +63,28 @@ const IDENTITY_VERIFICATION_REQUIRED_FALLBACK_INSTRUCTIONS = JSON.stringify({
'Identity verification is required. Visit verify_url, then poll poll_url for the operator token and retry.',
});

const API_ERROR_INSTRUCTIONS = JSON.stringify({
action: 'retry_with_backoff',
steps: [
'Verification is temporarily unavailable. Retry the request after 5–30 seconds with exponential backoff.',
'This is NOT a compliance denial — the user does not need to re-verify their identity. Send the same identity headers (X-Wallet-Address or X-Operator-Token) on retry.',
'If the request continues to fail after 3+ retries (~60 seconds total), surface the error to the user with the merchant\'s support contact.',
],
user_message:
'Verification is temporarily unavailable. Please try again in a moment — this is a transient issue, not a problem with your account.',
});

export const QUOTA_EXCEEDED_INSTRUCTIONS = JSON.stringify({
action: 'contact_merchant',
steps: [
'AgentScore identity verification is unavailable for this merchant. This is a merchant-side issue and is NOT recoverable via retry.',
'Do not retry: the same 503 will be returned until the merchant resolves the issue on their side.',
'Surface to the user with the merchant\'s support contact. The merchant (not the agent) needs to act.',
],
user_message:
'This merchant\'s identity verification is temporarily unavailable. Try again later, or contact the merchant directly.',
});

const TOKEN_EXPIRED_FALLBACK_INSTRUCTIONS = JSON.stringify({
action: 'deliver_verify_url_and_poll',
steps: [
Expand All @@ -74,6 +97,7 @@ const TOKEN_EXPIRED_FALLBACK_INSTRUCTIONS = JSON.stringify({
});

const DEFAULT_AGENT_INSTRUCTIONS: Partial<Record<DenialCode, string>> = {
api_error: API_ERROR_INSTRUCTIONS,
wallet_not_trusted: WALLET_NOT_TRUSTED_INSTRUCTIONS,
payment_required: PAYMENT_REQUIRED_INSTRUCTIONS,
identity_verification_required: IDENTITY_VERIFICATION_REQUIRED_FALLBACK_INSTRUCTIONS,
Expand All @@ -90,7 +114,7 @@ const DEFAULT_MESSAGES: Record<DenialCode, string> = {
api_error:
'AgentScore is unreachable. This is transient — retry in a few seconds.',
payment_required:
'AgentScore tier does not support assess. Contact support.',
'Assess endpoint not enabled for this merchant. Contact support.',
wallet_signer_mismatch:
'Payment signer does not match the wallet claimed via X-Wallet-Address. The signer and the claimed wallet must both resolve to the same AgentScore operator.',
wallet_auth_requires_wallet_signing:
Expand Down Expand Up @@ -138,11 +162,6 @@ export function denialReasonToBody(reason: DenialReason): Record<string, unknown
if (reason.expected_signer) body.expected_signer = reason.expected_signer;
if (reason.actual_signer) body.actual_signer = reason.actual_signer;
if (reason.linked_wallets && reason.linked_wallets.length > 0) body.linked_wallets = reason.linked_wallets;
// api_error denials get a default retry hint so agents know it's transient. Vendors can
// override by spreading their own next_steps into a custom onDenied body.
if (reason.code === 'api_error' && !(reason.extra && (reason.extra as Record<string, unknown>).next_steps)) {
body.next_steps = { action: 'retry', retry_after_seconds: 5 };
}
if (reason.extra) {
for (const [key, value] of Object.entries(reason.extra)) {
if (RESERVED_FIELDS.has(key)) {
Expand Down
Loading
Loading