-
-
20 lines in: 10 settled by the official table without a model call, 10 sent to the agent, 2 held back for a named person.
-
Three candidates ruled out, each with the chapter note that killed it, then the chosen code and citations resolve ok.
-
It refuses rather than guesses: the missing fact, the department that owns it, and the case waiting with its history intact.
-
One word inserted into a real quote, on camera. The production checker refuses it and the line stops before it can be filed.
-
The frozen evaluation, run once: 88.9% on dead-code reclassification against a 21.2% floor, and 423 of 423 injected citation faults caught.
-
Architecture: what the table settles, the one place a model is asked anything, and the three gates it cannot influence.
Inspiration
The US tariff schedule shipped three revisions in three weeks: Rev 14 on Jul 29, Rev 15 on Aug 03, Rev 16 on Aug 11. Somewhere in a manufacturer's catalog, a code that cleared customs last month no longer exists. A dead code often has two legal successors, and at eight digits they can be thousands of dollars apart on a single entry. Customs does not tell you which one you picked wrong. The penalty notice does.
Reclassifying one line means chapter notes, section notes, and 218,606 past CBP rulings, none of which join cleanly to a product catalog. A compliance officer does this by hand, line by line, every time the schedule moves. Most lines need no judgment at all. The ones that do are exactly the ones where a wrong guess becomes duty owed, plus penalties, plus a shipment sitting at the port.
What it does
Drop in a catalog export. The control plane splits the work before any model is involved.
A code the official correlation table maps one-to-one is settled by a program and never reaches the model. Sending it there would inflate the autonomy number and buy nothing. What is left is real judgment, and that goes to one bounded classification agent on Gemini 3.7 Flash with four read-only corpora: chapter and section notes, tariff lines, ruling search, ruling texts. On the frozen evaluation set that split sends 288 of 500 items to the agent and settles the rest by table.
The agent's answer is not taken on trust either. Three deterministic gates stand between it and a filing:
- Citation check. Every quoted word is re-resolved against the frozen snapshot. Punctuation may differ; words may not. This is what runs when the model hallucinates, and the demo video breaks a citation on camera to show it working.
- Duty and cost. Rates, chapter 99 measures, and the money the entry owes are computed outside the model's reach. The agent cannot see duty rates, so it cannot pick a code because the code is cheap.
- One signature. Role-gated approval closes the batch. The parts a program can compute never asked for trust; the judgment call left is the officer's, once.
When the description does not say enough to decide, the agent refuses to guess. The question goes out with a named owner, the case waits with its history intact in Cloud SQL, and when a person answers days later the re-run cites the answered fact. It refuses rarely and on purpose: 6 of 500 items, an escalation rate of 1.2% against a design cap of 10%.
How we built it
- Cloud Run hosts the FastAPI service and the UI, one instance, CPU never throttled because work continues on background threads after the HTTP response is sent.
- Cloud SQL (Postgres 17) holds every case, transcript, question and answer, so a case waiting days on a person survives container restarts. The same SQL runs against SQLite locally.
- Vertex AI / Gemini 3.7 Flash through the Google GenAI SDK, on the shared flex tier: half price, several times slower, and nothing here is interactive.
- The frozen snapshot ships inside the container image on purpose. The legal text a classification cites and the revision that read it are one artifact; a classification that cannot name the text it read is not auditable.
Other data sources used
Every source is a public government endpoint. None needs an API key or scraping.
| Source | Endpoint | Measured 2026-08-17 |
|---|---|---|
| CBP customs rulings (CROSS) | rulings.cbp.gov/api |
218,606 rulings, 62.9 MB |
| Tariff schedule (USITC) | hts.usitc.gov/reststop/exportList |
35,791 rows, revision 2026HTSRev16 |
| Chapter legal notes (USITC) | hts.usitc.gov/reststop/file |
98 chapters, 616,847 characters |
| Consolidated Screening List (ITA) | data.trade.gov |
25,939 entries |
| HS2017 to HS2022 correlation | UNSD tables + WCO Table II | 15,656 code pairs |
The four US government sources are uncopyrighted (17 U.S.C. §105). The correlation tables are not redistributable, so the repository ships fetch scripts and no third-party data: make data rebuilds a snapshot from the originals, which also makes the build reproducible from scratch rather than from a checked-in copy of someone else's file.
The WCO publishes the correlation tables stating they "have no legal status" and "constitute a guide only". The architecture takes that literally: the table decides where to look, never what to conclude.
Challenges we ran into
A wrong filename returns HTTP 200 with an empty body, not a 404. An empty screening list means every party passes screening, which is the most dangerous failure available here. Status codes are never sufficient, so the health gate checks byte and row floors and re-checks the sha256 of what landed on disk.
The evaluation set leaked its own answers. Ruling texts open with sentences like "you request a ruling on the applicability of subheading X", so the description carried the answer. Cutting the text before the first tariff reference would have destroyed 23% of the items. Redacting in place instead kept 100% of the product facts, and a gate re-verifies that no description can read its own answer; it caught three regex mistakes, one of which had taken leakage from 0 items to 58.
Deploying it broke it twice. At 1 GiB the container was killed mid-batch with 1030 MiB used, because the precedent index holds 218,606 rulings, and every case the agent had open came back stranded. Then the screen started going blank with a bare "Lost the server:" and no message, because drawing the flow read four tables per case and opened a connection for each: twenty cases asked Cloud SQL for eighty at once. Both limits are now load-bearing settings documented where they are set.
Accomplishments, and what the numbers are
The number worth reporting is not the accuracy of a model. It is how much of a regulated workflow can be settled without asking one, and what has to hold for the rest to be signable.
The evaluation set is frozen, leakage-redacted, and was run exactly once, on 2026-08-30.
| Stratum | n | 8-digit correct | Mechanical floor (carry the old code forward) |
|---|---|---|---|
| DEAD_CODE — the prior heading no longer exists | 288 | 88.9% | 21.2% |
| SCOPE_REVIEW — prior heading maps to several | 90 | 95.6% | 97.8% |
| SURVIVED — prior heading maps one-to-one | 122 | 100% | 100% |
| All | 500 | 92.8% | 54.2% |
Only DEAD_CODE tests reclassification. In the other two strata the prior code and the answer share their first six digits by construction, so one blended number would hide which part of the problem was actually solved.
Citation fault injection, re-run against the same production checker: 423 answers had one word inserted into a real quote. 423 caught, 0 missed. Escalation rate 1.2%; accuracy on what shipped, 94.0% at 8 digits.
The raw run output and the scoring are committed under eval/results/ so the numbers can be re-derived without spending anything.
What we learned
Two caveats measured before submission and reported because the set is run once. Three of the 500 items have a truth code that no longer exists in the current schedule, because HS2022 merged the heading; those are unanswerable by construction and the citation checker correctly refuses them. And the redaction that removed answer leakage also removed some legitimate context, which pushes the number down rather than up.
What's next
The schedule will move again next week. A table settles what a table can settle; one agent argues what needs arguing; a checker holds every word to the source; a person signs once.
Built With
- cloud-run
- cloud-sql
- fastapi
- gemini-3.7-flash
- google-genai-sdk
- postgresql
- python
- sqlite
- vertex-ai
Log in or sign up for Devpost to join the conversation.