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) is the unrestricted-goods counterpoint: same SDK, no identity gate on /purchase at all.
The pattern
Three lines of middleware config gate your route:-
Agent hits
/purchasewith anX-Operator-TokenorX-Wallet-Addressheader. The gate extracts the identity, callsPOST /v1/assesswith your policy, and either passes the request through (with assess data attached) or returns a 403 with reason codes. -
If no identity header is present, the gate auto-mints a verification session via
POST /v1/sessionsand returns a 403 carryingverify_url,session_id,poll_secret,poll_url, and agent-polling instructions. The agent opensverify_urlfor the user, pollspoll_urlwithX-Poll-Secret: {poll_secret}in the background, and retries once the user finishes Stripe Identity. Zero additional merchant code. -
If identity fails a compliance check (age, sanctions, jurisdiction), the default 403 includes the denial reasons + a
verify_urlfor resolvable cases (e.g. KYC expired).
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:
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’sPOST /purchase is the canonical integration. What follows is a trimmed version of their src/routes/purchase.ts with the exact gate setup.
1. Install
/v1/assess policy evaluation).
2. Mount AgentScore Gate
3. Access assess data in the handler
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:- Show
verify_urlto the user (it’s a ready-to-open URL with the session token embedded; don’t modify it). - While the user verifies, poll
poll_urlevery 5 seconds with headerX-Poll-Secret: {poll_secret}. - When the poll returns
status: "verified", extractoperator_tokenfrom the response. - Retry
POST /purchasewith headerX-Operator-Token: {operator_token}.
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 callsPOST /v1/assess via global.fetch. Mock it in Vitest:
The same pattern in every framework
Each adapter ships with the same option shape. Pick yours. The gate-conditional wrap 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
Next.js App Router
Fastify
Web Fetch (Cloudflare Workers / Deno / Bun / edge)
FastAPI (native Depends())
Flask
Django
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):Advanced: per-request session context
For the flagship case; where the verify page should show the specific product the agent was buying; use thegetSessionOptions and onBeforeSession hooks:
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’sopc_... token you gated on gets linked to every wallet it’s been observed paying from, across every merchant using AgentScore.
Agent-side integration
Your merchant wiring is done. On the other side of the wire, the agent calling your API needs to know how to pick an identity header, handle the new denial codes, and persist theagent_memory hint for cross-merchant reuse. That’s a separate concern; different audience, different code; and lives in a dedicated guide: Agent identity integration.
Merchants don’t need to know the agent-side details to wire the gate, but it’s worth scanning once so your llms.txt and 402 challenge copy matches what the agent guide tells agents to expect.
Observe denials in the dashboard
Every denial is logged against your API key. Open Dashboard → Usage 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
captureWallet surface at Dashboard → Identity 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 five-line config above.
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.
Next steps
- Compliance gating reference; policy fields deep dive
- AgentScore Commerce (Node.js) reference; all 5 framework adapters (Hono, Express, Fastify, Next.js, Web Fetch) plus payment + discovery + challenge + Stripe multichain helpers
- AgentScore Commerce (Python) reference; all 6 framework adapters (FastAPI, Flask, Django, AIOHTTP, Sanic, generic ASGI) plus payment + discovery + challenge + Stripe multichain helpers
- API Reference: /v1/assess; what the gate calls under the hood