> ## Documentation Index
> Fetch the complete documentation index at: https://docs.agentscore.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Agentic Commerce Quickstart

> Build an agent-gated checkout with one middleware call. A step-by-step using Martin Estate's wine API as the worked example.

Your API serves agents. An agent hits `POST /purchase` on behalf of a user who may or may not be KYC-verified, old enough, in an allowed jurisdiction, or on a sanctions list. You need to verify the human behind the agent before you deliver, without breaking the agent's retry loop.

This guide shows the drop-in pattern using `@agent-score/commerce` (Node.js) or `agentscore-commerce` (Python). We'll walk through Martin Estate's wine commerce API as the worked example because it exercises every gate (KYC, age, sanctions, jurisdiction); same pattern applies to any merchant. Sayer & Stone ([agents.sayerandstone.com](https://agents.sayerandstone.com)) is the unrestricted-goods counterpoint: same SDK, no identity gate on `/purchase` at all.

## The pattern

A single middleware call gates your route:

```ts theme={"dark"}
import { agentscoreGate } from '@agent-score/commerce/identity/hono';

app.use('/purchase', agentscoreGate({
  apiKey: process.env.AGENTSCORE_API_KEY!,
  userAgent: `my-api/${VERSION}`,
  requireKyc: true,
  requireSanctionsClear: true,
  minAge: 21,
  allowedJurisdictions: ['US'],
  createSessionOnMissing: { apiKey: process.env.AGENTSCORE_API_KEY! },
}));
```

Here's what happens:

1. **Agent hits `/purchase` with an `X-Operator-Token` or `X-Wallet-Address` header.** The gate extracts the identity, calls `POST /v1/assess` with your policy, and either passes the request through (with assess data attached) or returns a 403 with reason codes.

2. **If no identity header is present**, the gate auto-mints a verification session via `POST /v1/sessions` and returns a 403 carrying `verify_url`, `session_id`, `poll_secret`, `poll_url`, and agent-polling instructions. The agent opens `verify_url` for the user, polls `poll_url` with `X-Poll-Secret: {poll_secret}` in the background, and retries once the user finishes Stripe Identity. Zero additional merchant code.

3. **If identity fails a compliance check** (age, sanctions, jurisdiction), the default 403 includes the denial reasons + a `verify_url` for resolvable cases (e.g. KYC expired).

No hand-rolled fetches, no session-creation boilerplate, no `decision === 'deny'` branches.

### AgentScore Gate, conditional variant: support any x402 wallet (Coinbase awal, Phantom, etc.)

The mount above runs the gate on **every** request to `/purchase`. That's correct when the only buyers are agents that already carry an AgentScore credential. If you want **any spec-compliant x402 wallet** to discover prices anonymously and pay (Coinbase awal, Phantom, Solflare, …), wrap the gate so it fires only when a payment credential is already attached:

```ts theme={"dark"}
import { hasPaymentHeader } from '@agent-score/commerce/payment';

const _gate = agentscoreGate({ /* same options as above */ });

app.use('/purchase', async (c, next) => {
  if (!hasPaymentHeader(c.req.raw)) { await next(); return; }
  return _gate(c, next);
});
```

Anonymous browsers fall through to the handler unauthenticated; they get the full 402 with rails + per-order pricing without an AgentScore account. The gate runs at **settle time** on the retry leg (when `X-Payment` / `Authorization: Payment` arrives), and `createSessionOnMissing` still auto-mints a verification session so the agent can bootstrap KYC and replay the same payment authorization within its TTL window.

Flask, FastAPI, Express, etc. all support the same wrap; the test is purely "is one of `payment-signature` / `x-payment` / `Authorization: Payment` present on this request?"

## End-to-end Hono walkthrough

Martin Estate's `POST /purchase` is the canonical integration. What follows is a trimmed version of their `src/routes/purchase.ts` with the exact gate setup.

### 1. Install

```bash theme={"dark"}
bun install @agent-score/commerce hono
# or: npm install @agent-score/commerce hono
```

Grab an API key at [agentscore.com/dashboard](https://www.agentscore.com/dashboard) (free tier covers hobby usage, including 20 `/v1/assess` calls a month; paid tiers raise the quota).

### 2. Mount AgentScore Gate

```ts theme={"dark"}
import { Hono } from 'hono';
import { agentscoreGate, getAgentScoreData } from '@agent-score/commerce/identity/hono';

const app = new Hono();

app.use('/purchase', agentscoreGate({
  apiKey: process.env.AGENTSCORE_API_KEY!,
  // Prepends your app's identifier to outbound User-Agent for traceability in
  // AgentScore's server logs. Format: "{userAgent} (@agent-score/commerce@x.y.z)"
  userAgent: `your-merchant/${VERSION}`,
  // Wine-specific compliance policy. See the policy reference below.
  requireKyc: true,
  requireSanctionsClear: true,
  minAge: 21,
  allowedJurisdictions: ['US'],
  // Auto-create a verification session when no identity header is present.
  createSessionOnMissing: {
    apiKey: process.env.AGENTSCORE_API_KEY!,
    context: 'wine-purchase',
    productName: 'Martin Estate wine',
  },
}));
```

That's it for the compliance layer. Everything else below is your existing route code.

### 3. Access assess data in the handler

```ts theme={"dark"}
app.post('/purchase', async (c) => {
  // The gate populated this via c.set('agentscore', data) before calling next().
  const assess = getAgentScoreData(c);
  console.log('identity method:', assess?.identity_method); // 'wallet' | 'operator_token'
  console.log('policy checks:', assess?.policy_result?.checks);

  // ... your normal payment + fulfillment logic ...

  return c.json({ order_id: '...', status: 'completed' });
});
```

`getAgentScoreData(c)` returns the full `/v1/assess` response; `decision`, `decision_reasons`, `operator_verification`, `policy_result`, and any resolvable `verify_url`. Record it alongside the order for audit purposes.

### 4. What the agent sees on 403

When identity is missing, the default response is:

```json theme={"dark"}
{
  "error": {
    "code": "identity_verification_required",
    "message": "Identity verification is required to access this resource. Visit verify_url to complete KYC."
  },
  "verify_url": "https://www.agentscore.com/verify?session=sess_abc123",
  "session_id": "sess_abc123",
  "poll_secret": "ps_secret_xyz",
  "poll_url": "https://api.agentscore.com/v1/sessions/sess_abc123",
  "agent_instructions": "Please complete identity verification at the verify_url."
}
```

The agent's job:

1. Show `verify_url` to the user (it's a ready-to-open URL with the session token embedded; don't modify it).
2. While the user verifies, poll `poll_url` every 5 seconds with header `X-Poll-Secret: {poll_secret}`.
3. When the poll returns `status: "verified"`, extract `operator_token` from the response.
4. Retry `POST /purchase` with header `X-Operator-Token: {operator_token}`.

The retry should pass through; same request body, just with the verified identity.

If identity is present but fails policy with an UNFIXABLE reason (`age_insufficient`, `sanctions_flagged`, `jurisdiction_restricted`), the 403 has `error: "wallet_not_trusted"` with `reasons[]` and `agent_instructions.action: "contact_support"`. Fixable reasons (`kyc_required`, `kyc_pending`, `kyc_failed`) never surface as `wallet_not_trusted`; the gate auto-mints a verification session and re-routes to `identity_verification_required` with `verify_url` + `poll_secret`, identical UX to the no-identity path above. (`jurisdiction_restricted` is unfixable because the API only emits it after KYC is verified; the user's KYC'd country is in the merchant's blocked list, and re-doing KYC won't change it.)

### 5. Testing with mocked gate outcomes

The gate calls `POST /v1/assess` via `global.fetch`. Mock it in Vitest:

```ts theme={"dark"}
import { describe, expect, it, vi } from 'vitest';
import app from '../src/app';

describe('POST /purchase', () => {
  it('returns 403 when identity is missing', async () => {
    // Mock the session-creation call the gate makes on missing identity.
    global.fetch = vi.fn().mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: vi.fn().mockResolvedValueOnce({
        session_id: 'sess_test',
        verify_url: 'https://www.agentscore.com/verify?session=sess_test',
        poll_secret: 'ps_test',
        poll_url: 'https://api.agentscore.com/v1/sessions/sess_test',
        expires_at: '2026-04-22T13:00:00Z',
      }),
    });

    const res = await app.request('/purchase', {
      method: 'POST',
      body: JSON.stringify({ product_id: '...', quantity: 1 }),
    });

    expect(res.status).toBe(403);
    const body = await res.json();
    expect(body.error).toBe('identity_verification_required');
    expect(body.session_id).toBe('sess_test');
    expect(body.poll_url).toBe('https://api.agentscore.com/v1/sessions/sess_test');
  });

  it('lets the request through when identity passes policy', async () => {
    global.fetch = vi.fn().mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: vi.fn().mockResolvedValueOnce({
        decision: 'allow',
        decision_reasons: [],
        operator_verification: { level: 'kyc_verified' },
      }),
    });

    const res = await app.request('/purchase', {
      method: 'POST',
      headers: { 'X-Operator-Token': 'opc_test' },
      body: JSON.stringify({ product_id: '...', quantity: 1 }),
    });

    expect(res.status).toBe(200);
  });
});
```

## The same pattern in every framework

Each adapter ships with the same option shape. Pick yours. The [gate-conditional wrap](#gate-conditional-variant-support-any-x402-wallet-coinbase-awal-phantom-etc) applies identically to all of them; wrap the gate handler instead of mounting it directly when you want anonymous x402 wallets to discover prices.

### Express

```ts theme={"dark"}
import express from 'express';
import { agentscoreGate } from '@agent-score/commerce/identity/express';

const app = express();
app.use('/purchase', agentscoreGate({
  apiKey: process.env.AGENTSCORE_API_KEY!,
  requireKyc: true, minAge: 21, allowedJurisdictions: ['US'],
  createSessionOnMissing: { apiKey: process.env.AGENTSCORE_API_KEY! },
}));
```

### Next.js App Router

```ts theme={"dark"}
// app/api/purchase/route.ts
import { withAgentScoreGate } from '@agent-score/commerce/identity/nextjs';

export const POST = withAgentScoreGate(
  {
    apiKey: process.env.AGENTSCORE_API_KEY!,
    requireKyc: true, minAge: 21, allowedJurisdictions: ['US'],
    createSessionOnMissing: { apiKey: process.env.AGENTSCORE_API_KEY! },
  },
  async (_req, { data }) => Response.json({ decision: data!.decision }),
);
```

### Fastify

```ts theme={"dark"}
import Fastify from 'fastify';
import agentscoreGate from '@agent-score/commerce/identity/fastify';

const app = Fastify();
await app.register(agentscoreGate, {
  apiKey: process.env.AGENTSCORE_API_KEY!,
  requireKyc: true, minAge: 21, allowedJurisdictions: ['US'],
});
```

### Web Fetch (Cloudflare Workers / Deno / Bun / edge)

```ts theme={"dark"}
import { createAgentScoreGate } from '@agent-score/commerce/identity/web';

const guard = createAgentScoreGate({
  apiKey: env.AGENTSCORE_API_KEY,
  requireKyc: true, minAge: 21, allowedJurisdictions: ['US'],
});

export default {
  async fetch(req: Request) {
    const r = await guard(req);
    if (!r.allowed) return r.response;
    return handle(req, r.data);
  },
};
```

### FastAPI (native `Depends()`)

```python theme={"dark"}
from fastapi import Depends, FastAPI
from agentscore_commerce.identity.fastapi import AgentScoreGate, get_agentscore_data

app = FastAPI()
gate = AgentScoreGate(
    api_key=os.environ['AGENTSCORE_API_KEY'],
    require_kyc=True, min_age=21, allowed_jurisdictions=['US'],
)

@app.post('/purchase', dependencies=[Depends(gate)])
async def purchase(assess = Depends(get_agentscore_data)):
    return {'decision': assess['decision']}
```

### Flask

```python theme={"dark"}
from agentscore_commerce.identity.flask import agentscore_gate

agentscore_gate(
    app,
    api_key=os.environ['AGENTSCORE_API_KEY'],
    require_kyc=True, min_age=21, allowed_jurisdictions=['US'],
)
```

### Django

```python theme={"dark"}
# settings.py
MIDDLEWARE = ['agentscore_commerce.identity.django.AgentScoreMiddleware']
AGENTSCORE_GATE = {
    'api_key': os.environ['AGENTSCORE_API_KEY'],
    'require_kyc': True, 'min_age': 21, 'allowed_jurisdictions': ['US'],
}
```

Starlette, Litestar, Quart all work via the generic ASGI adapter: `from agentscore_commerce.identity import AgentScoreGate`. AIOHTTP and Sanic ship native adapters at `agentscore_commerce.identity.aiohttp` / `agentscore_commerce.identity.sanic`.

## Compliance policy reference

All policy fields are optional; set only what your vertical requires.

| Field                   | Type       | Examples                 | Description                                           |
| ----------------------- | ---------- | ------------------------ | ----------------------------------------------------- |
| `requireKyc`            | `boolean`  | `true`                   | Require a completed Stripe Identity verification      |
| `requireSanctionsClear` | `boolean`  | `true`                   | Fail on OFAC / UN / EU sanctions matches              |
| `minAge`                | `number`   | `18`, `21`               | Minimum age bracket enforced by the KYC provider      |
| `allowedJurisdictions`  | `string[]` | `['US']`, `['US', 'CA']` | Allowlist of ISO country codes; all others denied     |
| `blockedJurisdictions`  | `string[]` | `['KP', 'IR', 'CU']`     | Blocklist; takes precedence over allowedJurisdictions |

### Common vertical policies

**Wine / alcohol / age-restricted goods** (Martin Estate):

```ts theme={"dark"}
{ requireKyc: true, requireSanctionsClear: true, minAge: 21, allowedJurisdictions: ['US'] }
```

**Financial services / lending / regulated crypto**:

```ts theme={"dark"}
{ requireKyc: true, requireSanctionsClear: true, minAge: 18 }
```

**Restricted exports (US)**:

```ts theme={"dark"}
{ requireSanctionsClear: true, blockedJurisdictions: ['KP', 'IR', 'CU', 'RU', 'SY'] }
```

**Marketplace trust gate (non-regulated)**:

```ts theme={"dark"}
{ requireKyc: true } // just prove the agent is backed by a real human
```

## Advanced: per-request session context

For the flagship case; where the verify page should show the specific product the agent was buying; use the `getSessionOptions` and `onBeforeSession` hooks:

```ts theme={"dark"}
app.use('/purchase', agentscoreGate({
  apiKey: process.env.AGENTSCORE_API_KEY!,
  requireKyc: true, minAge: 21, allowedJurisdictions: ['US'],
  createSessionOnMissing: {
    apiKey: process.env.AGENTSCORE_API_KEY!,
    // Compute session options per-request. Hook receives the framework context so
    // it can read body/headers/state a prior middleware stashed.
    getSessionOptions: async (c) => {
      const body = await c.req.json();
      const product = await lookupProduct(body.product_id);
      return { productName: product.name }; // "2022 Cabernet Sauvignon" not "wine"
    },
    // Side-effect hook that runs after the session mints. Return value merges
    // into the 403 response, use it to attach a merchant-specific resume token
    // (e.g. a pending-order id).
    onBeforeSession: async (c, session) => {
      const pendingOrderId = await createPendingOrder(await c.req.json(), session);
      return { order_id: pendingOrderId };
    },
  },
}));
```

Martin Estate uses this exact pattern to pre-create `pending_identity` orders before the 403 fires; agents can then retry with just `{ operator_token, order_id }` instead of re-sending the full body.

## Post-payment: capture the signer wallet

After a successful payment, report the wallet that signed it back to AgentScore. Builds a cross-merchant credential↔wallet profile: the agent's `opc_...` token you gated on gets linked to every wallet it's been observed paying from, across every merchant using AgentScore.

```ts theme={"dark"}
import { captureWallet } from '@agent-score/commerce/identity/hono';

app.post('/purchase', async (c) => {
  // ... compliance gate passed, payment settled ...
  const signer = extractPaymentSigner(paymentCredential); // e.g. EIP-3009 `from`
  await captureWallet(c, {
    walletAddress: signer,
    network: 'evm',                     // or 'solana'
    idempotencyKey: paymentIntentId,    // prevents retry double-count
  });
  return c.json({ ok: true });
});
```

Fire-and-forget; no-ops silently if the request was wallet-authenticated (no operator\_token to link) or the API call fails. See the [Credentials reference](/api-reference/credentials) for the endpoint shape.

## Observe denials in the dashboard

Every denial is logged against your API key. Open [Dashboard → Usage](https://www.agentscore.com/dashboard) to see:

* Total gated requests, allow/deny split
* Top denial reason codes (so you can tune `minAge`, `allowedJurisdictions`, etc.)
* Sessions created via `createSessionOnMissing`, completion rate

Wallets captured via `captureWallet` surface at [Dashboard → Identity](https://www.agentscore.com/dashboard/verify) as a unified list with source labels (`claimed`, `captured`, `both`).

## Live example: buy a wine

Drop this into your agent:

> "Buy me wine; see `https://agents.martinestate.com/skill.md`."

That's the whole prompt. The agent fetches `/skill.md` (the merchant's skill manifest), learns the catalog endpoint, payment rails (Tempo MPP / x402 Base / Solana MPP / Stripe SPT), and identity requirements (KYC + 21+ + US-only), then asks which wine, takes a shipping address, walks through verification, pays, and ships real wine to your door, all gated by the gate config above.

<Note>
  This needs an agent that can make HTTP calls; Claude Code, a Slack/Discord agent like OpenClaw, or any custom SDK-driven agent. The chat-only surfaces (chatgpt.com, claude.ai) won't work.
</Note>

## Next steps

* [Compliance gating reference](/compliance-gating); policy fields deep dive
* [AgentScore Commerce (Node.js) reference](/integrations/node-commerce); all 5 framework adapters (Hono, Express, Fastify, Next.js, Web Fetch) plus payment + discovery + challenge + Stripe multichain helpers
* [AgentScore Commerce (Python) reference](/integrations/python-commerce); all 6 framework adapters (FastAPI, Flask, Django, AIOHTTP, Sanic, generic ASGI) plus payment + discovery + challenge + Stripe multichain helpers
* [API Reference: /v1/assess](/api-reference/post-assess); what the gate calls under the hood
