Accounting infrastructure for AI agents
An AI agent doing bookkeeping needs a ledger, not an accounting API. An API accepts what the caller sends. A ledger decides what may be recorded, refuses the rest, keeps history that cannot be rewritten, and logs what actually ran — which is what makes an occasionally wrong caller safe to give write access to.
The current answer to "can an AI do bookkeeping" is a connector: an MCP server or an API wrapper that lets an assistant read and write the accounting software you already pay for. That is useful, and it inherits whatever the underlying product assumes about who is calling it, which is a careful human who understands the consequences of what they are clicking.
A language model is not that caller. It is fast, tireless, usually right, and occasionally confidently wrong in ways that look exactly like being right. Software written for a careful human and then exposed to a model is software with the wrong threat model.
These pages describe the alternative: designing the storage layer on the assumption that the caller cannot be fully trusted, and letting the constraints do the work that supervision would otherwise have to do.
What is actually enforced
- Ledger invariants are enforced by Postgres triggers, not by application code.
- Journal entries and journal lines reject UPDATE and DELETE, and reject TRUNCATE.
- A reversal must be in the same book as the entry it reverses, and a reversal cannot itself be reversed.
- Row-level security is FORCEd on every table holding user data.
- The server refuses to start serving over a database connection whose role can bypass row-level security.
Why an API is the wrong shape for an agent
An accounting API is a remote-control interface for a product with a screen. Its safety properties are the ones a user interface provides: a confirmation dialog, a form that will not submit, a page that shows you what you are about to do. Strip the interface away and hand the endpoints to a model and most of those properties disappear, because they were never in the API.
What is left is validation of shape rather than validation of meaning. The request is well-formed, the amounts are numbers, the ids exist — and the entry it produces can still be nonsense.
What an agent-facing ledger has to guarantee
Four properties, and the useful part is where they are enforced. Not in a prompt asking the model to be careful, and not in application code a future refactor can route around — in the database, where every path to the data has to go through them.
- Balanced or rejected. An entry whose debits and credits disagree, or that has fewer than two lines, raises an exception at insert time.
- Append-only. Updates and deletes on journal entries and journal lines are refused outright, so a mistake is corrected by a reversal that stays on the record rather than by rewriting what happened.
- Periods stay closed. Once a period is locked, an entry dated inside it cannot be posted — not by you, not by your assistant, not by a later import.
- Locks only move forward. A lock cannot be walked backwards to reopen a month that has already been closed.
The division of labour
The model supplies judgement: reading a bank statement, recognising that a payment to a merchant processor is a fee rather than a purchase, noticing that a charge looks unlike the last eleven from the same vendor. This is genuinely hard and genuinely what language models are good at.
The ledger supplies certainty: arithmetic that balances, history that persists, and a record of what was done. This is genuinely easy for a database and impossible to guarantee from a model, no matter how the prompt is worded.
Keeping those two jobs in the two places that can actually do them is the entire architecture. Everything else on these pages is a consequence of it.
What this does not cover
- Constraints check structure, not meaning. An entry can be perfectly valid and still wrong about what a transaction was.
- Categorizing both sides of a transfer between your own accounts produces two valid entries for one movement, and no invariant catches it.
- None of this makes an agent accurate. It makes an inaccurate agent survivable.
The rest of this cluster
Common questions
- Why not just prompt the model to keep the books balanced?
- Because it will usually comply, and "usually" is not a property you want in the place your financial history lives. A constraint in the database holds regardless of how the request was phrased, which model made it, or whether the prompt was truncated.
- Is this different from an accounting MCP server?
- Most accounting MCP servers wrap a product you already subscribe to, so the protocol is a new interface onto an existing account. Here the ledger itself is what the assistant connects to, with no subscription underneath it. Both are legitimate; they solve different problems.
- What stops an agent from reading someone else's books?
- Row-level security, forced on every table, scoped by a user id set inside the transaction. Not a filter in application code that a missing WHERE clause could skip.
Checked against the implementation on 2026-08-09.