Skip to main content
POST
/
v1
/
assess
POST /v1/assess
curl --request POST \
  --url https://api.agentscore.sh/v1/assess
This endpoint must be enabled for your account (pricing). Accounts without it receive HTTP 402.

Request

Headers

HeaderRequiredDescription
X-API-KeyYesYour API key (paid or enterprise tier)
Content-TypeYesapplication/json

Body

Provide either address (wallet) or operator_token (credential). At least one is required.
{
  "address": "0xdb5aa553feeb2c3e3d03e8360b36fb0f7e480671",
  "chain": "base",
  "refresh": false,
  "policy": {
    "require_kyc": true,
    "require_sanctions_clear": true,
    "min_age": 21,
    "blocked_jurisdictions": ["KP", "IR"]
  }
}
Or with an operator credential (for non-wallet agents):
{
  "operator_token": "opc_abc123...",
  "policy": {
    "require_kyc": true,
    "min_age": 21
  }
}
FieldTypeRequiredDescription
addressstringOne of address/operator_tokenWallet address: EVM (0x... 40-hex) or Solana (base58, 32–44 chars). Network is auto-detected from the address format.
operator_tokenstringOne of address/operator_tokenOperator credential (opc_...) for non-wallet agents. Credential resolves to an account’s KYC status.
chainstringNoChain to score. If omitted, scores the chain with the highest existing score. Defaults to base for unknown addresses.
refreshbooleanNoForce score recomputation even if cached
policyobjectNoPolicy rules for allow/deny decision
policy.require_kycbooleanNoRequire operator KYC verification
policy.require_sanctions_clearbooleanNoRequire clean sanctions status
policy.min_agenumberNoMinimum age bracket (18 or 21)
policy.blocked_jurisdictionsstring[]NoISO country codes to block (blocklist)
policy.allowed_jurisdictionsstring[]NoISO country codes to allow (allowlist: only these pass)
signerobjectNoServer-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.addressstring | nullIf signer setRecovered payment-signer wallet. null indicates the rail carries no wallet signature (Stripe SPT, card): produces signer_match.kind = "wallet_auth_requires_wallet_signing".
signer.networkstringIf signer setKey-derivation family of the signer wallet. evm or solana.
testbooleanNoEnable 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:
AddressBehavior
0x0000000000000000000000000000000000000001allow: verified individual, US, 21+
0x0000000000000000000000000000000000000002deny: kyc_required, includes verify_url
0x0000000000000000000000000000000000000003deny: sanctions_flagged
0x0000000000000000000000000000000000000004allow: verified individual, DE
0x0000000000000000000000000000000000000005allow: verified individual, US, 18+ (denies on min_age: 21)
0x0000000000000000000000000000000000000006allow: verified individual, IR (denies on blocked_jurisdictions: ["IR"])
0x0000000000000000000000000000000000000007allow: 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.
{
  "address": "0x0000000000000000000000000000000000000001",
  "test": true,
  "policy": {
    "require_kyc": true,
    "min_age": 21
  }
}

Response

