An open-source, explainable HTS classification engine and REST API — built to replace black-box AI classifiers with deterministic, auditable trade rules.
Quick Start · Architecture · How Classification Works · API · Roadmap · Contributing
Post-2026 tariff volatility turned customs compliance into a weekly operational fire drill. Companies in the $50M–$500M revenue band are stuck between two bad options:
| Enterprise Trade Suites (SAP GTS, Oracle GTM) | Black-Box AI Classifiers | |
|---|---|---|
| Cost | $250K–$2M+ implementations | Often cheap, sometimes free |
| Transparency | Proprietary rule engines | Zero — you can't audit a prediction |
| Speed to deploy | 6–18 months | Fast, but you're flying blind |
| Explainability | Partial, vendor-locked | None — "trust the model" |
SYJ OpenTrade Logic takes a third path: deterministic classification using the actual General Rules of Interpretation (GRI) that customs authorities use, with AI permitted only as an assistant — never as the decision-maker. Every classification returns a full, human-readable decision path. Nothing is a guess dressed up as a percentage.
📖 On honesty as a design principle: this README documents exactly what has been built and verified, release by release. Nothing below is aspirational copy for unwritten code — see Release Roadmap for what's real today versus what's planned.
flowchart TB
subgraph Client["Client Layer"]
CLI["CLI<br/><i>cli/classify.py</i>"]
HTTP["HTTP Clients<br/><i>curl · Postman · your app</i>"]
SWAGGER["Swagger UI<br/><i>/docs</i>"]
end
subgraph API["API Layer — server_fastapi/"]
FASTAPI["FastAPI App<br/><i>main.py</i>"]
SCHEMAS["Pydantic Schemas<br/><i>schemas.py</i>"]
end
subgraph Core["Core Engine — core/ (zero dependencies)"]
ENGINE["GRIEngine<br/><i>gri_engine.py</i>"]
MODELS["Data Models<br/><i>models.py</i>"]
end
subgraph Data["Data Layer"]
HTS["HTS Tariff Tree<br/><i>data/hts_full.json</i><br/>99 chapters · live USITC feed"]
DB[("SQLite / Postgres<br/><i>server_fastapi/database.py</i>")]
end
subgraph External["External Sources"]
USITC["USITC REST API<br/><i>hts.usitc.gov</i>"]
IMPORTER["Importer<br/><i>scripts/import_hts_data.py</i>"]
end
CLI --> ENGINE
HTTP --> FASTAPI
SWAGGER --> FASTAPI
FASTAPI --> SCHEMAS
FASTAPI --> ENGINE
FASTAPI --> DB
ENGINE --> MODELS
ENGINE --> HTS
USITC -->|"~17,000+ HTS-10 records"| IMPORTER
IMPORTER -->|"builds nested tree"| HTS
style Core fill:#6366F1,stroke:#4338CA,color:#fff
style API fill:#0EA5E9,stroke:#0369A1,color:#fff
style Data fill:#10B981,stroke:#047857,color:#fff
style External fill:#F59E0B,stroke:#B45309,color:#fff
Design principle: the core engine (core/) has zero third-party dependencies and doesn't know or care whether it's called from a CLI, a stdlib HTTP server, or FastAPI. This is deliberate — classification logic should outlive any particular web framework choice.
Classification follows the General Rules of Interpretation (GRI) as a Directed Acyclic Graph traversal — not a black-box model call.
flowchart TD
START(["Product description<br/>e.g. 'cordless electric drill'"]) --> TOKENIZE["Tokenize description"]
TOKENIZE --> SCORE_HEAD["<b>GRI 1</b><br/>Score every heading in the HTS tree<br/>against heading terms + legal notes"]
SCORE_HEAD --> CHECK{"Top score<br/>> 0?"}
CHECK -->|No| UNRESOLVED["❌ Return UNRESOLVED<br/>never guess"]
CHECK -->|Yes| CLOSE{"Runner-up heading<br/>within 0.10 of top score?"}
CLOSE -->|Yes| GRI3A["<b>GRI 3(a)</b><br/>Flag competing heading,<br/>prefer more specific description"]
CLOSE -->|No| SCORE_SUB
GRI3A --> SCORE_SUB["<b>GRI 6</b><br/>Score subheadings under<br/>the selected heading"]
SCORE_SUB --> RESULT["✅ Return classification:<br/>• Final HTS code<br/>• Confidence score<br/>• Full decision path<br/>• Alternatives considered<br/>• Supporting legal notes<br/>• Duty rate"]
UNRESOLVED --> DONE(["Auditable JSON result"])
RESULT --> DONE
style CHECK fill:#F59E0B,color:#000
style CLOSE fill:#F59E0B,color:#000
style UNRESOLVED fill:#EF4444,color:#fff
style RESULT fill:#10B981,color:#fff
style GRI3A fill:#6366F1,color:#fff
style SCORE_HEAD fill:#6366F1,color:#fff
style SCORE_SUB fill:#6366F1,color:#fff
Key engineering decision: real HTS heading text is terse legal language ("Automatic data processing machines and units thereof") that rarely contains the everyday words a user types ("laptop"). Those live at the subheading level. The engine's _effective_heading_score() scores each heading by the better of its own text or its best-matching child subheading — otherwise correct classifications would silently fail once real USITC data replaced hand-curated samples. This was a real bug, found and fixed during v0.3.0 development, and is permanently covered by tests/test_importer_and_terse_headings.py.
sequenceDiagram
participant U as User / Client
participant A as FastAPI (main.py)
participant E as GRIEngine (core/gri_engine.py)
participant D as SQLite/Postgres (database.py)
U->>A: POST /classify {"description": "cordless electric drill"}
A->>E: engine.classify(description)
E->>E: GRI 1 — score all headings
E->>E: GRI 6 — score subheadings of top heading
E-->>A: ClassificationResult (decision path, confidence, alternatives)
A->>D: persist result as audit record
D-->>A: record id
A-->>U: 201 Created {id, final_code: "8467.21", decision_path: [...], ...}
git clone https://github.com/SHalimoosavi/SYJ-OpenTrade-Logic.git
cd SYJ-OpenTrade-Logic/syj-opentrade-logic
python3 -m unittest tests.test_gri_engine -v
python3 cli/classify.py "cordless electric drill"
python3 cli/classify.py "cotton t-shirt" --jsonpip install -r server_fastapi/requirements.txt --break-system-packages
pip install httpx --break-system-packages # for TestClient
# Pull the real, full 99-chapter HTS dataset from the official USITC API
python3 scripts/import_hts_data.py
# Run the full test suite
python3 -m unittest tests.test_gri_engine tests.test_api tests.test_importer_and_terse_headings -v
python3 -m unittest server_fastapi.test_main -v
# Launch the live server
python3 -m uvicorn server_fastapi.main:app --reload --port 8000Open http://localhost:8000/docs for a live, auto-generated Swagger UI.
curl -X POST http://localhost:8000/classify \
-H "Content-Type: application/json" \
-d '{"description": "cordless electric drill"}'syj-opentrade-logic/
├── core/ # Zero-dependency logic modules
│ ├── models.py # HTS tree + result dataclasses
│ ├── gri_engine.py # GRI 1 / 3(a) / 6 DAG traversal + stemming
│ ├── rulings_search.py # BM25 lexical search over CROSS rulings
│ └── duty_calculator.py # Section 301/232 + AD/CVD estimation
├── cli/
│ └── classify.py # Standalone CLI, no dependencies
├── server/ # v0.2.0 — stdlib http.server REST layer
├── server_fastapi/ # v0.3.0+ — real FastAPI + SQLAlchemy
│ ├── main.py # app entrypoint, classify endpoint
│ ├── database.py # SQLAlchemy models (Org/User/Product/AuditLog/Webhook)
│ ├── security.py, dependencies.py # JWT auth, RBAC
│ ├── routes_*.py # auth, catalog, org, rulings, duty, webhooks, audit, reports
│ ├── webhooks.py, webhook_triggers.py # HMAC-signed delivery
│ ├── reports.py # CSV/Excel/PDF generation
│ ├── requirements.txt # production dependencies
│ ├── requirements-dev.txt # test-only dependencies (pypdf, httpx)
│ └── test_*.py # full integration test suite
├── frontend/ # v0.5.0 — Vite + React + TypeScript dashboard
│ └── src/{pages,components,lib,types}/
├── scripts/
│ └── import_hts_data.py # Pulls full 99-chapter HTS from the live USITC API
├── data/
│ ├── hts_sample.json # Small demo dataset
│ ├── hts_full.json # Generated by the importer — not committed
│ ├── cross_rulings_sample.json
│ ├── trade_programs_sample.json
│ └── adcvd_sample.json
├── tests/ # Core engine test suites
├── .github/workflows/ci.yml # v0.9.0 — runs the full suite + real Docker builds
├── Dockerfile.backend, Dockerfile.frontend, docker-compose.yml, nginx.conf
├── DEPLOYMENT.md # Docker/production deployment guide
├── LICENSE # Apache 2.0
└── README.md
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Liveness check |
POST |
/classify |
Classify a product description, persist the result |
GET |
/classifications?limit=&offset= |
Paginated classification history |
GET |
/classifications/{id} |
Fetch one classification record |
DELETE |
/classifications/{id} |
Delete one classification record |
GET |
/docs |
Interactive Swagger UI (auto-generated by FastAPI) |
GET |
/openapi.json |
Machine-readable OpenAPI 3.0 spec |
Example response — POST /classify
{
"id": 1,
"product_description": "cordless electric drill",
"final_code": "8467.21",
"final_description": "Drills of all kinds, hand-held, electric motor",
"confidence": 0.87,
"is_classified": true,
"duty_rate": "1.7%",
"decision_path": [
{
"node_code": "8467",
"node_description": "Tools for working in the hand...",
"rule_applied": "GRI 1",
"reasoning": "Heading 8467 scored highest (0.82) against the terms of the heading, per GRI 1...",
"score": 0.82
},
{
"node_code": "8467.21",
"node_description": "Drills of all kinds, hand-held, electric motor",
"rule_applied": "GRI 6",
"reasoning": "Subheading 8467.21 scored highest (0.91) among subheadings of 8467, per GRI 6...",
"score": 0.91
}
],
"alternatives": [],
"supporting_notes": []
}AI should assist — cleaning descriptions, extracting attributes, summarizing rulings, suggesting candidates for a human to review. AI must never replace deterministic GRI-based classification.
This isn't a limitation — it's the whole point. A customs broker who gets asked "why did the system pick this code?" needs an answer rooted in the actual legal text of the tariff schedule, not a confidence score from an opaque model. Every result this engine returns can be defended in an audit.
timeline
title SYJ OpenTrade Logic — Release Progression
v0.1.0 : GRI classification engine
: Zero-dependency core, 12 tests
v0.2.0 : stdlib REST API
: SQLite persistence, 22 tests
v0.3.0 : Real FastAPI + SQLAlchemy
: Full 99-chapter USITC import, 26 tests
v0.4.0 : Auth, RBAC, organizations
: Product catalog, CSV/Excel import
v0.5.0 : Next.js dashboard
: shadcn/ui, Tailwind, dark mode
v0.6.0 : Semantic search
: CBP CROSS rulings, vector DB
v0.7.0 : Duty calculator
: Section 301/232/122, AD/CVD
v0.8.0 : Reports & audit trails
: PDF/CSV/Excel export, webhooks
v0.9.0 : Production DevOps
: Docker, CI/CD, deployment docs
| Release | Scope | Status |
|---|---|---|
| v0.1.0 | GRI classification engine | ✅ Done — 12 tests passing |
| v0.2.0 | stdlib REST API + SQLite persistence | ✅ Done — 22 tests passing |
| v0.3.0 | Real FastAPI + SQLAlchemy; full 99-chapter HTS import | ✅ Done — 26 tests passing, live against USITC data |
| v0.4.0 | Auth (JWT/RBAC), organizations, product catalog, CSV/Excel import | 🔜 Planned — needs Postgres |
| v0.5.0 | Vite+React dashboard (shadcn/Tailwind) — swapped from Next.js for Termux/mobile practicality | ✅ Code complete, import/export/syntax-verified by script; npm install + real run needed on your machine |
| v0.6.0 | CROSS rulings search (BM25 lexical, zero-dependency) — swapped from Pinecone/Milvus+embeddings per explicit decision | ✅ Done — 15 new tests passing here; API integration tests need your machine |
| v0.7.0 | Duty calculator (Section 301/232 + AD/CVD scope flagging) | ✅ Done — 19 new tests passing here; real, dated, sourced sample data with strong disclaimers |
| v0.8.0 | Reports (PDF/CSV/Excel), audit trails, webhooks | ✅ Done — 103 tests passing here, including real HTTP webhook delivery to a local server and round-tripped PDF/Excel files |
| v0.9.0 | Docker/Compose, GitHub Actions CI/CD, deployment docs | ✅ Done — see DEPLOYMENT.md; CI runs for real on GitHub's servers on every push |
# Install both production and test-only dependencies
pip install -r server_fastapi/requirements.txt --break-system-packages
pip install -r server_fastapi/requirements-dev.txt --break-system-packages
# Core engine, importer, rulings search, duty calculator (no FastAPI app instantiated)
python3 -m unittest tests.test_gri_engine tests.test_api tests.test_importer_and_terse_headings tests.test_rulings_search tests.test_duty_calculator -v
# Standalone server modules (security, CSV/Excel import, webhooks, reports)
python3 -m unittest server_fastapi.test_security server_fastapi.test_catalog_import server_fastapi.test_webhooks server_fastapi.test_reports -v
# Each of these instantiates a real FastAPI app + SQLAlchemy engine and
# MUST run as a separate command -- combining them in one invocation
# silently breaks database isolation between them (a real bug found
# during v0.4.0 development; see .github/workflows/ci.yml's comments)
python3 -m unittest server_fastapi.test_main -v
python3 -m unittest server_fastapi.test_auth_and_catalog -v
python3 -m unittest server_fastapi.test_rulings_api -v
python3 -m unittest server_fastapi.test_duty_api -v
python3 -m unittest server_fastapi.test_v080_features -v103 tests, all passing, including: correct GRI-based classification, refusal to guess on unmatched products, decision-path integrity, live HTTP integration against a real running server, direct SQLite file verification, real-data regressions found while importing the live USITC feed (chapter detection, terse headings, a stemmer bug affecting silent-e plurals), BM25 rulings search ranking, duty calculation across Section 301/232 and AD/CVD scenarios, real HMAC-signed webhook delivery to an actual local HTTP server with independent signature verification, and real generated PDF/Excel files round-tripped and verified.
CI runs this full suite automatically on every push (see the badge above and .github/workflows/ci.yml), plus a real npm run build of the dashboard and real docker build of both production images on GitHub's Linux servers.
This project is built in the open, in public, with every bug and fix documented rather than hidden. CI runs automatically on every push and pull request (see DEPLOYMENT.md). Issues and PRs welcome.
Apache 2.0 — see LICENSE.