WTWilly Tai
Case studyAgentic Engineering

Risk-tiering you can tune. Names you can't fake past.

Most new customers are routine and should onboard in seconds. A few need a human's eyes. This agent risk-tiers every applicant in deterministic code, catches the PEP and sanctions name variants an exact-string check would wave through, and refers the consequential cases to compliance. A language model never decides approve-or-refer. Here is exactly how the line is drawn, and the dial that moves it.

01 / The pain

Onboarding is either too slow or too reckless, and the dangerous miss is silent.

Reviewing every new customer by hand is slow, inconsistent, and exactly where good applicants drop off. Auto-approving everyone to be fast is reckless: a politically exposed person, a higher-risk jurisdiction, or a thin verification slips straight through, and now it's a MAS finding. The trap that costs the most is not the customer you reject. It is the sanctioned name you didn't recognise because it was spelled differently from your list. That miss is silent. Nothing fires, the account opens, and you learn about it from the regulator.

02 / Why the naive approach fails

"Just check the name against the list" is the false negative that ends careers.

The obvious screen is exact-string equality: applicant.name in pep_list. It demos perfectly and fails in production, because a sanctions or PEP list is a set of spellings, and a real applicant controls how they spell their own name:

So the real job is not a lookup. It is fuzzy name matching against the watchlist with a defined similarity cutoff, fused with a deterministic risk score over PEP status, jurisdiction and verification completeness, and then a tiered decision: auto-approve the clearly routine, refer everything else to a person.

NAIVE name in pep_list ✗ Mohammed / Muhammad✗ reversed name order✗ dropped middle name✗ over-refers common names✗ name ≠ risk score REFINED · fuzzy watchlist match + deterministic risk score Applicant name normalize · token-setedit-distance ≥ 0.85 + risk = PEP · jurisdiction · verificationlow / medium / high auto-approve · refer-to-analyst
fig.1 · why equality fails, and the fuzzy-match-plus-score that replaces it
03 / How it works

Deterministic code decides. The model only drafts the analyst's note.

This is the load-bearing decision of the whole system. Every risk score, every watchlist match, and the approve/refer gate is computed in Python from the applicant's attributes, so it is reproducible, re-runnable, and auditable to the field. The language model is downstream and read-only. It drafts the referral note about a decision the engine already made, and it must cite the exact fields that triggered the referral or it writes nothing. A model never decides whether to approve or refer.

DETERMINISTIC · computed, reproducible MODEL · language only, read-only, cited ApplicantPEP·country·tier Risk scorePython · explicit factors Fuzzy matchwatchlist · ≥ 0.85 Gateapprove/refer Referral noteLLM · cite-or-abstain Analystowns the decision Append-only, hash-chained ledgerevery score, match and decision above is written here · tamper-evident · replayable
fig.2 · where the line sits. Code scores, matches and decides; the model only narrates, cited.

Inside the engine, each applicant is scored from explicit attributes and screened against the watchlist. The parameters are named, not vibes:

Applicant name · pepcountrykyc_tier score pep == truetier → high country in {ID, MY}cross-border → medium kyc_tier == "Basic"thin verification → medium fuzzy(name, watchlist) ≥ 0.85watchlist hit → high Decision low → approvemed/high → referfactors cited low risk + no watchlist hit ⇒ auto-approve · any factor ⇒ refer to a human
fig.3 · the hard part. Three scored factors plus a fuzzy watchlist match, fused into one tier.
04 / One worked example

One the system clears, and one it catches that exact-match would have missed.

Take account W00001, "Daniel Bin Salim": SG, fully Verified, no PEP flag, closest watchlist name only 0.37 similar. Risk is low, so the engine auto-approves it in seconds and nothing reaches a human. Now the one that matters. Account W00005, "Fang Rahman". An exact-string screen finds nothing, because that spelling isn't on the list. But the list carries "Fhang Rahmann", and after normalisation the matcher scores the pair at 0.92, over the 0.85 cutoff, so the engine raises a watchlist hit, tiers the account high, and refers it for enhanced due diligence. Only then does the model draft the note, quoting both spellings and the score.

