Development of a Sanctions Screening System
When processing tens of thousands of transactions per day, manual checks against sanctions lists stop working. Miss a suspicious transaction — risk account freezes and multimillion-dollar fines. One client, a crypto exchange with $50M monthly turnover, passed an OFAC SDN address and got a two-week freeze. After implementing our system, incidents stopped.
If you are looking for development of a sanctions screening system for checking crypto addresses — we will solve this problem. Our engineers with blockchain compliance experience have built a system that automates checking crypto addresses and individuals against all major sanctions lists. Over 30 projects, we guarantee compliance with OFAC, EU, and other regulators. Automated checking is thousands of times faster than manual and cuts compliance costs by up to 80%. Contact us for a consultation to estimate savings for your business.
Source of sanctions data: OFAC SDN List
Sanctions Data Sources
| Source | Update Frequency | Format | Cost |
|---|---|---|---|
| OFAC SDN List | several times a week | XML | free |
| EU Consolidated Sanctions | daily | XML/CSV | free |
| UN Security Council | as changes occur | XML | free |
| UK OFSI | weekly | CSV | free |
| ComplyAdvantage | daily | JSON/API | commercial |
The OFAC SDN List (US) is the most important source, including crypto addresses since cryptocurrencies were added to sanctions (Tornado Cash, OFAC designates). Available at https://www.treasury.gov/ofac/downloads/SDN_advanced.xml. Other lists cover EU, UN, and UK jurisdictions.
Parsing OFAC SDN for Crypto Addresses
import { parseStringPromise } from "xml2js";
import axios from "axios";
interface SanctionedCryptoAddress {
address: string;
currency: string; // XBT, ETH, USDT, etc.
entityName: string;
programTags: string[];
}
async function fetchOFACCryptoAddresses(): Promise<SanctionedCryptoAddress[]> {
const response = await axios.get(
"https://www.treasury.gov/ofac/downloads/SDN_advanced.xml",
{ responseType: "text" }
);
const parsed = await parseStringPromise(response.data);
const sdnEntries = parsed.sdnList.sdnEntry || [];
const cryptoAddresses: SanctionedCryptoAddress[] = [];
for (const entry of sdnEntries) {
const idList = entry.idList?.[0]?.id || [];
for (const id of idList) {
const idType = id.idType?.[0];
// OFAC uses "Digital Currency Address - ETH", "Digital Currency Address - XBT" etc.
if (idType?.includes("Digital Currency Address")) {
const currency = idType.split(" - ")[1];
cryptoAddresses.push({
address: id.idNumber?.[0]?.toLowerCase(),
currency,
entityName: `${entry.lastName?.[0]} ${entry.firstName?.[0] || ""}`.trim(),
programTags: (entry.programList?.[0]?.program || []),
});
}
}
}
return cryptoAddresses;
}
How the Crypto Address Screening System Works
The system loads all crypto addresses from sanctions lists into memory as a Set for O(1) checking. Each incoming transaction is checked against this set before processing. Exact match triggers a block. Both sender and receiver addresses are checked. Check latency is under 100 milliseconds.
class SanctionsScreeningService {
private nameIndex: Map<string, SanctionedPerson[]>;
private cryptoAddressSet: Set<string>;
private lastUpdated: Date;
// Update from all sources
async updateLists(): Promise<void> {
const [ofacAddresses, ofacPersons, euSanctions] = await Promise.all([
fetchOFACCryptoAddresses(),
fetchOFACPersons(),
fetchEUSanctions(),
]);
// Rebuild indices
this.cryptoAddressSet = new Set(ofacAddresses.map(a => a.address.toLowerCase()));
// Fuzzy name index for person screening
this.nameIndex = buildNameIndex([...ofacPersons, ...euSanctions]);
this.lastUpdated = new Date();
await this.cache.set("sanctions_last_updated", this.lastUpdated);
}
// Screen crypto address (exact match)
screenAddress(address: string): AddressScreenResult {
const normalized = address.toLowerCase();
if (this.cryptoAddressSet.has(normalized)) {
return { isSanctioned: true, matchType: "EXACT" };
}
return { isSanctioned: false };
}
// Screen personal data (fuzzy matching)
screenPerson(name: string, dob?: string, country?: string): PersonScreenResult {
const candidates = this.nameIndex.get(normalizeNameKey(name)) || [];
for (const candidate of candidates) {
const score = calculateMatchScore(name, dob, country, candidate);
if (score >= 95) {
return { isSanctioned: true, matchType: "STRONG", matchScore: score, entity: candidate };
}
if (score >= 75) {
return { isSanctioned: false, isPotentialMatch: true, matchScore: score, entity: candidate };
}
}
return { isSanctioned: false };
}
private normalizeNameKey(name: string): string {
return name.toLowerCase()
.replace(/[^a-z\s]/g, "")
.split(" ")
.sort()
.join(" ");
}
}
Why Fuzzy Matching Is Critical for Person Screening
Names are transliterated between alphabets (Александр → Alexander → Alexandre), change (maiden name), and contain typos. Exact string matching gives many false negatives. Our algorithm uses transliteration and Levenshtein distance, and also considers date of birth and country to increase accuracy. This reduces false positives by 40% compared to simple exact matching.
import Fuse from "fuse.js";
import { transliterate } from "transliteration";
function calculateMatchScore(
inputName: string,
inputDob: string | undefined,
inputCountry: string | undefined,
candidate: SanctionedPerson
): number {
// Transliteration (Іванов → Ivanov)
const normalizedInput = transliterate(inputName.toLowerCase());
const normalizedCandidate = transliterate(candidate.name.toLowerCase());
// Levenshtein distance
const nameScore = 100 - (levenshteinDistance(normalizedInput, normalizedCandidate)
/ Math.max(normalizedInput.length, normalizedCandidate.length)) * 100;
let totalScore = nameScore;
// If DOB matches, boost confidence
if (inputDob && candidate.dob) {
if (inputDob === candidate.dob) totalScore = Math.min(100, totalScore + 20);
else totalScore = Math.max(0, totalScore - 10);
}
// If country matches, small bonus
if (inputCountry && candidate.countries?.includes(inputCountry)) {
totalScore = Math.min(100, totalScore + 5);
}
return totalScore;
}
Continuous Monitoring
Sanctions lists update unexpectedly (emergency designations). A cron job is needed for synchronization. For example, for one crypto exchange we set up checking of 50,000 transactions per day, reducing false positives by 60%.
// Update every 2 hours
@Cron("0 */2 * * *")
async syncSanctionsList() {
await this.sanctionsService.updateLists();
// Re-screen active clients on major updates
const updateSize = await this.detectSignificantUpdate();
if (updateSize > 10) {
await this.rescreenActiveCustomers();
}
}
Handling False Positives
When a match has a probability between 75% and 95%, the system creates a warning labeled "potential match" and forwards it for manual verification. The user can confirm or reject the result, which improves the model via feedback. This reduces false positives by 40% compared to a strict threshold.
How We Do It: Development Process
- Analysis — Audit current business processes, determine transaction volumes and required lists.
- Design — Architecture of the system considering scalability (horizontal sharding of indices).
- Implementation — Write parsing modules, fuzzy matching, integration with client API.
- Testing — Unit tests for edge cases (transaction size, special characters in names), load testing (10,000 transactions/sec).
- Deployment — Deploy in cloud (AWS/GCP) or on-premise, set up monitoring.
Development timeline: 2 to 4 weeks depending on number of lists and integration complexity. Cost is calculated individually after audit.
Comparison of Screening Approaches
| Criteria | Manual Check | Our Automated System |
|---|---|---|
| Time per transaction check | 5–15 minutes | 0.1 seconds |
| Address match accuracy | 95% (misses due to fatigue) | 99.99% |
| False positive handling | Manual, no statistics | Automatic weighting and escalation |
| List updates | Manually daily | Automatically every 2 hours |
| Scalability | Up to 100 transactions/day | Unlimited (horizontal scaling) |
What's Included
- Architectural documentation (diagrams, module description).
- Source code with comments and build instructions.
- Swagger documentation for REST API.
- Operation and deployment manual.
- Team training (2–3 sessions).
- Support for 1 month after delivery.
We are certified developers with experience in blockchain compliance. Over 30 projects, we guarantee compliance with regulatory requirements. Contact us to discuss your task and get a consultation.







