DEV Community

Mohammed Arshad Ansari
Mohammed Arshad Ansari

Posted on Originally published at hikmahtechnologies.com

A Ledger Is Not a Database Table

For months, my finance agent read the first 200 characters of each email and called it accounting.

It was not lying, exactly. It genuinely did classify email, genuinely did track subscriptions, and genuinely did produce a monthly spend figure. The figure was just wrong, and nothing in the system was capable of noticing.

This is the second of three posts about rebuilding lanes of AEGIS, my self-hosted agent platform. The first was about alerting; the third is about knowledge. All three converged on the same rule, and this is the lane where getting it wrong shows up as a wrong number.

What "working" actually looked like

Measured on production on 2026-09-05, over the data since 1 July:

  • The extractor never saw the email. The fetch pulled each message in full, then kept snippet[:500] — Gmail's preview line, which runs to about 200 characters, so the cap never even bit. Downstream, that snippet was read back as if it were the message body. Of 61 emails judged to be receipts, 30 had an amount. Of 35 recurring-charge rows, 19 carried amount_cents = 0.
  • The richest stream was discarded on purpose. A list of bank sender addresses existed to stop bank alerts minting fake subscriptions — correct for a subscription tracker, catastrophic for a finance agent. In 30 days, from one mailbox, that list threw away 46 UPI debit alerts, plus IMPS transfers, UPI credits, card spends, a credit-card statement and an inbound international remittance. None of it was recorded anywhere.
  • Nothing dated reached me. Sitting unactioned in the inbox at that moment: a credit-card statement due in two days, an advance-tax instalment due in ten, a declined subscription payment with a fix-by date, an electricity bill, and an "AWS past due". Tasks created: zero.
  • What it did send was noise. 73 Anomaly: ? Apple-shaped tasks in nine weeks, most with no amount. 27 chat pings in 30 days about the same four charges. The same "what is this vendor?" question asked six times, because vendor-name variants produced different dedupe keys.
  • The monthly total was fiction. One electricity account appeared as three vendors, so "total monthly burn" counted it three times. A client's own supplier invoices, sitting in a work mailbox, counted as my subscriptions.

The plumbing was in perfect health: 242 extraction calls in 30 days, zero failures, 212 completed workflow runs. Every dashboard was green and every number was wrong. That is the specific failure mode of agent systems, and it is why I now measure the output rather than the pipeline.

The decision: hledger is the record, Postgres is the index

The rebuild starts with one structural choice. The book of record is a plain-text double-entry journal — hledger — in a private git repo. Postgres holds an index over it for fast queries.

Three properties a table does not give you by default:

Every write is a diff. A journal entry is a few lines of text in a git commit. I can read it, a reviewer can read it, and git log is the audit trail I would otherwise have had to build.

The arithmetic is checked by something that isn't me. Every write runs hledger check --strict, and a failure reverts exactly the paths that write touched. An entry that does not balance, or that uses an account nobody declared, does not land. No amount of LLM confidence gets past a tool that does arithmetic.

The index is disposable. The rule in the codebase is blunt: never treat an amount in the index as authoritative — run hledger. That means an index bug is a display bug, not a financial one, and the whole index can be rebuilt from the journal whenever I want.

The mechanics are ordinary and worth stating anyway: core and worker share one checkout, serialised by a file lock, so every write goes through one module. A hand-rolled file write would skip the strict check and its revert — so there is exactly one writer, and the rest of the system asks it.


This is the first part. The full post — including the rest of the working details — is on my site: A Ledger Is Not a Database Table

Top comments (0)