Technical brief

How Aduaria works.

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.

Updated 2026-05 ~15 min read Built on Postgres + Anthropic

01 — The data layerThree datasets do the work

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.

DatasetOriginWhat's in itRecordsRefresh
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.

Why this matters: a generic AI asked "what's the HTS code for an LED desk lamp from China?" hallucinates frequently. The same model, given the top-40 candidate codes from the actual HTSUS plus the top-5 binding CBP rulings that cover similar products, classifies correctly with a citable rationale. We measured the lift — see the eval section.

02 — The AI layerClaude Sonnet 4.6, RAG-grounded

The classifier is a single Anthropic API call per line item. Three cached prompt blocks make the economics work:

┌─────────────────────────────────────────────────────────────┐ │ Block 1 · System instructions (cached 1 hour) │ │ Role · rules · citation discipline · confidence calibration│ ├─────────────────────────────────────────────────────────────┤ │ Block 2 · Top 5 CROSS rulings (cached 5 min) │ │ Binding precedent — the AI cites them in its rationale │ ├─────────────────────────────────────────────────────────────┤ │ Block 3 · Top 40 HTSUS candidates (cached 5 min) │ │ Pre-filtered by FTS over the description │ └─────────────────────────────────────────────────────────────┘ User: Model output (structured JSON): { hts_code, confidence, rationale, alternatives[3], flags[] }

Why Sonnet, not Opus

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.

Why three cached blocks

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.

How we measure the AI

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:

60%
Top-1 heading
+25pp vs no-RAG
40%
Top-1 subheading
+30pp vs no-RAG
85%
Top-K heading hit
primary or alt
$0.031
Cost per line
vs $0.025 no-RAG

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.

Honest about failure: the classifier currently struggles on cases where the input description itself is ambiguous — costume vs textile apparel (chapter 6211 vs 9505), leather vs textile footwear (6403 vs 6404), insulated vs non-insulated vessels (9617 vs 7323). The AI flags these for broker review rather than guessing. We're working through them by adding more CROSS rulings to the chapters that need them.

03 — Denied-party screeningEvery party, every list, on every entry

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.

What we screen against

ListIssuerWhat it means for an importerCount
OFAC SDNTreasuryCannot transact at all — full block19,315
BIS Entity ListCommerceLicense required to export to (Russia / Iran tech, dual-use)6,665
BIS Denied PersonsCommerceIndividuals denied export privileges2,949
State AECA DebarredState DeptArms-trafficking statutory bar1,570
BIS Unverified ListCommerceBona fides unconfirmed; extra license review432
State NonproliferationState DeptWMD / missile sanctions315
BIS Military End UserCommerceMilitary-affiliated buyers in China / Russia140
UFLPA Entity ListDHSGoods presumed made with forced labor — rebuttable presumption of denied entry14

How the match works

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.

What happens on a hit

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.

What we don't do yet: structured address + DOB matching for individuals, EU/UK sanctions parity, and an LLM agent that re-ranks fuzzy matches by checking address / programs / nationality alignment. Each of those is the next 1-2 weeks of work on this surface.

04 — The state machineOne source of truth for what can happen next

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.

┌──────────────────────────────────────────┐ │ │ draft ──► ai_review ──► ready ──► filed ──► released ──► cleared │ │ │ └──► hold ────┘ ├──► hold ├──► exam ──► released └──► rejected ──► draft

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.

05 — The architectureOne database, one API, modular by seam

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.

apps/ ├── web/ Next.js 15 — broker workspace, lifted from design prototype ├── api/ Hono — modules register routes through one gateway ├── cli/ tsx scripts — ingest, classify, eval, screen └── marketing/ static — aduaria.com packages/ ├── core/ Module registry, manifest types, event bus, env loader ├── db/ Drizzle + per-module schema composition + kernel tables ├── domain/ Shared types — Entry, Shipment, Supplier, LineItem ├── ai/ Anthropic SDK wrapper — classifyLine, extractInvoice ├── ace-filer/ CR / SE / SF builders + AS2 transport (stub) └── design-system/ Tailwind preset + UI primitives modules/ ├── customs-clearance/ state machine + ingest + classify + screen ├── tariff-intel/ HTSUS + CROSS + FTS retrieval ├── value-chain-compliance/ OpenSanctions ingest + fuzzy screening ├── supplier-network/ (stub) └── post-entry-audit/ (stub)

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.

06 — What's nextThe roadmap, in priority order

  1. Auth + workspace deploy — currently the workspace runs only locally; auth is the gate to app.commers.ai
  2. Section 301 / IEEPA tariff ingest + duty calculator — populate the tariff-alert feed; compute landed cost including additional duties
  3. Screening agent — LLM re-ranks top-N fuzzy matches by cross-checking address, country, programs against the line context
  4. ACE CERT integration — apply for the test filer code now (60-90 day cycle), build the AS2 transmission against CBP's test endpoint
  5. Post-entry audit module — anomaly detection on filed entries; CF28 / CF29 response handling

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.