Procurement Intelligence
Every supplier offer read, normalized and scored — instead of the handful a buyer had time to open.
- Role
- Architect & sole engineer
- Client
- a European B2B cosmetics distributor
- Scale
- 321 commits · ~86K LOC · built solo
Client system. Described here at the level of the problem, the architecture and my contribution. No source, no screenshots, no client data.
The same architecture — the confidence gates, the weighted scoring, the review queue — is running and screenshotted in OfferGrade, a demo built over invented data.
The problem
A distributor buys from around a hundred suppliers. Every one of them emails price lists — daily, weekly, whenever stock moves. The formats have nothing in common: Excel workbooks with the header row four rows down, PDFs, prices typed into the body of the message, columns labelled in German, French, Italian or Polish, and no agreement anywhere on whether a number is a net price, a recommended retail price, or something quoted only above a €75,000 order.
A buyer opens the ones they have time for. That is the whole system. Everything they don't open is a decision made by accident.
The question was not "can this be automated" — it was whether the messy half could be automated reliably enough to be trusted, because a normalizer that is right 80% of the time is worse than no normalizer at all.
What I built
A two-phase ingestion pipeline and a scoring engine behind an operator dashboard.
Phase 1 — ingestion. MSAL device-flow authentication into Microsoft Graph, incremental mailbox scanning with last-run state so a re-run doesn't reprocess a year of mail, SHA-256 content deduplication, and supplier attribution inferred from the sender domain — including the awkward case where supplier A forwards supplier B's list and the attribution has to survive the forward.
Phase 2 — normalization. A 3,245-line multilingual normalizer that finds the header row (or works without one), maps arbitrary column names onto a fixed schema via fuzzy matching, extracts EAN / REF / brand / price / currency / pack size, and classifies which of several price columns is actually the one you can buy at.
The scoring engine. Every normalized line is weighted against supplier price rank, delta from the last purchase, resulting margin, and a data-confidence score, then emits one of four actions — BUY, NEGOTIATE, REVIEW, IGNORE — with an override rule for offers confirmed as cheapest. A separate matcher cross-references live client requests against fresh offers, so when a customer asks for four hundred units of something, the buyer sees who can fill it and at what margin that same day.
The part that mattered most
Confidence, not extraction.
The easy failure mode is a pipeline that produces a clean-looking number from a column it misread. A price that is really a weight, a figure that is really RRP, a currency nobody converted — each of those reads as the best deal on the board until a human checks. So every extracted field carries a confidence value, low-confidence lines are routed to review rather than scored, and anomalies are surfaced explicitly instead of being silently averaged away.
That single decision is why the output gets used.
Architecture and delivery
- Backend — Python 3.11 / FastAPI, modular routes over a layered pipeline orchestrator; pandas, openpyxl and xlsxwriter for the spreadsheet layer; rapidfuzz for header and brand matching; Pydantic throughout.
- Auth — JWT with bcrypt, Azure AD for SSO, role-based access control over the operator surface.
- Storage — started on SQLite, migrated to PostgreSQL as volume grew; S3 via boto3 for the raw file archive.
- Frontend — Next.js 14 App Router, TypeScript, Tailwind, TanStack Table for the offer grid, Recharts for the analytics.
- Deployment — Railway and Vercel, with CI running ruff, mypy, pytest and eslint.
Designed, built, deployed and documented single-handedly across 321 commits and roughly 86,000 lines.
Where it got to
Running daily. Approximately 188,000 supplier offers scored across 112 suppliers and around 123,000 distinct products, from roughly 1,843 supplier files.
The buyer's morning changed from "which of these forty emails do I open" to "here are the eleven lines worth acting on".