opens in a new tab
interdict.

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

  1. Interdict top-1 · the harder metric 0.995
  2. yente recall · the easier metric 0.840

The comparison is deliberately stacked against us. Recall only asks whether the right record appears anywhere in the results; top-1 asks whether it is ranked first. Interdict clears yente’s easier bar using the harder one. Our own recall on the same set is 1.000, and 97.8% of variants land at or above the adjudication threshold. yente is OpenSanctions’ matcher, self-hosted here and scope-pinned to us_ofac_sdn — an independent implementation written by people who have never seen this code.

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.

  1. Delta landsA 6-hourly timer polls the feed; a content hash we have not seen opens a run on its own, under a lock.
  2. Full-book re-screenEvery counterparty, checkpointed per batch and crash-safe — a silently skipped row is a compliance breach.
  3. True hits HELDMoney stops. An idempotent hold lands on the counterparty and on every queued disbursement to it.
  4. 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.
  5. Funds RELEASEDA delisting retires the hold and the money moves again, with no human in the path.
  6. 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.

  1. 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.

  2. 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_sdn and consulted on every adjudication — an oracle consulted only where it agrees is not an oracle.

  3. 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.

  4. 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 the 0.62 no-hit floor, is refused — freezing money on evidence the screening plane cannot see is not allowed to execute.
  • Citation check. A matched_identifier that 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.

EXTERNAL — NOT OURS OFAC SLS SDN.XML · 19,199 rec. (08/07/2026) /changes/latest delta 302 → presigned S3 <publshInformation> — their typo 6h timer · launchd poll every 6h · unattended ingest job archive every fetch by content hash unseen hash money plane idempotent holds · release on delisting 10-business-day blocking report HOLD / RELEASE tx + outbox orchestrator · thin ORACLE GUARD — return path schema · citation-vs-record · disagree loop cap ≤2 round-trips → QUARANTINE fail-closed: held money stays HELD entry point: screen_counterparty() sole writer of decisions · tx + outbox EXTERNAL ORACLE — NOT OURS yente OpenSanctions, self-hosted · scope: us_ofac_sdn ONLY matcher · NO LLM deterministic plane — always right Jaro-Winkler + tokens · weak-alias · DOB checkpointed full-book re-screen adjudicator · Gemini GenAI SDK · gemini-3.5-flash-lite structured output · temperature 0 written rationale · cited_fields[] score() adjudicate() consulted by the guard on every decision Postgres 16 · hash-chained idempotent holds (UNIQUE) · append-only ledger · outbox · quarantine · rescreen_batches
Three agents in one process. The oracle guard sits at the routing boundary between the deterministic and model planes, so a verdict is checked before it can move money. Traced from the source as it stands, not from the plan.

↔ 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.

A stratified sample of 101 counterparties screened against the 19,199 SDN records of the 08/07/2026 publication and adjudicated by gemini-3.5-flash-lite. Sampled rather than run whole because free-tier Gemini caps requests per model per project per day. Printed by scripts/adjudication_quality.py; data/adjudication-quality.json is the raw result.
PopulationWhat it isExpected CorrectReached the model
sentinel (30)on the list, exact names HOLD30 / 3030
variant (30)same person, different transliteration HOLD29 / 3029
lookalike (25)different person, shared surname, contradicting DOB CLEAR25 / 250
ordinary (16)unrelated grantees CLEAR16 / 160

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.

1 in 60

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.

data/adjudication-quality.json
0 in 41

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.json
0

Adjudications 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.py
9.3 ms p50

Per 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.json
140 tests

CI 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.yml
6 silent bugs

Correctness 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
Every one of the 59 adjudications checked against yente, not only the ones where it agrees. The zero in the last row is the number that matters.
OutcomeCountWhat it means
Both flagged a hit58 Independent agreement on the designation.
We held, yente missed1 We were the stricter party — the safe direction to differ.
We cleared, yente flagged0 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.

sentinels.csv · sha256
66eb151cc7473024ed3f9fb4f43700edde828a5ab53d7122e6145c58e72b9b1b
Selection is byte-for-byte deterministic — no RNG, no name chosen by hand. Recorded in data/PROVENANCE.md and re-derivable from the pinned snapshot.
StratumRowsSelection rule
SDNTK (counter-narcotics)250 lowest 250 by sha256("interdict-sentinel-v1:" + uid) among SDNTK entries
IRAQ2100the same rule within IRAQ2
Remainder of the list50 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/latest delta 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 in data/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 origin column, 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.utc into datetime.UTC while the timer was invoking Python 3.9, so every fire died into a log nobody was reading — 16 consecutive tracebacks in data/archive/archiver.log, with the hole visible between the two surrounding captures in index.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 Ns hint 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.

The Interdict evidence console, adjudications view: a table of counterparties with the deterministic score, the verdict, the oracle guard result, yente's independent verdict, the adjudicating model and the written rationale for each decision
The adjudications view, exactly as a judge sees it. Every row carries the deterministic score, the guard result, yente’s independent verdict and the written rationale. The 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.