Screening Walkthrough
Sanctions, threat-intelligence, and entity-attribution screening for blockchain addresses. Designed for CASP / VASP compliance flows and on-chain risk monitoring.
This guide walks through three real workflows:
- Synchronous deposit gating — block sanctioned addresses at the point of deposit.
- Asynchronous heuristic analysis — deep behavioural risk score for compliance review.
- Bulk re-screening — re-check a customer book when sanctions lists update.
For the underlying tier / mode model, see Screening overview.
Endpoint map
| Endpoint | Mode | Latency | Use |
|---|---|---|---|
POST /v1/screen/address | fast / full | ~20ms sync (fast); ~20ms + 2-30s async (full) | Single-address screen |
POST /v1/screen/bulk | fast | ~20ms × N | Up to 100 addresses per call |
GET /v1/screen/methodology | — | — | Live description of tiers, lists, scoring |
GET /v1/screen/sanctions/status | — | — | Last-refresh time of every sanctions list |
Example 1 — synchronous deposit gating
The most common pattern: before crediting a deposit, run a fast screen and act on the result.
curl -X POST 'https://api.cryptachain.com/v1/screen/address' \
-H "X-API-Key: $CRYPTACHAIN_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"address": "0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b",
"chain": "ethereum",
"mode": "fast"
}'import os, requests
def screen(address: str, chain: str) -> dict:
r = requests.post(
"https://api.cryptachain.com/v1/screen/address",
json={"address": address, "chain": chain, "mode": "fast"},
headers={"X-API-Key": os.environ["CRYPTACHAIN_API_KEY"]},
)
r.raise_for_status()
return r.json()
s = screen("0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b", "ethereum")
if s["sanctions_match"]:
raise PermissionError(f"Sanctions: {s['sanctions_lists']}")
if s["risk_score"] >= 70:
print("HOLD for manual review")
else:
print("ALLOW")async function screen(address: string, chain: string) {
const r = await fetch('https://api.cryptachain.com/v1/screen/address', {
method: 'POST',
headers: {
'X-API-Key': process.env.CRYPTACHAIN_API_KEY!,
'Content-Type': 'application/json',
},
body: JSON.stringify({ address, chain, mode: 'fast' }),
});
if (!r.ok) throw new Error(`screen failed: ${r.status}`);
return r.json();
}Response for a sanctioned address (Tornado Cash deposit, OFAC-listed since August 2022):
{
"address": "0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b",
"chain": "ethereum",
"risk_score": 100,
"risk_level": "CRITICAL",
"sanctions_match": true,
"sanctions_lists": ["OFAC_SDN"],
"flags": [
{
"type": "SANCTIONS_HIT",
"severity": "CRITICAL",
"description": "Address appears on OFAC SDN list (Tornado Cash)",
"list": "OFAC_SDN",
"listed_date": "2022-08-08"
}
],
"labels": [
{ "type": "protocol", "name": "Tornado Cash", "category": "mixer", "source": "internal" }
],
"heuristics_status": "NOT_REQUESTED",
"chain_indexing_status": "FULLY_INDEXED",
"screened_at": "2026-04-04T14:30:00.123Z",
"cache_hit": false
}Sanctions hits are a hard stop
A sanctions_match: true result is not a recommendation — it is a regulatory finding. You must not process the transaction without legal review, and you must keep an audit record of the screening result (the response is the record). Treat the API response as compliance evidence, not signal.
Example 2 — async heuristic analysis (full mode)
fast mode returns Tier 0 (sanctions) + Tier 1 (threat-intel) + Tier 2 (entity-attribution) synchronously. To also get Tier 3 — behavioural clustering, fund-flow analysis, indirect-exposure score — pass mode: "full". The synchronous body returns immediately with a heuristics_job_id; the heuristic result is delivered via webhook (preferred) or by polling.
Polling pattern
job = screen("0xd8dA...", "ethereum", mode="full")
job_id = job["heuristics_job_id"]
for _ in range(15): # max 30 seconds
time.sleep(2)
r = requests.get(
f"https://api.cryptachain.com/v1/screen/jobs/{job_id}",
headers={"X-API-Key": os.environ["CRYPTACHAIN_API_KEY"]},
)
if r.json()["status"] == "COMPLETE":
result = r.json()
break
else:
raise TimeoutError("heuristics took >30s")Webhook pattern (recommended)
Configure a webhook URL in the developer dashboard, then handle the HEURISTICS_COMPLETE event:
app.post('/webhooks/cryptachain', (req, res) => {
// 1. ALWAYS verify the HMAC signature first
// see /webhooks/verify for the exact code
if (!verifyHmac(req.rawBody, req.header('X-CryptaChain-Signature'))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.rawBody);
if (event.type === 'HEURISTICS_COMPLETE') {
const { address, chain, risk_score, flags } = event.data;
db.screenings.update({ where: { address, chain }, data: { risk_score, flags } });
}
res.sendStatus(200);
});The webhook payload is the same shape as the polled /screen/jobs/{id} response. See Webhooks for the full event-type list, retry policy, and signature verification.
Example 3 — bulk re-screen on sanctions update
When a sanctions list refreshes (every 6 hours), CryptaChain fires a SANCTIONS_LIST_UPDATED webhook. The right response is to re-screen all addresses in your customer book against the new list. /screen/bulk accepts up to 100 addresses per call:
curl -X POST 'https://api.cryptachain.com/v1/screen/bulk' \
-H "X-API-Key: $CRYPTACHAIN_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"chain": "ethereum",
"mode": "fast",
"addresses": [
"0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b",
"0x...",
"0x..."
]
}'Response:
{
"chain": "ethereum",
"mode": "fast",
"screened_at": "2026-04-04T14:30:00Z",
"results": [
{ "address": "0xd8dA...", "risk_score": 5, "risk_level": "LOW", "sanctions_match": false, "flags": [] },
{ "address": "0xd90e...", "risk_score": 100, "risk_level": "CRITICAL", "sanctions_match": true, "flags": [...] }
]
}Bulk costs 1 credit per address (vs 2 for individual /screen/address). For a customer book of 10K addresses across two chains, that is 20K credits — comfortably inside the Standard tier's 500K monthly allowance, with room to re-screen every list refresh.
Common pitfalls
Cache and freshness
By default, screening results are cached for 5 minutes. If a sanctions list has just refreshed and you need the absolutely-latest result, pass ?cache=false. For deposit gating this is rarely needed (5-minute lag is acceptable for OFAC); for forensic investigations it matters. The cache_hit field in the response tells you whether the data came from cache.
False positives on shared addresses
Exchange hot wallets and mixer entry points can carry "indirect exposure" flags from co-mingled funds. Always inspect the labels array before acting on a risk_score alone — an address labelled category: "exchange" with risk_score: 60 is structurally different from an unlabeled address with risk_score: 60. Treat exchange labels as a strong dampener on the score.
Storing screening results
Persist the full API response (not just the score). Regulators ask for evidence of what you knew at the time; replaying a 6-month-old decision means showing the original flags, sanctions_lists, and screened_at timestamp. The full JSON is auditable; a recomputed score is not.
Try it live
The /playground lets you screen any address with your own key. Filter operations by tag Screening.
Next steps
- Overview — tiers, modes, latency / credits
- Fast vs Full — mode comparison table
- Risk score reference — every score range and flag type
- Webhooks — set up the async delivery channel
- CASP compliance — MiCA Article 38 implementation