Skip to main content
If your service sells regulated goods, handles PII, or needs to know who’s paying; AgentScore gives you identity verification for the agents and operators interacting with your API.
Want a step-by-step tutorial using Martin Estate’s wine commerce API as the worked example? See the Agent Commerce Quickstart; it walks through the AgentScore Gate middleware pattern end-to-end.

How it works

  1. An agent requests a purchase or action from your service
  2. Your service calls AgentScore to verify the operator’s identity
  3. AgentScore returns allow or deny based on your compliance policy
  4. If denied, the operator can self-serve verify via a URL you provide
You define the policy. AgentScore enforces it.

Define your compliance policy

A policy specifies what identity checks the operator must pass:
{
  "require_kyc": true,
  "require_sanctions_clear": true,
  "min_age": 21,
  "allowed_jurisdictions": ["US"]
}
FieldTypeDescription
require_kycbooleanOperator must have completed identity verification
require_sanctions_clearbooleanOperator must pass NAME-based sanctions screening on their KYC identity (separate from wallet-address OFAC screening, which fires automatically when a signer is in the request — see POST /v1/assess for the signer_sanctions block)
min_agenumberMinimum age (18 or 21)
blocked_jurisdictionsstring[]ISO country codes to block
allowed_jurisdictionsstring[]ISO country codes to allow (denies all others)

Check identity at transaction time

When an agent sends a request to your service, extract the identity from the headers and call POST /v1/assess:
import { AgentScore } from '@agent-score/sdk';

const agentscore = new AgentScore({ apiKey: process.env.AGENTSCORE_API_KEY });

const policy = {
  require_kyc: true,
  require_sanctions_clear: true,
  min_age: 21,
  allowed_jurisdictions: ['US'],
};

// In your request handler
const walletAddress = req.headers['x-wallet-address'];
const operatorToken = req.headers['x-operator-token'];

const result = walletAddress
  ? await agentscore.assess(walletAddress, { policy })
  : await agentscore.assess(null, { operatorToken, policy });

if (result.decision === 'deny') {
  return res.status(403).json({
    error: 'compliance_denied',
    reasons: result.decision_reasons,
    verify_url: result.verify_url,
  });
}

// Identity verified, proceed with transaction
Or use AgentScore Gate middleware for automatic enforcement:
import { Hono } from 'hono';
import { agentscoreGate } from '@agent-score/commerce/identity/hono';

const app = new Hono();
app.use('*', agentscoreGate({
  apiKey: process.env.AGENTSCORE_API_KEY!,
  requireKyc: true,
  requireSanctionsClear: true,
  minAge: 21,
  allowedJurisdictions: ['US'],
}));

Per-product policy + soft mode (commerce SDK)

The single-gate setup above attaches one policy to a route. Multi-product merchants where each item has different compliance needs (regulated wine ⨉ free-to-everyone merch ⨉ high-value print that wants KYC as a fraud signal but won’t block the sale) want the policy per product, not per route. Both agentscore-commerce (Python) and @agent-score/commerce (Node) ship parallel helpers for this. Field names follow each language’s convention (snake_case in Python, camelCase in Node) but the shape is identical. Python (agentscore_commerce.identity.policy):
  • PolicyBlock: typed shape carrying enforcement (hard | soft | absent), require_kyc, require_sanctions_clear, min_age, allowed_jurisdictions, allowed_shipping_countries, allowed_shipping_states. Vendors usually source these from a database row (one column per field).
  • build_gate_from_policy(policy, *, api_key); translates a block into an AgentScoreGate. Returns None when the block has no enforcement set, signalling “no gate; identity_status=‘anonymous’”.
  • run_gate_with_enforcement(request, gate, *, enforcement); runs the gate. On hard denial it returns status="denied" with a denial_status and denial_body for the caller to propagate. On soft denial it swallows the 403 and returns status="unverified" so the order completes with a degraded identity stamp. On success: status="verified". No gate: status="anonymous".
