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
{
"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) |
Response
{
"id": "uuid",
"credential": "opc_abc123...",
"prefix": "opc_abc1",
"label": "claude-code-agent",
"expires_at": "2026-04-10T12:00:00Z",
"created_at": "2026-04-09T12:00:00Z"
}
The credential value is shown only once. Store it securely.
409 — KYC required
If the account has not completed identity verification, credential creation returns 409 Conflict:
{
"error": {
"code": "kyc_required",
"message": "Complete identity verification before creating an operator credential."
},
"verify_url": "https://agentscore.sh/dashboard/verify",
"next_steps": {
"action": "complete_kyc_then_retry",
"user_message": "Visit the verify_url, complete Stripe Identity verification, then retry this request."
}
}
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.
{
"account_verification": {
"kyc_status": "verified",
"kyc_verified_at": "2026-04-07T17:13:56.525Z",
"jurisdiction": "US",
"age_verified": true,
"age_bracket": "21+",
"sanctions_status": "clear",
"operator_type": "individual"
},
"credentials": [
{
"id": "uuid",
"prefix": "opc_abc1",
"label": "claude-code-agent",
"expires_at": "2026-04-10T12:00:00Z",
"last_used_at": "2026-04-09T14:30:00Z",
"created_at": "2026-04-09T12:00:00Z"
}
]
}
When the account has no KYC, account_verification contains only the status:
{
"account_verification": {
"kyc_status": "none"
},
"credentials": []
}
Revoke credential
DELETE /v1/credentials/{id}
X-API-Key: as_live_...
{
"id": "uuid",
"revoked": true
}
Usage
Pass the credential as the X-Operator-Token header when calling /v1/assess or merchant endpoints:
curl -X POST https://api.agentscore.sh/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 the 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 unified reputation, 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
{
"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
{
"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:
{
"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-gate adapters expose
captureWallet(ctx, { walletAddress, network }) which reads the operator_token from the gate state — no need to re-pass it.