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
A single middleware call gates 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 attached, or when the buyer asks for a verification session:
conditionalAgentscoreGate (and the equivalent variant on every adapter) does exactly this for you.
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.
A buyer without an AgentScore identity can also get the verification link before preparing a payment: sending the same request with X-Verification-Session: create and no identity or payment credential runs the gate and returns the session 403 (verify_url, session_id, poll_secret, poll_url). This matters most on the Stripe Shared Payment Token rail, where the alternative is minting a token only to learn the buyer must verify first. Advertise the path in your 402 by including buildIdentityBootstrap() as identity_bootstrap (the Checkout orchestrator does this automatically).
Flask, FastAPI, Express, etc. all support the same wrap (should_run_conditional_gate in Python); the test is “is one of payment-signature / x-payment / Authorization: Payment present on this request, or does it carry X-Verification-Session: create with no identity?”
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 calls a month; paid tiers raise the quota).
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.code: "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.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.
Watch usage in the dashboard
Dashboard → Usage shows your quota used, rate limit and plan, daily API calls, and the successful versus error split. For allow/deny decisions and their reason codes, log the assess result the gate attaches to each request (getAgentScoreData(c)) alongside the order.
Wallets captured via 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 gate 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