Architecture v2.0

Charles Dana · Monce AI · May 2026

Two plants live: Valdosta GA and Bordeaux FR.

Request flow

                  HTTPS
                    │
          ┌─────────▼─────────┐
          │   Route53 A       │   saft.aws.monce.ai → 51.44.2.200 (EIP)
          └─────────┬─────────┘
                    │
          ┌─────────▼─────────┐
          │   nginx (443)     │   Let's Encrypt TLS, reverse proxy
          └─────────┬─────────┘
                    │  127.0.0.1:8002
          ┌─────────▼─────────┐
          │  uvicorn/FastAPI  │   systemd: saft.service, 3 workers
          │  api/app.py       │
          └─────────┬─────────┘
                    │
          ┌─────────▼─────────┐                 ┌─────────────────────┐
          │  Pipeline (7 st)  │ ──────────────▶ │  AWS Bedrock        │
          │  stages 0..6      │                 │  eu-west-3          │
          │  + plant routing  │                 │  Haiku 4.5          │
          └────┬─────────┬────┘                 │  Sonnet 4.6         │
               │         │                      └─────────────────────┘
               │         │  /matching router (APIRouter prefix)
               │         ▼
               │   ┌─────────────────────────────────┐
               │   │ matching_service.py             │
               │   │ /matching/health   /version     │
               │   │ /matching/snake    /article     │
               │   │ /matching/po       /reload      │
               │   └─────────────────────────────────┘
               │
          ┌────▼──────────────┐
          │  Snake v5.4.6     │   pure Python, in-process, lazy load
          │  6 SAT models     │   2 plants × {article, client, entity}
          └───────────────────┘
                    │
          ┌─────────▼─────────┐         ┌──────────────────────────┐
          │  s3_archiver.py   │ ──────▶ │ s3://monce-saft-archive  │
          │  fail-soft        │         │ <plant>/<yyyy-mm>/<task> │
          │  PDF + result     │         │ + models/ (versioned)     │
          └───────────────────┘         └──────────────────────────┘

Plant routing

Stage 0 scans page 1 for supplier markers. The router exposes an opaque plant string (valdosta or bordeaux) that Stage 4 uses to bind the correct PLANTS[plant] entry — articles file, clients file, entity file, and the three Snake models (data/models/<plant>/*). Adding a plant is a five-step procedure documented in the internal playbook.

PlantStage 0 markersSKUsCustomersPO format
Valdosta GASAFT AMERICA, Valdosta GA addresses, US tariff codes 9,42522Verizon Ariba / Satair / direct
Bordeaux FRSAFT BORDEAUX, A1908000 buyer ID, P\d{7} PO format 43,3322,990Saft Ferak (60-I prefix), framework POs

Pipeline stages

#StageCostTypical timeOutput
0Client + plant IDregex + 1 Haiku call< 1 s customer_id, plant routing, PO#, layout family
1Doc Analyzerregex + 1 Haiku call< 1 s page triage, layout family
2Unified Extractortext-mode (free) when totals reconcile, else 1 Sonnet/3-page chunk0.1–6 s structured JSON (mfr_pn, customer_pn, desc, qty, unit, price)
3Rules Enginedeterministic< 10 ms schedule-line merge, subtotal check, ISO dates
4Plant-aware Snake Matcherin-process SAT, plant-bound < 10 ms/linematched SKU from Mp
5Validation1 Haiku call1–2 s flags & overall confidence
6Routerdeterministic< 1 ms auto-approve / review / full review

Data layout

data/
 ├─ articles.json                    Valdosta master, 9,425 SKUs (legacy default path)
 ├─ clients.json                     Valdosta customers, 22
 ├─ saft_entities.json               Valdosta entity (SAFT AMERICA INC)
 ├─ bordeaux_articles.json           Bordeaux master, 43,332 SKUs (with 60-I synonyms)
 ├─ bordeaux_clients.json            Bordeaux customers, 2,990
 ├─ bordeaux_entity.json             Bordeaux entity (SAFT BORDEAUX)
 └─ models/
    ├─ article_matcher.json          Valdosta L=15  bucket=50  525 MB
    ├─ client_matcher.json           Valdosta                   1.8 MB
    ├─ entity_matcher.json           Valdosta                    37 KB
    └─ bordeaux/
       ├─ article_matcher.json       Bordeaux L=5   bucket=50  800 MB
       ├─ client_matcher.json        Bordeaux                  213 MB
       └─ entity_matcher.json        Bordeaux                   31 KB

Models are versioned in s3://monce-saft-archive/models/<plant>/<name>_matcher.json. The EC2 caches them locally with ETag tracking; first-touch downloads from S3, subsequent restarts no-op. POST /matching/reload hot-pulls fresh models without bouncing the service.

Deployment topology

LayerSpec
EC2r6i.large, 2 vCPU / 16 GB RAM, Ubuntu 24.04, 30 GB gp3, eu-west-3a
IAMinstance profile safran-po-ec2-profile → Bedrock + S3
AuthAWS_BEARER_TOKEN_BEDROCK env in systemd unit
Nginx443 TLS (Let's Encrypt) → 127.0.0.1:8002
Servicesaft.service, auto-restart, 3 worker coroutines, 4.3 GB heap with both plants warm
DNSRoute53 zone aws.monce.ai, A-record TTL 300, points at EIP 51.44.2.200
Certcertbot --nginx, renewal cron, valid until 2026-08-27
S3 archivemonce-saft-archive, versioned, SSE-S3, public-blocked
CI/CDGitHub Actions on push to main → rsync + restart, ~28 s

The /matching public route

The matcher is exposed as a stable HTTP surface for monceapp and other Monce services to call without rerunning the whole pipeline.

EndpointWhat
GET /matching/healthliveness, which plants/matchers are loaded, S3 store status
GET /matching/versionfull manifest, ETags, S3 VersionIds, load timings
POST /matching/reloadrefetch from S3, rebuild in-process Snake without restart
POST /matching/snakeraw text → Snake prediction across all 3 matchers for one plant
POST /matching/articlefull 5-tier cascade for one PN, plant-aware
POST /matching/pofull Stage 4 on a parsed PO — no PDF required

Extensibility