Node (@agent-score/commerce/identity/policy):
  • PolicyBlock: same fields in camelCase (enforcement, requireKyc, requireSanctionsClear, minAge, allowedJurisdictions, allowedShippingCountries, allowedShippingStates).
  • buildGateFromPolicy(policy, { apiKey }): translates a block into the options object the per-framework agentscoreGate(...) accepts (the Node SDK builds gates per framework; Hono, Express, Fastify, Next.js, Web; so the policy module emits options rather than a constructed gate, mirroring python’s build_gate_from_policy verb pattern). Returns null when no enforcement.
  • runGateWithEnforcement(enforcement, runGate): wraps the per-framework middleware in the hard/soft enforcement runner. The vendor passes a runGate adapter that resolves to { ok: true } on accept or { ok: false, status, body } on deny; the runner returns a structured GateResult.
In both languages, also:
  • shipping{Country,State}Allowed(...): per-product shipping allowlists. Country list is hard-enforced regardless of identity strictness; state list only fires for US shipments (e.g. wine).
Three modes per product:
enforcementIdentity required?On gate denialUse case
hardyespropagate the gate’s 403 (today’s regulated path)wine, cannabis, anything regulated
softoffered, not requiredswallow + stamp identity_status="unverified" on orderrequest KYC for fraud signal but accept anonymous sales
absent / nullnogate never fires; identity_status="anonymous"unregulated merch, ship anywhere
Persist identity_status on each order row (verified | unverified | anonymous) so ops/analytics can distinguish soft passes from hard passes from anonymous sales. A typical multi-product setup mixes enforcement modes; e.g. a regulated wine SKU with enforcement="hard" (KYC + 21 + US-only state allowlist) alongside unregulated merch (tee, sticker pack) with no policy. Single-file runnable examples: per_product_policy_merchant.py (Python) and per-product-policy-merchant.ts (Node).

Handle unverified operators

When an operator isn’t verified, the assess response includes a verify_url. Return it to the agent so the operator can self-serve:
{
  "decision": "deny",
  "decision_reasons": ["kyc_required"],
  "verify_url": "https://agentscore.sh/dashboard/verify?address=0x..."
}
The agent should tell the operator: “Identity verification is required. Visit [verify_url] to get verified.” For a smoother flow, create a verification session before returning the deny. This lets the agent poll for the result instead of requiring the operator to copy-paste credentials:
// No identity provided, create a verification session
const session = await agentscore.createSession({
  context: 'purchase',
  product_name: 'Premium API Access',
});

return res.status(403).json({
  error: 'identity_required',
  verify_url: session.verify_url,
  session_id: session.session_id,
  poll_secret: session.poll_secret,
  poll_url: session.poll_url,
  agent_instructions: {
    action: 'poll_for_credential',
    steps: [
      'Direct the user to visit verify_url',
      'Poll poll_url with X-Poll-Secret header every 5 seconds',
      'When status is "verified", extract operator_token from response',
      'Retry the original request with X-Operator-Token header',
    ],
  },
});
After the operator verifies, the agent polls and receives an operator_token. It retries the request; this time assess returns allow. The user closes the AgentScore tab; the agent finishes the transaction in the background.

What’s checked

CheckWhat it verifies
require_kycGovernment-issued photo ID via Stripe Identity
require_sanctions_clearOperator name not on OFAC/sanctions lists (OpenSanctions @ KYC), AND payment-signer wallet address not on the OFAC SDN crypto list (refreshed hourly from sdn_advanced.xml). A hit on either axis flips decision to deny.
min_ageAge bracket derived from ID (18+ or 21+)
blocked_jurisdictionsCountry from ID is not in blocked list
allowed_jurisdictionsCountry from ID is in allowed list

AgentScore Gate denial codes

