OFAC Sanctions Screening on Unmatched Payments
An unmatched payment is not just a reconciliation break — it is an item that has slipped past your straight-through path and now needs a human decision, and one of the first questions a compliance-aware pipeline must ask before it routes that item anywhere is whether any party to the transaction appears on a US sanctions list. This reference sits within the broader exception routing and fraud-pattern detection framework, and it treats sanctions screening as a mandatory gate that runs alongside — never after — the fraud-pattern detection signals that flag duplicates, velocity, and structuring. A break that turns out to involve a blocked person is not an operational nuisance; it is a strict-liability compliance event with a statutory reporting clock attached.
The Office of Foreign Assets Control (OFAC) administers US economic sanctions, and its rules apply to every US person and every transaction that touches the US financial system. Screening is a name-matching problem at its core, which is why the string-similarity techniques from the matching engine reappear here: the same deterministic-and-fuzzy matching logic that pairs a truncated remittance reference against a ledger entry is what compares a beneficiary name against 17,000-plus SDN entries and their aliases. The difference is the cost function. In reconciliation a false positive wastes four analyst minutes; in sanctions screening a false negative — processing a payment to a blocked party — is a civil or criminal violation, so the whole discipline is tuned for recall first and precision second. This guide covers when screening runs, which lists to load, how the 50% Rule extends them, how to dispose of a hit, and what you owe OFAC when a hit is real.
When Screening Runs and on Which Parties
Not every payment is screened the same way, and getting the trigger conditions right is the difference between a defensible program and one that either misses obligations or drowns operations in noise. For wires — Fedwire, CHIPS, and SWIFT MT103/MT202 — screening is interdiction-based and runs in real time before the message is released, because once value leaves the institution it cannot be blocked. Every party in the message is a screening target: the originator, the beneficiary, the originating (ordering) bank, the beneficiary bank, and any intermediary or correspondent banks named in fields 52–58 of an MT message or the equivalent pacs.008 agent elements. A payment can be clean on the beneficiary and still require blocking because an intermediary bank is a blocked entity.
For ACH, the picture is rail-specific. NACHA does not require an institution to screen every domestic entry line-by-line, but International ACH Transactions (IAT, identified by the IAT Standard Entry Class code) are different: they carry mandatory addenda records precisely so that OFAC-relevant party data travels with the entry, and the Gateway Operator, ODFI, and RDFI each have OFAC screening responsibilities on IAT traffic. An IAT entry has seven mandatory addenda records (types 10 through 16 in the addenda type code) carrying the name and physical address of the originator and receiver plus the originating and receiving DFI identity — those are your screening inputs. When an unmatched ACH break turns out to be an IAT entry whose party data never got screened because the addenda were dropped during ingestion, that is a control gap, not a reconciliation curiosity.
The practical rule for a reconciliation pipeline is that any unmatched or manually-touched payment must carry a screening result before it can be routed, released, or posted. If the real-time gate already cleared it, the reconciliation layer records and trusts that result against a list version; if the item reaches operations without a screening decision — a re-keyed wire repair, a manually-entered correction, a returned item being re-presented — it must be screened again before any human action, because manual handling is exactly where sanctioned parties get reintroduced.
The SDN List, the Consolidated List, and the 50% Rule
The primary list is the Specially Designated Nationals and Blocked Persons List (SDN) — the roster of individuals, entities, vessels, and aircraft whose property must be blocked. It carries not just primary names but a large set of aliases (aka), weak aliases, and low-quality transliterations, plus identifying data (dates of birth, passport numbers, addresses) that a mature program uses as secondary confirmation to knock down false positives. OFAC distributes it as fixed-format flat files (SDN.CSV, ADD.CSV, ALT.CSV) and as structured XML; the enhanced XML additionally carries program tags and richer feature data. You screen against the primary name and every alias, because a payment naming an alias is still a payment to the blocked person.
Beyond the SDN list, OFAC publishes the Consolidated Sanctions List, which aggregates the non-SDN lists — the Sectoral Sanctions Identifications (SSI) List, the Foreign Sanctions Evaders (FSE) List, the Non-SDN Palestinian Legislative Council (NS-PLC) List, and others. These carry narrower prohibitions than full blocking (an SSI entry may bar new debt of a certain tenor rather than freezing all property), so a match here does not automatically mean "block" — it means "apply the specific program restriction," which is why disposition logic must know which list produced the hit. A screening engine that collapses all lists into one boolean loses the information needed to decide between blocking, rejecting, and a partial restriction.
The trap that catches institutions is the OFAC 50 Percent Rule: any entity owned 50% or more, in the aggregate, directly or indirectly, by one or more blocked persons is itself blocked — even though its name never appears on the SDN list. A beneficiary company that screens clean by name can still be a blocked entity because two SDN individuals each own 30% of it. No name-matching engine can catch this from the payment alone; it requires ownership data (from a commercial beneficial-ownership provider or KYC records) layered onto the screening result. The engineering consequence is that a clean name-screen is necessary but not sufficient, and any ownership-based block must be recorded with the same rigor as a direct SDN hit.
Real-Time vs Batch Screening
Real-time (interdiction) screening sits inline in the payment flow and holds the message until a decision is made; it is mandatory for wires, where irrevocability means there is no second chance after release. Batch screening runs a file of accumulated items against the current list version — appropriate for ACH batches, customer-list rescreening after a list update, and re-screening open exceptions. Both modes call the same scoring core; they differ only in latency budget and in what they block. A real-time gate must return in low milliseconds so it cannot afford an exhaustive fuzzy sweep against every alias, so production designs put a fast blocking/indexing step first (exact and near-exact on a normalized key) and reserve full fuzzy scoring for the narrowed candidate set — the same candidate-reduction discipline the matching engine uses to avoid quadratic blowup.
A subtlety that reconciliation teams miss: the SDN list changes without notice, sometimes multiple times a week. An item that cleared on Monday's list is not clean on Wednesday's if a party was added Tuesday. That is why any long-lived exception — a break sitting in a queue for days awaiting research — must be re-screened against the current list before it is finally released, not just screened once on arrival. List staleness is a first-class failure mode, handled below.
Phase 1 — Extracting the Parties to Screen
Screening starts from the canonical transaction the ingestion layer already produced. The job here is to pull every screenable party into a uniform structure, tagging each with its role, because disposition later needs to know whether the hit was on a beneficiary (severe) or an intermediary bank (still blockable, different workflow). Amounts stay as integer cents throughout — sanctions screening never does arithmetic on amounts, but the blocked-property report needs the exact value, so it must survive untouched.
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
class PartyRole(str, Enum):
ORIGINATOR = "originator"
BENEFICIARY = "beneficiary"
ORIGINATING_BANK = "originating_bank"
BENEFICIARY_BANK = "beneficiary_bank"
INTERMEDIARY_BANK = "intermediary_bank"
@dataclass(frozen=True, slots=True)
class ScreenableParty:
role: PartyRole
raw_name: str
country: str | None = None
identifiers: dict[str, str] = field(default_factory=dict) # passport, DOB, BIC
def extract_parties(txn: "CanonicalTransaction") -> list[ScreenableParty]:
"""Pull every OFAC-screenable party out of a canonical ACH IAT or wire txn.
Wires expose all agents inline; ACH IAT carries originator/receiver identity
in mandatory addenda records 10-16, which must have survived ingestion.
"""
parties: list[ScreenableParty] = []
parties.append(ScreenableParty(PartyRole.ORIGINATOR, txn.originator_name,
txn.originator_country))
parties.append(ScreenableParty(PartyRole.BENEFICIARY, txn.beneficiary_name,
txn.beneficiary_country))
if txn.originating_bank_name:
parties.append(ScreenableParty(PartyRole.ORIGINATING_BANK,
txn.originating_bank_name))
if txn.beneficiary_bank_name:
parties.append(ScreenableParty(PartyRole.BENEFICIARY_BANK,
txn.beneficiary_bank_name))
for name in txn.intermediary_bank_names:
parties.append(ScreenableParty(PartyRole.INTERMEDIARY_BANK, name))
return [p for p in parties if p.raw_name and p.raw_name.strip()]
The guard on the final line matters: an IAT entry that arrives with an empty beneficiary name is not "clean," it is unscreenable, and it must be treated as an exception requiring the missing data — never silently passed as a non-hit. Missing party data is the most dangerous false negative because it looks exactly like a clear result.
Phase 2 — Scoring Parties Against the List
The screener normalizes each party name, retrieves candidate list entries through a blocking key, and scores each candidate. The scoring itself — normalization and similarity — is detailed in the dedicated fuzzy name matching against the OFAC SDN list guide; here the screener orchestrates it and returns structured candidate hits, each carrying its score, the matched list entry, and the list that produced it, so nothing about a potential match is thrown away before a human sees it.
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class SanctionsListEntry:
entry_id: str # e.g. SDN uid
list_name: str # "SDN", "SSI", "FSE", ...
primary_name: str
aliases: tuple[str, ...]
program: str # sanctions program tag
@dataclass(frozen=True, slots=True)
class CandidateHit:
party: ScreenableParty
entry: SanctionsListEntry
score: float # 0.0-1.0 normalized similarity
matched_name: str # the alias/primary that scored highest
def screen_party(
party: ScreenableParty,
index: "SanctionsIndex",
threshold: float = 0.85,
) -> list[CandidateHit]:
"""Return every list entry that scores at or above the recall-first threshold.
Recall-first: a low threshold surfaces more candidates for human review.
A missed true match is a violation; an extra false positive is analyst time.
"""
norm = index.normalize(party.raw_name)
hits: list[CandidateHit] = []
for entry in index.candidates_for(norm): # blocking step, not full scan
best_score, best_name = 0.0, ""
for name in (entry.primary_name, *entry.aliases):
s = index.similarity(norm, index.normalize(name))
if s > best_score:
best_score, best_name = s, name
if best_score >= threshold:
hits.append(CandidateHit(party, entry, best_score, best_name))
return sorted(hits, key=lambda h: h.score, reverse=True)
def screen_transaction(
txn: "CanonicalTransaction", index: "SanctionsIndex"
) -> list[CandidateHit]:
"""Screen every extracted party; any returned hit blocks auto-processing."""
all_hits: list[CandidateHit] = []
for party in extract_parties(txn):
all_hits.extend(screen_party(party, index))
return all_hits
An empty return from screen_transaction is the only condition that permits an item to continue toward routing. Any non-empty list means the payment stops and enters disposition — it is never a judgment the automated layer is allowed to make on its own for a real potential match.
Phase 3 — Hit Disposition: Hold, Investigate, Block or Reject
A candidate hit moves through a small, strictly-ordered state machine. On a potential match the item is placed on hold — for a wire, the message is not released; for a posted item, it is quarantined pending review. It then enters investigate, where an analyst compares the payment's identifying data against the SDN entry's secondary identifiers: does the date of birth match, the country, the passport, the address? Many hits die here as clear false positives — a common name colliding with an SDN alias — and the item is released with the false-positive rationale logged.
If investigation confirms a true match, the disposition splits on a single legal question: is there a blockable interest? If the sanctions program requires the property to be frozen, the funds are blocked — moved into a segregated, interest-bearing blocked account, where they stay until OFAC issues a license or the designation is removed. You do not return blocked funds to the sender. If instead the transaction is merely prohibited but there is no blockable property interest (for example, a payment involving a comprehensively-sanctioned jurisdiction where the correct action under the program is to refuse it), the transaction is rejected — not processed, and returned. Choosing block when you should reject, or reject when you should block, is itself a reportable error, so the disposition code must record which program drove the decision and why.
The one thing an engineer must not build is a path that lets an item leave hold without an attributable analyst decision. Every transition — hold, release, block, reject — is written to the append-only audit log with the actor, the timestamp, the list version screened against, and the scores that triggered the hold, so that months later an examiner can reconstruct exactly why a payment to a flagged name was released or why an account was frozen.
Compliance & Auditability
Sanctions screening is governed by OFAC's regulations at 31 CFR Chapter V, with the reporting and recordkeeping mechanics in 31 CFR Part 501 (the Reporting, Procedures and Penalties Regulations). Three obligations drive the system design:
- Blocked and rejected transaction reports — 10 business days. Under 31 CFR 501.603, any blocking of property or rejection of a prohibited transaction must be reported to OFAC within 10 business days of the action. The report identifies the parties, the property, and the amount — which is why the exact amount in cents and the full party data must survive from screening into the report. Blocking and rejecting are distinct reportable actions with distinct report contents.
- Annual report of blocked property. Holders of blocked property must file an annual report (31 CFR 501.603) covering property blocked as of June 30, due by September 30 — so blocked items are not "closed" once reported; they persist as a reportable holding until unblocked.
- Recordkeeping — 5 years. Under 31 CFR 501.601, records of blocked and rejected transactions, and of the screening that produced them, must be retained for at least five years. That five-year retention is the floor for the append-only screening audit trail: list version, parties screened, scores, analyst rationale, disposition, and report identifiers.
Liability under the sanctions programs is effectively strict — a violation does not require intent, which is precisely why the pipeline is engineered for recall over precision and why "we didn't know" is not a defense. The compliance posture is not "prove we caught everything" but "prove that every item was screened against a current list, that every hit was dispositioned by a named actor, and that every block and reject was reported on time," and the audit trail exists to make exactly those three things provable.
Testing & Verification
Screening logic is tested against known-answer fixtures built from public SDN aliases and deliberately-adversarial false positives. The non-negotiable test is that a true match is never silently cleared and that an unscreenable (empty-name) party never returns a clean result.
import pytest
def _party(role, name, country=None):
return ScreenableParty(role=role, raw_name=name, country=country)
def test_exact_sdn_alias_produces_hit(sdn_index):
party = _party(PartyRole.BENEFICIARY, "IBRAHIM AL-BADRI") # a listed alias
hits = screen_party(party, sdn_index)
assert hits, "a known SDN alias must produce at least one candidate hit"
assert hits[0].score >= 0.85
def test_common_name_below_threshold_is_recorded_not_dropped(sdn_index):
# A common name may collide weakly; it should surface for review, not auto-clear.
party = _party(PartyRole.ORIGINATOR, "JOHN SMITH")
hits = screen_party(party, sdn_index, threshold=0.85)
# Whatever the outcome, the decision is explicit, never a silent pass.
assert isinstance(hits, list)
def test_empty_beneficiary_is_never_treated_as_clean():
txn = make_iat_txn(beneficiary_name="") # addenda dropped in ingestion
parties = extract_parties(txn)
assert all(p.role != PartyRole.BENEFICIARY for p in parties)
# The caller must raise an unscreenable-party exception, not post the item.
def test_intermediary_bank_hit_blocks_even_with_clean_beneficiary(sdn_index):
txn = make_wire_txn(
beneficiary_name="ACME TRADING LLC",
intermediary_bank_names=["BANK MELLI IRAN"], # listed
)
hits = screen_transaction(txn, sdn_index)
assert any(h.party.role == PartyRole.INTERMEDIARY_BANK for h in hits)
The property worth asserting across the suite is monotonic: lowering the threshold must never remove a hit, because the whole program leans on recall. A regression that drops a candidate when the threshold falls is a screening failure even if every named test still passes.
Failure Modes
| Failure scenario | Root cause | Mitigation |
|---|---|---|
| True match missed on a foreign name | Transliteration variant (Arabic/Cyrillic → Latin) scores below threshold against the listed spelling | Normalize with a transliteration table, score against all aliases, keep the threshold recall-first, and screen identifiers (DOB, passport) as secondary signals |
| Operations flooded with false positives | Common name (e.g. "MOHAMMED", "LI WEI") collides with many SDN aliases | Use secondary identifiers to auto-downgrade, maintain a reviewed good-guy/whitelist with audit, tune per-list thresholds — never raise the global threshold enough to risk a miss |
| Clean item on Monday is a hit on Wednesday | List staleness — SDN list updated after the item was first screened | Version every screen against a list hash; re-screen any long-lived exception against the current list before release; automate daily list refresh with a change alert |
| IAT entry passes with no hit but was never screened | Missing addenda — originator/beneficiary party data dropped during ingestion | Treat empty screenable names as unscreenable exceptions, not non-matches; validate IAT addenda 10-16 presence at the ingestion boundary |
| Name-clean beneficiary is actually blocked | OFAC 50% Rule — entity owned ≥50% by blocked persons, not itself listed | Layer beneficial-ownership data onto the screen; block on aggregate ownership and record the ownership basis with the same rigor as a direct SDN hit |
| Block/reject reported late | No timer on the disposition; report treated as back-office paperwork | Start a 10-business-day clock the moment a block or reject is recorded; alert before breach; tie the report id back into the audit record |
Frequently Asked Questions
Does OFAC require screening every domestic ACH transaction?
Not line-by-line the way wires are interdicted. NACHA rules place explicit OFAC screening responsibilities on the parties to International ACH Transactions (IAT), which is why IAT entries carry mandatory party addenda. For domestic ACH, institutions rely on a risk-based program — screening customers at onboarding and on list updates, and screening entries where the risk warrants — rather than interdicting every PPD or CCD entry. Any unmatched or manually-handled item, though, should carry a fresh screening result before it is routed or posted, because manual handling is where sanctioned parties re-enter the flow.
What is the difference between blocking and rejecting a payment?
Blocking means freezing the property: the funds are moved into a segregated interest-bearing blocked account and held — you do not return them — until OFAC authorizes their release. Rejecting means refusing to process a prohibited transaction where there is no blockable property interest, and returning it. Which action is correct depends on the specific sanctions program and whether a blockable interest exists. Both are reportable to OFAC within 10 business days, but they are distinct actions with distinct reports, so the disposition must record which one was taken and the program that required it.
How does the OFAC 50% Rule change screening?
It means a name-clean screen is necessary but not sufficient. An entity that is owned 50% or more, in aggregate, by one or more blocked persons is itself blocked even though it never appears on the SDN list. No name-matching engine can detect this from the payment alone — it requires beneficial-ownership data layered onto the result. Programs that handle entity payments integrate an ownership data source and block on aggregate ownership, recording the ownership basis alongside the screening decision.
Why tune sanctions screening for recall instead of precision?
Because the cost function is asymmetric and liability is effectively strict. A false negative — processing a payment to a blocked party — is a civil or criminal violation regardless of intent. A false positive is analyst time. So thresholds are set low enough to surface borderline candidates for human review, and precision is recovered through secondary-identifier confirmation and a reviewed whitelist rather than by raising the threshold to a level that could let a true match slip through.
How long must screening records be kept?
At least five years. Under 31 CFR 501.601, records relating to blocked and rejected transactions must be retained for five years, and that retention floor governs the whole screening audit trail — the list version screened against, the parties, the scores, the analyst rationale, the disposition, and any block or reject report identifiers. Blocked property additionally generates an annual report until it is unblocked, so blocked items remain live records well beyond the initial 10-business-day report.
Related guides in this collection
- Fuzzy Name Matching Against the OFAC SDN List — the normalization and Jaro-Winkler/token-set scoring core that produces candidate hits.
- Fraud-Pattern Detection Signals in Payment Reconciliation — the sibling signal layer (duplicates, velocity, structuring) that runs alongside sanctions screening on the same breaks.
- Routing Reconciliation Exceptions into Prioritized Work Queues — where a held or investigated sanctions hit lands for analyst action.
- Modeling the Exception Lifecycle as a Finite-State Machine — the state-machine discipline that the hold-investigate-dispose flow shares.
- Exception Routing & Fraud-Pattern Detection — the parent reference for what happens to a break after the matcher gives up.
- Deterministic vs Fuzzy Matching Logic — the name-matching techniques reused here, tuned in reconciliation for precision rather than recall.