Skip to main content
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) is the unrestricted-goods counterpoint: same SDK, no identity gate on /purchase at all.

The pattern

A single middleware call gates your route:
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:
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

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

2. Mount AgentScore Gate

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

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:
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:

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

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.

Common vertical policies

Wine / alcohol / age-restricted goods (Martin Estate):
Financial services / lending / regulated crypto:
Restricted exports (US):
Marketplace trust gate (non-regulated):

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:
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.
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 for the endpoint shape.

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
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