A money-laundering detection rule that rewrites itself, and two language models that argue about what it finds — built in eight hours and running entirely on one machine, with nothing leaving it.
A panel split, mid-review — the intent lens (Qwen3.6-35B) reads the account as suspicious; the literal lens (Nemotron-3-Nano) reads the same evidence as benign. The models disagree, so the case escalates to a human for review.
01 — Overview
Banks screen transactions with rules: flag anyone depositing more than $10,000. Criminals know the rules and deposit $9,000. Meanwhile the alerts those rules do produce are overwhelmingly false, and analysts spend most of their week closing them.
The AML Review Console attacks both halves. A detection rule improves itself against an adversary that actively tries to evade it, and every alert it produces is reviewed by two language models reading the same evidence in two different ways. It was built in eight hours at the Dell × NVIDIA hackathon on a Dell Pro Max with GB10 — every model call local, no API, no data leaving the machine.
02 — The problem
Transaction monitoring fails in two directions at once. It misses the accounts that structure around it, and it buries the ones it does catch in false alarms.
Base rates dominate. At a 0.5–1% rate of genuine suspicion, even a 91%-accurate rule produces roughly twenty false alarms for every real case. Moving the threshold does not change this.
Thresholds are public. A literal $10,000 rule catches three of twelve structuring cases in the test corpus and none of the layering ones. It finds only the typologies that never structure in the first place.
The benchmark has to be honest. The comparator here is a four-scenario set modelled on what banks actually run — 96.4% false positives, reproduced from a 1% base rate in the corpus.
03 — What I built
I led the build and owned the architecture, the rule engine, the optimisation loop, and the evaluation harness.
A deterministic detection layer. The rule is JSON predicates over account facts, evaluated in Python with no model call.
A self-improving loop. A model proposes rewrites of the rule; an adversarial generator plants transaction sequences designed to evade it. Fifteen iterations, fifty-nine candidate rules, none invalid.
A two-model review panel. Every alert goes to an intent lens asking whether there was a legitimate purpose, and anything it flags goes to a literal lens asking whether the rule's evidence actually holds.
An evaluation harness with published provenance. Every figure in the repo traces to a run recorded in RESULTS.md.
04 — Architecture
Keeping the detection layer deterministic buys three things. Scoring becomes free. The adversary can verify its own evasions without a second model call. And escalation becomes a comparison between two verdicts, evaluated in code.
01 · DetectL0 — The Rule
JSON predicates over factsevaluated in Pythonno model call
↓alerts
02 · ImproveRewrite ↔ Adversary
model proposes rulesred plants evasionsevasion checked instantly
both agree → draft the filingthey disagree → escalate
No model is ever asked whether to escalate. Escalation is intent_verdict != literal_verdict, evaluated in code, so the escalation rate stays measurable across runs.
05 — Results
Scoring costs nothing once the rule is deterministic — 120,000 windows evaluate in under six seconds across twenty cores — so the corpus could be large enough for the numbers to mean something, and the optimisation loop needed roughly 400 model calls instead of 3,000.
The loop worked. Across fifteen iterations, recall went from 0.224 to 0.657 while precision went from 0.027 to 0.055 — recall roughly tripled while precision roughly doubled.
The panel sharpened it. Where both lenses agree, precision rises from 0.060 to 0.338 — about 5.6× — while retaining 95.2% of the genuine cases the rule had found. Thirty-seven percent of reviewed alerts escalate to a human.
And it transferred. The rule was tuned on 14,400 windows, then run against a completely fresh corpus — new seeds, new accounts, 1.44 million transactions it had never seen. Precision held at 0.059 against 0.060; recall at 0.647 against 0.667.
That corpus is a new generation from the same process, so the result tests whether a self-rewriting rule has learned the problem or its own training data.
The live view during the demo — windows scored as they arrive, with the false-positive rate for the rule alone shown against the rate after both lenses agree. The gap between those two numbers is why the rule is treated as a gate and the panel as the detector.
06 — Challenges
Building a corpus honest enough to measure against. A synthetic dataset with a convenient base rate produces flattering numbers. The corpus had to reproduce the real 1% rate and the real typologies before any result from it counted.
Resisting the metric that looks best. F1 on this data is dominated by the base rate and reads as a bad number to anyone unfamiliar with it. Precision and recall are reported separately throughout.
An eight-hour clock on borrowed hardware. Everything ran locally on a machine we had for the day. Deterministic scoring bought back most of the clock.
07 — Lessons & what's next
The deterministic layer made the model layer affordable. Free scoring, a self-checking adversary, and a measurable escalation rate all follow from it.
A split between two lenses reading the same evidence carries more information about a case than either model's confidence score.
The base-rate problem surfaced because every figure had to trace back to a run.
Next: a larger typology set, a held-out adversary the rule never trains against, and a case-disposition audit trail that would hold up in a regulatory review.