Bookkeeping by WhatsApp, without letting the parser touch the ledger.
Indian cash businesses already record their day in messages — “आज ₹25,000 cement kharida”. The hard part is not reading that sentence. It is making sure a machine that half-understood it can never write a wrong number into the books.
Financial systems · SheetFlow internal build
System pipeline
ReceiveExpress webhook
ParseDevanagari normalisation
StageStaging tables
Postjournal_entries
Internal build — no public deployment.
This is a back-office system with no public surface. It runs against a PostgreSQL database and a WhatsApp webhook, so there is no page to open — the evidence is the code and its test suite.
src/ledger/engine.ts — 438 lines
The single sanctioned write path into the ledger. Posts a journal entry, both balancing legs, both cached balances and the staging status flip inside one transaction, all-or-nothing.
src/parser/index.ts — 258 lines
Extracts amount, direction, payment mode, counterparty and category from mixed-script messages. Normalises Devanagari digits and applies Indian-scale multipliers (k, hazaar, lakh, crore) as exact BigInt paise.
src/staging/service.ts — 290 lines
The verification queue every inbound message lands in. Nothing reaches the ledger without a human confirming it.
src/parser/parser.test.ts — 21 tests, all passing
Run on 2026-08-08 with `npm run test:staging`: 21 pass, 0 fail. Four of them assert the parser refuses to guess rather than filling a field it did not see evidence for.
section handoff
The problem
What the build had to solve.
01
The people keeping these books type in three languages at once, and will not switch to an accounting app to do it.
02
A language model reading a money message will sometimes be confident and wrong, which is the one failure mode a ledger cannot absorb.
03
Rupee amounts in floating point silently lose precision, and a ledger that does not balance is worse than no ledger.
section handoff
Architecture
How the work moves through it.
Receive
Parse
Stage
Post
1
Receive
A WhatsApp webhook accepts the message exactly as sent — mixed script, shorthand amounts, no required format.
Express webhook
WhatsApp gateway
Raw message store
2
Parse
Tokens are extracted into a structured entry, and anything not clearly evidenced is reported as UNKNOWN rather than guessed.
Devanagari normalisation
BigInt paise arithmetic
Direction & mode detection
3
Stage
Every parsed entry lands in a verification queue as Pending Verification. This is a hard architectural boundary, not a workflow preference.
Staging tables
Pending Verification
Operator correction
4
Post
Only a human-confirmed entry is promoted into the double-entry ledger, as one atomic transaction that either lands completely or not at all.
journal_entries
ledger_lines
Cached balances
Chart of accounts
section handoff
The build
What it is made of.
Every row was read off the source, not from a project brief. Line counts, versions and limits are the ones in the files listed above.
Language
TypeScript, ES modules — 1,739 lines across 11 source files
Database
PostgreSQL, with money held as NUMERIC(15, 2)
Money arithmetic
BigInt paise in the parser; NUMERIC in the database. No amount becomes a JavaScript number.
Language model
Google Generative AI, for insight generation only — never on the write path
Concurrency
SELECT … FOR UPDATE row locking on the staging entry
Tests
21 parser tests via the Node test runner, all passing
section handoff
Engineering decisions
The choices, and why they went that way.
The parser is structurally forbidden from writing to the ledger
A model that misreads one message in fifty is useful for drafting and unacceptable for posting. Rather than trying to raise its accuracy to the point where direct writes would be safe, the write path was closed: parser output can only reach a staging table, and only a human-confirmed row can be promoted. The system is then allowed to be wrong, because being wrong is recoverable.
Verify: The ledger engine accepts a staging id and user-confirmed values, and re-derives nothing from the parsed row. There is no other function that inserts into journal_entries.
Money never becomes a floating-point number
IEEE-754 doubles cannot represent every two-decimal value, so a rupee amount that round-trips through a float has already lost. Amounts are parsed into BigInt paise, carried as exact decimal strings, and bound straight to PostgreSQL NUMERIC, which does the arithmetic.
Verify: Amounts are typed as `string` throughout, and the posting path validates them against a NUMERIC(15, 2) pattern before binding — leading signs and exponent notation are rejected outright.
A blank field beats a confident guess
Every parsed field is headed for a human verification queue. A field left UNKNOWN costs the operator a glance; a field confidently filled with the wrong direction gets approved by a tired person and lands in the books. The parser therefore refuses to infer a direction or a payment mode it did not see evidence for.
Verify: Four of the 21 tests assert exactly this — the parser reports UNKNOWN when both directions appear, when neither appears, and when a payment mode is unstated, and it declines to file a category it cannot support.
Two people tapping “verify” at once cannot double-post
The verification step is a message a human taps, and a message can be tapped twice. Locking the staging row for update means the second transaction blocks until the first commits, then reads the committed status and refuses — rather than interleaving past the check and posting the same entry twice.
Verify: The posting routine opens by locking the staging entry FOR UPDATE, and the status check runs inside that lock.
section handoff
Business outcome
What we can prove, and what we cannot.
Not publicly disclosed.
No business outcome has been measured against a stated method and confirmed with this client, so no figure is published. We would rather show you the system and let you judge it than print a number nobody verified.
section handoff
Questions
About this project.
What is the WhatsApp Accounting OS?
A SheetFlow internal build: a double-entry accounting engine on PostgreSQL that accepts bookkeeping entries sent over WhatsApp in English, Hindi or Hinglish. Messages are parsed into a staging queue, a human confirms or corrects each one, and only confirmed entries are posted to the ledger. It is not a deployed product and has no public URL.
How does it stop an AI mistake from corrupting the books?
The parser cannot write to the ledger at all. Its output goes only to a staging table marked Pending Verification, and the single function that writes to the ledger takes human-confirmed values rather than parsed ones. An AI misreading is therefore a queue item somebody corrects, never a wrong entry in the accounts.
What business results has the WhatsApp Accounting OS produced?
Not publicly disclosed. It is an internal build with no client engagement behind it, so there is no measured business outcome to report and none is claimed.