Buying a sanctions screening product does not give you a sanctions screening capability. The vendor ships an algorithm; you own the thresholds — and every threshold you set is a compliance decision that a supervisor will eventually ask you to justify.
Most of what is written about sanctions technology treats it as a procurement exercise: pick a screening engine, load the lists, tick the box. That framing hides the actual problem. Screening against OFAC’s lists is two hard technical problems wearing one compliance label — resolving a name to a person or entity, and resolving who that entity is owned by. Get either wrong and you have a control that either lets sanctioned money through or drowns your analysts in noise. Both failures are yours, not the vendor’s.
The list is not a list of names
The first mistake is treating OFAC’s Specially Designated Nationals (SDN) List as a lookup table of exact spellings. It is not. Each entry carries a primary name plus aliases — strong aliases (a.k.a.s the person actually uses) and weak aliases (partial names, low-quality transliterations) that OFAC itself flags as low-confidence. Names arrive transliterated from Arabic, Cyrillic, Farsi and Chinese, so a single individual can appear under half a dozen romanised forms. Alongside the SDN List sits the Consolidated Sanctions List of non-SDN designations. Screening is therefore never a string equality check against one column; it is a similarity problem against a messy, multilingual, alias-laden dataset that changes without a fixed schedule.
Free · 4 minutes
When two of your systems disagree, do you know which one to believe?
Fourteen questions on ownership, lineage, and quality — the difference between a number on a dashboard and a number you could defend. Banded finding on screen, full sheet by email.
That is before you reach the second, harder half of the problem. The list names the people and entities OFAC has designated. It does not name the entities those designated persons own — and under the 50 Percent Rule, those unnamed entities are blocked too.
Fuzzy matching, and why the threshold is the whole game
OFAC’s own Sanctions List Search tool is a useful reference implementation of the name-resolution problem, because OFAC has documented how it works. It runs two algorithms: Jaro-Winkler, a string-edit-distance measure, and Soundex, a phonetic one, and returns the higher of the two scores. It exposes a minimum name score slider from 0 to 100: set it to 100 and you get exact character matches only; set it to 50 and you get every name deemed roughly 50% similar. The lower the threshold, the more candidates surface — and the more of them are noise.
Here is the part that matters, and that OFAC is explicit about: it will not tell you what threshold to use. Its guidance states that no score constitutes a definitive match because every search has its own facts, and that firms must set their own risk-based threshold. Read that again in architecture terms. The single number that determines whether your screening lets a sanctioned party through or buries your team in false positives is a number OFAC deliberately declines to set for you. It is yours to choose, document and defend. A match threshold is not a tuning parameter you optimise for precision. It is a statement of risk appetite, and it should be approved, versioned and evidenced like one. If your threshold lives in a config file that only an engineer can change, and no board or compliance committee has ever seen the number, you have delegated a risk-appetite decision to whoever last edited the YAML. Production-grade programmes treat the whole screening pipeline as governed code, which is the argument I make at greater length in building an OFAC compliance programme as code.
The name-matching engine itself is a well-understood piece of architecture — normalise, tokenise, score against the list, rank candidates — and the same sub-five-second matching discipline shows up in adjacent problems like verification of payee at scale. The engineering is not the hard part. The governance of the threshold is.
The 50% Rule is a graph problem
OFAC’s 50 Percent Rule is where sanctions screening stops being a name-matching problem and becomes a data-lineage one. The rule: any entity owned 50 percent or more, in the aggregate, by one or more blocked persons is itself blocked — whether or not OFAC has ever named it. Two points from the guidance drive the architecture. First, ownership aggregates: if two separately designated persons each own 25 percent of an entity, that entity is blocked, and stakes are aggregated even across different sanctions programmes. Second, the rule speaks only to ownership, not control — an entity merely controlled by a blocked person is not automatically caught, though OFAC may designate it separately.
Ownership is also transitive. If a blocked person owns 60 percent of Entity A, Entity A is blocked; Entity A is now itself a blocked person, so an entity it owns 50 percent or more of is blocked in turn. Resolving this correctly means walking an ownership graph and iterating to a fixed point, not checking a single edge. The SDN List gives you the nodes it knows about; it never gives you the edges. Those you have to source — from a corporate-registry or beneficial-ownership data provider — and their quality, not your algorithm, sets the ceiling on how well the rule can ever be applied. This is a lineage discipline as much as a screening one, and the same ownership-resolution question underlies wallet screening at the transaction boundary.
A reference for both halves
The snippet below shows the two moving parts in one place: a Jaro-Winkler name score with an explicit, governed threshold, and an ownership-graph traversal that aggregates blocked stakes to a fixed point. It has no third-party dependencies so it runs as written, but it is a reference for the shape of the problem, not a production engine.
from collections import defaultdict
# --- Name resolution: Jaro-Winkler similarity (0..100) ---
def jaro_winkler(a: str, b: str) -> float:
a, b = a.upper(), b.upper()
if a == b:
return 100.0
la, lb = len(a), len(b)
if la == 0 or lb == 0:
return 0.0
window = max(la, lb) // 2 - 1
a_match, b_match = [False] * la, [False] * lb
matches = 0
for i in range(la):
lo, hi = max(0, i - window), min(i + window + 1, lb)
for j in range(lo, hi):
if not b_match[j] and a[i] == b[j]:
a_match[i] = b_match[j] = True
matches += 1
break
if matches == 0:
return 0.0
# count transpositions
k = transpositions = 0
for i in range(la):
if a_match[i]:
while not b_match[k]:
k += 1
if a[i] != b[k]:
transpositions += 1
k += 1
m = matches
jaro = (m / la + m / lb + (m - transpositions / 2) / m) / 3
# Winkler prefix bonus, capped at 4 characters
prefix = 0
for i in range(min(4, la, lb)):
if a[i] == b[i]:
prefix += 1
else:
break
return round((jaro + prefix * 0.1 * (1 - jaro)) * 100, 1)
# THRESHOLD is a governed risk-appetite decision, not a code default.
SCREEN_THRESHOLD = 85.0 # approved value; changing it is a change to risk posture
def is_name_hit(candidate: str, listed_names: list[str]) -> bool:
return any(jaro_winkler(candidate, n) >= SCREEN_THRESHOLD for n in listed_names)
# --- 50 Percent Rule: aggregate ownership to a fixed point ---
# edges: entity -> list of (owner, percentage_owned)
def blocked_by_ownership(edges: dict, sdn: set) -> set:
blocked = set(sdn)
changed = True
while changed: # iterate because ownership is transitive
changed = False
for entity, owners in edges.items():
if entity in blocked:
continue
# aggregate stakes held by ANY blocked person, across programmes
held = sum(pct for owner, pct in owners if owner in blocked)
if held >= 50:
blocked.add(entity)
changed = True
return blocked
# Example: two designated persons each hold 25% of ACME -> ACME blocked;
# ACME then holds 60% of SUBCO -> SUBCO blocked in turn.
edges = {
"ACME": [("IVANOV", 25), ("PETROV", 25)],
"SUBCO": [("ACME", 60)],
}
sdn = {"IVANOV", "PETROV"}
print(sorted(blocked_by_ownership(edges, sdn)))
# -> ['ACME', 'IVANOV', 'PETROV', 'SUBCO']
False positives are a design parameter, not an accident
The threshold you set and the false-positive load your analysts carry are the same dial viewed from two ends. Lower the threshold to catch more transliteration variants and weak aliases, and you generate more candidates a human must clear. Raise it to spare the analysts, and you risk missing the near-match that mattered. There is no setting that gives you both, and the failure mode I see most often is a firm that quietly raises its threshold to make an unmanageable queue go away — recreating, in the name of operational relief, exactly the gap the control existed to close. That is a compliance decision made by a queue length, and nobody signed it off.
So the false-positive rate is a governed number too. It sets analyst headcount, it sets how fast you can clear a payment, and it sets your exposure. The architecture that supports it needs a few things a demo never shows: a threshold under version control with an approval trail; a match-decision log that records who cleared what, on what evidence, against which list version; and list-refresh discipline so you are screening against a current dataset rather than yesterday’s — the point I develop in re-architecting sanctions screening for instant payments. Without the audit trail, a defensible threshold and an indefensible one look identical after the fact.
What to actually govern
Strip away the vendor language and a sanctions screening capability comes down to a handful of governed artefacts: the match threshold and its approval history; the source and freshness of your ownership data; the list-version pointer at the moment of each screening decision; and the evidence trail behind every cleared and every escalated hit. The matching algorithm and the graph traversal are engineering, and the engineering is the easy part. The number you point the algorithm at, and whether you can prove why you chose it, is the compliance.
A screening engine with an undocumented threshold is not a control. It is a setting waiting to be blamed on the person who last touched it.
Free interactive tool
Website compliance checklist
What your site has to do, based on what it actually does
Answer as much or as little as you like — the list builds as you go. Nothing is stored against your name and no email is required.
Everything that applies
Ordered by what to do first: legal requirements you can close quickly, then larger pieces of work, then what is expected rather than required. Not exhaustive, and not a legal audit.
Dated PDF, yours to keep or circulate.
Can you trust the architecture you have?
Architecture diagrams rarely show the reality of how systems actually operate. An independent review establishes what is really there.