We classify imports with Claude Sonnet 4.6, screen parties against the full US denied-party list set, and run a state machine that takes an entry from draft to cleared with the broker still in the loop on decisions that matter. Below is exactly how each piece fits.
Most of the broker's expertise lives in three places: the HTSUS, the CBP CROSS rulings archive, and the moving target of US denied-party lists. We ingest all three locally so the AI can use them as reasoning context, not as a black-box API.
| Dataset | Origin | What's in it | Records | Refresh |
|---|---|---|---|---|
| HTSUS | hts.usitc.gov | Every filable 10-digit US tariff code with duty rates, units, footnotes | 26,621 | Quarterly (USITC) |
| CROSS rulings | rulings.cbp.gov | CBP's binding precedent — 250K+ rulings since 1989. Cited by brokers daily. | 847 | On-demand, by chapter |
| Screening lists | data.opensanctions.org | OFAC SDN + BIS Entity List + UFLPA + DPL + UVL + DTC + ISN + MEU | 31,400 | Daily (CDN-served) |
All three land in a single Postgres database, indexed for full-text and trigram-fuzzy retrieval. The AI never sees them as a stream of API calls — it reads them as already-retrieved context.
The classifier is a single Anthropic API call per line item. Three cached prompt blocks make the economics work:
We started on Opus 4.7 with adaptive thinking enabled. The eval harness showed two things: thinking ate the output budget (rationales were being truncated, confidence numbers came back as the string "placeholder - recalculating") and Sonnet 4.6 with thinking disabled matched Opus on top-1 accuracy at ~60% fewer output tokens and 56% lower latency. For structured outputs over a well-scaffolded RAG context, the smaller model does the right thing.
Anthropic's prompt cache reads at 1/10th the price of a fresh input. Caching the system block for an hour means the bulk of the prompt (~6,500 tokens) is amortized across every classification in a session. Caching the per-shipment CROSS rulings and HTSUS candidates for 5 minutes catches the cross-line reuse within a single shipment (similar SKUs share candidates). Per-line input is small — ~30 tokens for the description.
We hold out CROSS rulings as eval cases. For each held-out ruling, we use its subject as the line description and check whether the classifier — without seeing that ruling — predicts the heading the ruling assigned. Then we score at 4-digit, 8-digit, and 10-digit precision. Two passes per case: no-RAG (HTSUS only, our baseline) and with-RAG (HTSUS + CROSS).
Most recent run, N=20, fixed seed:
Every run lands in a Notion database with its seed, sample, and complete per-case results so the eval is reproducible and we can track regressions over time. A future run with a different model, a different threshold, or a different retrieval strategy is one command away.
Before a shipment can move from draft to ai_review, Aduaria screens the importer, consignee, and supplier names against 31,400 sanctioned entities across 8 US government lists. The screen is deterministic (Postgres trigram + word-similarity match) and runs in under 600ms.
| List | Issuer | What it means for an importer | Count |
|---|---|---|---|
| OFAC SDN | Treasury | Cannot transact at all — full block | 19,315 |
| BIS Entity List | Commerce | License required to export to (Russia / Iran tech, dual-use) | 6,665 |
| BIS Denied Persons | Commerce | Individuals denied export privileges | 2,949 |
| State AECA Debarred | State Dept | Arms-trafficking statutory bar | 1,570 |
| BIS Unverified List | Commerce | Bona fides unconfirmed; extra license review | 432 |
| State Nonproliferation | State Dept | WMD / missile sanctions | 315 |
| BIS Military End User | Commerce | Military-affiliated buyers in China / Russia | 140 |
| UFLPA Entity List | DHS | Goods presumed made with forced labor — rebuttable presumption of denied entry | 14 |
Postgres has pg_trgm built-in — no separate vector
database. We use strict_word_similarity, which requires
word-boundary alignment, against a materialized view that flattens
every canonical name and every alias one row each. A hit on an
alias scores equally to a hit on the canonical name — important,
because sanctioned parties are constantly re-registered.
Corporate suffixes (LLC, Inc, Ltd, GmbH, BV, AG, …) are stripped before matching so they don't carry weight. The threshold for auto-block is 0.85 — high enough to avoid false-positives on common product words, low enough to catch real alias matches at 1.0.
Any block-band hit flips the shipment's flags to include screening-block, persists the matches to screening_results (one row per party-role × source-list × entity), logs an activity entry, and routes the shipment to hold instead of ai_review. The broker sees a red panel in the drawer with every hit's score, the list it came from, the canonical name, the alias that triggered it, and the country. Nothing moves forward until the broker clears it.
Every shipment moves through a typed state machine. The transition table is the single source of truth — the workspace, the API endpoints, and any future workflow engine all consult it.
The interesting transition is draft → ai_review. Triggering it runs the classifier on every line, then runs screening on every party. The outcome decides the next state:
hold and exam are the broker-attention branches. rejected (CBP rejection) returns to draft so the same shipment id replays the pipeline. cleared is terminal — that's a duty-paid, released-by-CBP entry.
The stack is boring on purpose: Postgres, Hono on Node, Next.js 15, Drizzle, pnpm monorepo. Where it earns its keep is the module seam: each feature (customs-clearance, tariff-intel, value-chain-compliance) owns its own Postgres schema, registers its routes, declares its dependencies, and runs its own migrations.
Adding a new module — say, a duty calculator or a supplier risk scorer — is a closed operation: drop a folder under modules/, ship a manifest, run migrations. No changes to the API gateway or the web shell. No new services to deploy until usage actually demands it.
The architectural rule that pays off most: modules MUST NOT write outside their own schema. Cross-module communication is via an explicit service call (with the dep declared in the manifest) or via the in-process event bus. This is what lets us eventually split the modulith into separate services without rewriting business logic.
Production CBP filings are gated on a licensed customs broker partnership, a $50K continuous bond, a CBP-issued production filer code, and passing ABI certification testing. All four are well-known, well-trodden paths — see CBP's CATAIR docs.