A shipping inbox-control system that reconciles expected shipments with cases and checks SI-to-BL fields against source evidence for sign-off.
Watch the Demo »
·
Screenshots
·
Report a Bug
Expand
Every email accounted for. Every expected shipment answered for.
Averis's shipping-operations team gets every kind of message in one inbox, up to 2,000 emails a day. For a document-checking request, an analyst compares the customer's Shipping Instruction (SI) with the draft Bill of Lading (BL) field by field.
Two failures matter, and only one of them is visible from the inbox. The first is a mismatch between the two documents that a tired reader misses. The second is a shipment that was expected and never arrived as an email at all. You cannot notice an email you never received.
LadingLens treats both failures as one control problem. It borrows the answer from double-entry bookkeeping: check the inbox against an independent record of what should have been there. Two independent controls and a named human sit around the inbox:
- Gate 1 accounts for every received email.
- Gate 2 reconciles what was supposed to arrive.
- Evidence comparison checks each valid SI/draft-BL pair over seven fields.
- A named human makes every consequential decision.
Watch the video presentation, or read the pitch deck, also as a PDF. See the deployment's limitations.
Built for Averis x Monash Hackathon 2026.
Captured at 1440x900 against the deployed service.
The five-minute walkthrough in the demo runbook, step by step:
-
Sign in as a guest. Open the live demo and choose Sign in as Guest on
/auth. The email and password fields do nothing. You land on/inbox, already seeded with all 520 synthetic emails.
-
Gate 1: every email is accounted for. The inbox lists every received email with its category and outcome. Only a
BL_COMPARISONemail goes on to evidence comparison.
-
Compare the evidence. Open a comparison case such as
/emails/email_001. The seven fields sit side by side, with the SI as the reference. Each verdict isMATCH,MISMATCHorREVIEW, and shows the evidence it came from in both documents.
-
Hand held cases to a person.
/reviewlists the 20 cases the system will not decide alone, with the reason for each.email_511's draft BL will not open,email_512is an image-only scan whose values were read by OCR, andemail_516has fields the customer left blank. A named reviewer approves, corrects or rejects each one.
-
Gate 2: catch what never arrived.
/reconciliationopens on its inputs: the expected-shipment ledger and the BL cases that arrived. Run reconciliation, and shipmentSHP-5RFR-37631, named by an SI request, expects a draft BL, but no email ever created a case for it. Gate 2 marks itMISSING_CASE, which Gate 1 could never catch on its own, and its exceptions join the review queue.
-
Check a pair of your own. Open
/judge; no sign-in is needed, and it opens the workspace's Upload page. Drop one SI and one draft BL, in either order, as TXT, PDF, DOCX or XLSX, up to 5 MiB each; the check reads each file to tell which is which. Confirm they are synthetic and choose Check documents. To check up to 20 pairs in one go, drop a.jsonbatch of dataset email records (with their attachment files) or pairs. A waiting screen follows the three pipeline steps while the live run works: you get all seven verdicts with evidence, or a plain failure with a retry button and a labeledPREPARED FALLBACKexample underneath.
-
Reset and repeat. Reset All on
/settingsreturns your guest workspace to the seed baseline exactly as shipped, ready for the next person.
- Every email receipted and categorized. All 520 bundle emails are hashed, persisted and categorized, and replaying the same batch changes nothing.
- Independent shipment reconciliation. Every expected shipment resolves to exactly one of six outcomes:
CASE_PRESENT,DOCUMENT_MISSING,MISSING_CASE,UNMATCHED_CASE,DUPLICATE_OR_AMBIGUOUSorSOURCE_STALE. - Seven-field comparison with source evidence. How precise the evidence is depends on the format: line and column for TXT, a text bounding box for a digital PDF, sheet and cell for XLSX, a table cell or paragraph for DOCX, and an approximate page and region for a scanned PDF.
- A locked match policy. When text differs, it gets a typed match probability: values of 0.85 and above are
MATCH, 0.30 and below areMISMATCH, and anything between is held for a reviewer. Numbers are compared in Python, never by a model. - Fail-closed AI. A Gemini or Jev failure is classified, audited and shown as a failure with a retry. It never becomes a verdict.
- Append-only audit trail. A Postgres trigger rejects in-place updates and deletes on every append-only table, including audit events, review actions, reconciliation results and model decisions.
- Human sign-off. Held cases can be approved, corrected or rejected. Reconciliation exceptions can be acknowledged, escalated or resolved.
- Control graph. Every case reads as one chain from email to shipment, with the verdict of each stage on its link and the parties, ports and shipments that connect cases traceable across them.
- Evaluation view. It counts classification coverage and each kind of outcome: comparison, processing status and reconciliation.
- Submission artifacts. The Upload page downloads
submission.jsonin the organizers' scored format, and/reconciliationdownloads the expected-shipments CSV ledger it reconciles against (a replacement CSV can be imported there too). - Guest workspaces. There is no sign-up. A guest's first action on a seed case copies it into their own workspace, and Reset All restores the shared baseline.
- Public judge mode.
/judgeruns a fresh SI/draft-BL pair through the same live pipeline as every other case, with no account.
The diagram is drawn with archify from architecture.json.
A single Cloud Run container serves the FastAPI API and the compiled React app. PostgreSQL is the system of record. Source documents are stored as create-only objects in a private Cloud Storage bucket. The runtime gets its secrets from Secret Manager.
Deploys are manual. The removed deploy workflow records the steps it ran: the database migrations as a Cloud Run job first, then the service deploy, then a smoke check of the live service.
Each kind of decision has exactly one owner:
| Owner | Decides | Never decides |
|---|---|---|
| Deterministic Python | File checks; parsing TXT, XLSX, DOCX and digital PDFs; normalization; both numeric comparisons; schema, state, persistence and audit | What a document is, or what its text means |
| Gemini 3.5 Flash | Field values from a scanned PDF or a document whose local parse is ambiguous. Every answer is schema-validated, and a grounded answer must appear verbatim in the source text | Clean digital documents, categories or equivalence |
Jev jev-1.13.0, pinned |
Email category, document role (SI, draft BL or other) and textual field equivalence | Numbers, arithmetic or persistence |
| Named human reviewer | Approving, correcting or rejecting a held case; assigning, acknowledging, escalating or resolving an exception | Nothing: theirs is the only final disposition |
docs/readme/export-architecture.mjs exports the diagram in the LadingLens palette. There is more detail in docs/references/architecture.md, docs/references/ai.md, docs/references/cloud.md and the API reference.
- Languages: TypeScript 6 for the web app and Python 3.12 for the API.
- Frontend: React 19, React Router 7, Vite 8 and Hugeicons, with Archivo and Martian Mono self-hosted.
- Backend: FastAPI, SQLAlchemy 2 (async, on asyncpg), Alembic, Pydantic Settings, PyMuPDF, openpyxl and python-docx.
- Data: PostgreSQL 16, and Google Cloud Storage for source documents and submission artifacts.
- AI and services: Gemini 3.5 Flash through
google-genai, and TypeSafe Jevjev-1.13.0throughtypesafe-sdk0.7.0. - Infrastructure: Cloud Run, as a service and a migration job; Artifact Registry and Secret Manager; and Docker.
- Tooling: Bun builds the web app and uv manages the API. Vitest and Testing Library test the web app, and Ruff and pytest lint and test the API.
This runs LadingLens locally, with the API on port 8080 and the Vite dev server in front of it, and needs no AI keys unless you want live /judge checks. See the demo runbook for more.
-
uv — installs the pinned Python 3.12 and the API's dependencies.
-
Bun — installs and builds the web app.
-
PostgreSQL 16 — for anything past the bare health check. With Docker, this starts one that matches the commands below:
docker run --name ladinglens-postgres -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=averis -p 5432:5432 -d postgres:16
-
Docker — only to run PostgreSQL as above or to build the full container image.
-
Clone the repository.
git clone https://github.com/M1KUAPP/LadingLens.git cd LadingLens -
Configure, install and migrate the API.
cd apps/api cp .env.example .env uv sync export DATABASE_URL=postgres://postgres:postgres@localhost:5432/averis uv run alembic upgrade head
Set the same
DATABASE_URLinapps/api/.envtoo. The server reads it from.env, butalembicnever reads.env. It takesDATABASE_URLfrom the shell, and without it falls back toalembic.ini's local default.All settings live in
apps/api/.env. The example file lists every one, and no value in it is a secret.Variable Needed for DATABASE_URLEverything past /api/health. A Neon-stylepostgres://URL is rewritten forasyncpgautomatically.GEMINI_API_KEY,GEMINI_API_KEY_2Live Gemini extraction on /judge. The second key is tried only after the first hits a rate limit.TYPESAFE_API_KEYLive Jev decisions on /judge.GCS_BUCKETDurable object storage. When unset, objects are kept in memory. GEMINI_MODEL,JEV_MODEL,DATA_POLICY,RULE_VERSIONLocked values. Keep them as .env.examplehas them.Without the AI keys, the seed baseline still works in full. A
/judgecheck fails closed with "Live AI checks are not configured on this server." -
Start the API.
uv run uvicorn app.main:app --reload --port 8080
At startup it builds the seed baseline: the real pipeline, replayed over the checked-in 520-email synthetic bundle, with prepared decisions in place of provider calls.
http://localhost:8080/api/health/readyreports whether the database is reachable. To rebuild the prepared data, run these fromapps/apiin order:uv run python scripts/build_seed_decisions.py(the decisions file),scripts/build_expected_shipments.py(the expected-shipment ledger) andscripts/build_web_fixtures.py(the web app's fixtures). -
Start the web app in a second terminal. Then open the URL Vite prints (
http://localhost:5173by default). Vite proxies/apito port 8080.cd apps/web bun install bun run dev -
Or build and run the full container, exactly as deployed. Run these from the repository root.
docker build -t ladinglens . docker run --rm -p 8080:8080 --env-file apps/api/.env \ -e DATABASE_URL=postgres://postgres:postgres@host.docker.internal:5432/averis \ --add-host=host.docker.internal:host-gateway ladinglensInside the container,
localhostis the container itself. The-eflag overridesDATABASE_URLfor the container only, soapps/api/.envstill works for step 3.--add-hostmakeshost.docker.internalreach your machine on Linux as well. The app is then athttp://localhost:8080. -
Run the checks. From the repository root, after
bun install, this runs Prettier, the API's Ruff checks and tests, and the web app's tests and build. The PostgreSQL integration tests run only whenTEST_DATABASE_URLis set, for example topostgresql+asyncpg://postgres:postgres@localhost:5432/averis.bun run check
See open issues for a full list of proposed features (and known issues).
Made with contrib.rocks.
See LICENSE for more information.
- Averis x Monash Hackathon 2026 — Averis and the organizers, MUMTEC and GDG on Campus at Monash University Malaysia, for the challenge and the synthetic dataset.
- Google Gemini — Gemini 3.5 Flash, which reads field values from scanned PDFs and documents whose local parse is ambiguous.
- TypeSafe — Jev
jev-1.13.0, which decides email categories, document roles and textual field equivalence. - archify — architecture diagrams.
- Hugeicons — the web app's icons.
- PyMuPDF — Dependencies keep their own licenses. The third-party notices list them, including PyMuPDF's AGPL-3.0 terms.
- Shields.io
- contrib.rocks








