Fortified Enterprise Fleet/All Things Agentic /3 agents/gemini-3.5-flash-lite /Cloud Firestore
Treasury changes the sanctions list. The money stops before anyone opens a laptop.
Every US person is strictly liable for a payment to an OFAC-designated party — including a 12-person humanitarian NGO with no compliance department. When Treasury changes the SDN list, the entire counterparty book has to be re-screened before the next disbursement run, a true hit blocked and reported inside 10 business days, and blocked funds released the moment a delisting lands.
Firco and its peers sell that to banks at $30k+/year. They do not sell to this operator at all, so it gets done by hand, late, or not at all. Interdict is the compliance department she does not have — a three-agent fleet that re-screens the book, stops the money, writes the rationale and drafts the statutory report, with nobody clicking anything.
Every decision it makes is graded by something outside this repository, and every figure on this page is printed by a script in it. There is no hosted product to try: the fleet, Postgres and the oracle run on your machine — which is why the call to action is a command, not a signup.
It out-screens the independent oracle on names that are not on the list.
n = 400 · make challenge-set
Why not the number that looks better
Screening the seeded book verbatim scores 1.000, and that number is close to worthless: those names were copied out of the very publication being searched, so finding them is a string-equality test wearing a costume. We do not report it.
What is actually being screened
Deterministic perturbations instead — transliteration families taken from OFAC’s own alias lists, token reordering, dropped particles, transcription confusables, dropped middle names. Each variant is derived from the SHA-256 of the name, so the challenge set is byte-identical on your machine.
Full per-row result set — every uid, perturbation, our score and yente’s
top hit — in data/g1-perturbed.json. Part of the gap is the oracle missing rather than
us winning, and the file lets you tell which is which.
One flow — nothing else
OFAC publishes. Interdict acts. Nobody clicks.
One narrow flow, end to end, with money consequences in both directions.
- Delta landsA 6-hourly timer polls the feed; a content hash we have not seen opens a run on its own, under a lock.
- Full-book re-screenEvery counterparty, checkpointed per batch and crash-safe — a silently skipped row is a compliance breach.
- True hits HELDMoney stops. An idempotent hold lands on the counterparty and on every queued disbursement to it.
- Lookalikes CLEAREDOn the record’s own evidence — weak-alias downweighting per OFAC’s flag, a contradicting date of birth taken from the feed’s own fields.
- Funds RELEASEDA delisting retires the hold and the money moves again, with no human in the path.
- Report draftedThe blocking report, against the real 10-business-day statutory clock, filed to an append-only ledger.
Transmission of that report to OFAC stays human. Interdict drafts it and files it to the ledger; it does not submit to the government, and it says so here, in the README and on screen.
Autonomy you can audit
Four decisions, no human click, each graded by an outsider.
An agent that moves money has to be checkable by someone other than the person who built it. So every one of the four acts without a click — and every one is scored against a record this project does not control.
-
HOLDan adjudicated true hit
Money stops. An idempotent hold lands on the counterparty and on every queued disbursement to it, in the same transaction that writes the decision.
Graded by the SDN record itself on treasury.gov — name, date of birth, passport.
make challenge NAME="…"reproduces the whole decision for a name you pick, not one we chose. -
CLEARa lookalike, auto-cleared
The disbursement proceeds with the reason on record — the signal breakdown that ended it, or the model’s written rationale when the band was close enough to spend one. In the graded run every clear came from the deterministic plane: a contradicting date of birth ends the question before adjudication is paid for.
Graded by OpenSanctions yente, scope-pinned to
us_ofac_sdnand consulted on every adjudication — an oracle consulted only where it agrees is not an oracle. -
RELEASEa delisting detected
The hold is retired and blocked funds move again. Today this leg runs as a labelled replay of Treasury’s real 08/07/2026 removals — the eight delisted parties are already gone from the current publication, so a live release cannot be staged without pretending. It says REPLAY on every screen.
Graded by Treasury’s own published delta, /changes/latest, archived at sha256
9403f40d9496…. The path to a live one is the sentinel book. -
REPORTthe statutory artifact
The blocking report is drafted against the statutory clock and filed to the hash-chained ledger, with the deadline tracked as a date the operator can be held to.
Graded by the federal calendar — 10 business days under 5 U.S.C. 6103 — and by the public SDN record every report field is copied from.
Architecture
Three agents, two planes, and one guard that decides what executes.
The deterministic matching plane is the oracle for the model plane, and the guard sits on the return path — so a verdict is checked before it is allowed to move money. Deterministic code decides what executes; the model explains, corroborates and signs.
They run in a single process. The separation is enforced by the
Adjudicator protocol and the oracle guard, not by a network hop. Every model call is
confined to interdict/adjudicator.py, which is what makes the guard in
interdict/orchestrator.py a real check rather than a formality. Calling this a fleet
of microservices would be a nicer diagram and a false one.
orchestrator thin · sole writer of decisions oracle guard
Routes matcher → adjudicator and applies the guard on the return of every hop. Exposes
screen_counterparty as the one entry point, and owns quarantine — the terminal
state where a human compliance officer is told the system could not safely decide.
- Oracle disagreement. A CLEAR on a deterministic score at or above
0.93, or a HOLD below the0.62no-hit floor, is refused — freezing money on evidence the screening plane cannot see is not allowed to execute. - Citation check. A
matched_identifierthat does not appear verbatim in the record is refused. A fabricated alias transcribed into a federal blocking report is the worst output this system could produce. - Loop cap. On refusal it asks once more with the disagreement stated, then stops. Hard cap of 2 round trips, enforced in code and again as a database constraint — an unbounded reconsider loop is the classic multi-agent failure.
- Fail-closed. While quarantined the disbursement stays held. A confused agent can never move money in either direction.
matcher deterministic · no LLM
Normalisation, Jaro-Winkler and token scoring over 43,775 indexed names, weak-alias downweighting, date-of-birth interval logic, checkpointed full-book re-screen. Always right about arithmetic; pure functions, testable to exhaustion.
adjudicator gemini-3.5-flash-lite · structured output
Writes the verdict, confidence and cited fields as strict structured output through the Google GenAI SDK at temperature 0. In the graded run all 59 adjudications came back HOLD, so it is exercised on confirming holds with a citation rather than on discrimination. We report that rather than hide it.
the money plane Postgres 16
Where correctness actually lives. Every decision lands in a constraint rather than in application code: idempotent holds
under UNIQUE … NULLS NOT DISTINCT, an illegal-transition trigger so
screened money cannot skip states, append-only triggers that reject UPDATE, DELETE and
TRUNCATE on the ledger, and a hash chain whose seq is assigned inside the same
advisory lock that builds it — so sequence order is chain order and the audit trail
cannot fork. The constraints are the product.
This is the structural answer to the track’s failure-tolerance question — how does the system recover when a worker agent loops or returns a hallucination? — given by the router, not by a paragraph in a README. It deliberately does not block a CLEAR backed by a contradicting date of birth or an entity-type mismatch. That case is exactly what the model is for.
↔ scroll the diagram
Measured results
Graded against ground truth the screening path cannot see.
The counterparty book carries the verdict a correct system must reach. The screening path never reads that column — the orchestrator does not know it exists.
| Population | What it is | Expected | Correct | Reached the model |
|---|---|---|---|---|
| sentinel (30) | on the list, exact names | HOLD | 30 / 30 | 30 |
| variant (30) | same person, different transliteration | HOLD | 29 / 30 | 29 |
| lookalike (25) | different person, shared surname, contradicting DOB | CLEAR | 25 / 25 | 0 |
| ordinary (16) | unrelated grantees | CLEAR | 16 / 16 | 0 |
The right-hand column is the honest part. Not one CLEAR-expected counterparty reached the model: a contradicting date of birth cuts a lookalike below the adjudication threshold before a model call is ever spent on it. That is the design working — and it means this table grades the matcher on clears and the model on holds. An earlier revision reported “lookalike CLEAR 60/60” as decision quality when the adjudicator had played no part in it. The model’s demonstrated contribution is confirming 59 holds with a citation and a signable rationale, and disagreeing with none of them.
Missed hits — a payment that would have gone to a designated party. One transliteration,
AZIZ ATRIQ, scored 0.6594 by the screening plane: below the adjudication bar, so
the model never saw it.
Frozen grantees — aid stopped in error. Reported separately from missed hits because the two failures are not equivalent, and a single blended accuracy number would hide which one happened.
data/adjudication-quality.jsonAdjudications where we cleared someone yente flagged, out of 59. Under strict liability the dangerous direction is being more permissive than the oracle, and we never are. 58 agreed; in the remaining 1 we were the stricter party.
scripts/adjudication_quality.pyPer counterparty against all 19,199 records; p95 68.8 ms, p99 106.7 ms. A 400-counterparty pass finishes in 7.6 s — and OFAC publishes roughly weekly. This is the deterministic plane only; adjudication is excluded because it is network-bound on Gemini.
make bench · data/bench.jsonCI runs a real Postgres and fails if the database tests silently skip. A green badge over skipped ledger invariants would read as proof of something that was never checked.
.github/workflows/ci.ymlCorrectness failures the suite and the real runs caught, each fixed in its own commit: a run that marked itself finished with most of the book unscreened; an abandoned mid-batch range the allocator stepped over forever; checkpoints rolled back by the crash they existed to survive; a ledger sequence drawn outside the chaining lock; transient rate limits filed as model-integrity quarantine; identity type inferred from an optional field.
git log| Outcome | Count | What it means |
|---|---|---|
| Both flagged a hit | 58 | Independent agreement on the designation. |
| We held, yente missed | 1 | We were the stricter party — the safe direction to differ. |
| We cleared, yente flagged | 0 | Under strict liability, being more permissive than the oracle is what ends in a payment to a designated party. |
The oracle guard passed all 59 verdicts as AGREE, and nothing
reached quarantine.
The anti-plant proof
The sentinel book: a release demo nobody could have staged.
The easy way to demo “releases funds on delisting” is to pre-seed a name you already know was removed and replay the delta. Hindsight, and rightly discounted. So the book was sealed in the opposite direction.
It contains 400 sentinels drawn from
entries that were on the SDN list at seal time, committed with their SHA-256 before
any later OFAC publication existed. If Treasury subsequently removes anyone in this book, the
release leg fires on an entry nobody could have known would be delisted — and git log
proves the book predates the removal.
66eb151cc7473024ed3f9fb4f43700edde828a5ab53d7122e6145c58e72b9b1b
| Stratum | Rows | Selection rule |
|---|---|---|
| SDNTK (counter-narcotics) | 250 | lowest 250 by sha256("interdict-sentinel-v1:" + uid) among SDNTK entries |
| IRAQ2 | 100 | the same rule within IRAQ2 |
| Remainder of the list | 50 | the same rule across everything not already claimed |
Why the strata are weighted
Measured across every OFAC publication from 2026-06-26 to 08-07: 396 removals in 7 publications, heavily concentrated — SDNTK carries 4.5× its share of removals and IRAQ2 13×. Stratifying the same 400 rows lifts the probability of a sentinel firing in a quiet window from ~28% to ~88%.
Why that is not cherry-picking
The weighting is on program membership — public and fixed at seal time. Which entries OFAC will actually remove stays unknowable, and within every stratum the ordering is by SHA-256 of the entry uid. The weighting cannot steer the outcome, only the odds of getting one at all.
As of 08/20/2026 no sentinel has fired. Treasury published again on
08/20 — 19,199 records to 19,249, with 47 additions and no removals in the archived delta — and
make verify-book reports all 400 sentinels still listed in it. So the release leg is
still the labelled replay, and this paragraph will say so until it is not true. That 08/20 delta
is archived alongside the 08/07 one: a real Treasury trigger published during the build that
nobody here chose, and the HOLD leg runs against it live.
Honesty as a feature
What is synthetic is labelled. What is real is reproducible.
Stated plainly, because a screening demo that blurs this is worthless.
Stated plainly
- Real, and none of it ours: the OFAC SDN publication, the
/changes/latestdelta and its 18 additions / 8 removals, every alias category including the 4,393 OFAC-flagged weak aliases, every date of birth, every sanctions programme, and the delisting actions — archived by content hash with the digests indata/PROVENANCE.md. - Ours, and labelled synthetic everywhere it appears: the payment book. A real
NGO’s grantee ledger is not ours to publish. Counterparties carry an
origincolumn, and the console banners it above every screen that shows money. - The RELEASE leg is a labelled REPLAY. The eight delisted parties are already gone from the 08/07 publication, so a live release cannot be staged against today’s list without pretending. The pre-removal state is reconstructed from the delta’s own records and Treasury’s real removals are then applied. It says REPLAY on every screen.
- The 6-hourly poll had a five-day outage, 2026-08-17 to 08-22, and the 08/20
publication was captured late because of it. A lint pass modernised
datetime.timezone.utcintodatetime.UTCwhile the timer was invoking Python 3.9, so every fire died into a log nobody was reading — 16 consecutive tracebacks indata/archive/archiver.log, with the hole visible between the two surrounding captures inindex.json. It is disclosed because “unattended” is a claim this project makes, and that is what an outage in it looks like. - Nothing runs on Google Cloud compute. Cloud Firestore’s free tier holds the mirrored ledger and is the one Google Cloud service in use; the agents, Postgres and yente run locally, because the GCP billing account this project had access to is closed. An earlier revision of the stack table claimed Cloud Run, Cloud SQL, Pub/Sub and Cloud Scheduler — none of them were ever deployed, and the claim is gone.
- The model has never issued a CLEAR. Every clear in the graded book came from the deterministic plane. The adjudicator is exercised on confirmation, not on discrimination.
- Decision quality is a 101-row stratified sample, not the full 536-row book — free-tier Gemini caps requests per model per project per day. The screening numbers are unaffected and measured across all 400.
- It is a batch job, not a real-time one. Free-tier Gemini allows five requests a
minute, so a full-book run takes roughly 90 minutes of wall clock and spends almost all of
it waiting. That is the honest number against a list Treasury republishes weekly, and the
adjudicator honours the server’s own
retry in Nshint rather than hammering it. The unattended part is that nobody starts it or watches it — not that it finishes in seconds. - Transmission to OFAC stays human. Interdict drafts the blocking report and files it to the ledger. It does not submit it.
Reproduce it yourself
The development loop and the judge’s verification path are the same
commands. run_rescreen.py refuses to run without a Gemini key rather than
substituting a test double — a reproduce command that quietly disables the thing being judged is
worse than one that fails.
# Screen ANY name the system has never seen — your pick, not our fixture
make challenge NAME="Ibrahim Al Rashid"
# Now contradict the date of birth: uid 2674's real DOB is 10 Dec 1948,
# a disjoint interval cuts the score below the adjudication bar and the hit dies
make challenge NAME="Abu Abbas" DOB="3 Mar 1990"
# Clean machine, end to end: install, bring up Postgres + yente, index the
# oracle, apply the schema, run 140 tests, print the perturbed screening number
make reproduce
# Verify the sentinel book was not planted — re-derive it from the pinned snapshot
curl -sSL -o data/SDN.XML \
"https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/SDN.XML"
shasum -a 256 data/SDN.XML # snapshot: ac00228a68345e5c0d7174713cf97e5d…
python3 scripts/seed_sentinels.py --sdn data/SDN.XML --out data/sentinels.csv
shasum -a 256 data/sentinels.csv # book: 66eb151cc7473024ed3f9fb4f43700ed…
Re-running against a later SDN snapshot will not reproduce the book hash, by design — entries added or removed since 08/07/2026 change the eligible pool. That is why the snapshot hash is recorded alongside the book hash.
model column names whichever adjudicator produced the verdict, so an
offline stand-in can never be mistaken for the product path — and the synthetic-book banner sits
above every screen that shows money. Every screenshot in this project is the console reading a
real run, re-taken whenever the numbers change. None of them is a mockup.