Skip to content

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:

  1. Synchronous deposit gating — block sanctioned addresses at the point of deposit.
  2. Asynchronous heuristic analysis — deep behavioural risk score for compliance review.
  3. Bulk re-screening — re-check a customer book when sanctions lists update.

For the underlying tier / mode model, see Screening overview.

Endpoint map

EndpointModeLatencyUse
POST /v1/screen/addressfast / full~20ms sync (fast); ~20ms + 2-30s async (full)Single-address screen
POST /v1/screen/bulkfast~20ms × NUp to 100 addresses per call
GET /v1/screen/methodologyLive description of tiers, lists, scoring
GET /v1/screen/sanctions/statusLast-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.

bash
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"
  }'
python
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")
typescript
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):

json
{
  "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

python
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")

Configure a webhook URL in the developer dashboard, then handle the HEURISTICS_COMPLETE event:

typescript
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:

bash
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:

json
{
  "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

CryptaChain API — built by CryptaCount Luxembourg S.a r.l.