All work

Case study 03

Utility-Bill Extraction Engine

Scanned utility bills in, audit-ready spreadsheets out — with a confidence score that says which rows a human still needs to read.

Full Stack Developer (Contract)Simplifai IncJan 2026 - Aug 2026 · delivered
Next.js 14TypeScriptFirebaseFirestoreGoogle Cloud VisionCloud Taskspdfjs-distExcelJSOpenRouter
63%
of the codebase
21,769 of 34,602 lines · top contributor
5
utility bill formats
plus a generic fallback profile
2
models cross-checked
per-field disagreement drives confidence
11.8K
lines, extraction engine
4,513 of its 6,050-line anchor core are mine

Team project at Simplifai Inc; I was top contributor — 80 of 194 commits and 63% of surviving lines — owning the anchor engine, the two-model pipeline, and Excel export.

Context

A North American energy-cost consultancy recovers overcharges for commercial clients by auditing their utility bills. The bills arrive as PDFs, many of them scans, and every utility lays its charges out differently. Turning them into spreadsheet rows was manual work measured in hours per building.

I was top contributor on the platform that replaced that work: 80 of 194 commits, and 21,769 of the codebase's 34,602 surviving TypeScript lines.

What I built

The pipeline

Eight stages between an uploaded PDF and a validated spreadsheet.

  • Layout extraction (pdfjs-dist) pulls text with positional geometry, custom-rendered to preserve the spacing that column alignment depends on.
  • Format classification matches the bill against five built-in profiles plus a generic fallback; new formats are Firestore documents with detection keywords, cached five minutes, no redeploy.
  • Anchor recovery locates labelled values by position on the page, without calling a model.
  • Two models then extract independently and their per-field disagreement is recorded, not discarded.
  • Anchor values backfill and override model output, then normalization reconciles units, dates, and meter identity.
  • Validation applies field-range rules, routes ambiguous rows to an AI judge, then auto-accepts above 0.85 confidence and queues the rest with a computed review priority.
  • An accuracy report scores weighted field-level correctness against manually verified golden records.
  • Excel export writes per-format column templates, themselves Firestore-configurable.

Geometric anchors

The 6,050-line core of the system, and the part I own most of.

  • An anchor that finds a labelled total at a known position is free, deterministic, and returns the same answer on every run — none of which is true of a model call.
  • Anchors are defined per format and edited through an admin surface rather than a deploy.

The review surface

  • Upload with live job-progress tracking, paginated job listings, and a job-detail view with collapsible validation results and per-row accuracy scoring.
  • An export modal with column pruning and local save, so a reviewer ships the sheet without leaving the app.

The long tail of formats

Where most of the engineering actually went.

  • Continuation pages that split a single statement across sheets.
  • Multi-month and multi-meter clusters printed on one bill.
  • Duplicate anchors resolved per statement rather than per document.

Decisions & trade-offs

Anchors before the model, not after it

Geometric extraction costs nothing and repeats exactly. A confident anchor can skip the model call entirely — cheaper per bill, and the same document extracts identically every time, which is what makes a regression detectable at all.

Two models disagreeing beats one model's self-reported confidence

A single model reports high confidence on wrong answers. Independent agreement between two is a signal you can actually route on, and it is what the 0.85 auto-accept threshold sits on.

Configuration in Firestore, not in code

Anchors, export headers, detection keywords, and validation thresholds are all admin-editable at runtime behind per-provider feature flags. Every utility eventually redesigns its bill; that shouldn't need a deploy.

The failure mode worth designing against is the silent one

A utility changed its bill design mid-2025. New bills parsed cleanly, so nothing looked broken — but on the older ones the parser stopped reading early and dropped the total without raising anything. Errors announce themselves; wrong-but-plausible numbers don't.

Outcome

  • Bills that took hours of manual transcription per building now return a validated spreadsheet from an upload.
  • Every row carries a confidence score, so review effort goes to the rows that need it instead of all of them.
  • New bill formats ship as configuration rather than code, and the client's team adds them without an engineer.