AccountCheck that firedObservationScore vs cutoffEvidenceResult
W00001Risk tierSG · Verified · no PEP · best name match 0.37lowW00001auto-approved
W00114PEP + cross-borderPEP flag set · country = ID (higher-risk)highW00114 · pep · IDreferred
W00005Fuzzy watchlist match"Fang Rahman" vs list "Fhang Rahmann"; exact match finds nothing0.92 ≥ 0.85W00005 · watchlistcaught; exact-match misses
Catching the near-miss is the whole point. An exact-string screen returns clean on "Fang Rahman" and a listed name onboards, a silent false negative. The fuzzy matcher scores it 0.92 against a 0.85 cutoff and refers it, while the genuinely clean applicant (0.37) still clears in seconds. Getting the variant right, without drowning the analyst in every common surname, is what separates a control from a checkbox.
05 / Numbers & honest trade-offs

What it found, and the dial that decides how much lands on an analyst's desk.

300
onboardings tested
The full population, not a sample.
166
auto-approved
Low-risk, cleared in seconds.
134
referred to a human
Any risk factor → a person decides.
7 + 1
PEPs · watchlist hit
7 PEPs flagged; 1 name caught by fuzzy match.

The 44.7% referral rate is a dial, not a verdict. 134 of 300 referred looks like a swamped team. Read it instead as where the auto-approve line currently sits. Three named parameters move it: the risk-tier threshold (today, any medium factor refers; promote "Basic verification only" into the auto-approve band and the rate drops sharply), the jurisdiction list ({ID, MY}, where narrowing or widening it directly trades cross-border referrals), and the fuzzy cutoff (raise it from 0.85 toward 0.95 and fewer name variants refer). Each is a lever an analyst-capacity model sets, not a fixed property of the system.

Which way to turn it: false negatives vs false positives. Tightening the dials (higher cutoff, shorter jurisdiction list, looser tier) shrinks the queue but raises the false-negative risk: the missed sanctioned name, the dangerous miss. Loosening them refers more and raises false positives, the analyst hours spent clearing the innocent. In regulated onboarding you bias toward recall: a false positive costs a few minutes; a false negative costs a licence. The engine does not pretend a referral equals a problem. It produces a cited candidate list a human triages, and the dial is set by policy, in the open.

Where these come from, honestly. Origin Pay's accounts are synthetic, seeded with PEPs and higher-risk profiles deliberately, so these figures prove the mechanism, the scoring, the fuzzy catch, and the tiered gate. They do not prove that this exact rate survives a specific real book. A deployed version connects to the live onboarding flow and a sanctions/PEP screening feed; the boundary and the dials are what carry over.

06 / Governance, shown

The append-only log is not a claim. Here is a row of it.

Every decision, each auto-approval and each referral, is written to a hash-chained ledger. Each entry's hash covers its contents and the previous hash, so altering any record after the fact breaks the chain at exactly that row. The tier, factors and decision are the gate's structured output. The model never writes them.

AI Control Ledger · entryintegrity verified · replay 300/300
seq        : 114
agent      : KYC Desk
action     : refer_kyc
subject    : W00114
tier       : high         decision : escalate → compliance (EDD)
factors    : [politically exposed person (PEP), cross-border (ID)]
evidence   : [W00114]
prev_hash  : 7a1d…b93f
hash       : c08e…41aa   ← = sha256(prev_hash + canonical(entry))
Threat model: append-only; a flipped decision, tier or factor changes the hash, so the chain breaks at the altered seq. Replay re-derives all 300 decisions from inputs, drift = 0.
07 / Old vs new

The same onboarding, re-engineered.

TRADITIONAL · exact match + manual queue name == list entry manual review queue transliterated name slips through RE-ENGINEERED · score + fuzzy match, all 300 167 auto-approved 133 referred, each cited fuzzy catches the variant · human decides
fig.4 · from exact-match-and-queue to scored, fuzzy-screened, tiered on the whole book

What this says about how I work.

I don't build AI that approves or rejects people. I draw the line first. The risk score, the watchlist match and the approve/refer gate all sit in code, reproducible to the field, and the model stays strictly downstream and cited. Then I confront the hard parts out loud: the transliteration that defeats exact match, the false-negative-versus-false-positive trade the dial controls, and the near-miss the system must get right. Designing where the auto-approve line sits, making it tunable by policy, and proving where it was drawn is what governing AI in financial services actually means.

See it run.

The artifact above is real output from the engine over Origin Pay's wallet accounts: the 167 auto-approved, the 133 referred, the 7 PEPs, each with its factors. The full interactive desk runs as a working application.

See the live governance demo →The full KYC desk runs on my machine. Ask me for a 90-second walkthrough or a screen-share.