# Authentication
Source: https://docs.agentscore.com/api-reference/authentication
How to get and use API keys.
## Getting an API key
1. Sign up at [agentscore.com](https://www.agentscore.com/sign-up)
2. Go to the [dashboard](https://www.agentscore.com/dashboard)
3. Copy your API key (starts with `as_live_`)
## Using your API key
Include the key in the `X-API-Key` header on every request:
```bash cURL theme={"dark"}
curl -H "X-API-Key: as_live_your_key_here" \
https://api.agentscore.com/v1/credentials
```
```javascript Node.js theme={"dark"}
const response = await fetch("https://api.agentscore.com/v1/credentials", {
headers: { "X-API-Key": process.env.AGENTSCORE_API_KEY },
});
```
```python Python theme={"dark"}
import requests
response = requests.get(
"https://api.agentscore.com/v1/credentials",
headers={"X-API-Key": "as_live_your_key_here"},
)
```
## Error responses
### Missing or invalid key (401)
```json theme={"dark"}
{
"error": {
"code": "signup_required",
"message": "Sign up for a free account to use the AgentScore API.",
"signup_url": "https://www.agentscore.com/sign-up"
}
}
```
### Endpoint not enabled for your account (402)
```json theme={"dark"}
{
"error": {
"code": "payment_required",
"message": "This endpoint is not enabled for your account. See https://www.agentscore.com/pricing"
}
}
```
## Rate limits and quotas
See [pricing](https://www.agentscore.com/pricing) for current per-second rate limits and request quotas per tier.
When you exceed the per-second rate limit, you'll receive a `429` with `code: "rate_limited"`. When you exceed your account quota, you'll receive a `429` with `code: "quota_exceeded"`.
Rate limit headers are included in every response:
| Header | Description |
| - | - |
| `X-RateLimit-Limit` | Requests per second allowed (e.g., `5/s` or `unlimited`) |
| `X-RateLimit-Remaining` | Requests remaining in current window |
| `X-RateLimit-Reset` | Seconds until the rate limit window resets |
| `X-Quota-Limit` | Total request quota for your tier |
| `X-Quota-Used` | Requests used in current quota period |
| `X-Quota-Reset` | Date when quota resets, e.g. `2026-05-01`. Omitted for enterprise. |
Enterprise tier returns only `X-RateLimit-Limit: unlimited` with no remaining, reset, or quota headers.
## Key management
* Keys can be regenerated from the dashboard
* Each account gets one active key
* Enterprise accounts can request multiple keys
# Operator Credentials
Source: https://docs.agentscore.com/api-reference/credentials
Create, list, and revoke operator credentials for non-wallet agent identity.
## Overview
Operator credentials (`opc_...`) let agents identify their human operator without a wallet. After the operator completes KYC, they (or their agent) can create credentials via the API or MCP tools.
Credentials expire after a configurable TTL (default 24 hours). Agents can self-serve new credentials via the API.
## Create credential
```
POST /v1/credentials
X-API-Key: as_live_...
Content-Type: application/json
```
```json theme={"dark"}
{
"label": "claude-code-agent",
"ttl_days": 1
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `label` | string | No | Human-readable label (max 100 chars) |
| `ttl_days` | number | No | Time to live in days (1-365, default 1) |
| `scope` | string | No | Omit for a standard credential, which requires the account to have completed KYC. Set to `"sign_in"` for a registration-only credential with no KYC requirement: it identifies the account but fails any compliance policy, exactly as no credential would. |
### Response (201)
```json theme={"dark"}
{
"id": "uuid",
"credential": "opc_abc123...",
"prefix": "opc_abc1",
"label": "claude-code-agent",
"scope": "merchant_issued",
"expires_at": "2026-04-10T12:00:00Z",
"created_at": "2026-04-09T12:00:00Z",
"agent_memory": {
"pattern_summary": "...",
"identity_paths": { "...": "..." },
"bootstrap": { "...": "..." },
"do_not_persist_in_memory": ["operator_token", "poll_secret"],
"persist_in_credential_store": ["operator_token"]
}
}
```
The `credential` value is shown only once. Store it securely.
`scope` is `"merchant_issued"` for a standard credential or `"sign_in"`. The `agent_memory` block appears only on the account's first credential. It is a cross-merchant pattern hint; LLM-hosted agents should persist the pattern (never secrets) to long-term memory so future AgentScore-gated endpoints become returning-customer encounters. Same block is emitted on `POST /v1/sessions`, on `missing_identity` bootstrap denials (when no auto-session is created), and inline on the first-mint `402 Payment Required` from merchants that echo the hint.
### 409: KYC required
If the account has not completed identity verification, creating a standard credential returns **409 Conflict** (a `sign_in` credential has no such check):
```json theme={"dark"}
{
"error": {
"code": "kyc_required",
"message": "Complete identity verification before creating an operator credential."
},
"verify_url": "https://www.agentscore.com/dashboard/verify",
"next_steps": {
"action": "complete_kyc_then_retry",
"user_message": "Visit the verify_url, complete Stripe Identity verification, then retry this request. Any credential minted before KYC would fail at merchant compliance checks."
}
}
```
## List credentials
```
GET /v1/credentials
X-API-Key: as_live_...
```
Returns active (non-expired, non-revoked) credentials for your account, along with the account's verification status.
```json theme={"dark"}
{
"account_verification": {
"kyc_status": "verified",
"kyc_verified_at": "2026-04-07T17:13:56.525Z",
"jurisdiction": "US",
"age_verified": true,
"age_bracket": "21+",
"sanctions_clear": true,
"sanctions_checked_at": "2026-04-07T17:13:56.525Z",
"operator_type": "individual"
},
"credentials": [
{
"id": "uuid",
"prefix": "opc_abc1",
"label": "claude-code-agent",
"scope": "merchant_issued",
"expires_at": "2026-04-10T12:00:00Z",
"last_used_at": "2026-04-09T14:30:00Z",
"created_at": "2026-04-09T12:00:00Z",
"refreshable": false
}
]
}
```
The sanctions gate is exposed as a paired field rather than a raw status:
| Field | Type | Description |
| - | - | - |
| `sanctions_clear` | `boolean \| null` | `true` only when the account has been screened AND the screening is within the freshness window. `false` when the account is on a sanctions list. `null` when never screened or when the screening is stale. |
| `sanctions_checked_at` | `string \| null` | ISO-8601 timestamp of the last sanctions screening, or `null` if never screened. Lets clients apply their own freshness logic if the default window is too loose. |
`credentials[].refreshable` is `true` when a live refresh token can still mint a
replacement for that credential. The list includes an expired credential while
this holds: revoking it is what stops the renewal, and a credential that stops
listing can never be revoked.
When the account has no KYC, `account_verification` contains only the status:
```json theme={"dark"}
{
"account_verification": {
"kyc_status": "none"
},
"credentials": []
}
```
## Revoke credential
```
DELETE /v1/credentials/{id}
X-API-Key: as_live_...
```
`{id}` is either the credential's `id` from `GET /v1/credentials` or the `opc_...`
value itself, so an agent holding only the credential can revoke it.
```json theme={"dark"}
{
"id": "uuid",
"revoked": true
}
```
Revoking also revokes the refresh token minted with the credential, and every
later rotation of that token along with the credentials those rotations minted,
in the same transaction. An agent in that chain cannot mint a replacement; it
has to run a fresh Passport login.
## Usage
Pass the credential as `operator_token` in the `/v1/assess` body, or as the `X-Operator-Token` header to a merchant endpoint:
```bash theme={"dark"}
curl -X POST https://api.agentscore.com/v1/assess \
-H "X-API-Key: as_live_..." \
-H "Content-Type: application/json" \
-d '{"operator_token": "opc_abc123...", "policy": {"require_kyc": true, "min_age": 21}}'
```
Or via AgentScore Gate middleware; agents send `X-Operator-Token` alongside requests to gated services.
## Report a captured wallet
Merchants call this after a successful payment to report the signer wallet back to AgentScore. Builds a cross-merchant credential↔wallet profile that powers cross-merchant sanctions and already-verified detection on future credentials from known wallets.
```
POST /v1/credentials/wallets
X-API-Key: as_live_...
Content-Type: application/json
```
```json theme={"dark"}
{
"operator_token": "opc_abc123...",
"wallet_address": "0xabcdef1234567890abcdef1234567890abcdef12",
"network": "evm"
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `operator_token` | string | Yes | The credential the agent authenticated with on the gated endpoint. |
| `wallet_address` | string | Yes | The signer wallet recovered from the payment payload (EIP-3009 `from` for x402, Tempo MPP DID address for mppx, base58 Solana pubkey). EVM addresses are lowercased server-side; Solana base58 addresses are kept verbatim. The EVM zero address (`0x0000…0000`) is rejected with `invalid_wallet`. |
| `network` | string | Yes | Key-derivation family. `"evm"` covers every EVM chain (Base, Tempo, Ethereum, …) because EOAs share identity across them. `"solana"` is a separate namespace. |
| `idempotency_key` | string (max 200 chars) | No | Stable per-payment key (e.g., Stripe PI id, x402 tx hash). When the same key is reported again for the same `(credential, wallet, network)`, the server no-ops: agent retries of the same logical payment don't inflate `transaction_count`. Values longer than 200 characters are truncated server-side. |
### Response
```json theme={"dark"}
{
"associated": true,
"first_seen": true
}
```
`first_seen` is `true` the first time this `(credential, wallet, network)` tuple is reported, and `false` on subsequent observations (internal transaction\_count is incremented).
When an `idempotency_key` matches the most recent capture's key, the response is:
```json theme={"dark"}
{
"associated": true,
"first_seen": false,
"deduped": true
}
```
and no state change happens (no `transaction_count` bump, no `last_seen` update).
### Auth & tier
Any authenticated tier (free or paid). Rate limits still apply. Call this fire-and-forget; a failed capture must never block a payment response.
### Errors
| Status | Code | Meaning |
| - | - | - |
| 400 | `bad_request` | Missing `operator_token`, `wallet_address`, or `network`. |
| 400 | `invalid_network` | Network must be `"evm"` or `"solana"`. |
| 400 | `invalid_wallet` | Malformed wallet address for the given network, or the EVM zero address. |
| 401 | `signup_required` | Missing or invalid API key. |
| 401 | `invalid_credential` | Operator credential not found, expired, or revoked. Returned as 401 (rather than 404) to prevent credential enumeration. |
| 429 | `rate_limited` | Rate limit exceeded. |
### SDK helpers
* **node-sdk**: `client.associateWallet({ operatorToken, walletAddress, network })`
* **python-sdk**: `client.associate_wallet(operator_token, wallet_address, network)` (plus async `aassociate_wallet`)
* **node-commerce** identity adapters expose `captureWallet(ctx, { walletAddress, network })` which reads the `operator_token` from the gate state; no need to re-pass it.
# Errors
Source: https://docs.agentscore.com/api-reference/errors
Error codes and response format.
## Error response format
All errors follow the same structure:
```json theme={"dark"}
{
"error": {
"code": "error_code",
"message": "Human-readable description."
}
}
```
Some errors include additional fields (e.g., `signup_url`).
## Error codes
| HTTP Status | Code | Description |
| - | - | - |
| 400 | `bad_request` | Invalid request parameters |
| 400 | `invalid_address` | Invalid wallet address |
| 400 | `invalid_identity` | Neither address nor operator\_token provided |
| 400 | `invalid_test_address` | Test mode requires a reserved test address |
| 401 | `signup_required` | Missing or invalid API key |
| 401 | `unauthorized` | Missing X-Poll-Secret header on session poll |
| 401 | `invalid_credential` | Operator credential doesn't exist (typo, never minted, fabricated). Permanent: the response body carries no auto-session because the agent likely has another valid token to try first. |
| 401 | `token_expired` | Operator credential was valid but is now revoked or past its TTL (the API doesn't disclose which). The 401 body carries an auto-minted verification session (`verify_url` + `session_id` + `poll_secret`) so the user can re-verify and the agent can poll for a fresh `operator_token`. |
| 402 | `payment_required` | Endpoint not enabled for this account |
| 403 | `account_cancelled` | Account has been cancelled |
| 404 | `not_found` | Resource not found |
| 429 | `rate_limited` | Rate limit exceeded (per-second) |
| 429 | `quota_exceeded` | Account quota exceeded |
| 500 | `internal_error` | Unexpected server error |
| 5xx | `api_error` | Transient infra failure on `/v1/assess`; body carries `agent_instructions` retry-with-backoff envelope |
## Common errors
### No API key (401)
```json theme={"dark"}
{
"error": {
"code": "signup_required",
"message": "Sign up for a free account to use the AgentScore API.",
"signup_url": "https://www.agentscore.com/sign-up"
}
}
```
### Endpoint not enabled for account (402)
```json theme={"dark"}
{
"error": {
"code": "payment_required",
"message": "This endpoint is not enabled for your account. See https://www.agentscore.com/pricing"
}
}
```
### Account canceled (403)
```json theme={"dark"}
{
"error": {
"code": "account_cancelled",
"message": "Account is cancelled"
}
}
```
### Rate limited (429)
```json theme={"dark"}
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded. Try again later."
}
}
```
The `Retry-After` header indicates how many seconds to wait.
### Quota exceeded (429)
```json theme={"dark"}
{
"error": {
"code": "quota_exceeded",
"message": "Account quota exceeded. See https://www.agentscore.com/pricing"
}
}
```
`quota_exceeded` is a per-account cap; distinct from `rate_limited` (per-second sliding window). Don't retry; the cap won't lift through retry alone.
Commerce SDKs can opt in to graceful degradation on this code via `failOpen` / `fail_open`; see [compliance-gating › Fail-open behavior](/compliance-gating#fail-open-behavior-opt-in). When fail-open is **off**, the gate's 503 response carries `agent_instructions` with `action: "contact_merchant"` (NOT `retry_with_backoff`), so agents surface the issue to the user instead of looping on a permanently-failing endpoint.
## Retryable infra errors (`api_error`)
503 responses with `error.code: "api_error"` carry a structured `agent_instructions` envelope. The `action` discriminates the cause and tells the agent how to recover:
### Transient 5xx / network timeout: `retry_with_backoff`
```json theme={"dark"}
{
"error": {
"code": "api_error",
"message": "Upstream service temporarily unavailable."
},
"agent_instructions": {
"action": "retry_with_backoff",
"steps": [
"Wait 5-30 seconds before retrying",
"Use exponential backoff if subsequent retries also fail",
"If the error persists for over 5 minutes, surface the issue to the user"
],
"user_message": "AgentScore is experiencing a transient issue. Retrying shortly."
}
}
```
### Merchant-side 429: `contact_merchant`
```json theme={"dark"}
{
"error": {
"code": "api_error",
"message": "AgentScore identity verification is unavailable for this merchant. Retrying will not help."
},
"agent_instructions": {
"action": "contact_merchant",
"steps": [
"AgentScore identity verification is unavailable for this merchant. This is a merchant-side issue and is NOT recoverable via retry.",
"Do not retry: the same 503 will be returned until the merchant resolves the issue on their side.",
"Surface to the user with the merchant's support contact. The merchant (not the agent) needs to act."
],
"user_message": "This merchant's identity verification is temporarily unavailable. Try again later, or contact the merchant directly."
}
}
```
Agents must read `agent_instructions.action` before retrying. **Do NOT loop on `api_error` blindly**; when the action is `contact_merchant`, the same 503 will keep returning until the merchant resolves the issue on their side.
The Node and Python commerce SDKs forward this envelope to buyers as part of the 503 response when `failOpen` is **off** (the default). When `failOpen` is **on**, the SDKs swallow the 5xx and pass the buyer through with `degraded: true` + `infra_reason: "api_error" | "quota_exceeded" | "network_timeout"` on the gate state; see [compliance-gating › Fail-open behavior](/compliance-gating#fail-open-behavior-opt-in).
# API Overview
Source: https://docs.agentscore.com/api-reference/overview
Base URL, authentication, tiers, and rate limits.
## Base URL
All API requests use the following base URL:
```
https://api.agentscore.com
```
## Authentication
Every request must include an API key in the `X-API-Key` header:
```bash theme={"dark"}
curl -H "X-API-Key: as_live_your_key_here" \
https://api.agentscore.com/v1/credentials
```
Get your API key from the [dashboard](https://www.agentscore.com/dashboard).
Requests without a valid API key receive:
```json theme={"dark"}
{
"error": {
"code": "signup_required",
"message": "Sign up for a free account to use the AgentScore API.",
"signup_url": "https://www.agentscore.com/sign-up"
}
}
```
## Tiers
See [pricing](https://www.agentscore.com/pricing) for current tier names, quotas, and rate-limit numbers. Endpoint availability shown below by paid vs. free; tier names in this table reflect the live pricing page.
| | Free | Paid | Enterprise |
| - | - | - | - |
| **POST /v1/assess** | Yes | Yes | Yes |
| **POST /v1/sessions** | Yes | Yes | Yes |
| **GET /v1/sessions/:token** | Yes | Yes | Yes |
| **Compliance policy (assess)** | Yes | Yes | Yes |
| **Custom policies** | No | No | Yes |
| **Support** | Community | Email / Slack | Dedicated |
The `/v1/assess` and `/v1/sessions` identity endpoints are open to all tiers including Free, gated by monthly quota rather than by tier; see [pricing](https://www.agentscore.com/pricing) for quotas.
## Endpoints
| Method | Path | Description | Min Tier |
| - | - | - | - |
| POST | `/v1/assess` | Identity assessment with policy evaluation | Free+ |
| POST | `/v1/sessions` | Create verification session | Free+ |
| GET | `/v1/sessions/:token` | Poll session status | Free+ |
| POST | `/v1/credentials` | Create operator credential | Free+ |
| GET | `/v1/credentials` | List operator credentials | Free+ |
| DELETE | `/v1/credentials/{id}` | Revoke operator credential | Free+ |
| POST | `/v1/credentials/wallets` | Report a wallet seen paying under a credential | Free+ |
## Response format
All responses are JSON. Successful responses return HTTP 200. Errors return the appropriate HTTP status code with an `error` object:
```json theme={"dark"}
{
"error": {
"code": "error_code",
"message": "Human-readable description."
}
}
```
## Versioning
The API is versioned via the URL path (`/v1/`). Breaking changes will be introduced under a new version prefix.
# POST /v1/assess
Source: https://docs.agentscore.com/api-reference/post-assess
POST https://api.agentscore.com/v1/assess
Identity verification gate with policy evaluation. Available on all tiers, metered by quota.
Available on every tier including Free, metered by your monthly quota ([pricing](https://www.agentscore.com/pricing)).
## Request
### Headers
| Header | Required | Description |
| - | - | - |
| `X-API-Key` | Yes | Your API key (any tier, including Free) |
| `Content-Type` | Yes | `application/json` |
### Body
Provide either `address` (wallet) or `operator_token` (credential). At least one is required.
```json theme={"dark"}
{
"address": "0xdb5aa553feeb2c3e3d03e8360b36fb0f7e480671",
"chain": "base",
"policy": {
"require_kyc": true,
"require_sanctions_clear": true,
"min_age": 21,
"blocked_jurisdictions": ["KP", "IR"]
}
}
```
Or with an operator credential (for non-wallet agents):
```json theme={"dark"}
{
"operator_token": "opc_abc123...",
"policy": {
"require_kyc": true,
"min_age": 21
}
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `address` | string | One of address/operator\_token | Wallet address: EVM (`0x...` 40-hex) or Solana (base58, 32–44 chars). Network is auto-detected from the address format. |
| `operator_token` | string | One of address/operator\_token | Operator credential (`opc_...`) for non-wallet agents. Credential resolves to an account's KYC status. |
| `chain` | string | No | Chain for the wallet (for example `base`). Must be a supported chain. When the response carries `verify_url` for a wallet `address`, the URL includes this chain. Defaults to `base` for EVM addresses and `solana` for Solana addresses. |
| `policy` | object | No | Policy rules for allow/deny decision |
| `policy.require_kyc` | boolean | No | Require operator KYC verification |
| `policy.require_sanctions_clear` | boolean | No | Require clean sanctions status |
| `policy.min_age` | integer | No | Minimum age, 0 to 130. Verified ages are recorded as `18+` or `21+` brackets, so a value above 21 always denies. |
| `policy.blocked_jurisdictions` | string\[] | No | ISO country codes to block (blocklist) |
| `policy.allowed_jurisdictions` | string\[] | No | ISO country codes to allow (allowlist: only these pass) |
| `signer` | object | No | Server-side wallet-signer verdicts. When present, the API resolves `signer.address` to its operator and emits both a `signer_match` block (wallet-binding) and a `signer_sanctions` block (OFAC SDN wallet check) on the response: lets commerce gates collapse multiple follow-up assess calls into one round trip. |
| `signer.address` | string \| null | If `signer` set | Recovered payment-signer wallet. `null` indicates the rail carries no wallet signature (Stripe SPT, card): produces `signer_match.kind = "wallet_auth_requires_wallet_signing"`. |
| `signer.network` | string | If `signer` set | Key-derivation family of the signer wallet. `evm` or `solana`. |
| `test` | boolean | No | Enable sandbox/test mode with reserved addresses |
## Test mode
Set `"test": true` to get controlled responses without hitting the database, rate limits, or billing. Test calls are not logged to the production audit trail.
Test mode only works with these reserved addresses:
| Address | Behavior |
| - | - |
| `0x0000000000000000000000000000000000000001` | `allow`: verified individual, US, 21+ |
| `0x0000000000000000000000000000000000000002` | `deny`: kyc\_required, includes `verify_url` |
| `0x0000000000000000000000000000000000000003` | `deny`: sanctions\_flagged |
| `0x0000000000000000000000000000000000000004` | `allow`: verified individual, DE |
| `0x0000000000000000000000000000000000000005` | `allow`: verified individual, US, 18+ (denies on `min_age: 21`) |
| `0x0000000000000000000000000000000000000006` | `allow`: verified individual, IR (denies on `blocked_jurisdictions: ["IR"]`) |
| `0x0000000000000000000000000000000000000007` | `allow`: verified individual, BR (denies on `allowed_jurisdictions: ["US"]`) |
Using `test: true` with any other address returns a 400 error.
Test responses include `"test": true` in the response body. If you include a `policy` object, the test fixtures evaluate the policy rules against their built-in verification data and return realistic `decision`, `decision_reasons`, and `policy_result` fields.
```json theme={"dark"}
{
"address": "0x0000000000000000000000000000000000000001",
"test": true,
"policy": {
"require_kyc": true,
"min_age": 21
}
}
```
## Response
```json theme={"dark"}
{
"decision": "allow",
"decision_reasons": [],
"policy_result": {
"all_passed": true,
"checks": [
{ "rule": "require_kyc", "passed": true, "required": "verified", "actual": "verified" },
{ "rule": "require_sanctions_clear", "passed": true, "required": "clear", "actual": "clear" }
]
},
"explanation": [
{
"rule": "require_kyc",
"passed": true,
"required": "verified",
"actual": "verified",
"message": "Identity verification is complete",
"how_to_remedy": null
},
{
"rule": "require_sanctions_clear",
"passed": true,
"required": "clear",
"actual": "clear",
"message": "Sanctions screening is clear",
"how_to_remedy": null
}
],
"identity_method": "wallet",
"operator_verification": {
"level": "kyc_verified",
"operator_type": "individual",
"verified_at": "2026-03-02T14:00:00Z"
},
"resolved_operator": "0xdb5aa553feeb2c3e3d03e8360b36fb0f7e480671",
"linked_wallets": [
"0xdb5aa553feeb2c3e3d03e8360b36fb0f7e480671",
"0x1111111111111111111111111111111111111111"
]
}
```
The assess endpoint returns identity verification data only.
### Wallet-mode response fields
When the request was authenticated by `X-Wallet-Address` (or `address` was supplied), the response includes:
| Field | Type | Description |
| - | - | - |
| `resolved_operator` | string | Canonical operator wallet (the earliest claimed wallet, or the lexically-smallest active-captured wallet). All same-operator sibling wallets resolve to this address. |
| `linked_wallets` | string\[] | All same-operator sibling wallets, normalized per network: EVM addresses lowercased, Solana base58 preserved verbatim. Includes both wallets claimed via identity verification and wallets captured via `POST /v1/credentials/wallets`. May contain a mix of EVM and Solana addresses for multi-chain operators. Merchants doing wallet-signer-match checks should accept a payment signed by any address in this list, regardless of chain. |
| `operator_handle` | string | Returned only on the `operator_token` path. A stable, pairwise handle (`oph_...`) for the account behind the credential: see below. |
| `signer_match` | object | Returned only when the request supplied `signer`. Server-side wallet-signer-match verdict: see below. |
| `signer_sanctions` | object | Returned only when the request supplied `signer`. Server-side OFAC SDN wallet-address verdict: see below. |
### `operator_handle`: the identity to key durable state on
When the request identifies the buyer with an `operator_token`, the response carries a stable pairwise handle for the account behind it.
Key merchant state that must outlive a credential (prepaid balances above all) on this handle rather than on the token. An `opc_` access token lives 24 hours and rotates silently off its refresh credential, so state keyed on the token instance is stranded every time one rotates, and revoking a leaked token would forfeit whatever it was holding. The handle derives from the account, so rotation, expiry and revocation cost you nothing.
It is **pairwise**: derived per (account, calling merchant), so the same buyer presents an unrelated handle to every merchant and handles cannot be correlated across them.
It carries **no compliance meaning**. A registration-only credential resolves to a handle exactly like a fully verified one does, so read `decision` and `policy_result` for policy questions. The handle answers only "which account is this?".
It is returned on denials too, since it is identity rather than a verdict, which lets you record a denial against the same buyer you would have served.
```json theme={"dark"}
{
"decision": "allow",
"decision_reasons": [],
"identity_method": "operator_token",
"operator_handle": "oph_9f2c41ab77e5d0836b1c4e9a2f5d7c08b3a6e124"
}
```
Because it rides this response, there is no separate lookup to make and no additional request metered against your account.
### `signer_match` response (when `signer` was supplied)
When the request body included `signer`, the response carries a `signer_match` block describing whether the supplied signer wallet resolves to the same operator as the claimed `address`. Commerce gates get the verdict in the same round trip as the assess call.
```json theme={"dark"}
{
"decision": "allow",
"decision_reasons": [],
"resolved_operator": "0xclaimed...",
"linked_wallets": ["0xclaimed...", "0xsigner..."],
"signer_match": {
"kind": "pass",
"claimed_operator": "0xclaimed...",
"signer_operator": "0xclaimed..."
},
"identity_method": "wallet"
}
```
| Field | Type | Description |
| - | - | - |
| `signer_match.kind` | string | `pass` (claimed and signer resolve to the same operator, or are byte-equal); `wallet_signer_mismatch` (operators differ); `wallet_auth_requires_wallet_signing` (request supplied `signer.address: null`: agent should switch to operator-token auth). |
| `signer_match.claimed_operator` | string \| null | Operator the claimed wallet resolves to. `null` if unlinked. |
| `signer_match.signer_operator` | string \| null | Operator the signer wallet resolves to. `null` if unlinked. |
| `signer_match.expected_signer` | string | Echoed on `wallet_signer_mismatch`: the claimed wallet, normalized. |
| `signer_match.actual_signer` | string | Echoed on `wallet_signer_mismatch`: the signer wallet, normalized. |
| `signer_match.linked_wallets` | string\[] | Same-operator linked wallets the agent could re-sign from to satisfy the claim. Omitted when the list is empty (the deny-guard zeroes the list under a top-level `deny`, but it's also omitted for any non-mismatch verdict). |
| `signer_match.agent_instructions` | string | JSON-encoded `{action, steps, user_message}` envelope for SDK denial bodies. Spread verbatim into the gate's 403 response. |
### `signer_sanctions` response (when `signer` was supplied)
In addition to the wallet-binding verdict, the same `signer` request field powers a wallet-address OFAC SDN check. AgentScore pulls the OFAC SDN Advanced XML hourly into an indexed `ofac_sanctioned_addresses` table; the lookup is keyed on the format-classified address family (`evm`, `solana`, ...). The verdict slots in under `signer_sanctions` alongside `signer_match`.
Three terminal shapes:
```json theme={"dark"}
{ "signer_sanctions": { "status": "clear" } }
```
```json theme={"dark"}
{
"signer_sanctions": {
"sanctioned": true,
"ofac_label": "ETH",
"sdn_uid": "19011",
"listed_at": "2018-05-03"
}
}
```
```json theme={"dark"}
{ "signer_sanctions": { "status": "unavailable" } }
```
| Field | Type | Description |
| - | - | - |
| `signer_sanctions.status` | string | `clear` (address not on the list) OR `unavailable` (the lookup itself failed; the API fail-closes; see below). |
| `signer_sanctions.sanctioned` | boolean | Present and `true` ONLY on a hit; omitted on `clear` / `unavailable`. |
| `signer_sanctions.ofac_label` | string | The Digital Currency Address label OFAC published the hit under (`ETH`, `XBT`, `USDT`, `SOL`, ...). Investigation-history metadata; the gate enforcement is keyed on `family`, not label. |
| `signer_sanctions.sdn_uid` | string | The SDN entry's Identity ID. Same `sdn_uid` may surface multiple addresses (one entity, multiple wallets); join key for audit. |
| `signer_sanctions.listed_at` | string \| null | ISO date OFAC initially designated the entity. May be `null` if upstream omits the date. |
**Unconditional enforcement on the signer block.** When a `signer` is supplied in the request body, `signer_sanctions` enforcement is automatic; no policy flag opts in or out. SDN hit (`sanctioned === true`) → `decision: deny` with `decision_reasons: ["sanctions_flagged"]`. Lookup failure (`status === "unavailable"`) → `decision: deny` with `decision_reasons: ["sanctions_check_unavailable"]`. The asymmetric cost (falsely allowing a sanctioned settle is an OFAC strict-liability violation, falsely denying a clean buyer is just bad UX) justifies the fail-closed posture. Wallet-OFAC SDN screening is strict-liability under US law; merchants cannot legally opt out of receiving funds from SDN-listed wallets, so the API does not expose an opt-out.
This is distinct from `policy.require_sanctions_clear`, which enforces the NAME-based sanctions screen on the resolved operator's KYC identity (sourced from the KYC vendor at verification time). `require_sanctions_clear` is opt-in; the wallet-address `signer_sanctions` enforcement is not.
### Deny response
When a policy check fails, the response includes `verify_url` if the denial is resolvable through verification:
```json theme={"dark"}
{
"decision": "deny",
"decision_reasons": ["kyc_required"],
"policy_result": {
"all_passed": false,
"checks": [
{ "rule": "require_kyc", "passed": false, "required": "verified", "actual": "none" }
]
},
"explanation": [
{
"rule": "require_kyc",
"passed": false,
"required": "verified",
"actual": "none",
"message": "Identity verification has not been completed for this account",
"how_to_remedy": "Direct the user to https://www.agentscore.com/dashboard/verify to complete Stripe Identity verification."
}
],
"identity_method": "wallet",
"operator_verification": { "level": "none" },
"verify_url": "https://www.agentscore.com/dashboard/verify?address=0x5678...&chain=base"
}
```
### Decision values
| Value | Meaning |
| - | - |
| `allow` | All policy rules passed, or no policy was specified |
| `deny` | One or more policy rules failed |
### Decision reasons
When all policy checks pass, `decision_reasons` is an empty array.
### Explanation
The `explanation` array is returned alongside `policy_result` whenever a policy is provided. Each element describes one rule evaluation:
| Field | Type | Description |
| - | - | - |
| `rule` | string | The policy rule name (e.g., `require_kyc`) |
| `passed` | boolean | Whether the rule passed |
| `required` | string | The required value for the rule |
| `actual` | string | The actual value found |
| `message` | string | Human-readable description of the result |
| `how_to_remedy` | string \| null | Action to resolve a failure, or `null` if the denial is not fixable (e.g., age below minimum on a verified ID) or the rule passed |
### Deny reasons
When a policy check fails, the following reason codes are used:
| Reason | When | `verify_url`? |
| - | - | - |
| `kyc_required` | Not verified | Yes |
| `kyc_pending` | KYC in progress | No |
| `kyc_failed` | KYC failed | Yes (can retry) |
| `sanctions_flagged` | Account-level sanctions match (KYC name screening under `require_sanctions_clear`) OR wallet-OFAC SDN hit on the `signer` block (always-on; no policy flag required) | No |
| `sanctions_check_unavailable` | Wallet-OFAC lookup failed (`signer_sanctions.status === "unavailable"`); fail-closed posture, always-on | No |
| `age_insufficient` | Below `min_age` | No |
| `jurisdiction_restricted` | Blocked country | No |
When a compliance denial includes a `verify_url`, the response contains a URL the agent's operator can visit to complete or retry verification. This field is only present on compliance denials that are resolvable through verification.
For merchant-initiated flows where you need to create a verification session and poll for completion, see [POST /v1/sessions](/api-reference/post-sessions).
`require_sanctions_clear` checks whether the operator has been flagged in sanctions screening, which happens during KYC verification. For operators who have not completed KYC, this check fails with `kyc_required`. Sanctions screening is performed automatically as part of the KYC process.
`require_sanctions_clear` enforces a sanctions-screening freshness window (default 90 days, configurable). The check passes only when the operator was screened within that window:
* **Screened-clear, fresh**: passes
* **Screened-clear, older than the freshness window**: denied with `kyc_required` (re-running KYC re-screens)
* **Unscreened** (no screening on record, or never KYC'd): denied with `kyc_required`
* **Sanctions hit**: denied with `sanctions_flagged`
The remediation for stale or unscreened operators is the same: complete (or re-run) KYC at the `verify_url` returned in the deny response.
The `actual` field on a `require_sanctions_clear` check reflects the effective verdict, not the raw column:
* `clear`: screened within the freshness window
* `flagged`: real sanctions hit (no remediation)
* `unscreened`: no screening on record, or screening is older than the freshness window
* `none`: operator has no verification record at all
### No policy provided
When you omit the `policy` field, the endpoint returns `decision: "allow"` with `decision_reasons: ["no_policy_applied"]`. Use this to read the identity fields without enforcement.
## Response headers
Every `/v1/assess` response (success and 429) carries account-level quota observability headers when the account has a per-period quota:
| Header | Description |
| - | - |
| `X-Quota-Limit` | Total quota for the current period (numeric) |
| `X-Quota-Used` | Current usage within the period (numeric) |
| `X-Quota-Reset` | Date when the monthly quota period resets, e.g. `2026-05-01`. Omitted for Enterprise / unlimited tiers. |
Direct HTTP callers can read these from `Response.headers`. The Node and Python SDKs surface them on `AssessResponse.quota` (via `assess()` / `aassess()`) so consumers can monitor approach-to-cap proactively (warn at 80%, alert at 95%) before a 429. See [`@agent-score/sdk`](https://www.npmjs.com/package/@agent-score/sdk) and [`agentscore-py`](https://pypi.org/project/agentscore-py/) READMEs for usage.
Accounts without a per-period quota (Enterprise / unlimited tiers) receive no `X-Quota-*` headers.
## Error responses
| Status | `error.code` | When | Retry posture |
| - | - | - | - |
| `400` | `invalid_identity` | Neither a valid wallet address nor `operator_token` was provided (malformed/missing identity). Note: `invalid_address` is returned only in test mode for a malformed reserved address. | Don't retry: fix the request |
| `400` | `invalid_policy` | A `policy` rule has the wrong type or an out-of-range value; the body names the offending `field` | Don't retry: fix the policy |
| `400` | `unsupported_chain` | `chain` is not a supported network | Don't retry: fix the request |
| `400` | `wallet_signer_mismatch` | Both `address` and `operator_token` were supplied and resolve to different accounts | Don't retry: send one identity, or a matching pair |
| `401` | `signup_required` | Missing/invalid `X-API-Key` header | Don't retry: sign up for a free account, then fix the key |
| `401` | `invalid_credential` | `operator_token` does not exist | Don't retry with the same token: switch tokens or start a session |
| `401` | `token_expired` | `operator_token` was revoked or is past its TTL; the body carries an auto-minted verification session | Complete the session, then retry with the new token |
| `429` | `rate_limited` | Per-second rate limit hit | Retry after `Retry-After` |
| `429` | `quota_exceeded` | Account-level cap reached | Don't retry: the cap won't lift through retry alone. Body carries `agent_instructions`; commerce SDKs can opt in to fail-open on this code via `failOpen` / `fail_open`. |
| `5xx` | `api_error` | Transient AgentScore infra failure | Retry with backoff per `agent_instructions` envelope (see [errors](/api-reference/errors)). Commerce SDKs surface this via `failOpen` / `fail_open` opt-in. |
The 429 + 5xx classes are what the Node and Python commerce SDKs gate on when `failOpen` is enabled; see [compliance-gating › Fail-open behavior](/compliance-gating#fail-open-behavior-opt-in).
## Example: middleware integration
```javascript theme={"dark"}
const assessment = await fetch("https://api.agentscore.com/v1/assess", {
method: "POST",
headers: {
"X-API-Key": process.env.AGENTSCORE_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
address: walletAddress,
policy: { require_kyc: true },
}),
}).then((r) => r.json());
if (assessment.decision === "deny") {
return res.status(403).json({
error: "wallet_not_trusted",
reasons: assessment.decision_reasons,
});
}
```
# POST /v1/sessions
Source: https://docs.agentscore.com/api-reference/post-sessions
POST https://api.agentscore.com/v1/sessions
Create a verification session for identity bootstrapping. Available on all tiers, gated by monthly quota.
This endpoint is available on all tiers, gated by monthly quota ([pricing](https://www.agentscore.com/pricing)).
## Overview
Verification sessions let merchants bootstrap identity for agents that don't have a wallet or operator credential. When an agent tries to perform a gated action without identity, the merchant creates a session and returns the verification URL to the agent.
The session returns two secrets:
* **`session_id`**: goes in the verification URL (user-facing)
* **`poll_secret`**: given to the agent for polling (never in URLs)
## Request
### Headers
| Header | Required | Description |
| - | - | - |
| `X-API-Key` | Yes | Your API key (any tier, including Free) |
| `Content-Type` | Yes | `application/json` |
### Body
```json theme={"dark"}
{
"context": "wine_purchase",
"product_name": "2022 Martin Estate Rose"
}
```
| Field | Type | Required | Description |
| - | - | - | - |
| `context` | string | No | Freeform context label (e.g., "wine\_purchase") |
| `product_name` | string | No | Product name shown during verification (max 200 chars) |
| `kind` | string | No | `"kyc"` (default) runs full identity verification. `"sign_in"` is registration-only: the user signs in with their AgentScore account, no identity documents, and the session mints a `sign_in` credential that identifies the account but passes no compliance policy. |
The merchant name shown on the verify page is derived from your account's merchant name (set in dashboard Settings), not from the request.
Verification is agent-driven: after the user completes KYC they close the tab, and the requesting agent retrieves the credential by polling `GET /v1/sessions/:token`. There is no browser redirect back to the merchant; this API does not accept `return_url` or any payment-method metadata.
## Response (201)
```json theme={"dark"}
{
"session_id": "sess_abc123...",
"poll_secret": "poll_xyz789...",
"kind": "kyc",
"verify_url": "https://www.agentscore.com/verify?session=sess_abc123...",
"poll_url": "https://api.agentscore.com/v1/sessions/sess_abc123...",
"expires_at": "2026-04-09T13:00:00Z",
"next_steps": {
"action": "deliver_verify_url_and_poll",
"poll_interval_seconds": 5,
"poll_secret_header": "X-Poll-Secret",
"steps": [
"Deliver verify_url to the end user so they can complete identity verification in the browser.",
"Poll poll_url every 5 seconds, sending poll_secret in the X-Poll-Secret header.",
"When status becomes verified, the operator_token is returned in that poll response exactly once, save it and retry the original merchant request with X-Operator-Token."
],
"user_message": "Share verify_url with the user so they can verify identity, then poll poll_url with X-Poll-Secret until an operator_token is issued."
},
"agent_memory": {
"pattern_summary": "...",
"identity_check_endpoint": "...",
"identity_paths": { "...": "..." },
"bootstrap": { "...": "..." },
"do_not_persist_in_memory": ["operator_token", "poll_secret"],
"persist_in_credential_store": ["operator_token"]
}
}
```
Sessions expire after 1 hour.
### Response fields
| Field | Type | Description |
| - | - | - |
| `session_id` | string | Opaque session identifier, shown in `verify_url` (user-facing) |
| `poll_secret` | string | Agent-only secret for the `X-Poll-Secret` header when polling |
| `kind` | string | `kyc` or `sign_in`, as requested |
| `verify_url` | string | Ready-to-open URL the end user visits to complete KYC |
| `poll_url` | string | Polling endpoint for session status |
| `expires_at` | string | ISO 8601 session expiry (1 hour TTL) |
| `next_steps` | object | Structured agent instructions for consuming the session (`action: "deliver_verify_url_and_poll"`, polling cadence, step-by-step) |
| `agent_memory` | object | Cross-merchant pattern hint: LLM-hosted agents should persist the pattern (never secrets) so future AgentScore merchants skip the cold-start flow. Also emitted on `POST /v1/credentials` success and on first-mint `missing_identity` denials |
### Errors
| Status | `error.code` | When |
| - | - | - |
| 400 | `bad_request` | `kind` is set to something other than `kyc` or `sign_in` |
| 401 | `signup_required` | Missing or invalid `X-API-Key` |
| 402 | `passport_quota_exceeded` | The Free plan's monthly Passport verifications are used up (see [pricing](https://www.agentscore.com/pricing)); paid plans have no cap |
## Polling
```
GET /v1/sessions/{session_id}
X-Poll-Secret: poll_xyz789...
```
| Status | Meaning |
| - | - |
| `pending` | User hasn't completed verification yet |
| `verified` | User verified: `operator_token` included in response |
| `consumed` | Token already issued to a previous poll |
| `expired` | Session expired (1 hour TTL) |
| `failed` | Stripe Identity verification did not succeed (unreadable ID, selfie mismatch, etc.) |
| `flagged` | Identity verified but sanctions screening flagged a potential match: credential issuance is blocked pending manual review |
### Poll response by status
**Pending**; user hasn't completed verification yet:
```json theme={"dark"}
{
"session_id": "sess_abc123...",
"status": "pending",
"retry_after_seconds": 5,
"next_steps": {
"action": "continue_polling",
"poll_interval_seconds": 5,
"eta_message": "Users typically complete identity verification in 2-5 minutes. Continue polling every 5 seconds until status becomes verified, consumed, or expired."
}
}
```
**Verified**; user verified, one-time `operator_token` included:
```json theme={"dark"}
{
"session_id": "sess_abc123...",
"status": "verified",
"operator_token": "opc_newtoken...",
"completed_at": "2026-04-09T12:30:00Z",
"token_ttl_seconds": 86400,
"next_steps": {
"action": "retry_merchant_request_with_operator_token",
"header_name": "X-Operator-Token",
"user_message": "Verification complete. Retry the original merchant request with X-Operator-Token set to the operator_token value."
}
}
```
The token is returned exactly once. Subsequent polls return `consumed` without the token.
**Consumed**; token already issued to a previous poll:
```json theme={"dark"}
{
"session_id": "sess_abc123...",
"status": "consumed",
"next_steps": {
"action": "use_stored_operator_token",
"user_message": "The operator_token was already delivered on a previous poll. If you did not save it, ask the merchant to create a new session."
}
}
```
**Expired**; session expired (1 hour TTL):
```json theme={"dark"}
{
"session_id": "sess_abc123...",
"status": "expired",
"next_steps": {
"action": "create_new_session",
"user_message": "This verification session has expired."
}
}
```
**Failed**; Stripe Identity verification did not succeed:
```json theme={"dark"}
{
"session_id": "sess_abc123...",
"status": "failed",
"next_steps": {
"action": "verification_failed",
"user_message": "Identity verification was not successful. The user may need to try again with a clearer photo or different ID. Create a new purchase to start a fresh verification session."
}
}
```
**Flagged**; identity verified but sanctions screening flagged a potential match. The user must contact support before a credential can be issued:
```json theme={"dark"}
{
"session_id": "sess_abc123...",
"status": "flagged",
"next_steps": {
"action": "contact_support",
"support_email": "support@agentscore.com",
"support_subject": "Sanctions screening dispute",
"user_message": "Identity was verified, but sanctions screening flagged a potential match. Credentials cannot be issued automatically. If you believe this is in error, contact support@agentscore.com to dispute."
}
}
```
### Poll response fields
| Field | Type | When present | Description |
| - | - | - | - |
| `next_steps` | object | Always | Action guidance that varies by status |
| `retry_after_seconds` | number | `pending` | Recommended poll interval in seconds |
| `token_ttl_seconds` | number | `verified` | TTL of the issued operator token in seconds |
## Rate limits
The poll endpoint is rate-limited to 30 requests per minute per IP. Responses include `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers.
# AgentScore Gate
Source: https://docs.agentscore.com/compliance-gating
Verify the identity of agents and operators before completing transactions. KYC, age verification, sanctions screening, and jurisdiction checks in one API call.
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 [Agentic Commerce Quickstart](/guides/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:
```json theme={"dark"}
{
"require_kyc": true,
"require_sanctions_clear": true,
"min_age": 21,
"allowed_jurisdictions": ["US"]
}
```
| Field | Type | Description |
| - | - | - |
| `require_kyc` | boolean | Operator must have completed identity verification |
| `require_sanctions_clear` | boolean | Operator 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](/api-reference/post-assess) for the signer\_sanctions block) |
| `min_age` | integer | Minimum age, 0 to 130. Verified ages are recorded as `18+` or `21+` brackets, so a value above 21 always denies. |
| `blocked_jurisdictions` | string\[] | ISO country codes to block |
| `allowed_jurisdictions` | string\[] | 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`:
```typescript Node.js theme={"dark"}
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
```
```python Python theme={"dark"}
from agentscore import AgentScore
client = AgentScore(api_key=os.environ['AGENTSCORE_API_KEY'])
policy = {
'require_kyc': True,
'require_sanctions_clear': True,
'min_age': 21,
'allowed_jurisdictions': ['US'],
}
wallet_address = request.headers.get('X-Wallet-Address')
operator_token = request.headers.get('X-Operator-Token')
if wallet_address:
result = client.assess(wallet_address, policy=policy)
elif operator_token:
result = client.assess(operator_token=operator_token, policy=policy)
else:
return jsonify({'error': {'code': 'identity_required', 'message': 'Send X-Wallet-Address or X-Operator-Token.'}}), 403
if result['decision'] == 'deny':
return jsonify({
'error': {'code': 'compliance_denied', 'message': 'Compliance policy denied this request.'},
'reasons': result['decision_reasons'],
'verify_url': result.get('verify_url'),
}), 403
# Identity verified, proceed with transaction
```
Or use AgentScore Gate middleware for automatic enforcement:
```typescript Hono theme={"dark"}
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'],
}));
```
```typescript Express 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,
requireSanctionsClear: true,
minAge: 21,
allowedJurisdictions: ['US'],
}));
```
```typescript Fastify 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,
requireSanctionsClear: true,
minAge: 21,
allowedJurisdictions: ['US'],
});
```
```typescript Next.js 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,
requireSanctionsClear: true,
minAge: 21,
allowedJurisdictions: ['US'],
},
async (_req) => Response.json({ ok: true }),
);
```
```typescript Web Fetch (Cloudflare Workers / edge) theme={"dark"}
import { createAgentScoreGate } from '@agent-score/commerce/identity/web';
const guard = createAgentScoreGate({
apiKey: env.AGENTSCORE_API_KEY,
requireKyc: true,
requireSanctionsClear: true,
minAge: 21,
allowedJurisdictions: ['US'],
});
export default {
async fetch(req: Request) {
const r = await guard(req);
if (!r.allowed) return r.response;
return Response.json({ ok: true });
},
};
```
```python FastAPI (native) theme={"dark"}
from fastapi import Depends
from agentscore_commerce.identity.fastapi import AgentScoreGate
gate = AgentScoreGate(
api_key=os.environ['AGENTSCORE_API_KEY'],
require_kyc=True,
require_sanctions_clear=True,
min_age=21,
allowed_jurisdictions=['US'],
)
@app.post('/purchase', dependencies=[Depends(gate)])
async def purchase():
...
```
```python Starlette / generic ASGI theme={"dark"}
from agentscore_commerce.identity import AgentScoreGate
app.add_middleware(
AgentScoreGate,
api_key=os.environ['AGENTSCORE_API_KEY'],
require_kyc=True,
require_sanctions_clear=True,
min_age=21,
allowed_jurisdictions=['US'],
)
```
```python Flask theme={"dark"}
from agentscore_commerce.identity.flask import agentscore_gate
agentscore_gate(
app,
api_key=os.environ['AGENTSCORE_API_KEY'],
require_kyc=True,
require_sanctions_clear=True,
min_age=21,
allowed_jurisdictions=['US'],
)
```
```python Django theme={"dark"}
# settings.py
AGENTSCORE_GATE = {
'api_key': os.environ['AGENTSCORE_API_KEY'],
'require_kyc': True,
'require_sanctions_clear': True,
'min_age': 21,
'allowed_jurisdictions': ['US'],
}
MIDDLEWARE = [..., 'agentscore_commerce.identity.django.AgentScoreMiddleware']
```
```python AIOHTTP theme={"dark"}
from aiohttp import web
from agentscore_commerce.identity.aiohttp import agentscore_gate_middleware
app = web.Application()
app.middlewares.append(agentscore_gate_middleware(
api_key=os.environ['AGENTSCORE_API_KEY'],
require_kyc=True,
require_sanctions_clear=True,
min_age=21,
allowed_jurisdictions=['US'],
))
```
```python Sanic theme={"dark"}
from agentscore_commerce.identity.sanic import agentscore_gate
agentscore_gate(
app,
api_key=os.environ['AGENTSCORE_API_KEY'],
require_kyc=True,
require_sanctions_clear=True,
min_age=21,
allowed_jurisdictions=['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, signaling "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:
| `enforcement` | Identity required? | On gate denial | Use case |
| - | - | - | - |
| `hard` | yes | propagate the gate's 403 (today's regulated path) | wine, cannabis, anything regulated |
| `soft` | offered, not required | swallow + stamp `identity_status="unverified"` on order | request KYC for fraud signal but accept anonymous sales |
| absent / `null` | no | gate 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`](https://github.com/agentscore/python-commerce/blob/main/examples/per_product_policy_merchant.py) (Python) and [`per-product-policy-merchant.ts`](https://github.com/agentscore/node-commerce/blob/main/examples/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:
```json theme={"dark"}
{
"decision": "deny",
"decision_reasons": ["kyc_required"],
"verify_url": "https://www.agentscore.com/dashboard/verify?address=0x..."
}
```
The agent should tell the operator: *"Identity verification is required. Visit \[verify\_url] to get verified."*
### Session-based verification (recommended)
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:
```typescript theme={"dark"}
// 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: { code: 'identity_verification_required', message: 'Verify your identity to continue.' },
verify_url: session.verify_url,
session_id: session.session_id,
poll_secret: session.poll_secret,
poll_url: session.poll_url,
agent_instructions: {
action: 'deliver_verify_url_and_poll',
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.
The commerce SDKs do this for you: set `createSessionOnMissing` (Node) or `create_session_on_missing` (Python) on the gate and it mints the session and returns this 403 with the canonical `agent_instructions`.
## What's checked
| Check | What it verifies |
| - | - |
| `require_kyc` | Government-issued photo ID via Stripe Identity |
| `require_sanctions_clear` | Operator name not on sanctions lists (OpenSanctions, screened at KYC). Separately, and with no policy flag, a payment-signer wallet supplied as `signer` is checked against the OFAC SDN crypto list (refreshed hourly from `sdn_advanced.xml`); a hit on either flips `decision` to `deny`. |
| `min_age` | Age bracket derived from ID (18+ or 21+) |
| `blocked_jurisdictions` | Country from ID is not in blocked list |
| `allowed_jurisdictions` | Country 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.
| Code | When | `agent_instructions.action` + key fields |
| - | - | - |
| `missing_identity` | Neither `X-Wallet-Address` nor `X-Operator-Token` present, and no auto-session was created | `probe_identity_then_session`: try wallet on signing rails → stored `opc_...` → session flow; `agent_memory` cross-merchant bootstrap hint included |
| `identity_verification_required` | No identity header, but the merchant's gate auto-minted a verification session: body includes `verify_url`, `session_id`, `poll_secret`, `poll_url` | `deliver_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_expired` | `X-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_credential` | `X-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_mismatch` | `X-Wallet-Address` was sent but the payment signer resolves to a different operator | `resign_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_signing` | `X-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 Solana MPP) |
| `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_required` | The 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_error` | AgentScore 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](/api-reference/errors#retryable-infra-errors-api_error) |
### Wallet-mode response fields
When `identity_mode: "wallet"`, 402 and 403 bodies also include these fields so agents know exactly which address must sign:
| Field | Type | Description |
| - | - | - |
| `identity_mode` | `"wallet"` \| `"operator_token"` | Which identity path was used |
| `required_signer` | string | Wallet address normalized per network: EVM lowercased, Solana base58 preserved verbatim. The payment MUST be signed by this address or a `linked_wallets` entry. |
| `linked_wallets` | string\[] | 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_constraint` | string | Human-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:
```ts node-commerce (Hono / Express / Fastify) theme={"dark"}
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}`);
}
// ...
});
```
```python python-commerce (FastAPI) theme={"dark"}
from agentscore_commerce.identity.fastapi import AgentScoreGate, get_gate_degraded_state
gate = AgentScoreGate(api_key="ask_...", fail_open=True)
@app.post("/purchase", dependencies=[Depends(gate)])
async def purchase(request: Request):
state = get_gate_degraded_state(request)
if state["degraded"]:
logger.warning("gate degraded: %s", state["infra_reason"])
# ...
```
`infraReason` / `infra_reason` is one of:
| Value | When |
| - | - |
| `quota_exceeded` | AgentScore returned 429 |
| `api_error` | AgentScore returned 5xx (transient infra issue) |
| `network_timeout` | Request 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
| Vertical | Recommended 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 SLAs | `failOpen: 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` / `wallet_claimed` / `kyc_verified`)
* Whether each policy check passed or failed
```json theme={"dark"}
{
"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](https://stripe.com/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:
```bash theme={"dark"}
# Always allow (verified individual, US, 21+)
curl -X POST https://api.agentscore.com/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.com/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.com/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.com/v1/assess \
-H "X-API-Key: your-api-key" \
-d '{"address": "0x0000000000000000000000000000000000000004", "test": true, "policy": {"require_kyc": true}}'
```
## Pricing
See [pricing](https://www.agentscore.com/pricing) for plans, quotas, and which tier includes compliance gating.
## Next steps
Full assess endpoint reference.
Create verification sessions for agent polling.
Create and manage operator credentials.
How operators verify their identity.
# Agentic Commerce Quickstart
Source: https://docs.agentscore.com/guides/agent-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 attached, or when the buyer asks for a verification session:
```ts theme={"dark"}
import { shouldRunConditionalGate } from '@agent-score/commerce/payment';
const _gate = agentscoreGate({ /* same options as above */ });
app.use('/purchase', async (c, next) => {
if (!shouldRunConditionalGate(c.req.raw)) { await next(); return; }
return _gate(c, next);
});
```
`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'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": "{\"action\":\"deliver_verify_url_and_poll\",\"steps\":[\"...\"],\"user_message\":\"...\"}"
}
```
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.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 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.code).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 when the operator's name matched a sanctions list at KYC. Wallet-address OFAC screening of the payment signer runs regardless of this flag |
| `minAge` | `number` | `18`, `21` | Minimum age. Verified ages are `18+` or `21+` brackets, so a value above 21 always denies |
| `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.
## Watch usage in the dashboard
[Dashboard → Usage](https://www.agentscore.com/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](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.
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](/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
# AgentScore Commerce (Node.js)
Source: https://docs.agentscore.com/integrations/node-commerce
Identity middleware + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for agentic commerce.
`@agent-score/commerce` is the full merchant-side SDK for agentic commerce. One install bundles identity gating, payment-protocol helpers, 402 challenge builders, discovery doc generators, and Stripe multichain support. Subpath imports keep bundle size focused: identity-only consumers pay no cost for payment helpers, and vice versa.
## Installation
```bash theme={"dark"}
npm install @agent-score/commerce
```
Framework + integration packages are optional peer deps; install only what you use:
```bash theme={"dark"}
npm install hono mppx @x402/core @x402/evm stripe # whatever your stack needs
```
## Subpaths
| Import | What it gives you | | |
| - | - | - | - |
| `@agent-score/commerce` (top-level) | `Checkout` orchestrator + `CheckoutContext` + `CheckoutGateConfig` + `CheckoutValidationError` + `DiscoveryProbeConfig` + `MountUcpRoutesOptions` + `SettleOutcome` + `MppxComposeOutcome` + `PricingResult` (one config object, hooks for `preValidate`/`computePricing`/`onSettled`/`mintRecipients`/`composeMppx`, auto-derived x402+mppx servers, per-framework adapters `handleHono`/`handleExpress`/`handleFastify`/`handleNextjs`/`handleWeb`, signed UCP routes via `mountUcpRoutes{Hono,Express,Fastify}`). Plus `computeFirstCheckout`: variable-cost pay-per-result helper (compute-first + exact-x402) that works on every exact-mode rail without upto / Permit2 / Settlement-Overrides. `createQuoteCache`: content-hash quote cache used by the compute-first helper (in-memory default; pass `redisUrl` for distributed deployments). `createResultCache`: the neutral keyed JSON-value cache underneath it (same body-hash key builder); cache any probe-leg result a `Checkout` merchant replays on the settle leg, for example a paid upstream call made in `preValidate`. `createDefaultOnDenied({merchantName, supportEmail, supportContext?, walletNotTrustedMessage?, paymentRequiredMessage?})`: canonical `onDenied(reason) => {status, body, headers?}` factory matching `Checkout`'s gate hook (handles `wallet_signer_mismatch`, `wallet_not_trusted` unfixable fallback, `payment_required`, `token_expired`/`invalid_credential`/`api_error`). \`hasPaymentHeader(req | headers)`: discriminator that splits discovery legs (no payment credential → 402) from settle legs (`payment-signature`/`x-payment`/`Authorization: Payment JWT-BYTES`); `hasX402Header`/`hasMppxHeader`: granular dispatch helpers (x402 vs MPP credential present). `malformedPaymentCredential(req | headers)`: wire-shape gate for payment credentials (values that are not base64 JSON and not token-shaped are rejected); `Checkout`runs it before any merchant hook by default, opt out with`credentialPreCheck: false`. `defaultReadOnlyOnDenied(reason)`: canonical `onDenied` for read-only resource gates (`GET /orders/:id`): collapses every denial to 401 `unauthorized`+`Cache-Control: no-store`while still spreading`denialReasonToBody`. `extractOwnerScope(headers)`(returns optional`walletAddress`+ optional`operatorTokenHash`): pull canonical owner identity from `X-Wallet-Address`/`X-Operator-Token`with safe token hashing. Plus factories:`pricingResult`(cents → typed PricingResult with optional`decimals`for sub-cent precision),`validationResponse\` (4xx envelope per framework). |
| `@agent-score/commerce/identity/{hono,express,fastify}` | Trust gate middleware: KYC, age, sanctions (account name + signer wallet), jurisdiction. Context-getter pattern: `agentscoreGate(opts)` middleware + `getAgentScoreData(ctx)` / `getGateDegradedState(ctx)` / `getGateQuotaInfo(ctx)` / `getSignerVerdict(ctx)` accessors, `captureWallet(...)`. | | |
| `@agent-score/commerce/identity/policy` | Per-product compliance helpers: `PolicyBlock`, `buildGateFromPolicy`, `runGateWithEnforcement`, `shippingCountryAllowed`, `shippingStateAllowed`, `validateShippingAgainstPolicy` (one-call country+state validator raising `CheckoutValidationError` with the canonical envelope on miss). All five are also re-exported at `@agent-score/commerce` top-level. | | |
| `@agent-score/commerce/identity/{nextjs,web}` | Same gate, wrapper pattern: `withAgentScoreGate(opts, handler)` / `createAgentScoreGate(opts) => guard(req)`. The `data` + `degraded` + `infraReason` fields land directly on the handler arg / guard result (no separate getter). Plus shared `captureWallet`, `getSignerVerdict`. | | |
| `@agent-score/commerce/payment` | `networks`, `USDC`, `rails` registries; `paymentDirective`, `buildPaymentHeaders` (one-call WWW-Authenticate + PAYMENT-REQUIRED bundle), `wwwAuthenticateHeader`, `paymentRequiredHeader`, `aliasAmountFields` (opt-in v1↔v2 amount-field shim; the 402 builders do NOT apply it by default: strict x402 v2 settlement matches the whole accepts object, so an extra field breaks settle; reach for it only when a specific client is hardcoded to read `maxAmountRequired`), `settlementOverrideHeader`, `dispatchSettlementByNetwork`, `extractPaymentSigner` (recovers signer from a Request: x402 EIP-3009 `payload.authorization.from` OR MPP `Authorization: Payment BASE64` `did:pkh:eip155:CHAIN:ADDR` / `did:pkh:solana:GENESIS:ADDR` source DID, including Solana `TransferChecked` authority fallback when `@solana/kit` is installed), `extractPaymentSignerFromAuth` (header-string variant for callers that already have the `Authorization` value in hand), `detectRailFromHeaders` (returns `"x402"` / `"mpp"` / `null` from inbound headers), `buildIdempotencyKey`; `createX402Server`, `buildX402AcceptsFor402` (one-call helper for the 402-emit path; derives `extra.name` from the registered scheme so EIP-712 domain matches the on-chain USDC contract), `createMppxServer`, `buildDefaultCheckoutRails({tempo?, x402Base?, solanaMpp?, stripe?})` (canonical 4-rail `rails` dict factory: flipping `network` alone derives the right `token` + `chainId`; Base Sepolia → Sepolia USDC + chainId 84532, Solana devnet → devnet USDC mint. Solana's `network` accepts both CAIP-2 and the raw `@solana/mpp` form `mainnet-beta` / `devnet` / `localnet`. Explicit overrides always win), `buildMppxComposeRails({amountUsd, tempoRecipient?, solanaRecipient?, ...})` (per-call intent factory replacing the hand-rolled `[['tempo/charge',{...}],['solana/charge',{...}],['stripe/charge',{...}]]` array; auto-handles USD→atomic conversion for Solana; auto-drops `stripe/charge` when `amountUsd < 0.50` since Stripe's fixed \~$0.30 fee makes sub-50-cent charges unprofitable; sub-50-cent APIs pass `includeStripe: false` explicitly), `composeMppxRequest` (typed wrapper around `mppx.compose(...intents)(request)` returning a narrowed `MppxComposeResult` union; replaces the `(mppx as any).compose(...)` cast in custom `composeMppx` hooks), `mppxChallengeHeaders` (one-call `Object.fromEntries(challenge.headers)` for the 402 path); drop-in x402 helpers: `validateX402NetworkConfig` (boot-time guard), `verifyX402Request` (parse + validate inbound X-Payment), `processX402Settle` (verify-then-settle in one call), `classifyX402SettleResult` (maps the tagged settle result to a recommended HTTP status / code / next_steps so merchants get a controlled envelope without coupling to facilitator-specific error text), `classifyOrchestrationError` (same `ClassifiedX402Error` shape but for uncaught exceptions thrown elsewhere in the orchestration; conservative substring fallback returns `null` for unknown errors so merchants rethrow instead of swallowing); `zeroAmountCarveOut` (skip CDP / mppx upstream verify+settle for $0 settles where the upstream rejects value=0 payloads; parses the credential, lifts signer + network, returns a `ZeroSettleResult` shaped identically to the success path so callers branch on rail, not on result shape); `usdToAtomic` (BigInt-based USD → atomic value, ROUND\_HALF\_UP, rejects NaN / negative / infinite; for Tempo / Solana / Base USDC amount construction). | | |
| `@agent-score/commerce/discovery` | `isDiscoveryProbeRequest`, `buildDiscoveryProbeResponse` (with optional `x402Sample` for x402-aware crawlers; the sample envelope always carries a v2 `resource` and threads optional `extensions`, and `Checkout` fills both from its own `url`/`resourceInfo`/`discoveryExtensions`, because envelope validators require `resource` and read the Bazaar example input to build valid follow-up probe bodies), `sampleX402AcceptForNetwork`, `buildWellKnownMpp`, `buildWellKnownX402` (x402scan v1 `/.well-known/x402` shape: `{version: 1, resources: ["METHOD /path"]}`), `buildLlmsTxt`, `buildSkillMd` (Claude-Skill-compatible `/skill.md` agent-discovery manifest: strictly agent-facing data only, no internal posture), `buildRedemptionSkillMd` (delivery-neutral redemption-code template; printed mailers, emailed codes, API trial credits all covered; `endpointPath`/`deliveryIntro`/`bodyShape`/`bodyRules`/`extraRecoveryRows` overrides for non-goods shapes), `buildMerchantIndexJson` + `standardEndpointDescriptions({kind})` (canonical `/` discovery body for goods vs api merchants), `buildSuccessNextSteps` (universal Passport-active success block), `buildAgentscoreOnboardingSteps`, `agentscoreOpenApiSnippets`, `siwxSecurityScheme` + `xPaymentInfoExtension` + `xGuidanceExtension` per the x402scan OpenAPI spec, `createBazaarDiscovery`; `noindexNonDiscoveryPaths` (Hono middleware) + `noindexNonDiscoveryPathsExpress` + `noindexNonDiscoveryPathsFastify` + `wrapNoindexResponse` / `applyNoindexHeader` (Web Fetch + Next.js): emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces. Plus the UCP/JWKS publish surface: `buildSignedUcpResponse`, `buildSignedJwksResponse`, `wellKnownPreflightResponse`, `defaultA2aServices`, `bootstrapUcpSigningKey`, framework-neutral `SignedDiscoveryResponse` + per-framework wrappers `signedResponse{Hono,Express,Fastify,Nextjs,Web}`. | | |
| `@agent-score/commerce/challenge` | `buildAcceptedMethods`, `buildIdentityMetadata` (auto-attached by `Checkout` when wallet header present), `buildHowToPay`, `buildAgentInstructions` (auto-emits per-rail `compatible_clients`; pure helper `compatibleClientsByRails(rails)` returns the same map for vendors building custom 402s), `build402Body` (pass `x402.resource` for x402 v2 `ResourceInfo` metadata, `url` plus optional `serviceName` / `tags` / `description` / `mimeType` / `iconUrl`, and `x402.extensions` for declared extensions; both surface on the 402 body for spec-compliant crawlers, and `respond402` mirrors `resource` into the PAYMENT-REQUIRED header), `buildPricingBlock` (cents → dollar-string; optional `shippingCents` + `discountCents`; pass `discountCents` for redemption codes / coupons so the 402 body surfaces `discount` alongside `subtotal` / `total` for agent-renderable savings; optional `decimals` for sub-cent precision so per-token / per-byte unit pricing advertises real amounts instead of rounding to two decimals), `firstEncounterAgentMemory`, `Receipt` type (canonical 200-receipt shape); `respond402`: drop-in 402 emit that preserves mppx's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `buildValidationError`: structured 4xx body builder (`{error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}`) so vendors compose body shapes by name instead of inlining at every validation site. | | |
| `@agent-score/commerce/middleware/{hono,express,fastify,nextjs,web}` | Framework-specific rate-limit middleware. Hono / Express / Fastify expose middleware factories (`rateLimitHono`, `rateLimitExpress`, `rateLimitFastify`); Next.js exposes `withRateLimit(opts, handler)`; Web Fetch exposes `createRateLimit(opts) => guard(req)`. Shared options: `windowSeconds` (default 60), `maxRequests` (default 60), `keyResolver` (default first hop of `x-forwarded-for`), `redisUrl` (lazy-imports `ioredis` when set, in-memory `Map` fallback otherwise), `keyPrefix`. `ioredis` is an optional peer dep; install it only for distributed limiting. | | |
| `@agent-score/commerce/stripe-multichain` | `createMultichainPaymentIntent` (returns `{ paymentIntentId, depositAddresses }`; read `depositAddresses[network]` directly), `createPayToAddressFromStripePI({request, amountCents, stripe, piCache, networks?, staticRecipients?, metadata?, orderId?, preferredNetwork?})`: per-order payTo resolver matching `Checkout.mintRecipients`: on the settle leg, reuses the buyer's signed-against payTo from the MPP credential (after `piCache.hasAddress` check OR a `staticRecipients` match; the static address is always-accepted because the merchant owns it); on the discovery leg, mints a fresh PI for the rails NOT covered by `staticRecipients`, caches the merged map, registers static addresses with `piCache.cacheAddress` so verify-leg lookups pass. `mintMultichainRecipients({...same opts})`: structured variant returning `{ recipients, paymentIntentId?, reusedFromCredential }` for the full per-rail map (typical multi-rail merchant hook). Use `staticRecipients: { solana: '' }` for low-margin endpoints where Solana per-call ATA rent (\~$0.50 against MPP spec §13.6) dominates revenue; the SDK skips Stripe minting on that network, reuses the static recipient forever, and (when paired with a one-time external pre-funding of the recipient's USDC ATA) lets every settle pay only the ~$0.001 per-tx fee. `simulateCryptoDeposit`, `createMppxStripe`; `createPiCache` (TTL'd PI / deposit-address cache, Redis-backed when `redisUrl` set), `simulateDepositIfTestMode` (gates on `sk_test_` and looks up the PI for you), `STRIPE_TEST_TX_HASH_SUCCESS` / `STRIPE_TEST_TX_HASH_FAILED` constants. | | |
| `@agent-score/commerce/api` | Everything from `@agent-score/sdk` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `isAgentScoreTestAddress`. Don't add `@agent-score/sdk` as a separate dep: the two can drift versions. | | |
| `@agent-score/commerce` (top-level: publishers + key/token helpers) | Identity-publishing helpers for cross-vendor standards: `buildA2AAgentCard` (A2A v1.0 Agent Card, served at `/.well-known/agent-card.json`), `buildUCPProfile` (Google Universal Commerce Protocol, served at `/.well-known/ucp`). Both return unsigned payloads: vendor signs + publishes. Key + token helpers: `loadUCPSigningKeyFromEnv` (cached env-driven loader: reads `UCP_SIGNING_KEY_JWK_PRIVATE` JSON JWK, detects alg from shape, falls back to ephemeral when unset, sanitizes errors so key bytes never reach logs, concurrent-safe via Promise-pinned cache); `hashOperatorToken` (sha256 hex of plaintext `opc_...`; for merchants persisting `operator_token_id` to their own DB without ever storing the plaintext); `extractOwnerScope(headers)` (returns optional `walletAddress` + optional `operatorTokenHash`) (canonical owner-identity extractor for caller-scoped resource queries; reads `X-Wallet-Address` / `X-Operator-Token`, hashes the token so plaintext never leaves the request). | | |
## Identity model
Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-Token`). Default checks operator-token first, then wallet. Address normalization is network-aware: EVM lowercased, Solana base58 preserved verbatim.
`DenialReason` codes (`missing_identity`, `identity_verification_required`, `token_expired`, `invalid_credential`, `wallet_signer_mismatch`, `wallet_auth_requires_wallet_signing`, `wallet_not_trusted`, `api_error`, `payment_required`) each carry a structured `agent_instructions` JSON block describing concrete recovery actions.
`createSessionOnMissing` auto-mints a verification session for two paths: cold-start (no identity headers) AND `wallet_not_trusted` with fixable reasons (`kyc_required` / `kyc_pending` / `kyc_failed`). Both paths rewrite the denial to `identity_verification_required` before reaching `onDenied`, so merchants only handle one code. **When the merchant omits `createSessionOnMissing` from the gate config, `Checkout` auto-defaults it** from `gate.apiKey` + `gate.baseUrl` + `gate.context` + `gate.merchantName`; every gated route gets the bootstrap UX out of the box; merchants that need per-request session context or `onBeforeSession` side effects (goods merchants pre-minting an order\_id) supply their own config to override. `buildVerificationRequiredBody(reason, opts?)` collapses the per-merchant body-mapping boilerplate into one call (`status: 403, body: buildVerificationRequiredBody(reason, { message, agentInstructions, extra })`). The gate middleware extracts the payment signer pre-evaluate (`extractPaymentSigner(req, x402Header)` covers MPP `did:pkh:eip155` + Solana `did:pkh:solana` + Solana `TransferChecked` authority fallback + x402 EIP-3009) and passes `signer: { address, network }` to `/v1/assess`; one round trip carries the gate verdict AND the wallet-signer-match outcome AND the `signer_sanctions` OFAC SDN wallet-address verdict. Merchants read both back synchronously via `getSignerVerdict(ctx)` (or the `gate.getSignerVerdict()` accessor on the wrapper adapters) off the gate's cache (no extra HTTP call). **Wallet-OFAC SDN enforcement is unconditional** whenever the signer is supplied; an SDN hit OR an unavailable wallet-sanctions lookup flips the gate decision to deny before the handler runs (fail-closed; OFAC strict-liability), without requiring `policy.require_sanctions_clear` to opt in. `policy.require_sanctions_clear` is the separate NAME-based screen on the operator's KYC identity. `captureWallet` is fire-and-forget; POSTs the signer to `/v1/credentials/wallets` so the operator's cross-merchant credential↔wallet profile builds up over time.
**Getting a `verify_url` before paying.** The gate runs on the settle leg, so a buyer with no identity would otherwise reach the session 403 only by attaching a payment credential first (a Stripe Shared Payment Token buyer would have to mint a token just to learn they need to verify). A request carrying `X-Verification-Session: create` with no identity header and no payment credential runs the gate instead, and gets the same session-bearing 403 (`verify_url`, `session_id`, `poll_secret`, `poll_url`) without paying. `Checkout` advertises the path in its discovery 402 as an `identity_bootstrap` block whenever the request has no identity header, and every `conditional*` gate adapter honors the header. A merchant hand-rolling its own conditional wrap uses `shouldRunConditionalGate(request)` from `@agent-score/commerce/payment` (a payment credential, or `requestsVerificationSession(request)`) and advertises the path by passing `buildIdentityBootstrap()` into `build402Body`'s `extra` as `identity_bootstrap`. The header is opt-in so discovery crawlers replaying an example body never mint sessions; merchants without an identity gate neither advertise nor honor it.
## Identity publishing (cross-vendor standards)
Two helpers compose AgentScore identity into the payload formats published to other agent-commerce ecosystems. Each returns an unsigned object; your service signs + serves it however its key infrastructure works.
```typescript theme={"dark"}
import {
buildA2AAgentCard,
buildUCPProfile,
mppPaymentHandler,
stripeSptPaymentHandler,
ucpA2AExtension,
x402PaymentHandler,
} from '@agent-score/commerce';
// 1. A2A v1.0 Agent Card, published at /.well-known/agent-card.json.
// Per UCP §A2A binding (https://ucp.dev/specification/checkout-a2a/), agents
// that serve UCP via the A2A transport MUST declare the canonical UCP extension
// URI in `capabilities.extensions[]` so platforms arriving via A2A detect UCP
// support without re-fetching the profile. Pass an empty capabilities map when
// you serve UCP at the discovery layer but have no formal capability bindings
// yet. Skills declare what the agent can do; they are AgentSkill objects
// (top-level on the card). The card is returned unsigned; to sign it, compute
// RFC 7515 JWS entries over the card body and pass them as `signatures`.
const card = buildA2AAgentCard({
name: 'My Agent Service',
description: 'Buy products via agent payments.',
url: 'https://agents.example.com',
version: '1.0.0',
skills: [
{ id: 'purchase', name: 'Purchase', description: 'Buy products via agent payments.', tags: ['commerce', 'payment'] },
],
extensions: [ucpA2AExtension()],
});
// 2. Google Universal Commerce Protocol profile, published at /.well-known/ucp
// Output shape: { ucp: { version, services, capabilities, payment_handlers,
// name?, supported_versions? }, keys: [...] }; services /
// capabilities / payment_handlers are MAPS keyed by reverse-DNS service /
// capability / handler name (UCP spec §3 + §6).
const profile = buildUCPProfile({
name: 'My Agent Service',
services: {
'dev.ucp.shopping': [
{
version: '2026-08-25',
spec: 'https://ucp.dev/2026-08-25/specification/overview',
transport: 'mcp',
endpoint: 'https://agents.example.com/api/ucp/mcp',
schema: 'https://ucp.dev/services/shopping/mcp.openrpc.json',
},
],
},
payment_handlers: {
...mppPaymentHandler({ networks: [{ network: 'tempo-mainnet', chain_id: 4217, recipient: TEMPO_ADDR }] }),
...x402PaymentHandler({ networks: [{ network: 'base-8453', recipient: BASE_ADDR }] }),
...stripeSptPaymentHandler({ spec: { profileId: 'profile_5xKvNqM9BaH' } }),
},
keys: yourJWKS,
// Optional: declare merchant gate policy as a `com.agentscore.identity` capability
// binding inside the public profile. Static policy declaration only; no per-operator
// claims. Per-operator identity attestation flows through the AP2 risk-signal endpoint.
agentscore_gate: { require_kyc: true, min_age: 21, allowed_jurisdictions: ['US'] },
});
```
Note: ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol. Not an identity-publishing surface. ACP merchants integrate through the existing `build402Body` + `buildPaymentHeaders` + Stripe SPT rail.
### Signing UCP profiles (vendor extension; opt-in for trust-mode verifiers)
UCP §6 doesn't mandate profile-body JWS signing; production UCP merchants commonly ship unsigned. AgentScore's `agentscore-profile+jws` is a vendor extension layered on top of the unsigned UCP profile for trust-mode verifiers (regulated-commerce, AP2-aware) that opt into auditable cryptographic provenance. Vanilla UCP agents read the canonical body and ignore the `signature` field. Sign + verify via the optional `jose` peer dep (install via `bun add jose`):
```typescript theme={"dark"}
import {
buildJWKSResponse,
generateUCPSigningKey,
signUCPProfile,
verifyUCPProfile,
} from '@agent-score/commerce';
// One-time per merchant: generate or load a key. Persist privateJWK in your secrets manager.
const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'merchant-2026-05' });
// On every /.well-known/ucp request:
const profile = buildUCPProfile({
name: 'My Agent Service',
services: { /* keyed by service name, see above */ },
payment_handlers: { /* keyed by handler reverse-DNS name, see above */ },
keys: [publicJWK],
});
const signed = await signUCPProfile(profile, {
signingKey: privateKey,
kid: publicJWK.kid,
alg: 'EdDSA',
});
return c.json(signed);
// On /.well-known/jwks.json:
return c.json(buildJWKSResponse([publicJWK]));
```
Verifiers reconstruct the canonical body (everything except `signature`, keys sorted at every level), look up the kid in JWKS, and check the JWS. `verifyUCPProfile(signed, jwks)` does this for you. It enforces the JWS protected header `typ: "agentscore-profile+jws"` (vendor-namespaced; UCP §6 doesn't define a profile-as-JWS typ), restricts `alg` to `EdDSA` / `ES256`, requires a `kid`, rejects duplicate kids, and compares canonical body bytes against the JWS payload. Failures throw `UCPVerificationError` with a discriminated `code` field. Both EdDSA (Ed25519) and ES256 are supported. EdDSA is the default and recommended.
`signUCPProfile` rejects profiles containing non-integer `Number` values: cross-language float canonicalization is not stable, so use decimal strings (e.g. `"9.99"`) for any monetary or fractional fields you put in `extras`.
**Persisting the private JWK.** Mint once via `generateUCPSigningKey()`, export with `jose.exportJWK(privateKey)`, store in your secret manager. On each container start, read the secret, `jose.importJWK(jwk, alg)` to re-hydrate. KMS-backed flows require an adapter that exposes a `KeyLike` jose can call; `jose` does not natively wrap KMS endpoints. See `examples/signed-ucp-merchant.ts` for the runtime-independent re-hydrate pattern.
**Key rotation.** Mint a new key with a new `kid`, add the public JWK to your JWKS endpoint alongside the old one, then sign new profiles with the new key. Drop the old JWK after your verifier-side cache TTL has elapsed.
**Inline JWK in the profile vs separate JWKS endpoint.** UCP §6 mandates the separate `/.well-known/jwks.json` endpoint as the canonical trust source. The profile's `keys[]` is informational; verifiers MUST resolve the kid against the JWKS to prevent a swap-after-sign attack.
## Quick start: full merchant via the `Checkout` orchestrator
`Checkout` is the canonical merchant surface for **fixed-price one-shot** endpoints; one config object, hooks for the merchant-specific pieces, and the SDK handles 402 emit, identity gating, x402 verify+settle, mppx compose, \$0 carve-out, identity\_metadata auto-attach, and the per-framework adapter.
For **variable-cost pay-per-result** endpoints (per-result search, per-token LLM, per-byte transcoding), reach for `computeFirstCheckout`; same config shape, but the probe leg runs the work, caches the result by content-hash of the request body, and emits a 402 with the EXACT computed price. The retry pays that exact amount and the merchant returns the cached body. Works on every exact-mode rail (x402-exact Base, tempo/charge, solana/charge, Stripe SPT) without upto / Permit2 / Settlement-Overrides. Tradeoff: the work runs on the unpaid probe leg, so mount `rateLimitHono` (`@agent-score/commerce/middleware/hono`) globally; it's load-bearing.
```typescript theme={"dark"}
import { Hono } from 'hono';
import { Checkout, pricingResult, validateShippingAgainstPolicy } from '@agent-score/commerce';
import { defaultA2aServices } from '@agent-score/commerce/discovery';
const checkout = new Checkout({
rails: {
tempo: { recipient: process.env.TEMPO_RECIPIENT! },
x402_base: { recipient: process.env.X402_BASE_RECIPIENT!, network: 'eip155:8453' },
solana_mpp:{ recipient: process.env.SOLANA_RECIPIENT!, network: 'solana:mainnet' },
stripe: { profileId: process.env.STRIPE_PROFILE_ID! },
},
url: 'https://merchant.example/purchase',
preValidate: async (ctx) => {
const body = (ctx.request.body ?? {}) as { product_slug?: string; shipping?: { country?: string; state?: string } };
const product = await lookupProduct(body.product_slug);
validateShippingAgainstPolicy({ country: body.shipping?.country ?? '', state: body.shipping?.state ?? '', policy: product, productName: product.name });
return { product };
},
computePricing: async (ctx) => pricingResult({
subtotalCents: ctx.state.product.priceCents,
taxCents: ctx.state.product.taxCents,
taxRate: ctx.state.product.taxRate,
taxState: ctx.state.product.taxState,
}),
onSettled: async (ctx, outcome) => ({ ok: true, order_id: ctx.referenceId, tx_hash: outcome.txHash }),
cdpApiKeyId: process.env.CDP_API_KEY_ID,
cdpApiKeySecret: process.env.CDP_API_KEY_SECRET,
mppxSecretKey: process.env.MPP_SECRET_KEY,
gate: {
apiKey: process.env.AGENTSCORE_API_KEY!,
merchantName: 'Merchant',
requireKyc: true, requireSanctionsClear: true, minAge: 21, allowedJurisdictions: ['US'],
},
// Optional: empty-body POSTs without a payment header auto-route to a sample 402
// so x402 crawlers (awal x402 details, x402-proxy, ...) can discover the surface.
discoveryProbe: {
realm: 'merchant.example',
sampleRail: 'tempo-mainnet',
sampleAmountUsd: 1.0,
sampleRecipient: process.env.TEMPO_RECIPIENT!,
},
});
```
**Solana MPP requires a static recipient with a pre-funded token account.** Point the Solana rail at a fixed wallet you control (as with `SOLANA_RECIPIENT` above) and pre-fund that wallet's USDC associated token account (ATA) once. Per-order rotating Solana deposit addresses (for example Stripe-multichain minted addresses) do not settle on Solana MPP, because the settlement transaction does not create the recipient's token account. Tempo and x402 (Base) settle fine to per-order recipients; only Solana needs the static, pre-funded wallet. When you mint per-order recipients via `@agent-score/commerce/stripe-multichain`, pass `staticRecipients: { solana: '' }` so Solana is served from your fixed wallet, or leave Solana out of the rail set.
```ts theme={"dark"}
const app = new Hono();
// One-call mount: GET /.well-known/ucp + GET /.well-known/jwks.json + OPTIONS preflights.
// Express / Fastify have the same helper (`mountUcpRoutesExpress` / `mountUcpRoutesFastify`).
// Next.js / Web Fetch use file-based routing; call `buildSignedUcpResponse` directly inside the route handler.
checkout.mountUcpRoutesHono(app, {
name: 'Merchant',
services: defaultA2aServices({ agentCardUrl: 'https://merchant.example/.well-known/agent-card.json' }),
signingKid: 'merchant-2026-05',
});
app.post('/purchase', (c) => checkout.handleHono(c));
```
The 402 body Checkout emits auto-attaches `identity_mode` + `required_signer` + `signer_constraint` (and `linked_wallets` when the gate populated them) when an inbound `X-Wallet-Address` header is present; so agents self-correct at discovery instead of at the 403 retry.
See the [compute-first merchant example](https://github.com/agentscore/node-commerce/blob/main/examples/compute-first-merchant.ts) for the canonical variable-cost pattern via `computeFirstCheckout`.
## Fail-open (opt-in)
By default AgentScore Gate fails closed on AgentScore-side infra failure (429 / 5xx / network timeout); buyer gets 503. Pass `failOpen: true` to opt in to graceful degradation, then read the per-request degraded state via `getGateDegradedState(c)`:
```ts theme={"dark"}
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) console.warn(`[gate] degraded: ${infraReason}`);
// ...
});
```
`getGateDegradedState` is exported by every Node adapter (Hono, Express, Fastify). For `withAgentScoreGate` (Next.js / Web Fetch), the `degraded` + `infraReason` fields land on the `gate` object passed to your handler. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of `failOpen`; see [compliance-gating › Fail-open behavior](/compliance-gating#fail-open-behavior-opt-in).
## Examples
The [examples/](https://github.com/agentscore/node-commerce/tree/main/examples) directory has runnable single-file Hono apps for each common merchant scenario:
| Example | Scenario |
| - | - |
| [`api-provider.ts`](https://github.com/agentscore/node-commerce/blob/main/examples/api-provider.ts) | Per-call API billing on multiple rails: Tempo MPP + x402 (Base) + Solana MPP; no compliance gate |
| [`identity-only.ts`](https://github.com/agentscore/node-commerce/blob/main/examples/identity-only.ts) | Compliance gate without payment (vendor handles their own) |
| [`multi-rail-merchant.ts`](https://github.com/agentscore/node-commerce/blob/main/examples/multi-rail-merchant.ts) | Full agent-commerce: identity + Tempo MPP + x402 + Stripe SPT |
| [`stripe-multichain-merchant.ts`](https://github.com/agentscore/node-commerce/blob/main/examples/stripe-multichain-merchant.ts) | Stripe-anchored multichain (PaymentIntent → tempo/base/solana deposit addresses) |
| [`compute-first-merchant.ts`](https://github.com/agentscore/node-commerce/blob/main/examples/compute-first-merchant.ts) | Pay-per-result variable-cost merchant via `computeFirstCheckout`. Probe runs the work + caches by body content-hash; settle replays the cached result. Works on every exact-mode rail (no upto / Permit2 / Settlement-Overrides). |
| [`compliance-merchant.ts`](https://github.com/agentscore/node-commerce/blob/main/examples/compliance-merchant.ts) | Regulated-goods merchant: full compliance gate + custom `onDenied` composing the denial helpers (`verificationAgentInstructions`, `isFixableDenial`, `buildSignerMismatchBody`, `buildContactSupportNextSteps`, `denialReasonStatus` / `denialReasonToBody` to assemble the pair for any DenialReason) |
| [`per-product-policy-merchant.ts`](https://github.com/agentscore/node-commerce/blob/main/examples/per-product-policy-merchant.ts) | Multi-product merchant where each row carries its own compliance policy (hard gate, soft, or none). Uses `PolicyBlock`, `buildGateFromPolicy`, `runGateWithEnforcement`, and the one-call `validateShippingAgainstPolicy`. |
| [`signed-ucp-merchant.ts`](https://github.com/agentscore/node-commerce/blob/main/examples/signed-ucp-merchant.ts) | Signed UCP profile + JWKS endpoint via the one-call `checkout.mountUcpRoutesHono(app, ...)`. Demonstrates ephemeral-for-dev / env-JWK-for-prod and `bootstrapUcpSigningKey` lifespan-hook usage. |
## Links
* [npm](https://www.npmjs.com/package/@agent-score/commerce)
* [GitHub](https://github.com/agentscore/node-commerce)
# AgentScore Pay (CLI)
Source: https://docs.agentscore.com/integrations/pay-cli
@agent-score/pay; one CLI for agent payments across the x402/MPP ecosystem. Built by AgentScore.
`@agent-score/pay` is the universal agent-payment CLI for shell-tool LLM agents. It manages encrypted keystores, parses 402 challenges from any merchant, signs the right credential per rail, and submits the request; all from one shell command.
AgentScore Pay works with **any x402/MPP merchant in the ecosystem**; AgentScore-gated or not. It contacts AgentScore APIs only when the merchant's 402 challenge requires AgentScore identity. For everything else, pay walks the open x402 protocol like any other compliant client.
| Built by | AgentScore |
| - | - |
| Native rails | x402 EVM (Base mainnet/sepolia), MPP (Tempo mainnet/Moderato testnet, Solana mainnet/devnet via `solana/charge`) |
| Discovers via | x402 Bazaar (Coinbase) + mpp.dev/services |
| Identity | merchant-driven: pass `X-Operator-Token`, `X-Wallet-Address`, both, or neither |
| Hints for unsupported rails | yes: points to the right tool (e.g. [`link-cli`](https://github.com/stripe/link-cli) for Stripe SPT) |
## Installation
```bash npm theme={"dark"}
npm install -g @agent-score/pay
```
```bash Homebrew (macOS) theme={"dark"}
brew install agentscore/tap/agentscore-pay
```
```bash npx (no install) theme={"dark"}
npx @agent-score/pay
```
Releases are signed with [Sigstore cosign](https://www.sigstore.dev/) (keyless, GitHub OIDC). The Homebrew formula tarball is sha256-pinned to the npm release.
## First-run setup
Create encrypted keystores for all three native chains in one shot:
```bash theme={"dark"}
agentscore-pay init
```
By default this creates a single BIP-39 mnemonic and derives an EVM wallet (used for both Tempo and Base) and a Solana wallet. Pass `--no-mnemonic` for separate raw keys per chain. The keystore is encrypted with AES-256-GCM (scrypt KDF); set `AGENTSCORE_PAY_PASSPHRASE` to skip the prompt in scripts.
To get testnet funds:
```bash theme={"dark"}
agentscore-pay faucet --chain base # prints Circle + Coinbase faucet URLs
agentscore-pay faucet --chain solana # prints Circle + faucet.solana.com
agentscore-pay init --fund-tempo-testnet # auto-funds via tempo_fundAddress
```
Verify balances:
```bash theme={"dark"}
agentscore-pay balance --network testnet
```
## Paying any x402 endpoint
`agentscore-pay pay` is a drop-in replacement for `curl`. It hits the URL, parses the 402 challenge, signs the credential on the appropriate rail, submits via the right header, and returns the merchant's 200 response. Works with any merchant that speaks x402 or MPP on a rail pay can sign.
```bash theme={"dark"}
agentscore-pay pay POST https://merchant.example/api \
--chain base \
-H 'Content-Type: application/json' \
-d '{"product_id":"...","quantity":1}' \
--max-spend 5
```
Flags:
| Flag | Purpose |
| - | - |
| `--chain {base,solana,tempo}` | Which keystore to sign with. Determines rail (x402 EVM on base, MPP `solana/charge` on solana, MPP `tempo/charge` on tempo). |
| `--network {mainnet,testnet}` | Which network to target. Defaults to mainnet. |
| `--max-spend ` | Hard cap on the payment amount; aborts before signing if exceeded. |
| `--name ` | Use a named keystore instead of `default` (multi-wallet setups). |
| `-H` / `-d` | curl-compatible header / body flags. |
| `--format {toon,json,yaml,md,jsonl}` | Output format. `--json` is shorthand for `--format json`. TOON is the default in agent contexts (token-efficient). |
| `--dry-run` | Show what would be signed and sent without submitting. Useful for agent dev/debug: preview rail selection, signer wallet, expected request shape before any real payment. |
### Idempotency
Every `agentscore-pay pay` invocation generates a stable `X-Idempotency-Key` header; a SHA-256 hash of `(url + method + body + signer)`; so retries within a single invocation reuse the same key. Merchants that honor Stripe-pattern dedup (which includes most production payment systems) won't double-charge if a payment settles but the network response is lost mid-flight. Pass your own `-H 'X-Idempotency-Key: '` to override the auto-generated key (useful for cross-process idempotency: same logical purchase across multiple `agentscore-pay pay` invocations).
### Test-mode addresses
AgentScore reserves seven EVM addresses (`0x0000…0001` through `0x0000…0007`) as deterministic test fixtures; KYC verified, sanctions clear, age gates passing; so dev/test merchants don't burn real KYC credits. `agentscore-pay` recognizes these automatically; the recognizer `isAgentScoreTestAddress(addr)` and `AGENTSCORE_TEST_ADDRESSES` are exported from `@agent-score/sdk` (the canonical home) for your own dev/test fixtures.
### Decimals handling
Spec-compliant 402 responses carry `decimals` in the rail requirements. When a merchant omits it, pay applies a strict policy: canonical USDC (matched against the per-chain Circle USDC registry) silently uses 6 decimals; any unrecognized asset triggers `merchant_spec_violation` and aborts before signing. Refusing to guess avoids orders-of-magnitude mis-billing on tokens with non-6-decimal precision.
## Discovery and probing
Find merchants and probe challenges before paying:
```bash theme={"dark"}
agentscore-pay discover --limit 10 # browse the x402 Bazaar + mpp.dev registry
agentscore-pay discover --chain base --max-price 0.01 # filter to your rail and budget
agentscore-pay check POST https://merchant.example/api -d '...' # see all rails the merchant accepts
agentscore-pay fund-estimate https://merchant.example/api -d '...' # how many calls your balance covers per rail
```
`check` and `discover` surface **all** rails the merchant accepts; including ones pay can't fund directly. For unsupported rails pay knows about (Polygon, other x402 networks, Stripe SPT, …) it prints a structured hint pointing to the right tool, so the agent can complete the payment with a sibling CLI without trial-and-error.
Example (`check` output against a multi-rail merchant):
```
POST https://merchant.example/api → 402 Payment Required (x402)
Accepted rails (natively supported by agentscore-pay):
x402 exact/eip155:8453 $0.01 pay_to=0xAbC1
mpp solana/charge $0.01 pay_to=2LWb…
Other rails accepted (use a compatible client):
x402 Polygon (x402) $0.01
→ https://polygon.technology
x402 Stripe Shared Payment Token $0.01
→ @stripe/link-cli (npm i -g @stripe/link-cli)
```
## Identity model
AgentScore Pay is identity-agnostic; it only sends what the merchant or the agent asks for. The `X-Operator-Token` and `X-Wallet-Address` headers are AgentScore's identity scheme; merchants that don't use AgentScore can ignore them entirely (or use their own identity headers; pay forwards anything the agent passes via `-H`).
For AgentScore-gated merchants, two paths:
* **Operator token**: pass `-H 'X-Operator-Token: opc_...'` OR run `agentscore-pay passport login` once and pay auto-attaches it on every settle leg. Reusable across every AgentScore merchant until expiry. Required for rails that have no wallet signature (Stripe SPT, card).
* **Wallet auth**: pass `-H 'X-Wallet-Address: 0x...'` (or Solana base58). Works on rails that carry a wallet signature (Tempo MPP, Solana MPP, x402 EIP-3009 on Base). The wallet must resolve to the same operator as the payment signer; same-operator linked wallets are fungible.
### AgentScore Passport (one-command identity)
```bash theme={"dark"}
agentscore-pay passport login # opens verify URL, polls until verified, saves opc_ to ~/.agentscore/passport.json
agentscore-pay passport status # token prefix + expiry
agentscore-pay passport logout # revoke remote (credential + its refresh token) + remove local file
```
After `passport login`, every subsequent `agentscore-pay ` call auto-attaches `X-Operator-Token` from the stored Passport. Suppress with `--skip-passport` for explicit-anonymous traffic; caller-supplied `-H "X-Operator-Token: ..."` always wins.
**Token lifecycle:** the access `operator_token` is 24h and the rotating `refresh_token` is 90d. agentscore-pay refreshes the access token silently on the next call after it expires; no agent action needed. The user is only prompted to re-verify in browser (`Open this URL to renew:`) when both have expired (\~90 days of inactivity), or when the Passport was minted via cold-start bootstrap rather than `passport login` (merchant-initiated sessions don't issue a refresh\_token). `agentscore-pay`'s "expires in N days" warning fires only when re-verify is genuinely needed, not on every short-lived access rotation.
### Header precedence
Order of preference for the identity header pay attaches on the settle leg:
1. Caller-supplied `-H 'X-Operator-Token: ...'` or `-H 'X-Wallet-Address: ...'`; wins
2. Stored Passport's `operator_token`; auto-attached as `X-Operator-Token` (skip with `--skip-passport`)
3. `X-Wallet-Address` from the `--chain` keystore; last-resort fallback for first-payment flows where the merchant's gate captures the wallet under the operator
## Native rails
| Rail | Networks | Header | Recovery |
| - | - | - | - |
| **x402 exact** (EVM) | Base mainnet (`eip155:8453`), Sepolia (`eip155:84532`) | `X-Payment` | EIP-3009 USDC `TransferWithAuthorization`; gasless from agent (facilitator pays gas) |
| **Solana MPP** | Solana mainnet-beta, devnet | `Authorization: Payment` | MPP `solana/charge` directive signed against the merchant's challenge |
| **Tempo MPP** | Tempo mainnet (4217), Moderato testnet (42431) | `Authorization: Payment` | MPP `tempo/charge` directive signed against the merchant's challenge |
Address normalization is network-aware; EVM addresses are lowercased before comparison; Solana base58 addresses are preserved verbatim because base58 is case-sensitive.
## Other commands
```bash theme={"dark"}
agentscore-pay wallet list # list named keystores per chain
agentscore-pay wallet create --chain base --name trading # add a named wallet
agentscore-pay wallet import --chain solana --mnemonic "..." # import BIP-39 phrase
agentscore-pay qr --chain base --amount 5 # ASCII QR with EIP-681 / solana: URI
agentscore-pay fund --chain base # Receive QR + balance polling, send USDC from any wallet, exchange, or fiat onramp
agentscore-pay fund --chain base --via stripe-onramp --amount 25 # Stripe Crypto Onramp (card → USDC) on base/solana mainnet; pay never auto-opens browser, prints the hosted crypto.link.com URL
agentscore-pay fund --chain base --via stripe-onramp --amount 25 --quote-only # Preview the Stripe onramp price (fees + USDC out) without minting a session
agentscore-pay send --chain base --to 0xRecipient --amount 5 # Raw USDC transfer (default --asset usdc)
agentscore-pay send --chain base --to 0xRecipient --amount 0.005 --asset native # Send 0.005 ETH (native gas) on Base
agentscore-pay send --chain solana --to --amount 0.01 --asset native # Send 0.01 SOL on Solana
agentscore-pay revoke --chain base --token --spender # revoke an ERC-20 allowance (EVM only)
agentscore-pay whoami # wallets + balances + active config in one payload
agentscore-pay history --limit 20 # past payments from ~/.agentscore/history.jsonl
agentscore-pay limits set --daily 5 --per-call 0.5 --per-merchant 2 # persistent USD caps
agentscore-pay agent-guide # structured how-to for LLM tool-loop agents
# Passport (no API key required, public session endpoint)
agentscore-pay passport login # one-command browser login → operator_token saved + auto-attached
agentscore-pay passport status # token prefix + expiry
agentscore-pay passport logout # remove local; remote revoke (credential + its refresh token) if AGENTSCORE_API_KEY is set
# Other identity commands (require AGENTSCORE_API_KEY)
agentscore-pay assess --address 0xabc... --require-kyc --min-age 21
agentscore-pay sessions create --context "wine purchase" # low-level session API; passport login is the wrapper most agents want
agentscore-pay credentials list
agentscore-pay credentials create --label "claude-code" --ttl-days 7
agentscore-pay associate-wallet --operator-token opc_... --wallet-address 0xabc... --network evm
```
Run `agentscore-pay --help` (or `agentscore-pay --help`) for full flags. For LLM agents, `agent-guide` prints a structured how-to (golden path, testnet path, funding, auxiliary commands, pitfalls, identity-error recovery patterns, exit codes); also available as JSON via `--format json`.
### Identity-error recovery + quota observability
`assess`, `sessions`, `credentials`, and `associate-wallet` surface the SDK's typed errors as structured CliErrors. The JSON envelope's `code` discriminates the recovery path: `config_error` (API-key issue OR token-expired/invalid; `extra` carries `verify_url`/`session_id`/`poll_secret` for the verify-poll flow), `insufficient_balance` (endpoint not enabled; surface to user), `quota_exceeded` (account cap reached; surface to user, agent retry won't fix), `network_error` (transient; retry with backoff). `agentscore-pay agent-guide` lists each pattern with the exact recovery action.
The `pay ` command additionally throws two non-TTY-only codes when a Passport-required state is hit; surfaced as a structured envelope instead of blocking on the inline browser-redirect flow, so unattended agents can route to `passport login`. `passport_login_required` fires when the stored Passport's access token expired AND silent refresh did not succeed (extra carries `previous_token_prefix`); `passport_required_by_merchant` fires when a merchant returns 403 with bootstrap fields and the agent has no usable Passport (extra carries `verify_url`/`session_id`/`poll_secret`/`poll_url`/`order_id`). Both have `next_steps.action: "passport_login"`. In a human TTY, pay drives the inline browser flow itself instead of throwing.
When the account has a per-period quota, `assess` (and other identity commands) emits a `quota: { limit, used, reset }` block on the success envelope, captured from `X-Quota-Limit` / `X-Quota-Used` / `X-Quota-Reset` response headers. Agents can monitor approach-to-cap proactively (warn at 80%, alert at 95%) before hitting `quota_exceeded`.
```bash theme={"dark"}
agentscore-pay assess --address 0xabc... --json | jq '.quota'
# → { "limit": 1000, "used": 780, "reset": "2026-06-01" }
```
## Unlocking the keystore
Two ways to satisfy the passphrase prompt, in precedence order:
| Mechanism | Best for | Persistence |
| - | - | - |
| `AGENTSCORE_PAY_PASSPHRASE=` env var | Containers, CI, serverless, daemons, any non-interactive agent | Process / shell session |
| Per-call interactive prompt | First-time setup, one-off commands | None, never written |
**Env var wins when set**; it produces no on-disk artifact and is the right primitive for every agent context (CI secrets, container env, K8s secrets, Lambda config, MCP host config). There is no on-disk passphrase cache: it would hold the passphrase in cleartext beside the keystores it unlocks.
For agent authors: prefer the env var and read it from your secret store at agent startup. Don't bake it into the agent's prompt or memory. `agentscore-pay agent-guide` emits a structured how-to with the precedence baked in.
## MCP server
`agentscore-pay --mcp` runs the CLI as a stdio JSON-RPC MCP server, exposing every command (wallet + payment + identity) as a tool. `agentscore-pay mcp add` registers the binary with Claude Code, Cursor, Amp, and other MCP-aware agents. `agentscore-pay --llms` emits a markdown manifest of every command (or `--llms --format json` for JSON Schema).
## Scripting
Output adapts to context; terminal use gets pretty-printed, pipes/agents get a structured envelope (TOON by default; token-efficient, \~40% fewer tokens than JSON). Pass `--json` (or `--format yaml|md|jsonl`) to lock the format. `AGENTSCORE_PAY_PASSPHRASE` skips the passphrase prompt for unattended runs:
```bash theme={"dark"}
RESPONSE=$(AGENTSCORE_PAY_PASSPHRASE=$PAY_PASS \
agentscore-pay pay POST "$URL" \
-H 'Content-Type: application/json' \
-d "$BODY" \
--max-spend "$MAX_SPEND" \
--json)
echo "$RESPONSE" | jq .body.order
```
Exit codes are documented in `agentscore-pay --help`; non-zero exits indicate an error category (`invalid_input`, `unsupported_rail`, `network_error`, `merchant_error`, etc.) so scripts can branch.
## Source + releases
* npm: [npmjs.com/package/@agent-score/pay](https://www.npmjs.com/package/@agent-score/pay)
* GitHub: [github.com/agentscore/pay](https://github.com/agentscore/pay)
* Homebrew tap: [github.com/agentscore/homebrew-tap](https://github.com/agentscore/homebrew-tap)
Each release publishes signed native binaries (darwin-arm64, darwin-x64, linux-arm64, linux-x64, windows-x64) alongside the npm package, with cosign signatures + certificates for offline verification.
# AgentScore SDK (Python)
Source: https://docs.agentscore.com/integrations/python
Official Python client for AgentScore.
## Installation
```bash theme={"dark"}
pip install agentscore-py
```
## Quick start
```python theme={"dark"}
from agentscore import AgentScore
client = AgentScore(api_key="your-api-key")
# On-the-fly assessment with decision
assessment = client.assess(
"0xdb5aa553feeb2c3e3d03e8360b36fb0f7e480671",
policy={"require_kyc": True},
)
if assessment["decision"] == "deny":
print("Denied:", assessment["decision_reasons"])
```
## API reference
### Constructor
```python theme={"dark"}
AgentScore(
api_key: str,
base_url: str = "https://api.agentscore.com",
timeout: float = 10.0,
user_agent: str | None = None, # custom prefix prepended to default UA
)
```
Set `user_agent` to prepend your app's identifier so AgentScore support can trace outbound traffic back to you:
```python theme={"dark"}
client = AgentScore(api_key="...", user_agent="myapp/1.0")
# Outbound UA: "myapp/1.0 (agentscore-py/x.y.z)"
```
### `client.assess(address=None, chain=None, policy=None, operator_token=None, signer=None)`
On-the-fly trust assessment with policy evaluation. Available on every tier, gated by monthly quota. Provide `address` (wallet) or `operator_token` (credential).
```python theme={"dark"}
# With wallet address
result = client.assess(
"0x1234...",
policy={"require_kyc": True},
)
# With operator credential (non-wallet agents)
result = client.assess(
operator_token="opc_abc123...",
policy={"require_kyc": True, "min_age": 21},
)
# With an explicit chain
result = client.assess(
"0x1234...",
policy={"require_kyc": True},
chain="base", # optional
)
if result["decision"] == "deny":
print("Denied:", result["decision_reasons"])
```
### Server-side signer-match + OFAC SDN screening
Pass `signer={"address", "network"}` to opt into server-side wallet-signer-match and OFAC SDN wallet-address screening in a single round trip:
```python theme={"dark"}
result = client.assess(
"0xclaimed...",
signer={"address": "0xsigner...", "network": "evm"},
policy={"require_sanctions_clear": True},
)
# Wallet-binding verdict
match = result.get("signer_match") # {"kind": ..., ...} or None
if match and match["kind"] == "wallet_signer_mismatch":
# Signer wallet resolves to a different operator than the claimed address
print("Mismatch:", match["expected_signer"], "vs", match["actual_signer"])
# OFAC SDN wallet-address verdict (discriminated union)
sanctions = result.get("signer_sanctions")
if sanctions and sanctions.get("sanctioned"):
print("OFAC hit:", sanctions["ofac_label"], sanctions["sdn_uid"])
```
Set `signer.address=None` for rails with no wallet signer (Stripe SPT, card); the response's `signer_match.kind` will be `wallet_auth_requires_wallet_signing`. Wallet-OFAC SDN enforcement on the `signer` block is unconditional whenever a signer is supplied; no `policy.require_sanctions_clear` opt-in required. A `signer_sanctions` hit OR `status: "unavailable"` flips `decision` to `deny` with `decision_reasons` including `sanctions_flagged` or `sanctions_check_unavailable` respectively (fail-closed; OFAC strict-liability). `policy.require_sanctions_clear` is the separate NAME-based screen on the resolved operator's KYC identity.
The `aassess()` async variant takes the same `signer` keyword.
## Compliance assessment
Use compliance policy fields to enforce KYC, sanctions, age, and jurisdiction requirements:
```python theme={"dark"}
result = client.assess(
"0x1234...",
policy={
"require_kyc": True,
"require_sanctions_clear": True,
"min_age": 18,
"blocked_jurisdictions": ["KP", "IR"],
},
)
if result["decision"] == "deny":
print("Denied:", result["decision_reasons"])
print("Policy checks:", result["policy_result"]["checks"])
# If denial is resolvable through verification, verify_url is present
if result.get("verify_url"):
print("Verification required:", result["verify_url"])
```
## Quota observability
`assess()` / `aassess()` responses include an optional `quota` field captured from `X-Quota-Limit` / `X-Quota-Used` / `X-Quota-Reset` headers so callers can monitor approach-to-cap proactively before hitting 429:
```python theme={"dark"}
result = client.assess("0x1234...", policy={"require_kyc": True})
quota = result.get("quota")
if quota and quota["limit"] and quota["used"]:
pct = quota["used"] / quota["limit"]
if pct >= 0.95:
log.warning("AgentScore quota at %.0f%%; resets at %s", pct * 100, quota["reset"])
```
`reset` is the UTC date (`YYYY-MM-DD`) the calendar-month quota resets, e.g. `"2026-06-01"`; every field is `None` when the API sent no quota headers (Enterprise and unlimited plans).
## Typed errors
The SDK raises subclasses of `AgentScoreError` so callers can branch on the class without parsing `err.code`:
```python theme={"dark"}
from agentscore.errors import (
AgentScoreError,
InvalidCredentialError,
PaymentRequiredError,
QuotaExceededError,
RateLimitedError,
TimeoutError,
TokenExpiredError,
)
try:
result = client.assess(operator_token="opc_...", signer={"address": "0xs", "network": "evm"})
except TokenExpiredError as err:
# 401 token_expired; credential revoked or TTL-expired. Body carries an auto-minted
# verification session so the agent can recover without an API key.
print("Verify URL:", err.verify_url, "session_id:", err.session_id, "poll_secret:", err.poll_secret)
except InvalidCredentialError:
# 401 invalid_credential; operator_token doesn't exist; permanent state.
...
except QuotaExceededError:
# 429 quota_exceeded; don't retry; wait for the quota reset window.
...
except RateLimitedError as err:
# 429 rate_limited; the SDK already waited out Retry-After and retried once, so back off before trying again.
...
except PaymentRequiredError:
# 402; vendor tier misconfig.
...
except AgentScoreError:
# 5xx / network failures wrap to AgentScoreError(code="network_error", status_code=0).
...
```
On `wallet_signer_mismatch` responses (verdict, not exception), the SDK consumer can spread `signer_match.agent_instructions` (a JSON-encoded `{action, steps, user_message}` envelope) directly into a 403 body without re-deriving recovery copy.
### `client.create_session(context=None, product_name=None, kind=None)`
Create a verification session for identity bootstrapping. Returns session details for agent polling. `kind="sign_in"` makes it registration-only: the buyer signs in with an AgentScore account, no identity documents. (The deprecated `address` and `operator_token` parameters are ignored by the API.)
```python theme={"dark"}
session = client.create_session(
context="purchase",
product_name="Premium Access",
)
print(session["session_id"]) # sess_...
print(session["poll_secret"]) # poll_...
print(session["verify_url"]) # URL for operator to visit
print(session["poll_url"]) # URL for agent to poll
```
The merchant name shown to operators on the verify page is derived from your account's merchant name (Dashboard → Settings).
### `client.poll_session(session_id, poll_secret)`
Poll a verification session. Returns `operator_token` when the operator completes verification.
```python theme={"dark"}
result = client.poll_session(session["session_id"], session["poll_secret"])
if result["status"] == "verified":
token = result["operator_token"] # opc_... (returned once)
elif result["status"] == "pending":
pass # keep polling
elif result["status"] == "expired":
pass # session timed out
```
### `client.create_credential(label=None, ttl_days=None)`
Create an operator credential. Default TTL is 24 hours, configurable 1-365 days.
```python theme={"dark"}
cred = client.create_credential(label="my-agent", ttl_days=7)
print(cred["credential"]) # opc_... (shown once, save it)
print(cred["id"]) # UUID for management
print(cred["expires_at"]) # ISO timestamp
```
### `client.list_credentials()`
List active (non-expired, non-revoked) credentials.
```python theme={"dark"}
result = client.list_credentials()
for cred in result["credentials"]:
print(cred["prefix"], cred["label"], cred["expires_at"])
```
### `client.revoke_credential(credential_id)`
Revoke a credential by ID.
```python theme={"dark"}
client.revoke_credential("credential-uuid")
```
### `client.associate_wallet(operator_token, wallet_address, network, idempotency_key=None)`
Report a signer wallet that paid under a credential. Used by merchants after successful payment to build a cross-merchant credential↔wallet profile.
```python theme={"dark"}
client.associate_wallet(
operator_token="opc_...",
wallet_address=signer_from_payment, # EIP-3009 `from`, Tempo MPP DID, or Solana pubkey
network="evm", # or "solana"
idempotency_key=payment_intent_id, # optional, retries no-op
)
```
Fire-and-forget. Response is `{"associated": True, "first_seen": bool, "deduped"?: True}`.
## Async support
All methods have async counterparts prefixed with `a`:
```python theme={"dark"}
import asyncio
from agentscore import AgentScore
async def main():
async with AgentScore(api_key="your-api-key") as client:
decision = await client.aassess("0x1234...", policy={"require_kyc": True})
print(decision["decision"])
asyncio.run(main())
```
The async context manager (`async with`) automatically closes the HTTP client on exit.
## Sync context manager
```python theme={"dark"}
from agentscore import AgentScore
with AgentScore(api_key="your-api-key") as client:
result = client.assess("0x1234...", policy={"require_kyc": True})
print(result["decision"])
```
## Error handling
```python theme={"dark"}
from agentscore import AgentScore, AgentScoreError
client = AgentScore(api_key="your-api-key")
try:
result = client.assess("0xinvalid")
except AgentScoreError as e:
print(e.code) # "invalid_identity"
print(e.status_code) # 400
print(e.status) # 400 (alias for .status_code)
print(str(e)) # error message
```
The `.status` property mirrors `.status_code` so polyglot codebases can use the same attribute name regardless of which SDK raised the error.
`AgentScoreError.details: dict[str, Any]` carries response-body fields beyond `{code, message}`; `verify_url`, `linked_wallets`, `claimed_operator`, `actual_signer`, `expected_signer`, `reasons`, `agent_memory`; so callers can branch on granular denial codes without re-parsing:
```python theme={"dark"}
try:
client.assess("0xabc...", policy={"require_kyc": True})
except AgentScoreError as e:
if e.code == "wallet_signer_mismatch":
linked = e.details.get("linked_wallets", [])
# re-sign from any address in `linked`, or switch to X-Operator-Token
elif e.code == "token_expired":
verify_url = e.details.get("verify_url")
# surface verify_url to the user so they can re-verify
```
## Links
* [PyPI](https://pypi.org/project/agentscore-py/)
* [GitHub](https://github.com/agentscore/python-sdk)
# AgentScore Commerce (Python)
Source: https://docs.agentscore.com/integrations/python-commerce
Identity middleware + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for agentic commerce.
`agentscore-commerce` is the full merchant-side SDK for agentic commerce in Python. One install bundles identity gating, payment-protocol helpers, 402 challenge builders, discovery doc generators, and Stripe multichain support. Submodule imports keep dependencies focused.
## Installation
```bash theme={"dark"}
pip install agentscore-commerce[fastapi] # or [flask], [django], [aiohttp], [sanic], [starlette], [stripe]
```
Accept x402 payments through the Coinbase facilitator (mints per-endpoint CDP JWTs via `cdp-sdk`):
```bash theme={"dark"}
pip install 'agentscore-commerce[fastapi,x402,coinbase]'
# Set CDP_API_KEY_ID and CDP_API_KEY_SECRET in the environment.
```
The `[mppx]` extra adds Tempo MPP + Stripe SPT helpers.
## Build the x402 accepts entry for a 402 challenge
```python theme={"dark"}
from agentscore_commerce.payment import build_x402_accepts_for_402, create_x402_server
x402_server = await create_x402_server(facilitator="coinbase", rails=["x402-base-mainnet"])
x402_accepts = build_x402_accepts_for_402(
x402_server,
network="eip155:8453",
price=f"${total_usd}",
pay_to=os.environ["TREASURY_BASE_RECIPIENT"],
max_timeout_seconds=300,
)
```
Returns a list of plain dicts ready for the 402 body's `accepts[]`. `extra.name` is derived from the registered scheme metadata so the EIP-712 domain matches the on-chain USDC contract.
## Submodules
| Import | What it gives you | |
| - | - | - |
| `agentscore_commerce` (top-level) | `Checkout` orchestrator + `CheckoutContext` + `CheckoutGateConfig` + `CheckoutValidationError` + `DiscoveryProbeConfig` + `SettleOutcome` + `MppxComposeOutcome` + `PricingResult` (one config object, hooks for `pre_validate`/`compute_pricing`/`on_settled`/`mint_recipients`/`compose_mppx`, auto-derived x402+pympp servers, per-framework adapters `handle_fastapi`/`handle_flask`/`handle_django`/`handle_aiohttp`/`handle_sanic`, signed UCP routes via `mount_ucp_routes_{fastapi,flask,django,aiohttp,sanic}`). Plus `compute_first_checkout`: variable-cost pay-per-result helper (compute-first + exact-x402) that works on every exact-mode rail without upto / Permit2 / Settlement-Overrides; `create_quote_cache`: content-hash quote cache used by the compute-first helper (in-memory default; pass `redis_url` for distributed deployments). `create_result_cache`: the neutral keyed JSON-value cache underneath it (same body-hash key builder); cache any probe-leg result a `Checkout` merchant replays on the settle leg, for example a paid upstream call made in `pre_validate`. `create_default_on_denied({merchant_name, support_email, support_context?, wallet_not_trusted_message?, payment_required_message?})`: canonical `on_denied(reason) -> DefaultOnDeniedResult` factory matching `Checkout`'s gate hook (handles `wallet_signer_mismatch`, `wallet_not_trusted` unfixable fallback, `payment_required`, `token_expired`/`invalid_credential`/`api_error`). `has_payment_header(request_or_headers)`: discriminator that splits discovery legs (no payment credential → 402) from settle legs (`payment-signature` / `x-payment` / `Authorization: Payment JWT-BYTES`); `has_x402_header` / `has_mppx_header`: granular dispatch helpers (x402 vs MPP credential present). \`malformed\_payment\_credential(request | headers)`: wire-shape gate for payment credentials (values that are not base64 JSON and not token-shaped are rejected); `Checkout`runs it before any merchant hook by default, opt out with`credential\_pre\_check=False`. `default\_read\_only\_on\_denied(reason)`: canonical `on\_denied` for read-only resource gates (`GET /orders/:id`): collapses every denial to 401 `unauthorized`+`Cache-Control: no-store`while still spreading`denial\_reason\_to\_body`. Returns `DefaultOnDeniedResult(body, status, headers)`; FastAPI / Flask / aiohttp / Sanic `on\_denied`callbacks accept an optional 3-tuple`(body, status, headers)`to thread headers through.`extract\_owner\_scope(headers) -> OwnerScope`: pull canonical owner identity from `X-Wallet-Address`/`X-Operator-Token`with safe token hashing. Plus factories:`pricing\_result`(cents → typed`PricingResult`), `validation\_response\_`(4xx envelope per framework),`make\_mppx\_compose\_hook\`. |
| `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware: KYC, age, sanctions (account name + signer wallet), jurisdiction. `AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic), `capture_wallet(...)`, `get_signer_verdict(...)`, `get_agentscore_data(...)`. Each adapter also exports a conditional variant that wraps the gate so it fires only on settle legs (anonymous discovery flows through and gets a 402 with all rails): FastAPI / ASGI expose `ConditionalAgentScoreGate`; Django exposes `ConditionalAgentScoreMiddleware`; Flask + Sanic expose `conditional_agentscore_gate(app, ...)`; aiohttp exposes `conditional_agentscore_gate_middleware(...)`. The existing `agentscore_gate(app, ...)` and `AgentScoreGate(...)` accept an optional `condition=` callable for inline gating. | |
| `agentscore_commerce.identity.policy` | Per-product compliance helpers: `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`, `validate_shipping_against_policy` (one-call country+state validator raising `CheckoutValidationError` with the canonical envelope on miss). All five are also re-exported at `agentscore_commerce` top-level. | |
| `agentscore_commerce.identity` (publishers + key/token helpers) | Cross-vendor identity-publishing helpers: `build_a2a_agent_card` (A2A v1.0 Agent Card, served at `/.well-known/agent-card.json`), `build_ucp_profile` (Google Universal Commerce Protocol, served at `/.well-known/ucp`). Both return unsigned dataclasses with `to_dict()`: vendor signs + publishes. Key + token helpers: `load_ucp_signing_key_from_env` (cached env-driven loader: reads `UCP_SIGNING_KEY_JWK_PRIVATE` JSON JWK, detects alg from shape, falls back to ephemeral when unset, sanitizes errors so key bytes never reach logs, concurrent-safe via threading.Lock); `hash_operator_token` (sha256 hex of plaintext `opc_...`; for merchants persisting `operator_token_id` to their own DB without ever storing the plaintext); `extract_owner_scope(headers) -> OwnerScope` (canonical owner-identity extractor for caller-scoped resource queries; reads `X-Wallet-Address` / `X-Operator-Token`, hashes the token so plaintext never leaves the request). | |
| `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_headers` (one-call WWW-Authenticate + PAYMENT-REQUIRED bundle), `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (opt-in v1↔v2 amount-field shim; the 402 builders do NOT apply it by default: strict x402 v2 settlement matches the whole accepts object, so an extra field breaks settle; reach for it only when a specific client is hardcoded to read `maxAmountRequired`), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (accepts positional `x402_payment_header` AND/OR `authorization_header=` kwarg; recovers signer from x402 EIP-3009 `payload.authorization.from` OR MPP `Authorization: Payment BASE64` `did:pkh:eip155:CHAIN:ADDR` / `did:pkh:solana:GENESIS:ADDR` source DID), `parse_did_pkh_address` (parses raw `did:pkh:FAMILY:CHAIN:ADDR` DIDs into a typed `PaymentSigner`; used by `extract_payment_signer` and re-exported for callers receiving a typed source string from pympp), `detect_rail_from_headers` (returns `"x402"` / `"mpp"` / `None` from inbound headers), `build_idempotency_key`; `create_x402_server`, `create_mppx_server`; `build_default_checkout_rails(tempo=, x402_base=, solana_mpp=, stripe=)` (canonical 4-rail `rails` dict factory: merchants pass per-rail overrides instead of redeclaring the recipient sentinel + network/chain\_id/token boilerplate. Flipping `network` alone derives the right `token` / `chain_id`: Base Sepolia → Sepolia USDC + chain\_id 84532, Solana devnet → devnet USDC mint. Solana's `network` accepts CAIP-2 or the raw `@solana/mpp` form `mainnet-beta` / `devnet` / `localnet`), `build_mppx_compose_rails(amount_usd=, tempo_recipient=, solana_recipient=, ...)` (per-call intent factory replacing the hand-rolled `[("tempo/charge", {...}), ("solana/charge", {...}), ("stripe/charge", {...})]` list; auto-handles USD→atomic conversion for Solana; auto-drops `stripe/charge` when `amount_usd < 0.50` since Stripe's fixed \~$0.30 fee makes sub-50-cent charges unprofitable; sub-50-cent APIs pass `include_stripe=False` explicitly), `is_evm_network` / `is_solana_network` (CAIP-2 discriminators), `has_payment_header` (settle-leg discriminator); drop-in x402 helpers: `validate_x402_network_config` (boot-time guard), `verify_x402_request` (parse + validate inbound X-Payment), `process_x402_settle` (verify-then-settle in one call), `classify_x402_settle_result` (maps the tagged settle result to a recommended HTTP status / code / next_steps so merchants get a controlled envelope without coupling to facilitator-specific error text), `classify_orchestration_error` (same `ClassifiedX402Error` shape but for uncaught exceptions thrown elsewhere in the orchestration; conservative substring fallback returns `None` for unknown errors so merchants rethrow instead of swallowing); `zero_amount_carve_out` (skip CDP / pympp upstream verify+settle for $0 settles where the upstream rejects value=0 payloads; parses the credential, lifts signer + network, returns a `ZeroSettleResult` shaped identically to the success path so callers branch on rail, not on result shape); `usd_to_atomic` (Decimal-based USD → atomic int, ROUND\_HALF\_UP, rejects NaN / negative / infinite; for Tempo / Solana / Base USDC amount construction). | |
| `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers; the sample envelope always carries a v2 `resource` and threads optional `extensions`, and `Checkout` fills both from its own `url`/`resource_info`/`discovery_extensions`, because envelope validators require `resource` and read the Bazaar example input to build valid follow-up probe bodies), `sample_x402_accept_for_network`, `build_well_known_mpp`, `build_well_known_x402` (x402scan v1 `/.well-known/x402` shape), `build_llms_txt`, `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest), `build_redemption_skill_md` (delivery-neutral redemption-code template; printed mailers, emailed codes, API trial credits all covered; `endpoint_path`/`delivery_intro`/`body_shape`/`body_rules`/`extra_recovery_rows` overrides for non-goods shapes), `build_merchant_index_json` + `standard_endpoint_descriptions(kind=)` (canonical `/` discovery body for goods vs api merchants), `build_success_next_steps` (universal Passport-active success block), `build_agentscore_onboarding_steps`, `agentscore_openapi_snippets`, `siwx_security_scheme` + `x_payment_info_extension` + `x_guidance_extension`, `build_bazaar_discovery_payload`; `NoindexNonDiscoveryMiddleware` (ASGI) + `install_flask_noindex` (Flask) + `DjangoNoindexMiddleware` (Django): emit `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces; pure helpers `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for any framework not listed. Plus the UCP/JWKS publish surface: `build_signed_ucp_response`, `build_signed_jwks_response`, `well_known_preflight_response`, `default_a2a_services`, `bootstrap_ucp_signing_key`, framework-neutral `SignedDiscoveryResponse` + per-framework wrappers `signed_response_{fastapi,flask,django,aiohttp,sanic}`. | |
| `agentscore_commerce.challenge` | `build_accepted_methods`, `build_identity_metadata` (auto-attached by `Checkout` when wallet header present), `build_how_to_pay`, `build_agent_instructions` (auto-emits per-rail `compatible_clients`; pure helper `compatible_clients_by_rails(rails)` returns the same map for vendors building custom 402s), `build_402_body` (pass `x402=X402PaymentRequired(resource=X402ResourceInfo(...))` for x402 v2 resource metadata, `url` plus optional `service_name` / `tags` / `description` / `mime_type` / `icon_url` emitted with x402 camelCase keys, and `extensions` for declared extensions; both surface on the 402 body for spec-compliant crawlers, and `respond_402` mirrors `resource` into the PAYMENT-REQUIRED header), `build_pricing_block` (cents → dollar-string; optional `shipping_cents` + `discount_cents`; pass `discount_cents` for redemption codes / coupons so the 402 body surfaces `discount` alongside `subtotal` / `total` for agent-renderable savings; optional `decimals` for sub-cent precision so per-token / per-byte unit pricing advertises real amounts instead of rounding to two decimals), `first_encounter_agent_memory`, `Receipt`/`ReceiptNextSteps`/`ProductInfo`/`ShippingAddress` dataclasses (canonical 200-receipt shape); `respond_402`: drop-in 402 emit that preserves pympp's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `build_validation_error`: structured 4xx body builder (`{error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}`) so vendors compose body shapes by name instead of inlining at every validation site. | |
| `agentscore_commerce.middleware.{fastapi,flask,django,aiohttp,sanic,asgi}` | Framework-specific rate-limit middleware. FastAPI: `rate_limit_fastapi(...)` (FastAPI dependency) plus the ASGI `RateLimitMiddleware` re-export. Flask: `rate_limit_flask(app, ...)` installer. Django: class-based async `RateLimitMiddleware` configured via `settings.AGENTSCORE_RATE_LIMIT`. aiohttp: `rate_limit_aiohttp(...)` middleware factory. Sanic: `rate_limit_sanic(app, ...)` installer. `asgi.RateLimitMiddleware` works with any starlette-compatible app. Shared options: `window_seconds` (default 60), `max_requests` (default 60), `key_resolver` (default first hop of `x-forwarded-for`), `redis_url` (lazy-imports `redis.asyncio` when set, in-memory `dict` fallback otherwise), `key_prefix`. `redis` is an optional peer dep (install via the `redis` extra). | |
| `agentscore_commerce.stripe_multichain` | `create_multichain_payment_intent` (returns `MultichainPaymentIntentResult`; read `result.deposit_addresses[network]` directly), `create_pay_to_address_from_stripe_pi(authorization_header=, amount_cents=, stripe=, pi_cache=, networks=, static_recipients=, metadata=, order_id=, preferred_network=)`: per-order payTo resolver matching `Checkout.mint_recipients`: on the settle leg, reuses the buyer's signed-against payTo from the MPP credential (after `pi_cache.has_address` check OR a `static_recipients` match; the static address is always-accepted because the merchant owns it); on the discovery leg, mints a fresh PI for the rails NOT covered by `static_recipients`, caches the merged map, registers static addresses with `pi_cache.cache_address` so verify-leg lookups pass. `mint_multichain_recipients(...same kwargs)`: structured variant returning `MintMultichainRecipientsResult(recipients, payment_intent_id, reused_from_credential)` for the full per-rail map (typical multi-rail merchant hook). Use `static_recipients={"solana": ""}` for low-margin endpoints where Solana per-call ATA rent (\~\$0.50 against MPP spec §13.6) dominates revenue; the SDK skips Stripe minting on that network and reuses the static recipient forever; pair with a one-time external USDC transfer to pre-create the recipient's USDC ATA and every settle pays only the per-tx fee. `SolanaMppRailSpec.ata_creation_required` defaults to `True` (data-only; solana method registration through `create_mppx_server` is a follow-up; merchants building the solana method directly via `pympp` should pass `ata_creation_required` themselves to the charge factory). `simulate_crypto_deposit`, `create_mppx_stripe`; `create_pi_cache` (TTL'd PI / deposit-address cache, Redis-backed when `redis_url` set), `simulate_deposit_if_test_mode` (gates on `sk_test_` and looks up the PI for you), `simulate_deposit_for_outcome(outcome=, deposit_address=, get_payment_intent_id=, stripe_secret_key=, stripe_version=)` (dispatches the simulator based on a Checkout / compute\_first\_checkout settle outcome), `network_for_outcome` (outcome → simulator network arg, handles both Checkout-shaped `rail_key` and compute-first-shaped `mpp_method`, accepts bare scheme names AND `SCHEME/charge` forms), `STRIPE_TEST_TX_HASH_SUCCESS` / `STRIPE_TEST_TX_HASH_FAILED` constants. | |
| `agentscore_commerce.api` | Everything from `agentscore-py` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `is_agentscore_test_address`. Don't add `agentscore-py` as a separate dep: the two can drift versions. | |
## Identity model
Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-Token`). Default checks operator-token first, then wallet. Address normalization is network-aware: EVM lowercased, Solana base58 preserved verbatim.
`DenialReason` codes (`missing_identity`, `identity_verification_required`, `token_expired`, `invalid_credential`, `wallet_signer_mismatch`, `wallet_auth_requires_wallet_signing`, `wallet_not_trusted`, `api_error`, `payment_required`) each carry a structured `agent_instructions` JSON block describing concrete recovery actions.
`create_session_on_missing` auto-mints a verification session for two paths: cold-start (no identity headers) AND `wallet_not_trusted` with fixable reasons (`kyc_required` / `kyc_pending` / `kyc_failed`). Both paths rewrite the denial to `identity_verification_required` before reaching `on_denied`, so merchants only handle one code. **When the merchant omits `create_session_on_missing` from the gate config, `Checkout` auto-defaults it** from `gate.api_key` + `gate.base_url` + `gate.context` + `gate.merchant_name`; merchants that need `on_before_session` side effects (e.g. pre-minting an order\_id) supply their own config to override. `build_verification_required_body(reason, message=?, agent_instructions=?, extra=?)` collapses the per-merchant body-mapping boilerplate. The gate middleware extracts the payment signer pre-evaluate (`extract_payment_signer(x402_header)`) and passes `signer={address, network}` to `/v1/assess`; one round trip carries the gate verdict AND the wallet-signer-match outcome AND the `signer_sanctions` OFAC SDN wallet-address verdict. Merchants read both back synchronously via `get_signer_verdict(request)` off the gate's cache (no extra HTTP call). **Wallet-OFAC SDN enforcement is unconditional** whenever the signer is supplied; an SDN hit OR an unavailable wallet-sanctions lookup flips the gate decision to deny before the handler runs (fail-closed; OFAC strict-liability), without requiring `policy.require_sanctions_clear` to opt in. `policy.require_sanctions_clear` is the separate NAME-based screen on the operator's KYC identity. `capture_wallet` is fire-and-forget; POSTs the signer to `/v1/credentials/wallets` so the operator's cross-merchant credential↔wallet profile builds up over time.
**Getting a `verify_url` before paying.** The gate runs on the settle leg, so a buyer with no identity would otherwise reach the session 403 only by attaching a payment credential first (a Stripe Shared Payment Token buyer would have to mint a token just to learn they need to verify). A request carrying `X-Verification-Session: create` with no identity header and no payment credential runs the gate instead, and gets the same session-bearing 403 (`verify_url`, `session_id`, `poll_secret`, `poll_url`) without paying. `Checkout` advertises the path in its discovery 402 as an `identity_bootstrap` block whenever the request has no identity header, and every conditional gate adapter honors the header. A merchant hand-rolling its own conditional wrap uses `should_run_conditional_gate(request)` from `agentscore_commerce.payment` (a payment credential, or `requests_verification_session(request)`) and advertises the path by passing `build_identity_bootstrap()` into `build_402_body`'s `extra` as `identity_bootstrap`. The header is opt-in so discovery crawlers replaying an example body never mint sessions; merchants without an identity gate neither advertise nor honor it.
## Identity publishing (cross-vendor standards)
Two helpers compose AgentScore identity into the payload formats published to other agent-commerce ecosystems. Each returns an unsigned dataclass; your service signs + serves it however its key infrastructure works.
```python theme={"dark"}
from agentscore_commerce import (
A2AAgentSkill,
AgentScoreGatePolicy,
StripeRailSpec,
TempoRailSpec,
UCPPaymentHandlerBinding,
UCPServiceBinding,
UCPSigningKey,
build_a2a_agent_card,
build_ucp_profile,
mpp_payment_handler,
stripe_spt_payment_handler,
ucp_a2a_extension,
x402_payment_handler,
X402BaseRailSpec,
)
# 1. A2A v1.0 Agent Card, published at /.well-known/agent-card.json.
# Per UCP §A2A binding (https://ucp.dev/specification/checkout-a2a/), agents that
# serve UCP via the A2A transport MUST declare the canonical UCP extension URI in
# capabilities.extensions[] so platforms arriving via A2A detect UCP support
# without re-fetching the profile. Pass an empty capabilities map when you serve
# UCP at the discovery layer but have no formal capability bindings yet.
# Skills declare what the agent can do; they are AgentSkill objects (top-level on
# the card). The card is returned unsigned; to sign it, compute RFC 7515 JWS
# entries over the card body and pass them as `signatures`.
card = build_a2a_agent_card(
name="My Agent Service",
description="Buy products via agent payments.",
url="https://agents.example.com",
version="1.0.0",
skills=[
A2AAgentSkill(
id="purchase",
name="Purchase",
description="Buy products via agent payments.",
tags=["commerce", "payment"],
),
],
extensions=[ucp_a2a_extension()],
)
# 2. Google Universal Commerce Protocol profile, published at /.well-known/ucp
# Output shape: {"ucp": {"version", "services", "capabilities",
# "payment_handlers", "name?", "supported_versions?"}, "keys": [...]}
# services / capabilities / payment_handlers are MAPS keyed by reverse-DNS
# service / capability / handler name (UCP spec §3 + §6).
profile = build_ucp_profile(
name="My Agent Service",
services={
"dev.ucp.shopping": [
UCPServiceBinding(
version="2026-08-25",
spec="https://ucp.dev/2026-08-25/specification/overview",
transport="mcp",
endpoint="https://agents.example.com/api/ucp/mcp",
schema="https://ucp.dev/services/shopping/mcp.openrpc.json",
),
],
},
payment_handlers={
**mpp_payment_handler(networks=[TempoRailSpec(recipient=TEMPO_ADDR)]),
**x402_payment_handler(networks=[X402BaseRailSpec(recipient=BASE_ADDR)]),
**stripe_spt_payment_handler(spec=StripeRailSpec(profile_id="profile_5xKvNqM9BaH")),
},
keys=[UCPSigningKey(kid="me", kty="EC", alg="ES256", crv="P-256", extras={"x": "...", "y": "..."})],
# Optional: declare merchant gate policy as a `com.agentscore.identity` capability
# binding inside the public profile. Static policy declaration only; no per-operator
# claims. Per-operator identity attestation flows through the AP2 risk-signal endpoint.
agentscore_gate=AgentScoreGatePolicy(
require_kyc=True, min_age=21, allowed_jurisdictions=["US"],
),
)
```
Note: ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol. Not an identity-publishing surface. ACP merchants integrate through the existing `build_402_body` + `build_payment_headers` + Stripe SPT rail.
### Signing UCP profiles (vendor extension; opt-in for trust-mode verifiers)
UCP §6 doesn't mandate profile-body JWS signing; production UCP merchants commonly ship unsigned. AgentScore's `agentscore-profile+jws` is a vendor extension layered on top of the unsigned UCP profile for trust-mode verifiers (regulated-commerce, AP2-aware) that opt into auditable cryptographic provenance. Vanilla UCP agents read the canonical body and ignore the `signature` field. `agentscore-commerce` ships four helpers that ride on the `joserfc` optional dep (`pip install agentscore-commerce[ucp]`):
```python theme={"dark"}
from agentscore_commerce import (
UCPSigningKey,
UCPVerificationError,
build_jwks_response,
generate_ucp_signing_key,
sign_ucp_profile,
verify_ucp_profile,
)
# One-time per merchant: generate or load a key. Persist private_key in your secrets manager.
key = generate_ucp_signing_key(kid="merchant-2026-05")
# On every /.well-known/ucp request:
profile = build_ucp_profile(
name="My Agent Service",
services={...}, # keyed by service name, see above
payment_handlers={...}, # keyed by handler reverse-DNS name, see above
keys=[UCPSigningKey.from_jwk(key.public_jwk)],
)
signed = sign_ucp_profile(
profile.to_dict(),
signing_key=key.private_key,
kid=key.public_jwk["kid"],
alg="EdDSA",
)
return signed
# On /.well-known/jwks.json:
return build_jwks_response([key.public_jwk])
```
`verify_ucp_profile` enforces the JWS protected header `typ='agentscore-profile+jws'` (vendor-namespaced; UCP §6 doesn't define a profile-as-JWS typ), restricts `alg` to `EdDSA`/`ES256`, requires a `kid`, rejects duplicate kids, and compares canonical body bytes against the JWS payload. Failures raise `UCPVerificationError` with a discriminated `code` attribute.
`sign_ucp_profile` rejects profiles containing `float` values: cross-language float canonicalization is not stable. Use decimal strings (e.g. `"9.99"`) for monetary or fractional fields in `extras`.
**Persisting the private JWK.** Mint once via `generate_ucp_signing_key()`, serialize via `key.private_key.as_dict(private=True)`, store in your secret manager. On each container start, read the secret, `OKPKey.import_key(jwk_dict)` (or `ECKey.import_key` for ES256) to re-hydrate. KMS-backed flows require subclassing the joserfc Key to delegate the sign hook.
**Key rotation.** Mint a new key with a new `kid`, add the public JWK to your JWKS endpoint alongside the old one, then sign new profiles with the new key. Drop the old JWK after your verifier-side cache TTL has elapsed.
**Inline JWK in the profile vs separate JWKS endpoint.** UCP §6 mandates the separate `/.well-known/jwks.json` endpoint as the canonical trust source. The profile's `keys[]` is informational; verifiers MUST resolve the kid against the JWKS to prevent a swap-after-sign attack.
Verifiers reconstruct the canonical body (everything except `signature`, keys sorted at every level), look up the kid in JWKS, and check the JWS. `verify_ucp_profile(signed, jwks)` does this for you. Both EdDSA (Ed25519) and ES256 are supported. EdDSA is the default and recommended. Profiles signed by Node verify in Python and vice versa: cross-language byte-identical canonicalization.
## Quick start: full merchant via the `Checkout` orchestrator
`Checkout` is the canonical merchant surface; one config object, hooks for the merchant-specific pieces, and the SDK handles 402 emit, identity gating, x402 verify+settle, mppx compose, \$0 carve-out, identity\_metadata auto-attach, and the per-framework adapter. Most merchants reach for `Checkout` first and only drop to lower-level helpers when they need custom flows (variable-cost streaming, multi-protocol composition).
```python theme={"dark"}
import os
from fastapi import FastAPI, Request
from agentscore_commerce import (
Checkout, CheckoutGateConfig, DiscoveryProbeConfig, PricingResult, pricing_result,
SolanaMppRailSpec, StripeRailSpec, TempoRailSpec, X402BaseRailSpec,
validate_shipping_against_policy,
)
from agentscore_commerce.discovery import default_a2a_services
app = FastAPI()
async def _pre_validate(ctx):
body = ctx.request.body or {}
product = await lookup_product(body.get("product_slug"))
validate_shipping_against_policy(
country=body.get("shipping", {}).get("country", ""),
state=body.get("shipping", {}).get("state", ""),
policy=product,
product_name=product["name"],
)
return {"product": product}
async def _compute_pricing(ctx) -> PricingResult:
return pricing_result(
subtotal_cents=ctx.state["product"]["price_cents"],
tax_cents=ctx.state["product"]["tax_cents"],
tax_rate=ctx.state["product"]["tax_rate"],
tax_state=ctx.state["product"]["tax_state"],
)
async def _on_settled(ctx, outcome):
return {"ok": True, "order_id": ctx.reference_id, "tx_hash": outcome.tx_hash}
checkout = Checkout(
rails={
"tempo": TempoRailSpec(recipient=os.environ["TEMPO_RECIPIENT"]),
"x402_base": X402BaseRailSpec(recipient=os.environ["X402_BASE_RECIPIENT"], network="eip155:8453"),
"solana_mpp":SolanaMppRailSpec(recipient=os.environ["SOLANA_RECIPIENT"], network="solana:mainnet"),
"stripe": StripeRailSpec(profile_id=os.environ["STRIPE_PROFILE_ID"]),
},
url="https://merchant.example/purchase",
pre_validate=_pre_validate,
compute_pricing=_compute_pricing,
on_settled=_on_settled,
cdp_api_key_id=os.environ.get("CDP_API_KEY_ID"),
cdp_api_key_secret=os.environ.get("CDP_API_KEY_SECRET"),
mppx_secret_key=os.environ.get("MPP_SECRET_KEY"),
gate=CheckoutGateConfig(
api_key=os.environ["AGENTSCORE_API_KEY"],
merchant_name="Merchant",
require_kyc=True, require_sanctions_clear=True, min_age=21, allowed_jurisdictions=["US"],
),
# Optional: empty-body POSTs without a payment header auto-route to a sample 402
# so x402 crawlers (awal x402 details, x402-proxy, ...) can discover the surface.
discovery_probe=DiscoveryProbeConfig(
realm="merchant.example",
sample_rail="tempo-mainnet",
sample_amount_usd=1.0,
sample_recipient=os.environ["TEMPO_RECIPIENT"],
),
)
```
**Solana MPP requires a static recipient with a pre-funded token account.** Point the Solana rail at a fixed wallet you control (as with `SOLANA_RECIPIENT` above) and pre-fund that wallet's USDC associated token account (ATA) once. Per-order rotating Solana deposit addresses (for example Stripe-multichain minted addresses) do not settle on Solana MPP, because the settlement transaction does not create the recipient's token account. Tempo and x402 (Base) settle fine to per-order recipients; only Solana needs the static, pre-funded wallet. When you mint per-order recipients via `agentscore_commerce.stripe_multichain`, pass `static_recipients={"solana": ""}` so Solana is served from your fixed wallet, or leave Solana out of the rail set.
```python theme={"dark"}
# One-call mount: GET /.well-known/ucp + GET /.well-known/jwks.json + OPTIONS preflights.
# Flask / Django / aiohttp / Sanic each have a sibling helper
# (`mount_ucp_routes_flask` etc.) with the same kwargs.
checkout.mount_ucp_routes_fastapi(
app,
name="Merchant",
services=default_a2a_services(agent_card_url="https://merchant.example/.well-known/agent-card.json"),
signing_kid="merchant-2026-05",
)
@app.post("/purchase")
async def purchase(request: Request):
return await checkout.handle_fastapi(request)
```
The 402 body Checkout emits auto-attaches `identity_mode` + `required_signer` + `signer_constraint` (and `linked_wallets` when the gate populated them) when an inbound `X-Wallet-Address` header is present; so agents self-correct at discovery instead of at the 403 retry.
## Fail-open (opt-in)
By default AgentScore Gate fails closed on AgentScore-side infra failure (429 / 5xx / network timeout); buyer gets 503. Pass `fail_open=True` to opt in to graceful degradation, then read the per-request degraded state via `get_gate_degraded_state(request)`:
```python theme={"dark"}
from fastapi import Depends, FastAPI, Request
from agentscore_commerce.identity.fastapi import AgentScoreGate, get_gate_degraded_state
app = FastAPI()
gate = AgentScoreGate(api_key=os.environ["AGENTSCORE_API_KEY"], fail_open=True)
@app.post("/purchase", dependencies=[Depends(gate)])
async def purchase(request: Request):
state = get_gate_degraded_state(request)
if state["degraded"]:
logger.warning("gate degraded: %s", state["infra_reason"])
# ...
```
`get_gate_degraded_state` is exported by every Python adapter (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI middleware) and reads the framework-appropriate request state. Signatures take a request argument on every adapter except Flask, which reads from `g` and takes no arguments (`get_gate_degraded_state()`). Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of `fail_open`; see [compliance-gating › Fail-open behavior](/compliance-gating#fail-open-behavior-opt-in).
## Examples
The [examples/](https://github.com/agentscore/python-commerce/tree/main/examples) directory has runnable single-file FastAPI apps for each common merchant scenario:
| Example | Scenario |
| - | - |
| [`api_provider.py`](https://github.com/agentscore/python-commerce/blob/main/examples/api_provider.py) | Per-call API billing on multiple rails: Tempo MPP + x402 (Base) + Solana MPP; no compliance gate |
| [`identity_only.py`](https://github.com/agentscore/python-commerce/blob/main/examples/identity_only.py) | Compliance gate without payment (vendor handles their own) |
| [`multi_rail_merchant.py`](https://github.com/agentscore/python-commerce/blob/main/examples/multi_rail_merchant.py) | Full agent-commerce: identity + Tempo MPP + x402 + Stripe SPT |
| [`stripe_multichain_merchant.py`](https://github.com/agentscore/python-commerce/blob/main/examples/stripe_multichain_merchant.py) | Stripe-anchored multichain (PaymentIntent → tempo/base/solana deposit addresses) |
| [`compute_first_merchant.py`](https://github.com/agentscore/python-commerce/blob/main/examples/compute_first_merchant.py) | Pay-per-result variable-cost merchant via `compute_first_checkout`. The probe runs the work and caches it by body content-hash; the settle replays the cached result at the exact computed price. Exact-mode rails only (no upto, Permit2 or Settlement-Overrides). |
| [`compliance_merchant.py`](https://github.com/agentscore/python-commerce/blob/main/examples/compliance_merchant.py) | Regulated-goods merchant: full compliance gate + custom `on_denied` composing the denial helpers (`verification_agent_instructions`, `is_fixable_denial`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `denial_reason_status` / `denial_reason_to_body` to assemble the (status, body) pair for any DenialReason) |
| [`per_product_policy_merchant.py`](https://github.com/agentscore/python-commerce/blob/main/examples/per_product_policy_merchant.py) | Multi-product merchant where each row carries its own compliance policy (hard gate, soft, or none). Uses `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, and the one-call `validate_shipping_against_policy`. |
| [`signed_ucp_merchant.py`](https://github.com/agentscore/python-commerce/blob/main/examples/signed_ucp_merchant.py) | Signed UCP profile + JWKS endpoint via the one-call `checkout.mount_ucp_routes_fastapi(app, ...)`. Demonstrates ephemeral-for-dev / env-JWK-for-prod and `bootstrap_ucp_signing_key` lifespan-hook usage. |
## Links
* [PyPI](https://pypi.org/project/agentscore-commerce/)
* [GitHub](https://github.com/agentscore/python-commerce)
# AgentScore SDK (TypeScript)
Source: https://docs.agentscore.com/integrations/typescript
Official TypeScript/Node.js client for AgentScore.
## Installation
```bash theme={"dark"}
npm install @agent-score/sdk
```
## Quick start
```typescript theme={"dark"}
import { AgentScore } from "@agent-score/sdk";
const client = new AgentScore({ apiKey: process.env.AGENTSCORE_API_KEY });
// On-the-fly assessment with decision
const assessment = await client.assess("0xdb5aa553feeb2c3e3d03e8360b36fb0f7e480671", {
policy: { require_kyc: true },
});
if (assessment.decision === "deny") {
console.log("Denied:", assessment.decision_reasons);
}
```
## API reference
### Constructor
```typescript theme={"dark"}
new AgentScore(options: {
apiKey: string;
baseUrl?: string; // default: "https://api.agentscore.com"
timeout?: number; // default: 10000 (ms)
userAgent?: string; // custom prefix prepended to default UA, e.g. "myapp/1.0"
})
```
Set `userAgent` to prepend your app's identifier to outbound requests so AgentScore support can trace traffic back to you:
```typescript theme={"dark"}
const client = new AgentScore({ apiKey, userAgent: "myapp/1.0" });
// Outbound UA: "myapp/1.0 (@agent-score/sdk@x.y.z)"
```
### `client.assess(address, options?)`
On-the-fly trust assessment with policy evaluation. Available on every tier, gated by monthly quota.
```typescript theme={"dark"}
// With wallet address
const result = await client.assess("0x1234...", {
policy: { require_kyc: true },
chain: "base",
});
// With operator credential (non-wallet agents)
const result = await client.assess(null, {
operatorToken: "opc_abc123...",
policy: { require_kyc: true, min_age: 21 },
});
if (result.decision === "deny") {
console.log("Denied:", result.decision_reasons);
}
```
Returns `AssessResponse` with `decision`, `decision_reasons`, and `identity_method` (`"wallet"` or `"operator_token"`).
### Server-side signer-match + OFAC SDN screening
Pass `signer: { address, network }` to opt into server-side wallet-signer-match and OFAC SDN wallet-address screening in a single round trip:
```typescript theme={"dark"}
const result = await client.assess("0xclaimed...", {
signer: { address: "0xsigner...", network: "evm" },
policy: { require_sanctions_clear: true },
});
// Wallet-binding verdict
if (result.signer_match?.kind === "wallet_signer_mismatch") {
console.log("Mismatch:", result.signer_match.expected_signer, "vs", result.signer_match.actual_signer);
}
// OFAC SDN wallet-address verdict; discriminated union
// { status: 'clear' } | { sanctioned: true, ofac_label, sdn_uid, listed_at } | { status: 'unavailable' }
if (result.signer_sanctions && "sanctioned" in result.signer_sanctions) {
console.log("OFAC hit:", result.signer_sanctions.ofac_label, result.signer_sanctions.sdn_uid);
}
```
Set `signer.address: null` for rails with no wallet signer (Stripe SPT, card); the response's `signer_match.kind` will be `wallet_auth_requires_wallet_signing`. Wallet-OFAC SDN enforcement on the `signer` block is unconditional whenever a signer is supplied; no `policy.require_sanctions_clear` opt-in required. A `sanctioned: true` OR `status: "unavailable"` verdict flips `decision` to `deny` with `decision_reasons` including `sanctions_flagged` or `sanctions_check_unavailable` respectively (fail-closed; OFAC strict-liability). `policy.require_sanctions_clear` is the separate NAME-based screen on the resolved operator's KYC identity.
## Compliance assessment
Use compliance policy fields to enforce KYC, sanctions, age, and jurisdiction requirements:
```typescript theme={"dark"}
const result = await client.assess("0x1234...", {
policy: {
require_kyc: true,
require_sanctions_clear: true,
min_age: 21,
blocked_jurisdictions: ["KP", "IR"],
},
});
if (result.decision === "deny") {
console.log("Denied:", result.decision_reasons);
console.log("Policy checks:", result.policy_result.checks);
// If denial is resolvable through verification, verify_url is present
if (result.verify_url) {
console.log("Verification required:", result.verify_url);
}
}
```
## Sessions & credentials
```typescript theme={"dark"}
// Create a verification session (for merchant use)
const session = await client.createSession({
context: "wine_purchase",
product_name: "Cabernet 2022",
});
// session.verify_url: give to user
// session.poll_url: URL the agent polls
// session.poll_secret: X-Poll-Secret header for the agent's polls
// Registration-only session: the buyer signs in with an AgentScore account, no identity documents.
await client.createSession({ kind: "sign_in" });
// Poll for completion
const poll = await client.pollSession(session.session_id, session.poll_secret);
if (poll.status === "verified") {
console.log("Credential:", poll.operator_token);
}
// Create a credential directly (if user already KYC'd)
const cred = await client.createCredential({ label: "claude-code", ttl_days: 1 });
console.log("Use this:", cred.credential);
// List and revoke
const list = await client.listCredentials();
await client.revokeCredential(list.credentials[0].id);
```
## Report captured wallets
After an agent paid under a credential, report the signer wallet so AgentScore can build a cross-merchant profile:
```typescript theme={"dark"}
await client.associateWallet({
operatorToken: "opc_...",
walletAddress: signerFromPayment, // EIP-3009 `from`, Tempo MPP DID, or Solana pubkey
network: "evm", // or "solana"
idempotencyKey: paymentIntentId, // optional, retries of same payment no-op
});
```
Fire-and-forget. Response is `{ associated: true, first_seen: boolean, deduped?: true }`.
## Quota observability
`assess()` responses include an optional `quota` field captured from `X-Quota-Limit` / `X-Quota-Used` / `X-Quota-Reset` headers so callers can monitor approach-to-cap proactively before hitting 429:
```typescript theme={"dark"}
const result = await client.assess("0x1234...", { policy: { require_kyc: true } });
if (result.quota?.limit && result.quota.used != null) {
const pct = result.quota.used / result.quota.limit;
if (pct >= 0.95) {
log.warn(`AgentScore quota at ${Math.round(pct * 100)}%; resets at ${result.quota.reset}`);
}
}
```
`reset` is the UTC date (`YYYY-MM-DD`) the calendar-month quota resets, e.g. `"2026-06-01"`; every field is `null` when the API sent no quota headers (Enterprise and unlimited plans).
## Error handling
The SDK throws typed errors. Subclasses of `AgentScoreError` let callers branch on `instanceof` without parsing `err.code`:
```typescript theme={"dark"}
import {
AgentScoreError,
InvalidCredentialError,
PaymentRequiredError,
QuotaExceededError,
RateLimitedError,
TimeoutError,
TokenExpiredError,
} from "@agent-score/sdk";
try {
const result = await client.assess(null, {
operatorToken: "opc_...",
signer: { address: "0xs", network: "evm" },
});
} catch (err) {
if (err instanceof TokenExpiredError) {
// 401 token_expired; credential revoked or TTL-expired. The 401 body carries an
// auto-minted verification session so the agent recovers without an API key.
console.log("verify_url:", err.verifyUrl, "session_id:", err.sessionId, "poll_secret:", err.pollSecret);
} else if (err instanceof InvalidCredentialError) {
// 401 invalid_credential; operator_token doesn't exist (typo, never minted).
} else if (err instanceof QuotaExceededError) {
// 429 quota_exceeded; wait for the quota reset window; don't retry.
} else if (err instanceof RateLimitedError) {
// 429 rate_limited; the SDK already waited out Retry-After and retried once, so back off before trying again.
} else if (err instanceof PaymentRequiredError) {
// 402; vendor tier misconfig.
} else if (err instanceof AgentScoreError) {
// Anything else: 4xx/5xx + network failures wrap to AgentScoreError with code "network_error"/"timeout"/etc.
console.log(err.code, err.message, err.status);
}
}
```
On `wallet_signer_mismatch` responses (verdict, not exception), spread `signer_match.agent_instructions` (a JSON-encoded `{action, steps, user_message}` envelope) directly into a 403 body; the SDK never re-derives recovery copy locally.
`AgentScoreError.details: Record` carries response-body fields beyond `{code, message}`; `verify_url`, `linked_wallets`, `claimed_operator`, `actual_signer`, `expected_signer`, `reasons`, `agent_memory`; so callers can branch on granular denial codes without re-parsing:
```typescript theme={"dark"}
try {
await client.assess("0xabc...", { policy: { require_kyc: true } });
} catch (err) {
if (!(err instanceof AgentScoreError)) throw err;
if (err.code === "wallet_signer_mismatch") {
const linked = err.details.linked_wallets as string[] | undefined;
// re-sign from any address in `linked`, or switch to X-Operator-Token
}
if (err.code === "token_expired") {
const verifyUrl = err.details.verify_url as string | undefined;
// surface verifyUrl to the user so they can re-verify
}
}
```
## TypeScript types
The SDK exports all response types:
```typescript theme={"dark"}
import type {
AssessResponse,
} from "@agent-score/sdk";
```
## Links
* [npm](https://www.npmjs.com/package/@agent-score/sdk)
* [GitHub](https://github.com/agentscore/node-sdk)
# x402 client compatibility
Source: https://docs.agentscore.com/integrations/x402-clients
Which x402 / MPP / Tempo CLIs are verified to complete payments against AgentScore-built merchants. Updated reactively as we smoke-test new clients.
If you're an agent (or a human testing a payment flow) and you want to know which CLI to use to pay an AgentScore-built merchant, this is the canonical list. Every entry has been smoke-tested end-to-end against a `@agent-score/commerce`–powered merchant. When a new client is verified, it gets added; when a verified client breaks, the row gets a note.
Merchants built on `@agent-score/commerce` emit an `agent_instructions.compatible_clients` field on every 402 response and the same per-rail matrix in their static `/skill.md` discovery manifest; agents can read it inline (or fetch the manifest upfront) without ever loading this page. The 402 body and the skill manifest both source from one SDK constant; this page mirrors it.
## What works
| Client | Tempo MPP | x402 Base | Solana MPP | Stripe SPT | Notes |
| - | - | - | - | - | - |
| **`agentscore-pay`** (npm: `@agent-score/pay`) | ✅ | ✅ | ✅ | n/a | Universal CLI, all native rails. Auto-injects `X-Wallet-Address` from keystore when no identity header is supplied. |
| **`tempo request`** (Tempo CLI extension via `tempo add request`) | ✅ | n/a | n/a | n/a | MPP-only. Native to the Tempo CLI. |
| **`x402-proxy`** (npm: `x402-proxy`, by Cascade) | ✅ | ✅ | n/a | n/a | Closest functional twin to `agentscore-pay` for x402-protocol rails. Doesn't speak MPP `solana/charge`. |
| **`purl`** (Stripe purl) | ⚠️ | ✅ | n/a | n/a | Base x402 works **only when `--network` flag is omitted**. Tempo MPP fails on multi-rail `WWW-Authenticate` parsing. |
| **`@stripe/link-cli`** | n/a | n/a | n/a | ✅ | MPP-native via Stripe Shared Payment Token. |
| **`awal`** (Coinbase) | n/a | ❌ | n/a | n/a | awal pre-discovers via empty-body POST and never sends the request body, so per-order priced merchants like Martin Estate and Sayer & Stone can't surface their accepts to it. Works against fixed-price endpoints listed in Coinbase Bazaar. |
Legend: ✅ verified end-to-end · ⚠️ partial / specific workaround · ❌ doesn't work today · n/a not applicable to this rail.
## Recommended commands per rail
**Tempo MPP**
* `tempo request -X POST -H 'X-Operator-Token: opc_...' --json '{...}' `
* `agentscore-pay pay POST --chain tempo -H 'X-Operator-Token: opc_...' -d '{...}'`
**x402 on Base**
* `agentscore-pay pay POST --chain base -H 'X-Operator-Token: opc_...' -d '{...}'`
* `purl -X POST -H 'X-Operator-Token: opc_...' -H 'Content-Type: application/json' -d '{...}'` *(no `--network` flag)*
* `x402-proxy fetch -X POST -d '{...}' --evm-key --network base --protocol x402 `
**Solana via MPP `solana/charge`**
* `agentscore-pay pay POST --chain solana -H 'X-Operator-Token: opc_...' -d '{...}'`
**Stripe Shared Payment Token (Link wallet)**
* `link-cli mpp pay --spend-request-id --method POST --data '{...}' --header 'X-Operator-Token: opc_...'`
## Why some clients don't fully work
For the partial entries above, the failures come from client-side assumptions that don't fit the protocol shape AgentScore-built merchants emit. We share the diagnostics in case other ecosystem players want to reproduce or fix them:
* **`purl --network` flag**: purl's negotiator compares the flag value (e.g. `base`) against the CAIP-2 string in the 402's `accepts[]` (e.g. `eip155:8453`) without normalizing aliases. With no flag passed, purl's filter short-circuits and it correctly picks the first compatible challenge. Workaround: omit the flag.
* **`purl` Tempo MPP**: purl's MPP parser raises `Duplicate parameter: id` on multi-`Payment` `WWW-Authenticate` headers (one MPP directive per rail, comma-separated, per RFC 7235).
* **`awal`**: pre-discovers payment requirements via empty-body HEAD/GET/POST/PUT/DELETE/PATCH, then signs against whatever it finds. Variable-priced merchants need the actual request body to compute per-order pricing + per-order deposit addresses, so awal can't discover anything to sign. Bazaar-registered fixed-price endpoints are awal's working surface today.
## Are funds at risk on a failed payment?
No. x402 facilitators (Coinbase's, primarily) **simulate** transactions before broadcasting. A simulation failure doesn't broadcast, doesn't move USDC, doesn't consume gas. A signed-but-not-broadcast tx becomes inert when the `validBefore` window (EVM EIP-3009) expires, typically within minutes. Buyers can switch to a different client and retry without losing anything.
## How to add your client to this list
Email [support@agentscore.com](mailto:support@agentscore.com) with the client and the rails it supports. We run the smoke ourselves and add the entry once it passes. We don't accept self-reported entries; the matrix only lists what we've directly run.
# AgentScore Pay (MCP Server)
Source: https://docs.agentscore.com/mcp/overview
Give AI agents and assistants access to wallet, payment, and identity tools via the Model Context Protocol.
`agentscore-pay --mcp` exposes the AgentScore CLI as an MCP server over stdio. AI agents and coding assistants can register the binary once and call every wallet, payment, and identity command as a tool; wallet management, x402/MPP payments, on-the-fly assessments, verification sessions, operator credentials, and wallet attribution all in one server.
## Install
```bash theme={"dark"}
npm install -g @agent-score/pay
# or: npx @agent-score/pay
```
Set `AGENTSCORE_API_KEY` for identity tools (`assess`, `sessions`, `credentials`, and `associate-wallet`) available on every tier and metered by quota (paid tiers raise the limits). Wallet and payment tools are local and need no key.
## Register with your agent
```bash theme={"dark"}
agentscore-pay mcp add # auto-detect (Claude Code, Cursor, Amp, ...)
agentscore-pay mcp add --agent claude-code # target a specific agent
```
This writes the MCP config for your agent so the tools appear automatically.
### Manual config: Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
```json theme={"dark"}
{
"mcpServers": {
"agentscore-pay": {
"command": "agentscore-pay",
"args": ["--mcp"],
"env": {
"AGENTSCORE_API_KEY": "as_live_your_key_here"
}
}
}
}
```
### Manual config: Cursor
Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global) with the same shape.
## Available tools
Every command from `agentscore-pay --help` is exposed as a tool. The most agent-relevant ones:
### Wallet + payment
| Tool | Purpose |
| - | - |
| `init` | First-run setup: creates encrypted keystores for Tempo, Solana, Base |
| `wallet_create` / `wallet_import` / `wallet_address` / `wallet_list` | Keystore CRUD |
| `balance` | USDC balance across chains |
| `fund` / `faucet` | Receive QR + balance poll on mainnet; programmatic mint or faucet URL on testnet |
| `pay` | Send an HTTP request and auto-handle the 402 round-trip |
| `check` | Probe a 402 response and show accepted rails (without paying) |
| `discover` | List paid services from the x402 Bazaar + MPP services directory |
| `fund-estimate` | "How many calls does my balance cover for this URL?" |
### Identity (any tier: set `AGENTSCORE_API_KEY`)
| Tool | Purpose |
| - | - |
| `assess` | On-the-fly assessment with policy (KYC, age, sanctions, jurisdiction) |
| `sessions_create` / `sessions_get` | Identity verification flow: returns `verify_url` + polls for `operator_token` |
| `credentials_create` / `credentials_list` / `credentials_revoke` | Operator credential (`opc_...`) lifecycle |
| `associate-wallet` | Report a signer wallet seen paying under a credential (cross-merchant attribution) |
Run `agentscore-pay --llms` for the full machine-readable manifest (or `--llms --format json` for JSON Schema).
## Denial-code handling
`assess`, `sessions`, `credentials` and `associate-wallet` turn the API's typed errors into structured `CliError` codes with `hint` lines, so agents act from the response without parsing raw 4xx/5xx shapes: `config_error` (an API-key problem, or an operator token that expired or is not recognized; `extra` carries `verify_url` / `session_id` / `poll_secret` when the API minted a recovery session), `insufficient_balance` (the endpoint is not enabled for the account), `quota_exceeded` (account cap reached; retrying will not help), and `network_error` (rate limit, timeout or transient failure; retry with backoff). A compliance `deny` is not an error: it comes back as `decision: "deny"` with `decision_reasons`.
A merchant's own denial codes (`missing_identity`, `identity_verification_required`, `wallet_not_trusted`, ...) reach the agent through `pay`, in the merchant's 403 body and its `agent_instructions`.
## Use cases
**Agent-to-agent trust**; run `assess` on another agent's wallet before initiating a transaction.
**Gateway pre-screening**; an agent acting as a gateway screens incoming requests with `assess`.
**Autonomous payment**; discover a 402-gated endpoint, probe it with `agentscore-pay check`, dry-run with `agentscore-pay pay --dry-run`, then settle the payment via `agentscore-pay pay`. All without leaving the MCP loop.
**Identity bootstrap**; an agent encounters an AgentScore-gated merchant for the first time, receives a `missing_identity` denial, calls `sessions_create` → polls `sessions_get` → uses the returned `operator_token` to retry the original request.
## Environment variables
| Variable | Description |
| - | - |
| `AGENTSCORE_API_KEY` | API key for identity tools (`assess`, `sessions`, `credentials`, `associate-wallet`). Get one at [agentscore.com/sign-up](https://www.agentscore.com/sign-up). |
| `AGENTSCORE_PAY_PASSPHRASE` | Skip the interactive passphrase prompt for keystore operations. |
| `AGENTSCORE_PAY_HOME` | Override the state dir (default `~/.agentscore`). |
## Running locally
```bash theme={"dark"}
AGENTSCORE_API_KEY=as_live_your_key_here agentscore-pay --mcp
```
The server communicates over stdio using the MCP protocol. Pipe `{"jsonrpc":"2.0","id":1,"method":"tools/list"}` to it to enumerate tools.
## Links
* [npm](https://www.npmjs.com/package/@agent-score/pay)
* [GitHub](https://github.com/agentscore/pay)
* [AgentScore Pay docs](/integrations/pay-cli)
# AgentScore Passport
Source: https://docs.agentscore.com/passport
Verify your identity once; your AgentScore Passport works on every gated merchant in the agentic economy.
AgentScore Passport is a portable identity credential for buyer-side agents. Verify your identity once (KYC + age + jurisdiction + sanctions) and your Passport works on every AgentScore-gated merchant (wine, regulated commerce, age-restricted services) without re-verifying per site.
The Passport is held by your agent's wallet on your behalf. Merchants see only the derived facts (`verified`, `21+`, `US-Based`, `Sanctions Clear`); never your raw PII (name, address, ID images, DOB).
## Two ways to provision a Passport
### From the `agentscore-pay` CLI (recommended for shell-tool agents)
The fastest path. One command opens a verify URL, polls until you complete KYC in the browser, and saves the credential to your agent's wallet:
```bash theme={"dark"}
agentscore-pay passport login
```
After that, every `agentscore-pay ` call against an AgentScore-gated merchant attaches your credential automatically; no per-call prompting, no "log in first" walls. Tokens are short-lived but `agentscore-pay` refreshes them silently in the background, so the user-visible UX is "verify once, never think about it again."
```bash theme={"dark"}
# Inspect what's saved
agentscore-pay passport status
# Remove the local credential (and revoke remotely if --api-key is set)
agentscore-pay passport logout
# Skip auto-attach for an explicit-anonymous request
agentscore-pay pay POST --skip-passport ...
```
### From the dashboard (browser)
Go to [agentscore.com/dashboard/verify](https://www.agentscore.com/dashboard/verify). Sign in (free account). Optionally link a wallet by signing a challenge message. Complete identity verification with Stripe Identity (\~2 minutes, government ID + selfie). Generate operator credentials manually for agents that don't auto-provision.
## What gets shared
Merchants you transact with see only these derived facts:
| Fact | Example |
| - | - |
| Verified | yes |
| Age bracket | `21+` |
| Jurisdiction | `US` |
| Sanctions clear | yes |
Raw PII (name, address, ID document images, date of birth) is never shared. Identity verification is handled by [Stripe Identity](https://stripe.com/identity); sanctions screening by [OpenSanctions](https://www.opensanctions.org) against OFAC, UN, EU, UK, and other public watchlists. Both process PII transiently; AgentScore stores only the derived results.
KYC is optional during the wallet-link flow if you're only using AgentScore for unregulated services. Merchants that gate on KYC will surface a verify prompt when you attempt to transact.
## Cold-start bootstrap (no Passport yet)
If you skip `passport login` and run `agentscore-pay ` against an AgentScore-gated merchant, the merchant returns a 403 with a verify URL. Pay drives the verification inline; prints the URL, waits for you to complete KYC in the browser, then retries the original request automatically.
That single shell command becomes: agent → bootstrap 403 → user clicks → verify → settle. Zero per-merchant configuration.
The trade-off vs `passport login` first: bootstrap-minted Passports come from a merchant-initiated session, which doesn't issue a refresh\_token, so they re-verify every 24h when the access token expires. `passport login` mints a 24h access + 90d refresh pair; `agentscore-pay` rotates silently for \~90 days before any user-visible re-verify. Doing `passport login` once up front gets the better long-term UX.
## Multiple wallets, one Passport
Your Passport applies to every wallet you link. Add wallets from the dashboard with the "EVM wallet" or "Solana wallet" button; KYC carries over; you don't re-verify per wallet. One EVM signature covers all EVM chains (Base, Tempo, etc.); Solana uses its own signature. When you pay from any linked wallet, merchants resolve it to your Passport via [`POST /v1/assess`](/api-reference/post-assess).
Captured wallets (wallets that paid under one of your operator credentials but were never explicitly claimed) also inherit your account's KYC. The first time you pay from a fresh wallet under a verified Passport, AgentScore captures the wallet → operator mapping; from then on, the wallet resolves like any claimed wallet.
## Revoking access
* **Wallet**: unlink from the dashboard verify page.
* **Operator credential**: `agentscore-pay passport logout`, `DELETE /v1/credentials/:id`, or revoke from the dashboard. Revoking a credential also revokes the refresh token minted with it and every later rotation in that chain, so the agent cannot renew itself; it has to log in again.
* **Verification itself**: contact support to delete your verification record.
An agent that has been idle past its 24h access expiry still holds a live refresh token. Its credential keeps listing in the dashboard and in `GET /v1/credentials` (marked `refreshable`) even though the access token has expired, because revoking it is the only thing that stops the renewal.
## Operator credentials in detail
Each Passport login mints an `operator_token` (`opc_...`, 24h) plus a `refresh_token` (`prt_...`, 90d); the access credential is what your agent presents as `X-Operator-Token`; the refresh credential stays on disk and is single-use rotating. A credential is identity, not verification: it identifies *who* the operator is, and the underlying account's KYC determines whether merchants accept it. Access credentials are short-lived and rotated automatically when held by `agentscore-pay` (the wallet exchanges the refresh\_token for a fresh pair the next time it's used after access expiry); merchants check the live KYC state on each request via [`POST /v1/assess`](/api-reference/post-assess).
A credential token alone is not verification. If your account hasn't completed KYC, creating a standard credential with `POST /v1/credentials` returns **409 `kyc_required`** (a registration-only `sign_in` credential skips this check, and passes no compliance policy). Complete KYC first; every subsequent credential inherits the verified status automatically.
## Privacy posture
* **No PII at rest in AgentScore** beyond derived facts (verified / age\_bracket / jurisdiction / sanctions\_status).
* **No merchant-by-merchant disclosure of PII.** Merchants see only the derived facts above.
* **Revocable.** You can revoke any credential or unlink any wallet at any time. A revoke stops renewal, not just the current token.
* **Auditable.** Every credential issuance + every merchant assessment is logged on your account.
# Quickstart
Source: https://docs.agentscore.com/quickstart
Verify the human behind an agent in under 2 minutes.
## 1. Get an API key
Sign up at [agentscore.com/dashboard](https://www.agentscore.com/dashboard) to get your API key. `POST /v1/assess` and `POST /v1/sessions` are available on every tier including Free, gated by monthly quota; see [pricing](https://www.agentscore.com/pricing) for current quotas and rate limits.
## 2. Verify the buyer behind an agent
Call `POST /v1/assess` with a wallet address and a policy. AgentScore resolves the operator behind the wallet and returns an explicit allow/deny decision against your policy:
```bash theme={"dark"}
curl -X POST https://api.agentscore.com/v1/assess \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"address": "0xdb5aa553feeb2c3e3d03e8360b36fb0f7e480671",
"policy": { "require_kyc": true }
}'
```
The address can be EVM (`0x...` hex) or Solana (base58); the network is detected from the format. Beyond `require_kyc`, a policy can require an age band, a sanctions-clear result, and an allowed jurisdiction list.
## 3. Read the decision
Response abbreviated; see [POST /v1/assess](/api-reference/post-assess) for the full format.
```json theme={"dark"}
{
"decision": "allow",
"decision_reasons": []
}
```
A `deny` carries machine-readable `decision_reasons` (for example `kyc_required`, `sanctions_flagged`, `jurisdiction_restricted`) so your code, or the agent, knows what to do next.
## 4. Enforce in your application
```javascript theme={"dark"}
const assessment = await fetch("https://api.agentscore.com/v1/assess", {
method: "POST",
headers: {
"X-API-Key": process.env.AGENTSCORE_API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
address: walletAddress,
policy: { require_kyc: true },
}),
}).then((r) => r.json());
if (assessment.decision === "deny") {
return res.status(403).json({
error: "wallet_not_trusted",
reasons: assessment.decision_reasons,
});
}
// Proceed with the transaction
```
This is a minimal raw-API integration. For production merchants, use [AgentScore Commerce (Node.js)](/integrations/node-commerce) or [AgentScore Commerce (Python)](/integrations/python-commerce); they ship a drop-in identity gate that auto-routes fixable compliance reasons (`kyc_required`, `kyc_pending`, `kyc_failed`) into a self-service verification flow (mints a session, returns `verify_url` + `poll_secret`, agent polls for a fresh `operator_token`), and surfaces `contact_support` for unfixable reasons (`sanctions_flagged`, `age_insufficient`, `jurisdiction_restricted`). The raw `decision === "deny"` example above collapses every denial into a single bare 403, which works for prototyping but loses the structured agent-recovery contract.
## Next steps
Merchant side: drop-in AgentScore Gate middleware, Martin Estate as the worked example.
Official Node.js / TypeScript client.
Official Python client with async support.
Identity middleware + payment helpers + 402 builders for Hono, Express, Fastify, Next.js, and Web Fetch.
Identity middleware + payment helpers + 402 builders for FastAPI, Flask, Django, AIOHTTP, and Sanic.
Add identity checks to Claude and Cursor.
Full endpoint documentation.