Taking a multi-tenant finance agent into production
An AI accounting platform's agent reads invoices and bank statements, reconciles them, and posts the results into each client's ERP. We built the production system around the model, so it can be trusted with real money for many companies at once.
The problem
The prototype answered questions about a company's books convincingly. Production asked for more: many client companies in one database, real invoices and bank statements in every format, writes into each client's own ERP, and finance teams who need to know exactly what the agent did and why. The existing row-level security turned out to be inert, and nothing structural stopped the model from acting on its own.
What we built
The backend, integrations, and interface around the agent. The model reads documents and proposes records; deterministic code computes every amount and tax line; a single committer writes to the client's ERP only after a person confirms. Tenant isolation moved into the database, and every stage is observable.
How it works
- Human-gated writes: the model has no code path to the ERP. It drafts a proposal, a person confirms it, and one deterministic committer performs the write.
- Document reading for invoices, bills, and bank statements across PDFs, scans, and spreadsheets, with guards that catch misread fields, such as a date read as an amount, before a draft is shown.
- Odoo and Zoho Books behind one interface and one write path that is idempotent and reads back what it wrote, including bank reconciliation across currencies.
- Forced row-level security in Postgres and a least-privilege application role, so the agent's connection cannot see another client's rows.
- Durable background jobs with state in Postgres and a queue for delivery, so syncs and long extractions survive restarts without posting twice.
- A model gateway in front of every LLM call, Prometheus and Grafana monitoring, and CI gates on schema, API contract, and tenant isolation.
The result
An agent that can be given real work. It drafts invoices, bills, and reconciliations from the documents a finance team already has, and nothing reaches a ledger until a person confirms it. Isolation between clients is enforced by the database, and CI checks those guarantees on every pull request.
The full write-up
This page is the summary. Each part of the system has its own write-up, with the code and the failure that shaped it:
- Multi-tenancy for finance agents, and why row-level security has to be forced: how cross-client access became impossible at the database, and the CI gates that keep it that way.
- The tenant fallback that served one company's books to another: the integration bug that isolation alone did not catch.
- One interface, three ERPs that agree on nothing: putting Odoo and Zoho Books behind a single write path.
- Postgres holds the state, the queue holds the delivery: background jobs that survive restarts without doing the work twice.