DEV Community

Dave Sng
Dave Sng

Posted on

Building a Production-Grade Sanctions Screening System in Python: AML Compliance in 2026

Why Sanctions Compliance Is More Urgent Than Ever in 2026

Global regulators are accelerating enforcement. In early 2026, the US OFAC, EU, and UK OFSI have collectively levied over $230 million in fines against financial institutions that failed to implement adequate sanctions screening. Meanwhile, crypto compliance requirements have expanded dramatically — any platform accepting digital assets must perform real-time counterparty screening.

For developers and FinTech teams, this means one thing: sanctions screening cannot be an afterthought — it must be embedded at every layer of your business logic.


The Core Challenges of Sanctions Screening

Manual screening has fundamental flaws:

  • Name variations: The same person may appear in multiple languages and spellings (e.g., Владимир Путин vs Vladimir Putin vs V. Putin)
  • High false positive rates: Crude keyword matching floods compliance teams with noise
  • Stale data: Sanctions lists update daily — manually maintaining a local database is impractical
  • Multi-list coverage: You need simultaneous coverage of OFAC SDN, EU Consolidated, UN Consolidated, UK OFSI, and more

Building a Sanctions Screening Workflow in Python

Here's a production-ready three-layer pipeline:

Layer 1: Input Normalization

import unicodedata
import re

def normalize_name(name: str) -> str:
    """Normalize name: remove diacritics, unify case, clean whitespace"""
    # Convert Unicode diacritics to ASCII
    normalized = unicodedata.normalize("NFKD", name)
    ascii_name = normalized.encode("ascii", "ignore").decode("ascii")
    # Lowercase and strip extra whitespace
    return re.sub(r"\s+", " ", ascii_name.lower().strip())

# Test
print(normalize_name("Müller, Hans-Jörg"))  # -> "muller, hans-jorg"
Enter fullscreen mode Exit fullscreen mode

Layer 2: Fuzzy Matching Engine

from rapidfuzz import fuzz, process
from typing import List, Tuple

SANCTIONS_LIST = [
    "john smith",
    "vladimir putin",
    "ali hassan al-majid",
    "kim jong un",
]

def fuzzy_screen(
    name: str,
    sanctions_list: List[str],
    threshold: float = 85.0
) -> List[Tuple[str, float]]:
    """
    Screen against sanctions list using fuzzy matching.
    Returns entries exceeding the threshold.
    """
    normalized = normalize_name(name)
    results = process.extract(
        normalized,
        sanctions_list,
        scorer=fuzz.token_sort_ratio,
        limit=5
    )
    return [(match, score) for match, score, _ in results if score >= threshold]

# Test name variants
hits = fuzzy_screen("V. Putin", SANCTIONS_LIST)
print(hits)  # [("vladimir putin", 90.0)]

hits = fuzzy_screen("Jon Smith", SANCTIONS_LIST)
print(hits)  # [("john smith", 91.7)]
Enter fullscreen mode Exit fullscreen mode

Layer 3: API Integration for Enterprise Coverage

