The idea
Small businesses buy the same hundred things every month — cases of cups, filters, packaging, parts — and they check the invoice total, not the unit price. Suppliers know this. This is a service you forward invoices to: it extracts every line item, matches it to the same item on previous invoices, and keeps a price series per SKU per vendor. Once a week it sends one email: these six items cost more than they did last quarter, here's the per-unit delta and what it adds up to annually.
Why build this
A cafe with forty recurring line items and a 3% margin loses more to unnoticed price drift than to almost anything else it actively manages. Accounting software records that you paid the invoice; it does not tell you the box of gloves went from $18.40 to $22.10 in three increments. Spend-analytics vendors solve this for companies with a procurement department and a six-figure budget, and nobody solves it for the 9-person business whose invoices arrive as PDF email attachments.
The reason to build it now is that line-item extraction stopped being the hard part. Two years ago this meant per-vendor template rules that broke whenever a supplier redesigned their PDF. A vision-capable model now reads an unfamiliar invoice layout well enough that the work shifts to the genuinely interesting problem: deciding that "GLV-NITR-M CS/1000" and "Nitrile Gloves, Medium (case)" are the same thing.
Stack sketch
- Ingest: a per-tenant address (
acme@in.yourapp.com) via Postmark inbound webhooks, plus drag-and-drop upload for the ones that arrive by post and get scanned. - Extraction:
pdfplumberfor PDFs with a text layer,ocrmypdf+ Tesseract for scans, then Claude Sonnet 5 through the Messages API with a tool-use schema that forces{description, sku, qty, uom, unit_price, line_total}per row. Reject any parse where the line totals don't sum to the invoice total within a cent — that one check catches most hallucinated rows. - Matching: Postgres with
pg_trgmfor fuzzy description matching andpgvectorover embedded descriptions for the cases trigrams miss. Anything below a confidence threshold goes to a confirm queue instead of silently merging. - App: Django + HTMX, Celery and Redis for the parse queue, Postgres for everything. Price history is a materialized view refreshed on ingest.
- Alerts: weekly digest through Resend; optional Slack webhook.
Scope for v1
In: email ingest, line-item extraction, the confirm queue for uncertain item matches, a price-history chart per item, and the weekly increase digest with an annualized dollar impact.
Out, deliberately: QuickBooks and Xero sync, purchase-order matching, multi-currency, approval workflows, a mobile app, and anything that writes back to accounting. V1 is read-only and lives entirely beside the books — that's what makes it a ten-minute setup instead of an integration project.
Where it could go
The obvious second act is benchmarking. Once a few hundred businesses have normalized item catalogs, you can answer "you pay $22.10 for this case; the median in your region is $19.40" — anonymized and aggregated, and far more valuable than your own history alone. That's the feature that turns a $29/month utility into something a buyer will not cancel.
After that, a negotiation packet: one page per supplier showing every increase they took in eighteen months, the effective annual rate, and the competing quote you pasted in, ready to send. The same price series also supports menu and quote repricing — if your input costs moved 4%, which of your own prices are now underwater? That question has a much bigger dollar figure attached than the one you started with.
Watch out for
Unit of measure is where this breaks: a supplier who switches a SKU from "each" to "case of 24" produces a 24x apparent spike, and one false alarm like that costs you the user's trust in every alert after it. Normalize to a base unit at extraction time, flag UoM changes as their own event rather than a price change, and never let an unreviewed match feed the digest.