When AgentScore Gate rejects a request before hitting /v1/assess, the 403 body uses one of the codes below. Every code carries a structured agent_instructions payload (JSON-encoded {action, steps, user_message}) so agents can recover deterministically from the response alone; no discovery-doc round trip required.
CodeWhenagent_instructions.action + key fields
missing_identityNeither X-Wallet-Address nor X-Operator-Token present, and no auto-session was createdprobe_identity_then_session: try wallet on signing rails → stored opc_... → session flow; agent_memory cross-merchant bootstrap hint included
identity_verification_requiredNo identity header, but the merchant’s gate auto-minted a verification session: body includes verify_url, session_id, poll_secret, poll_urldeliver_verify_url_and_poll: share verify_url with the user, poll poll_url with X-Poll-Secret until status=verified, retry with the returned opc_...
token_expiredX-Operator-Token was valid but is no longer (covers both TTL-expired and explicitly revoked: the API deliberately doesn’t disclose which). 401 body carries an auto-minted session so the agent recovers without an API key.deliver_verify_url_and_poll: same flow as identity_verification_required; poll returns a fresh opc_...
invalid_credentialX-Operator-Token doesn’t match any credential: typo, fabricated, or never minted. Permanent: retrying the same token will keep failing, no auto-session issued.switch_token_or_restart_session: try a different stored opc_..., or drop the header and re-bootstrap via the merchant’s createSessionOnMissing flow
wallet_signer_mismatchX-Wallet-Address was sent but the payment signer resolves to a different operatorresign_or_switch_to_operator_token; body also carries claimed_operator, actual_signer_operator, expected_signer, actual_signer, linked_wallets: re-sign from expected_signer (or any linked_wallets entry), or switch to X-Operator-Token
wallet_auth_requires_wallet_signingX-Wallet-Address was sent with a rail that has no wallet signature (Stripe SPT, card)switch_to_operator_token: drop X-Wallet-Address, retry with X-Operator-Token, or use a wallet-signing rail (Tempo MPP, x402 EIP-3009 on Base, or x402 SPL Token on Solana)
wallet_not_trusted/v1/assess returned deny with an UNFIXABLE policy reason (sanctions_flagged / age_insufficient / jurisdiction_restricted). Body includes reasons[].contact_support: re-verification won’t fix this; surface the merchant’s support contact. Fixable reasons (kyc_required, kyc_pending, kyc_failed) never reach this code: the gate auto-mints a verification session and re-routes to identity_verification_required with poll fields, identical UX to missing_identity. Note: 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: re-doing KYC won’t change the country).
payment_requiredThe merchant’s AgentScore plan doesn’t include /v1/assess. Merchant-side misconfig: not recoverable agent-side.contact_merchant: surface to the merchant via their support channel; no agent-side fix available
api_errorAgentScore API failure. Body’s agent_instructions.action discriminates: retry_with_backoff for transient 5xx / network timeout, contact_merchant for a 429 (merchant-side issue).Read agent_instructions.action. For retry_with_backoff: exponential backoff over 5–30s, surface to user after ~5min sustained. For contact_merchant: DO NOT retry; surface the merchant’s support contact to the user. Identity headers remain valid in both cases; this is NOT a compliance denial. See errors › Retryable infra errors

Wallet-mode response fields

When identity_mode: "wallet", 402 and 403 bodies also include these fields so agents know exactly which address must sign:
FieldTypeDescription
identity_mode"wallet" | "operator_token"Which identity path was used
required_signerstringWallet address normalized per network: EVM lowercased, Solana base58 preserved verbatim. The payment MUST be signed by this address or a linked_wallets entry.
linked_walletsstring[]Same-operator sibling wallets, normalized per network. May mix EVM (0x...) and Solana (base58) for multi-chain operators; any may sign in place of required_signer.
signer_constraintstringHuman-readable explanation of the same-operator rule

Fail-open behavior (opt-in)

