If you're building a fintech app, a payment platform, or any service that moves money, you're legally required to screen your users against the OFAC SDN (Specially Designated Nationals) list.
Fail to do it, and you're looking at fines that start at $50,000 per violation.
But implementing it yourself is surprisingly tricky. The OFAC list has 18,000+ entries. Names are in multiple languages, transliterations vary wildly, and the same person might appear under a dozen aliases. A simple string match will miss half the results.
In this guide, I'll show you how to implement proper OFAC screening with fuzzy matching in under 15 minutes.
What Is OFAC Screening?
The Office of Foreign Assets Control (OFAC) is a US Treasury department that publishes a list of individuals, companies, and countries subject to economic sanctions.
You need to screen against this list if you:
- Process payments (Stripe, PayPal, crypto)
- Onboard new users (KYC/AML compliance)
- Work with businesses in regulated industries (banking, lending, insurance)
- Handle cross-border transactions
Why Simple String Matching Fails
The SDN list entry for a sanctioned person might look like this:
PUTIN, Vladimir Vladimirovich
a.k.a. "ПУТИН, Владимир Владимирович"
a.k.a. "POUTINE, Vladimir"
If your user signs up as "Vladimir Poutine" or "V. Putin" — a === check returns false. You've missed a match.
Proper OFAC screening requires:
- Fuzzy matching — handle typos, transliterations, name variations
- Alias checking — check all known aliases for each entry
- Configurable threshold — tune sensitivity vs false positives
- Risk scoring — return confidence percentage, not just true/false
Implementation
Option 1: Build It Yourself
You could download the SDN CSV from treasury.gov, parse it, implement Levenshtein distance or Jaro-Winkler scoring, cache it in memory, and rebuild the index daily.
This takes 2-3 days to build correctly and needs ongoing maintenance as the list updates weekly.
Option 2: Use an API
const response = await fetch('https://api.stackapi.dev/api/v1/screening/screen', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Vladimir Putin',
type: 'individual', // or 'company'
matchThreshold: 80 // 50-100, higher = stricter
})
});
const result = await response.json();
Response:
{
"success": true,
"data": {
"entity": "Vladimir Putin",
"isMatch": true,
"riskLevel": "CRITICAL",
"confidence": 97,
"matches": [
{
"matchedName": "PUTIN VLADIMIR VLADIMIROVICH",
"confidence": 97,
"listName": "OFAC SDN",
"program": "UKRAINE-EO13662",
"aliases": ["ПУТИН ВЛАДИМИР ВЛАДИМИРОВИЧ"]
}
],
"screenedAt": "2025-01-15T10:30:00.000Z",
"listsChecked": ["OFAC SDN"]
}
}
Integrating Into Your User Onboarding Flow
Here's a complete KYC middleware for Express:
const { screening } = require('./api-client');
async function kycMiddleware(req, res, next) {
const { fullName, companyName, entityType } = req.body;
const nameToCheck = entityType === 'company' ? companyName : fullName;
try {
const result = await fetch('https://api.stackapi.dev/api/v1/screening/screen', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FINGUARD_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: nameToCheck,
type: entityType,
matchThreshold: 85 // strict for onboarding
})
}).then(r => r.json());
if (result.data.riskLevel === 'CRITICAL' || result.data.riskLevel === 'HIGH') {
// Log for compliance audit trail
await auditLog.record({
action: 'KYC_BLOCKED',
name: nameToCheck,
riskLevel: result.data.riskLevel,
confidence: result.data.confidence,
matches: result.data.matches
});
return res.status(403).json({
error: 'Account creation blocked pending compliance review',
referenceId: auditLog.lastId
});
}
// Attach screening result to request for downstream use
req.kycResult = result.data;
next();
} catch (err) {
// Fail closed — never allow if screening fails
console.error('Screening service error:', err);
return res.status(503).json({ error: 'Service temporarily unavailable' });
}
}
// Apply to your registration route
app.post('/api/register', kycMiddleware, registerHandler);
Important: Always fail closed. If the screening service is unavailable, block the operation — never default to allowing it.
Batch Screening for Existing User Bases
Need to screen your entire existing user base? Use the batch endpoint:
async function screenAllUsers(users) {
const BATCH_SIZE = 100;
const results = [];
for (let i = 0; i < users.length; i += BATCH_SIZE) {
const batch = users.slice(i, i + BATCH_SIZE);
const response = await fetch('https://api.stackapi.dev/api/v1/screening/batch-screen', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FINGUARD_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
entities: batch.map(user => ({
name: user.fullName,
type: 'individual',
matchThreshold: 80
}))
})
}).then(r => r.json());
results.push(...response.data.results);
console.log(`Screened ${Math.min(i + BATCH_SIZE, users.length)} / ${users.length}`);
// Respect rate limits
await new Promise(r => setTimeout(r, 1000));
}
return results.filter(r => r.isMatch);
}
// Usage
const flaggedUsers = await screenAllUsers(allUsers);
console.log(`${flaggedUsers.length} users flagged for review`);
Understanding Risk Levels
| Risk Level | Confidence | Recommended Action |
|---|---|---|
LOW |
No match found | Allow |
MEDIUM |
70-84% match | Manual review |
HIGH |
85-94% match | Escalate to compliance |
CRITICAL |
95-100% match | Block immediately |
For most production use cases, HIGH and CRITICAL should block immediately. MEDIUM can go to a manual review queue.
Tuning the Match Threshold
The matchThreshold parameter controls sensitivity:
// Strict — fewer false positives, may miss some matches
{ matchThreshold: 90 }
// Recommended for production KYC
{ matchThreshold: 80 }
// Broad — catches more variations, more false positives
{ matchThreshold: 70 }
For names from regions with many transliteration variants (Arabic, Cyrillic, Chinese), 75-80 is a good starting point.
Adding PEP Screening
OFAC is just one list. Many compliance frameworks also require screening against PEP (Politically Exposed Persons) databases:
// Screen against OFAC SDN
const ofacResult = await screenEntity(name, 'individual');
// Screen against PEP list (heads of state, senior officials, etc.)
const pepResult = await fetch('https://api.stackapi.dev/api/v1/compliance/pep', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.FINGUARD_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ name, matchThreshold: 75 })
}).then(r => r.json());
const overallRisk = [ofacResult.data.riskLevel, pepResult.data.riskLevel]
.sort((a, b) => riskWeight(b) - riskWeight(a))[0];
Getting Started (Free Tier)
- Subscribe at RapidAPI — FinGuard API
- The free tier includes 200 screening lookups/month
- Batch screening requires the ULTRA plan
Compliance Disclaimer
This article is for educational purposes. OFAC screening requirements vary by jurisdiction and business type. Consult a compliance professional for your specific situation.
Building something with OFAC screening? Drop a comment — I'd love to hear your use case.
Top comments (0)