{
  "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. For reputation scores, use GET /v1/reputation/:address.

Wallet-mode response fields

When the request was authenticated by X-Wallet-Address (or address was supplied), the response includes:
FieldTypeDescription
resolved_operatorstringCanonical operator wallet (the earliest claimed wallet, or the lexically-smallest active-captured wallet). All same-operator sibling wallets resolve to this address.
linked_walletsstring[]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.
signer_matchobjectReturned only when the request supplied signer. Server-side wallet-signer-match verdict: see below.
signer_sanctionsobjectReturned only when the request supplied signer. Server-side OFAC SDN wallet-address verdict: see below.

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. Lets commerce gates skip the legacy 2 follow-up assess calls.
{
  "decision": "allow",
  "decision_reasons": [],
  "resolved_operator": "0xclaimed...",
  "linked_wallets": ["0xclaimed...", "0xsigner..."],
  "signer_match": {
    "kind": "pass",
    "claimed_operator": "0xclaimed...",
    "signer_operator": "0xclaimed..."
  },
  "identity_method": "wallet",
}
FieldTypeDescription
signer_match.kindstringpass (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_operatorstring | nullOperator the claimed wallet resolves to. null if unlinked.
signer_match.signer_operatorstring | nullOperator the signer wallet resolves to. null if unlinked.
signer_match.expected_signerstringEchoed on wallet_signer_mismatch: the claimed wallet, normalized.
signer_match.actual_signerstringEchoed on wallet_signer_mismatch: the signer wallet, normalized.
signer_match.linked_walletsstring[]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_instructionsstringJSON-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:
{ "signer_sanctions": { "status": "clear" } }
{
  "signer_sanctions": {
    "sanctioned": true,
    "ofac_label": "ETH",
    "sdn_uid": "19011",
    "listed_at": "2018-05-03"
  }
}
{ "signer_sanctions": { "status": "unavailable" } }
FieldTypeDescription
signer_sanctions.statusstringclear (address not on the list) OR unavailable (the lookup itself failed; the API fail-closes — see below).
signer_sanctions.sanctionedbooleanPresent and true ONLY on a hit; omitted on clear / unavailable.
signer_sanctions.ofac_labelstringThe 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_uidstringThe SDN entry’s Identity ID. Same sdn_uid may surface multiple addresses (one entity, multiple wallets); join key for audit.
signer_sanctions.listed_atstring | nullISO 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:
{
  "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://agentscore.sh/dashboard/verify to complete Stripe Identity verification."
    }
  ],
  "identity_method": "wallet",
  "operator_verification": { "level": "none" },
  "verify_url": "https://agentscore.sh/dashboard/verify?address=0x5678...&chain=base",
}

Decision values

ValueMeaning
allowAll policy rules passed, or no policy was specified
denyOne 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:
FieldTypeDescription
rulestringThe policy rule name (e.g., require_kyc)
passedbooleanWhether the rule passed
requiredstringThe required value for the rule
actualstringThe actual value found
messagestringHuman-readable description of the result
how_to_remedystring | nullAction 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:
ReasonWhenverify_url?
kyc_requiredNot verifiedYes
kyc_pendingKYC in progressNo
kyc_failedKYC failedYes (can retry)
sanctions_flaggedAccount-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_unavailableWallet-OFAC lookup failed (signer_sanctions.status === "unavailable"); fail-closed posture, always-onNo
age_insufficientBelow min_ageNo
jurisdiction_restrictedBlocked countryNo
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.
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"]. This is useful for on-the-fly scoring without enforcement.

Unknown addresses

If the address is not yet in the database, /v1/assess creates a minimal entry and triggers scoring as a side effect. The assess response is returned immediately based on identity + compliance state; GET /v1/reputation/:address is the endpoint that returns score data.

Response headers

Every /v1/assess response (success and 429) carries account-level quota observability headers when the account has a per-period quota:
HeaderDescription
X-Quota-LimitTotal quota for the current period (numeric)
X-Quota-UsedCurrent usage within the period (numeric)
X-Quota-ResetISO-8601 timestamp when the period resets, or the literal never for unlimited / lifetime caps
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 and agentscore-py READMEs for usage. Accounts without a per-period quota (Enterprise / unlimited tiers) receive no X-Quota-* headers.

Error responses

Statuserror.codeWhenRetry posture
400invalid_requestMalformed body, unknown address format, missing required fieldDon’t retry: fix the request
401invalid_api_keyMissing/invalid X-API-Key headerDon’t retry: fix the key
429quota_exceededAccount-level cap reachedDon’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.
5xxapi_errorTransient AgentScore infra failureRetry with backoff per agent_instructions envelope (see 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.

Example: middleware integration

const assessment = await fetch("https://api.agentscore.sh/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,
  });
}