By default AgentScore Gate fails closed: any AgentScore-side infrastructure failure (HTTP 429, 5xx, network timeout) returns 503 to the buyer. This is the correct posture for regulated commerce; better to outage than to ship to a sanctioned wallet because our API blipped. Some merchants (low-stakes commerce, high-uptime SLAs) prefer graceful degradation. Set failOpen: true (Node) / fail_open=True (Python) to opt in. When opted in AND the failure is infra-shape, the buyer passes through and the gate state carries a degraded flag merchants can log/alert on:
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, or refund async.
    console.warn(`[gate] degraded: ${infraReason}`);
  }
  // ...
});
infraReason / infra_reason is one of:
ValueWhen
quota_exceededAgentScore returned 429
api_errorAgentScore returned 5xx (transient infra issue)
network_timeoutRequest to /v1/assess timed out or failed at the network layer
failOpen does NOT bypass compliance denials. sanctions_flagged, age_insufficient, jurisdiction_restricted, wallet_signer_mismatch, kyc_required and other real policy outcomes still return 403 regardless of the flag. failOpen only covers “we couldn’t reach AgentScore to ask,” never “AgentScore said no but we’ll allow anyway.” For Web Fetch / Next.js (createAgentScoreGate / withAgentScoreGate), the degraded + infraReason fields land directly on the GuardResult.allowed variant / handler’s gate parameter; no separate getter needed.

Recommendations by vertical

VerticalRecommended posture
Regulated commerce (alcohol, cannabis, age-gated, sanctioned-jurisdiction)failOpen: false (default): better to outage than to bypass compliance
Low-stakes commerce (cheap API calls, content access)failOpen: true is reasonable: accept the trade for uptime
Enterprise customers with tight SLAsfailOpen: true with degraded logging as the audit trail

What sellers see

Sellers receive binary decisions; allow or deny. You never see the operator’s name, address, date of birth, or ID documents. The only data exposed:
  • Verification level (none / claimed / verified)
  • Whether each policy check passed or failed
{
  "decision": "allow",
  "operator_verification": {
    "level": "kyc_verified",
    "operator_type": "individual"
  },
  "policy_result": {
    "all_passed": true,
    "checks": [
      { "rule": "require_kyc", "passed": true },
      { "rule": "require_sanctions_clear", "passed": true },
      { "rule": "min_age", "passed": true, "required": 21 },
      { "rule": "allowed_jurisdictions", "passed": true }
    ]
  }
}

Privacy

  • AgentScore does not store ID documents; they are processed by Stripe Identity and never leave Stripe
  • We store derived facts only: verification status, jurisdiction (country code), age bracket, sanctions status
  • If our database is breached, attackers see “operator X is verified, US, individual”; no identity data

Sandbox testing

Use test: true with reserved test addresses to simulate compliance scenarios:
# Always allow (verified individual, US, 21+)
curl -X POST https://api.agentscore.sh/v1/assess \
  -H "X-API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"address": "0x0000000000000000000000000000000000000001", "test": true, "policy": {"require_kyc": true, "min_age": 21}}'

# Always deny (unverified)
curl -X POST https://api.agentscore.sh/v1/assess \
  -H "X-API-Key: your-api-key" \
  -d '{"address": "0x0000000000000000000000000000000000000002", "test": true, "policy": {"require_kyc": true}}'

# Sanctions flagged
curl -X POST https://api.agentscore.sh/v1/assess \
  -H "X-API-Key: your-api-key" \
  -d '{"address": "0x0000000000000000000000000000000000000003", "test": true, "policy": {"require_sanctions_clear": true}}'

# Verified entity (DE jurisdiction)
curl -X POST https://api.agentscore.sh/v1/assess \
  -H "X-API-Key: your-api-key" \
  -d '{"address": "0x0000000000000000000000000000000000000004", "test": true, "policy": {"require_kyc": true}}'

Pricing

See pricing for plans, quotas, and which tier includes compliance gating.

Next steps

POST /v1/assess

Full assess endpoint reference.

Sessions

Create verification sessions for agent polling.

Credentials

Create and manage operator credentials.

AgentScore Passport

How operators verify their identity.