Local fuzzy matching works for prototypes, but production environments need:

  • Coverage of 100+ global sanctions lists
  • Entity relationship graphs (subsidiaries/family members of sanctioned targets)
  • Audit logs (required proof chain for regulators)
  • Sub-second response times (don't degrade payment UX)

This is exactly what SanctionShield AI addresses. It aggregates global sanctions data through a single REST API endpoint, using AI to handle name variants and entity disambiguation:

import httpx
import json

def screen_with_sanctionshield(
    name: str,
    api_key: str,
    rapidapi_host: str = "sanctionshield-ai.p.rapidapi.com"
) -> dict:
    """
    Call SanctionShield AI API for sanctions screening.
    https://rapidapi.com/shadowlabsai/api/sanctionshield-ai
    """
    url = "https://sanctionshield-ai.p.rapidapi.com/screen"

    headers = {
        "x-rapidapi-key": api_key,
        "x-rapidapi-host": rapidapi_host,
        "Content-Type": "application/json"
    }

    payload = {
        "name": name,
        "entity_type": "individual",  # or "organization"
        "fuzzy_matching": True,
        "include_relatives": True     # screen related entities too
    }

    response = httpx.post(url, json=payload, headers=headers, timeout=5.0)
    response.raise_for_status()
    return response.json()


# Use in a payment flow
def process_payment(sender_name: str, amount: float, api_key: str):
    result = screen_with_sanctionshield(sender_name, api_key)

    if result.get("is_sanctioned"):
        matches = result.get("matches", [])
        print(f"[BLOCKED] {sender_name} matched sanctions list")
        for match in matches:
            print(f"  - {match['name']} ({match['list']}) score: {match['score']:.1%}")
        return {"status": "blocked", "reason": "sanctions_hit"}

    print(f"[CLEARED] {sender_name} passed screening, processing ${amount:.2f}")
    return {"status": "approved"}
Enter fullscreen mode Exit fullscreen mode

Building a Batch Screening Pipeline

For KYC onboarding flows, you typically need to screen entire customer databases:

import asyncio
import httpx
from dataclasses import dataclass, field
from typing import List

@dataclass
class ScreeningResult:
    name: str
    is_sanctioned: bool
    matches: List[dict] = field(default_factory=list)
    error: str | None = None

async def batch_screen(
    names: List[str],
    api_key: str,
    concurrency: int = 10
) -> List[ScreeningResult]:
    """Async batch sanctions screening with concurrency control"""
    semaphore = asyncio.Semaphore(concurrency)

    async def screen_one(name: str) -> ScreeningResult:
        async with semaphore:
            async with httpx.AsyncClient() as client:
                try:
                    resp = await client.post(
                        "https://sanctionshield-ai.p.rapidapi.com/screen",
                        json={"name": name, "fuzzy_matching": True},
                        headers={
                            "x-rapidapi-key": api_key,
                            "x-rapidapi-host": "sanctionshield-ai.p.rapidapi.com"
                        },
                        timeout=5.0
                    )
                    data = resp.json()
                    return ScreeningResult(
                        name=name,
                        is_sanctioned=data.get("is_sanctioned", False),
                        matches=data.get("matches", [])
                    )
                except Exception as e:
                    return ScreeningResult(name=name, is_sanctioned=False, error=str(e))

    return list(await asyncio.gather(*[screen_one(n) for n in names]))


async def main():
    customer_names = [
        "John Smith", "Ali Hassan", "Kim Jong-un",
        "Maria Garcia", "Ivan Petrov"
    ]
    results = await batch_screen(customer_names, api_key="YOUR_API_KEY")

    blocked = [r for r in results if r.is_sanctioned]
    cleared = [r for r in results if not r.is_sanctioned and not r.error]
    print(f"Screening complete: {len(cleared)} cleared, {len(blocked)} blocked")
    for r in blocked:
        print(f"  BLOCKED: {r.name}")

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Production Deployment Recommendations

Scenario Recommended Approach
Real-time payment gateway Synchronous API call, 3s timeout, fallback to local list on failure
KYC onboarding flow Async batch screening + results stored in audit database
Periodic re-screening Daily cron job to re-screen existing customers
High-risk industries Enable include_relatives=True to cover related entities

Critical: Compliance Documentation

During regulatory review, you need to prove that every transaction and customer was screened. Make sure to preserve:

import datetime

def log_screening_result(result: dict, name: str):
    """Log screening result for audit trail"""
    audit_record = {
        "timestamp": datetime.datetime.utcnow().isoformat(),
        "screened_name": name,
        "is_sanctioned": result.get("is_sanctioned"),
        "lists_checked": result.get("lists_checked", []),
        "match_count": len(result.get("matches", [])),
        "api_version": result.get("api_version"),
        "request_id": result.get("request_id")  # for dispute resolution
    }
    # Store in immutable audit log (append-only DB or S3)
    print(json.dumps(audit_record))
Enter fullscreen mode Exit fullscreen mode

Summary

Sanctions compliance in 2026 is non-negotiable. The core components of a reliable screening system are:

  1. Normalize inputs: Handle Unicode, name variants, and typos
  2. Multi-list coverage: Single data sources aren't enough — you need global aggregation
  3. Audit logging: Complete records of every screening are the foundation of compliance proof
  4. Async batch processing: KYC flows need high throughput without blocking main processes

For teams that need enterprise-grade coverage, SanctionShield AI provides a turnkey solution covering OFAC, EU, UN, UK OFSI and other major lists — accessible via a single RapidAPI integration.


Code examples in this article run on Python 3.12+. Install dependencies with pip install rapidfuzz httpx.

Top comments (0)