API reference
One base URL, JSON in and out, bearer keys. Everything the dashboard shows is available here, including the evidence behind every decision. No SDK needed; the examples use curl and fetch.
https://clearlist.xyz/api/v1Authentication
Create keys in the dashboard. Live keys start with cl_live_, test keys with cl_test_. Test keys run against the same lists and policy; their decisions are tagged mode: "test" so you can filter them out. Send the key as a bearer token, or in an x-api-key header.
curl https://clearlist.xyz/api/v1/lists \
-H "Authorization: Bearer cl_test_…"Keys are stored hashed; a lost key is revoked and replaced, not recovered.
POST /screen
Evaluates one subject against the lists, public labels, counterparty exposure, your policy and your allowlist. Persists the decision and returns it with status 201.
| subject | Subject | Required. One of the four subject shapes below. Shorthand: put the subject fields at the top level instead of under subject. |
| idempotency_key | string | Optional, up to 128 chars. Same key within the org returns the original decision instead of screening again. |
| metadata | object | Optional, up to 20 string pairs. Echoed back on the decision and visible in the dashboard. Put your user id here. |
curl https://clearlist.xyz/api/v1/screen \
-H "Authorization: Bearer cl_live_…" \
-H "Content-Type: application/json" \
-d '{
"subject": { "type": "address", "address": "0x098b716b8aaf21512996dc57eb0615e2383e2f96" },
"idempotency_key": "withdrawal_91f2",
"metadata": { "user_id": "usr_4421", "flow": "withdrawal" }
}'const res = await fetch("https://clearlist.xyz/api/v1/screen", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CLEARLIST_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
subject: { type: "address", address: withdrawalAddress },
idempotency_key: withdrawalId,
metadata: { user_id: user.id, flow: "withdrawal" },
}),
});
const decision = await res.json();
if (decision.outcome === "block") {
return reject(decision.user_message); // safe to show
}
if (decision.outcome === "review") {
await holdForReview(withdrawalId, decision.id); // an analyst clears or confirms it
}Subject types
Addresses are normalised (EVM lower-cased, bech32 lower-cased, base58 as-is) and the chain is detected from the format when you do not pass one. Names are matched against every alias on every list; a strong name match on its own produces review, never block, unless a date of birth, nationality or identifier corroborates it, or your policy says otherwise.
// Address: chain auto-detected from the format when omitted.
{ "type": "address", "address": "0x098b…2f96", "chain": "ethereum", "exposure": true }
// Solana address: base58 is detected; exposure runs through Helius or any Solana RPC.
{ "type": "address", "address": "6wjq…mr4U", "exposure": true }
// Name: corroborating identifiers raise a strong match from review to block.
{ "type": "name", "name": "Viktor Bout", "dob": "1967-01-13",
"nationality": "RU", "country": "AE", "id_number": "…", "entity_type": "individual" }
// Country: ISO 3166-1 alpha-2. ip is informational in v1.
{ "type": "country", "country": "IR", "ip": "203.0.113.7" }
// Transaction: both sides screened; exposure on both unless exposure:false.
{ "type": "transaction", "chain": "solana",
"from": "7xKX…", "to": "9aBc…",
"amount": "250.00", "asset": "USDC", "tx_hash": "…", "exposure": true }The decision
The response is the full record: outcome, reasons with evidence, raw matches, exposure results, the exact list batches used, and the policy version. A sample block decision for the address above:
{
"id": "dec_8f3k2m9q1x7p4v6n0b5c2",
"org_id": "org_demo",
"outcome": "block",
"reasons": [
{
"code": "address.sanctioned",
"severity": "critical",
"summary": "The address 0x098b…2f96 appears on the OFAC SDN list under the DPRK3 program (Lazarus Group), batch published 2026-09-30.",
"evidence": {
"list": "OFAC_SDN",
"batch_id": "batch_2026_09_30_sdn",
"entity_id": "OFAC_SDN:27307",
"primary_name": "LAZARUS GROUP",
"programs": [
"DPRK3"
],
"asset_code": "ETH",
"address": "0x098b716b8aaf21512996dc57eb0615e2383e2f96",
"source_url": "https://sanctionssearch.ofac.treas.gov/Details.aspx?id=27307"
}
},
{
"code": "exposure.direct",
"severity": "high",
"summary": "2 of 184 scanned transactions interact directly with a sanctioned counterparty (hop 1).",
"evidence": {
"chain": "ethereum",
"provider": "etherscan",
"scanned_txs": 184,
"hits": [
{
"counterparty": "0xa0e1c89ef1a489c9c7de96311ed5ce5d32c20e4b",
"hop": 1,
"direction": "out",
"tx_count": 2,
"via": "OFAC_SDN:27307"
}
]
}
}
],
"subject": {
"type": "address",
"address": "0x098b716b8aaf21512996dc57eb0615e2383e2f96",
"chain": "ethereum"
},
"matches": {
"addresses": [
{
"address": "0x098b716b8aaf21512996dc57eb0615e2383e2f96",
"chain": "ethereum",
"list": "OFAC_SDN",
"batch_id": "batch_2026_09_30_sdn",
"entity_id": "OFAC_SDN:27307",
"primary_name": "LAZARUS GROUP",
"programs": [
"DPRK3"
]
}
],
"names": [],
"labels": []
},
"exposure": [
{
"status": "ok",
"chain": "ethereum",
"address": "0x098b716b8aaf21512996dc57eb0615e2383e2f96",
"provider": "etherscan",
"scanned_txs": 184,
"counterparties": 61,
"hits": [
{
"counterparty": "0xa0e1c89ef1a489c9c7de96311ed5ce5d32c20e4b",
"hop": 1,
"direction": "out",
"tx_count": 2,
"first_seen": "2026-08-14T09:12:00Z",
"last_seen": "2026-09-02T17:40:00Z",
"via": {
"address": "0xa0e1c89ef1a489c9c7de96311ed5ce5d32c20e4b",
"chain": "ethereum",
"list": "OFAC_SDN",
"batch_id": "batch_2026_09_30_sdn",
"entity_id": "OFAC_SDN:27307",
"primary_name": "LAZARUS GROUP",
"programs": [
"DPRK3"
]
}
}
],
"computed_at": "2026-10-06T14:02:11Z",
"cached": false
}
],
"lists": {
"OFAC_SDN": {
"batch_id": "batch_2026_09_30_sdn",
"published_at": "2026-09-30T00:00:00Z",
"fetched_at": "2026-10-06T06:00:03Z",
"entry_count": 17842
},
"OFAC_CONS": {
"batch_id": "batch_2026_09_30_cons",
"published_at": "2026-09-30T00:00:00Z",
"fetched_at": "2026-10-06T06:00:05Z",
"entry_count": 512
},
"EU_FSF": {
"batch_id": "batch_2026_10_03_eu",
"published_at": "2026-10-03T00:00:00Z",
"fetched_at": "2026-10-06T06:00:09Z",
"entry_count": 5231
},
"UK_OFSI": {
"batch_id": "batch_2026_10_02_uk",
"published_at": "2026-10-02T00:00:00Z",
"fetched_at": "2026-10-06T06:00:12Z",
"entry_count": 4790
},
"UN_SC": {
"batch_id": "batch_2026_09_28_un",
"published_at": "2026-09-28T00:00:00Z",
"fetched_at": "2026-10-06T06:00:14Z",
"entry_count": 1123
}
},
"policy_version": 3,
"user_message": "We can't process this transaction. Contact support with reference dec_8f3k2m9q1x7p4v6n0b5c2.",
"latency_ms": 31,
"created_at": "2026-10-06T14:02:11Z",
"idempotency_key": "withdrawal_91f2",
"metadata": {
"user_id": "usr_4421",
"flow": "withdrawal"
}
}The TypeScript shape, verbatim from the service:
type Outcome = "allow" | "review" | "block";
interface Decision {
id: string; // dec_…
org_id: string;
outcome: Outcome;
reasons: Reason[]; // empty when outcome is allow with nothing found
subject: Subject; // echoed, normalised
matches: {
addresses: AddressMatch[];
names: NameMatch[];
labels: LabelMatch[];
};
exposure: ExposureResult[] | null;
lists: Partial<Record<ListCode, ListVersion>>; // exact batches used
policy_version: number;
user_message: string; // safe to show your end user; never names the entry
latency_ms: number;
created_at: string; // ISO 8601
idempotency_key?: string | null;
metadata?: Record<string, string> | null;
}
interface Reason {
code: ReasonCode;
severity: "info" | "low" | "medium" | "high" | "critical";
summary: string; // one plain-English sentence
evidence: Record<string, unknown>; // list, entity_id, batch_id, counterparty, hop, score…
}
type ReasonCode =
| "address.sanctioned" | "address.labeled"
| "name.strong_match" | "name.strong_match_corroborated" | "name.possible_match"
| "country.sanctioned" | "country.restricted"
| "exposure.direct" | "exposure.indirect" | "exposure.unavailable"
| "allowlist.hit" | "policy.override";
type ListCode = "OFAC_SDN" | "OFAC_CONS" | "EU_FSF" | "UK_OFSI" | "UN_SC";
interface ListVersion {
batch_id: string;
published_at: string | null; // as stated by the publisher
fetched_at: string;
entry_count: number;
}
interface NameMatch {
entity_id: string; list: ListCode; batch_id: string;
matched_alias: string; primary_name: string;
score: number; // 0..1
programs: string[]; entity_type: string;
corroborated_by: string[]; // e.g. ["dob", "nationality"]
source_url: string | null;
}
interface AddressMatch {
address: string; chain: Chain | null; list: ListCode; batch_id: string;
entity_id: string; primary_name: string; programs: string[];
}
interface LabelMatch {
address: string; chain: Chain; label: string;
category: "mixer" | "hack" | "scam" | "darknet" | "ransomware"
| "sanctioned_service" | "exchange" | "bridge" | "protocol" | "other";
source: string; source_url: string | null; confidence: number;
}
interface ExposureResult {
status: "ok" | "unavailable" | "skipped";
chain: Chain; address: string; provider: string | null;
depth: 1 | 2;
scanned_txs: number; counterparties: number;
hits: ExposureHit[];
paths: Array<{ hops: string[]; via: AddressMatch | LabelMatch; value_usd: number | null; tx_count: number }>;
inbound_flagged_pct: number | null; // share (0..100) of priced inbound value from flagged counterparties
outbound_flagged_pct: number | null;
flagged_value_usd: number | null; // across direct flagged counterparties, where estimable
flagged_tx_count: number;
values: { inbound_usd: number | null; outbound_usd: number | null; native_symbol: string | null;
native_price_usd: number | null; priced_counterparties: number };
hop2?: { candidates: number; scanned: number; max: number; terminal: number; cached: number };
computed_at: string; cached: boolean; note?: string;
}
interface ExposureHit {
counterparty: string; hop: 1 | 2; direction: "in" | "out" | "both";
tx_count: number; first_seen: string | null; last_seen: string | null;
via: AddressMatch | LabelMatch;
path: string[]; // [subject, counterparty] or [subject, intermediate, counterparty]
value_usd: number | null; // hop 1: both sides; hop 2: the smaller leg of the path
value_in?: ValueSide; value_out?: ValueSide; // hop 1 only
intermediate?: string; // hop 2 only
}
interface ValueSide {
native: string | null; // whole units, e.g. "0.25" SOL
tokens: Array<{ mint_or_contract: string; symbol?: string; amount: string; decimals: number | null }>;
usd_estimate: number | null; // stables at 1:1 + native at a cached spot price; null when unpriceable
}
// Derived from the chain registry; see "Supported chains" and GET /api/v1/chains.
type Chain = "solana" | "ethereum" | "base" | "bsc" | "robinhood" | "arbitrum"
| "optimism" | "polygon" | "avalanche" | "arc" | "linea" | "scroll"
| "zksync" | "blast" | "mantle" | "gnosis" | "celo" | "cronos"
| "sonic" | "berachain" | "unichain" | "worldchain" | "abstract" | "apechain"
| "opbnb" | "sei" | "hyperevm" | "monad" | "polygon-zkevm" | "fraxtal"
| "taiko" | "moonbeam" | "ethereum-classic" | "bitcoin" | "tron" | "litecoin"
| "bitcoin-cash" | "bitcoin-sv" | "bitcoin-gold" | "dogecoin" | "dash" | "zcash"
| "verge" | "monero" | "xrp" | "other";How outcomes combine: every reason carries the outcome your policy assigns to it and the decision takes the worst one. An allowlist hit for the subject downgrades the outcome to allow and is itself reported as a reason, so nothing is silently suppressed.
Batch
Body is { "subjects": Subject[], "metadata"?: {…} }. Each subject is screened and persisted as its own decision; the response is an array of decisions in the same order. Counts as one screen per subject.
curl https://clearlist.xyz/api/v1/screen/batch \
-H "Authorization: Bearer cl_live_…" \
-H "Content-Type: application/json" \
-d '{ "subjects": [
{ "type": "address", "address": "0x098b716b8aaf21512996dc57eb0615e2383e2f96" },
{ "type": "name", "name": "Viktor Bout", "dob": "1967" },
{ "type": "country", "country": "KP" }
] }'Decisions
| outcome | allow | review | block | Filter by outcome. |
| type | address | name | country | transaction | Filter by subject type. |
| since, until | ISO 8601 | Created-at window. |
| cursor | string | From the previous page's next_cursor. |
| limit | 1..100 | Default 50. |
Returns { data: Decision[], next_cursor: string | null }.
The evidence file is the decision plus the complete list records behind every match: the entity, all its aliases and identifiers, its designated addresses, and the list batch metadata (publisher URL, checksum, published and fetched dates). It is what you attach when a bank or partner asks why a decision was made.
Reviews
Any decision with outcome review opens a review automatically. Resolving one records a separate resolution; the decision itself is never modified. Dashboard users can do the same with one click.
| status | cleared | confirmed | Cleared means false positive. Confirmed means the match is real. |
| resolution | string | Required. Your note; it becomes part of the evidence. |
| allowlist | boolean | When clearing an address or name subject, also add it to the allowlist. |
Allowlist
Your false-positive suppression. Entries are an address, a normalised name, or a specific list entity id (to suppress one entry while keeping the name screened against everything else). A hit on an allowlisted subject still appears as an allowlist.hit reason with the original match attached.
| subject_type | address | name | entity | What the value is. |
| value | string | Address, name or entity id (e.g. OFAC_SDN:27307). Normalised server-side. |
| reason | string | Required. Written into every evidence export that touches the entry. |
| expires_at | ISO 8601 | Optional expiry. |
Policy
The per-organisation rules: name thresholds, what each address label category does, country block and review lists, and exposure settings. Saving bumps the version; every decision records the version it ran under.
{
"name": {
"min_score": 0.8,
"review_threshold": 0.85,
"block_threshold": 0.93,
"block_on_strong_uncorroborated": false
},
"address": {
"sanctioned": "block",
"labeled": {
"mixer": "review",
"hack": "review",
"darknet": "review",
"ransomware": "block",
"scam": "review",
"sanctioned_service": "block"
}
},
"country": {
"block": [
"CU",
"IR",
"KP",
"SY"
],
"review": [
"RU",
"BY",
"VE",
"MM",
"AF",
"YE",
"LY",
"SD",
"SO",
"IQ"
]
},
"exposure": {
"enabled": true,
"direct_sanctioned": "block",
"direct_labeled": "review",
"indirect": "allow",
"ignore_categories": [
"exchange",
"bridge",
"protocol"
],
"max_txs": 200
}
}exposure also accepts depth (1 or 2, default 1) and hop2_max_counterparties (default 15, max 50); see Exposure below.
Exposure
Counterparty exposure answers “who has this address transacted with” and weighs the answer by value. It runs on transaction subjects (both sides), on address subjects that pass exposure: true, and on its own through the endpoint below. Every result carries the same shape (ExposureResult above): the direct counterparties that are sanctioned or publicly labeled, how much moved between them and the subject, and the share of the subject’s inbound and outbound value that touched flagged addresses.
| depth 1 | default | The subject's recent transactions (up to policy.exposure.max_txs, 200 by default) are read and every distinct counterparty is checked against the government lists and the public labels. Synchronous, 2.5 s budget. |
| depth 2 | opt in | After hop 1, the most valuable clean counterparties (not flagged, not an exchange, bridge or protocol, not a program or contract) are each scanned one hop, up to hop2_max_counterparties of them, inside an 8 s overall budget. Hits found there are hop-2 hits with the path subject → intermediate → flagged. Best effort: when the budget runs out, the note says scanned_hop2: n of m and the result is partial. |
| value_usd | estimate | USD stables (USDC, USDT, DAI, USDe, PYUSD, USDS, FDUSD) at 1:1 matched by mint or contract address, never by symbol; native SOL, ETH, BNB, POL and AVAX at a spot price cached for ten minutes from a public source. Other tokens are listed but not priced. When nothing on a side could be priced the estimate is null, and taint percentages are over the priced portion only. |
| inbound_flagged_pct | 0..100 | null | Priced inbound value that came directly from flagged counterparties, as a share of all priced inbound value. null when no inbound value could be priced. outbound_flagged_pct is the mirror. |
| flagged_value_usd | number | null | Sum of both sides across the direct flagged counterparties, where estimable. flagged_tx_count is the matching transaction count. |
| paths | array | One entry per flagged counterparty per route: hops, the list entry or label it was reached through, the value and the transaction count. For hop 2 the value and count are the smaller leg of the path, an upper bound on what could have flowed along it. |
| caching | 6 h | Results are cached per address and chain for six hours. A hop-2 scan reuses cached hop-1 scans of its intermediates, and a depth-1 result resumes into depth 2 without rescanning hop 1. Pass fresh: true to bypass. |
In a decision, hop-1 hits become exposure.direct reasons (value-aware when values were readable: “sent $12,400 across 3 transactions to …”) with the action your policy sets for sanctioned and labeled counterparties; hop-2 hits become exposure.indirect reasons under policy.exposure.indirect (allow by default, reported at info severity) with the full path and all routes in the evidence. Every exposure reason also carries the subject’s taint figures in evidence.taint.
| address | string | Required. Normalised server-side. |
| chain | Chain | Optional; detected from the address format when omitted. |
| depth | 1 | 2 | Optional; defaults to policy.exposure.depth, then 1. |
| fresh | boolean | Optional; skip the cache. |
curl https://clearlist.xyz/api/v1/exposure \
-H "Authorization: Bearer cl_live_…" \
-H "Content-Type: application/json" \
-d '{ "address": "73GiQ6TE3Tjtx5yFXTCppMQof6Gqam93pD4fJ5NweApz", "chain": "solana", "depth": 2 }'{
"status": "ok",
"chain": "solana",
"address": "73GiQ6TE3Tjtx5yFXTCppMQof6Gqam93pD4fJ5NweApz",
"provider": "helius",
"depth": 2,
"scanned_txs": 100,
"counterparties": 38,
"hits": [
{
"counterparty": "HXk3…mixr",
"hop": 1,
"direction": "out",
"tx_count": 3,
"first_seen": "2026-09-12T08:41:10.000Z",
"last_seen": "2026-09-30T19:02:44.000Z",
"via": {
"address": "HXk3…mixr",
"chain": "solana",
"label": "Mixer pool",
"category": "mixer",
"source": "public-labels",
"source_url": "https://…",
"confidence": 0.9
},
"path": [
"73Gi…eApz",
"HXk3…mixr"
],
"value_usd": 12400,
"value_in": {
"native": null,
"tokens": [],
"usd_estimate": null
},
"value_out": {
"native": "4.2",
"tokens": [
{
"mint_or_contract": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"symbol": "USDC",
"amount": "11800",
"decimals": 6
}
],
"usd_estimate": 12400
}
},
{
"counterparty": "0x098b…2f96",
"hop": 2,
"direction": "in",
"tx_count": 1,
"first_seen": "2026-09-02T17:40:00.000Z",
"last_seen": "2026-09-02T17:40:00.000Z",
"via": {
"address": "0x098b…2f96",
"chain": null,
"list": "OFAC_SDN",
"batch_id": "batch_2026_09_30_sdn",
"entity_id": "OFAC_SDN:27307",
"primary_name": "LAZARUS GROUP",
"programs": [
"DPRK3"
]
},
"path": [
"73Gi…eApz",
"9aBc…int1",
"0x098b…2f96"
],
"value_usd": 950,
"intermediate": "9aBc…int1"
}
],
"paths": [
{
"hops": [
"73Gi…eApz",
"HXk3…mixr"
],
"via": {
"label": "Mixer pool",
"category": "mixer"
},
"value_usd": 12400,
"tx_count": 3
},
{
"hops": [
"73Gi…eApz",
"9aBc…int1",
"0x098b…2f96"
],
"via": {
"list": "OFAC_SDN",
"entity_id": "OFAC_SDN:27307"
},
"value_usd": 950,
"tx_count": 1
}
],
"inbound_flagged_pct": 0,
"outbound_flagged_pct": 38.4,
"flagged_value_usd": 12400,
"flagged_tx_count": 3,
"values": {
"inbound_usd": 2210.5,
"outbound_usd": 32291.7,
"native_symbol": "SOL",
"native_price_usd": 142.86,
"priced_counterparties": 31
},
"hop2": {
"candidates": 22,
"scanned": 11,
"max": 15,
"terminal": 4,
"cached": 3
},
"computed_at": "2026-10-06T14:02:11.000Z",
"cached": false,
"note": "ignored per policy.exposure.ignore_categories: exchange (2), protocol (3); scanned_hop2: 11 of 15 (budget exhausted)"
}Each POST counts as one exposure in your usage; the GET does not. Both need a plan that includes exposure (402 otherwise). Limits worth knowing: hop 1 is synchronous and bounded by max_txs; hop 2 is best effort within its budget and never expands exchanges, bridges, protocols, programs or contracts; values are estimates for ranking and explanation, not accounting; EVM chains need the Etherscan v2 key and Solana works keyless over a public RPC (fewer transactions per scan) or with Helius; Bitcoin, Tron and the other screening-only chains return status: "unavailable".
Monitors
Enrol an address or a name and it is rescreened whenever any list publishes a new batch. If the outcome changes you get a monitor.changed webhook and a new decision. Monitored subjects count toward your plan’s monitoring allowance (see pricing); the rescans themselves are not metered. Monitoring is available on paid plans.
{ "subject": { "type": "address", "address": "0x…" }, "metadata": { "customer_id": "cus_123" } }Webhooks
Signed POSTs to your endpoint. Events: decision.created, monitor.changed, review.resolved, lists.refreshed. Deliveries time out after 10 seconds and retry with backoff (1 min, 5 min, 30 min, 2 h, 12 h). Respond 2xx to acknowledge.
{ "url": "https://api.example.com/clearlist", "events": ["decision.created", "monitor.changed"] }Every delivery carries clearlist-signature, clearlist-event and clearlist-delivery-id headers. The body is { id, event, created_at, data }. Verify the signature against the raw body:
import { createHmac, timingSafeEqual } from "node:crypto";
// Header: clearlist-signature: t=<unix seconds>,v1=<hex hmac-sha256(secret, `${t}.${rawBody}`)>
export function verifyWebhookSignature(secret: string, header: string | null, rawBody: string, toleranceSec = 300) {
if (!header) return false;
const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=") as [string, string]));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(parts.v1 ?? "", "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && timingSafeEqual(a, b);
}
// Next.js route handler
export async function POST(req: Request) {
const raw = await req.text(); // verify the raw body, not a re-serialised one
if (!verifyWebhookSignature(process.env.CLEARLIST_WEBHOOK_SECRET!, req.headers.get("clearlist-signature"), raw)) {
return new Response("bad signature", { status: 401 });
}
const event = JSON.parse(raw); // { id, event, created_at, data }
return new Response("ok");
}Lists
Status of every source: current batch id, published and fetched timestamps, entry and address counts. Sources: OFAC SDN, OFAC Consolidated (non-SDN), EU Financial Sanctions File, UK OFSI Consolidated List, UN Security Council Consolidated List. Fetched from the publishers every six hours; a refresh is rejected if the parsed count drops below half of the previous batch, so a broken upstream file never empties a list.
Supported chains
45 chains, 32 of them EVM. Two levels of coverage. Screening (is this exact address on a government list, or in the public labels) works on every chain, because designated addresses are strings. Exposure (who has this address transacted with) needs an indexer: every EVM chain below runs through the Etherscan v2 API, Solana through Helius or a Solana RPC. Bitcoin, Tron, the UTXO chains, Monero and XRP are screening-only today; a screen on them completes with exposure[].status = "unavailable" rather than failing.
Pass chain on an address subject to pin it; omit it and the chain is detected from the address format. EVM addresses are matched against designations on any EVM chain, since the same key controls the same address everywhere. The registry is the single source of truth for the engine, the dashboard and this page.
| Chain | id | Family | Screening | Exposure |
|---|---|---|---|---|
| Solana | solana | Solana | Lists + labels | Helius or Solana RPC |
| Ethereum | ethereum | EVM1 | Lists + labels | Etherscan v2 |
| Base | base | EVM8453 | Lists + labels | Etherscan v2 |
| BNB Smart Chain | bsc | EVM56 | Lists + labels | Etherscan v2 |
| Robinhood Chain | robinhood | EVM4663 | Lists + labels | Etherscan v2 |
| Arbitrum One | arbitrum | EVM42161 | Lists + labels | Etherscan v2 |
| OP Mainnet | optimism | EVM10 | Lists + labels | Etherscan v2 |
| Polygon | polygon | EVM137 | Lists + labels | Etherscan v2 |
| Avalanche C-Chain | avalanche | EVM43114 | Lists + labels | Etherscan v2 |
| Arc | arc | EVM5042 | Lists + labels | Etherscan v2 |
| Linea | linea | EVM59144 | Lists + labels | Etherscan v2 |
| Scroll | scroll | EVM534352 | Lists + labels | Etherscan v2 |
| ZKsync Era | zksync | EVM324 | Lists + labels | Etherscan v2 |
| Blast | blast | EVM81457 | Lists + labels | Etherscan v2 |
| Mantle | mantle | EVM5000 | Lists + labels | Etherscan v2 |
| Gnosis | gnosis | EVM100 | Lists + labels | Etherscan v2 |
| Celo | celo | EVM42220 | Lists + labels | Etherscan v2 |
| Cronos | cronos | EVM25 | Lists + labels | Etherscan v2 |
| Sonic | sonic | EVM146 | Lists + labels | Etherscan v2 |
| Berachain | berachain | EVM80094 | Lists + labels | Etherscan v2 |
| Unichain | unichain | EVM130 | Lists + labels | Etherscan v2 |
| World Chain | worldchain | EVM480 | Lists + labels | Etherscan v2 |
| Abstract | abstract | EVM2741 | Lists + labels | Etherscan v2 |
| ApeChain | apechain | EVM33139 | Lists + labels | Etherscan v2 |
| opBNB | opbnb | EVM204 | Lists + labels | Etherscan v2 |
| Sei EVM | sei | EVM1329 | Lists + labels | Etherscan v2 |
| HyperEVM | hyperevm | EVM999 | Lists + labels | Etherscan v2 |
| Monad | monad | EVM143 | Lists + labels | Etherscan v2 |
| Polygon zkEVM | polygon-zkevm | EVM1101 | Lists + labels | Etherscan v2 |
| Fraxtal | fraxtal | EVM252 | Lists + labels | Etherscan v2 |
| Taiko | taiko | EVM167000 | Lists + labels | Etherscan v2 |
| Moonbeam | moonbeam | EVM1284 | Lists + labels | Etherscan v2 |
| Ethereum Classic | ethereum-classic | EVM | Lists + labels | Not yet |
| Bitcoin | bitcoin | Bitcoin | Lists + labels | Not yet |
| Tron | tron | Tron | Lists + labels | Not yet |
| Litecoin | litecoin | UTXO | Lists + labels | Not yet |
| Bitcoin Cash | bitcoin-cash | UTXO | Lists + labels | Not yet |
| Bitcoin SV | bitcoin-sv | UTXO | Lists + labels | Not yet |
| Bitcoin Gold | bitcoin-gold | UTXO | Lists + labels | Not yet |
| Dogecoin | dogecoin | UTXO | Lists + labels | Not yet |
| Dash | dash | UTXO | Lists + labels | Not yet |
| Zcash | zcash | UTXO | Lists + labels | Not yet |
| Verge | verge | UTXO | Lists + labels | Not yet |
| Monero | monero | Other | Lists + labels | Not yet |
| XRP Ledger | xrp | Other | Lists + labels | Not yet |
Errors
Every non-2xx response is the same envelope with a stable code:
{
"error": {
"code": "validation_error",
"message": "Request validation failed.",
"details": [
{
"path": [
"subject",
"address"
],
"message": "Address is too short."
}
]
}
}| 400 validation_error | Body or query failed validation; details lists each issue. | |
| 400 invalid_json | Body is not JSON. | |
| 401 unauthenticated | No key sent. | |
| 401 invalid_key | Key malformed, unknown or revoked. | |
| 404 not_found | No such resource in your organisation. | |
| 409 already_resolved | Review was already cleared or confirmed. | |
| 429 rate_limited | Per-key limit hit; see retry-after. | |
| 500 internal_error | Our fault. The request is safe to retry. |
Exposure never fails a screen: when a chain provider is slow or down, the decision completes with exposure[].status = "unavailable" and an exposure.unavailable info reason, so you can decide whether to hold.
Rate limits
Per key, by plan: 300 on Free, 600 on Startup, 2,000 on Growth, 10,000 on Foundation & Enterprise requests per minute (token bucket). Every response carries x-ratelimit-remaining; a 429 carries retry-after in seconds. Address screens typically return in under 50 ms; exposure adds up to 2.5 s when it has to hit a chain provider.
Idempotency
Pass idempotency_key on a screen (your withdrawal id, your session id) and a retry returns the original decision rather than creating a second one. Keys are scoped to your organisation and never expire. Without a key, each call is a new decision, which is what you want for periodic re-checks.