DEV Community

Koustubh
Koustubh

Posted on

Reading Bills and Statements Without AI, and the Money They Found

Part of gharasathi, a private household AI. Start here for all the series.

Bank data tells you what left the account. It rarely tells you why. A direct debit to the childcare centre doesn't show the fee, the subsidy, or which days were absences. An energy debit doesn't show how many kilowatt-hours you used, or that the price changed on 1 July. That detail lives in PDFs attached to emails.

This part covers how Hishob reads those documents, and why it deliberately doesn't use the LLM to do it.

Getting the documents: labels, not inbox access

Documents pipeline

  • Gmail filters apply labels such as gharasathi/utilities and gharasathi/childcare. The nightly job (aapla-patrakar) reads only those labels.
  • It reads without changing anything. IMAP EXAMINE opens the mailbox read-only, and BODY.PEEK[] fetches messages without setting the \Seen flag. A test runs the client against a fake IMAP server and fails if any command could change the mailbox.
  • A drop folder. Any PDF dropped into Nextcloud's Gharasathi/Inbox is read too, then filed as Statements/<type>/<vendor>/<date>-<type>.pdf. Exact duplicates are detected by SHA-256.
  • Re-reading. When a parser improves, every filed document is read again the next night, because each extraction records the parser version that produced it.

Ingestion doesn't care what a label is for. The labels map to consumers, and "statements" is just the first. Reminders from email could be a later one.

Why parsers instead of the LLM

A 122B model would read a utility bill fine 99% of the time. For money, I want to know about the other 1%.

aapla-olakh runs pdftotext -layout and then a hand-written parser for each document type: an energy retailer's electricity, gas and broadband bills, the water utility's bills, and our childcare centre's statements. Each parser checks the document against itself:

  • usage tiers × rates = the charges, and the tiers add up to the meter reading;
  • charges + credits = the bill total;
  • the childcare statement's running balance, fee − subsidy = gap for every day, the summary totals, and the amount due.

If anything doesn't add up, it's a warning, not a guess. All 120 real documents (87 childcare statements, 17 energy and broadband bills, 16 water bills) now parse with zero warnings. Getting there took fixes for column layouts that changed between years, a step label wrapped onto its own line, and the subsidy column being blank before the subsidy was approved.

Every extracted field keeps the exact line it came from. When anna or the dashboard explains a debit, it quotes the statement.

Matching documents to the bank

  • A bill matches a debit when the amount is exactly equal and the debit falls between 7 days before and 14 days after the due date.
  • Childcare uses each statement's own "payment" lines, and the latest statement wins for each day of care.
  • Reversed debits are tracked: "returned by the bank, then paid a week later" is a status, not two unrelated transactions.
  • Debits to these vendors with no document are listed as "payments with no bill on file", with a note on how to add the missing bill.

Bills and statements, amounts hidden

What the documents found

The most useful finding, the childcare absence allowance from part 1, came from a single line the parser reads off every statement: YTD Absences at service NN. In the bank data it was only a run of unusually large debits in March. The detector compares the year's count with the 42-day allowance, warns when it gets within 12 days, and projects where the year will end.

Other findings, all from documents plus the bank:

  • The childcare subsidy passed on was less than the government's entitlement for two short periods. The statement prints both figures, so the gap is exact.
  • The CCS rate changed five times in two years. Each step is shown with what it adds to the gap per fortnight.
  • Water use was up 34% on the same quarter last year. The bill prints last year's daily average, so this needs no estimate.
  • Prices changed on 1 July: electricity per kWh went down 8% and water went up 7%. The bill checks itself, so the rates are exact.
  • Sixteen payments had no bill on file, mostly broadband. Those are the bills to forward next.

Childcare fees by month: the subsidy (light) and what we paid (solid)

Read from 87 statements. As the subsidy rate stepped down, our share of each month's fee grew. Amounts and rates are removed for the screenshot.

Electricity, gas and water use per day against the same time last year

Each bar is one bill's use per day, and the tick is the same period a year earlier. Figures are removed for the screenshot.

Cutting spending: what actually held up

Combining the documents with the bank data and the analyst, the cuts that held up when I checked them myself were:

  • Dining crept up about 15% between two half-years.
  • Car insurance renewed 13% higher than the previous renewal: shop around.
  • Broadband went up 7% mid-year: compare plans.
  • A gym membership charged weekly: keep it only if it's used.
  • Transfers labelled "family support" included flights and food delivery, so dining and travel are bigger than they look. Re-labelling them from the transfer memo is next.
  • The mortgage: not a refinance (part 2), but keeping more cash in the offset.

None of these needed a cloud service, and none of the documents left the house.

What's next

More document types (insurance renewals, kids' activity invoices), and an energy plan comparison that uses our real usage from the bills. Beyond that, money is only one kind of household record. The same rules apply to any other kind: the data stays home, code does the counting, and the model explains.

Top comments (0)