Problem
Accounting software assumes an accountant is already there
Every company we operate has the same problem in a slightly different shape: money arrives in a bank statement, the reason it arrived lives in a CRM, and someone has to decide which ledger it posts to. The existing tools either demand that decision from a trained accountant on every single line, or guess at it with rules so brittle that a renamed narration breaks the month.
The tempting fix is to point a language model at the bank statement and let it post. That fails for a reason that has nothing to do with model quality: a misposted transaction does not look like an error. It looks like a balanced book with the wrong story in it, and it surfaces weeks later as a trial balance that won't tie. The cost of a wrong answer is not a bad answer — it is an audit.
Architecture
One posting path, and the AI is nowhere near it
Everything that moves money goes through a single service with an idempotency key on (reference, event_type). Nothing else in the codebase writes a journal entry. That one constraint is what makes the rest safe: the AI layer can be as speculative as it likes, because it can only ever produce a suggestion that a rules engine and a human confirm before the posting path ever sees it.
Tenancy is physical rather than logical — a separate PostgreSQL database per company, with its own migration tree distinct from the platform's. Client-specific behaviour lives in adapter modules under an integrations tree, and core is forbidden from importing client code in either direction. When a client needs a new field, it goes in a client-scoped link table keyed by that client's own identifiers, never as a column on a core model.
The core stayed generic across four very different companies because client logic was never allowed to touch it — only to sit beside it.
Statement ingestion is its own layer: institution-specific adapters for Axis, HDFC, ICICI, Kotak, RBL, IDFC First credit cards and Scapia Federal, a universal parser for everything else, and a detector that identifies the format before any parsing is attempted. Password-protected credit card PDFs are handled in the same flow.
AI components
Four tiers, ordered by what they cost
- Language detection — Hindi and Indian-language input is routed differently from English from the first hop, because a translation round-trip loses exactly the accounting nouns that matter.
- Native intent classification — a deterministic classifier resolves most requests outright. Only results below a 0.7 confidence threshold are allowed to escalate.
- Slot extraction — dates, amounts, ledger names and entity ids are pulled out as typed arguments, so the executor acts on structure rather than on generated text.
- Constrained execution — the agent selects from a fixed catalogue of real API operations, each permission-checked against the calling user. It cannot compose an arbitrary call.
- Tiered model fallback — tier one is a free India-native reasoning model; Claude Haiku is the paid fallback and fires only when tier one fails to return valid structured output. That call costs roughly $0.0001.
- Transaction classification — a dedicated path suggests the posting type for a single bank narration in the confirm modal, and returns a null suggestion rather than a guess when it cannot decide.
- Learned pattern memory — every human-confirmed decision is stored as a reusable pattern. The largest tenant has accumulated 116 of them, and each one permanently removes a transaction shape from the model's workload.
Every model call is capped — 500 characters of user message, the last two conversation turns, an eight-second timeout — and every one is required to return JSON against a schema. Invalid JSON is treated as a failed call, not as an answer. On any failure at any tier, the system returns the rules-based result. No AI path in Arthastra is a hard dependency.
What it runs today
Four companies close their books on it
Arthastra is the accounting system of record for four operating companies, each in its own database. Its largest tenant has posted 6,139 vouchers across 15,451 journal legs in the current financial year to date, against 1,078 ledgers, with 424 invoices issued and 4,653 bank statement rows ingested and classified.
Beyond posting, it runs the statutory layer: a GST engine with GSTR-1, GSTR-3B and GSTR-2B reporting, TDS reports, trial balance and Tally migration paths, financial year closure with voucher locking, scheduled backups, and a reconciliation hub that matches bank lines against CRM records. It is a monorepo of roughly 70,000 lines of Python across 225 backend modules and 125,000 lines of TypeScript, at 3,105 commits.
6,139Vouchers, FY to date
15,451Journal legs
1,078Ledgers
4,653Bank rows classified
Outcome
The classification work stopped being a person's job
The measurable result is not an accuracy score. It is that bank statement classification moved from a line-by-line manual task to a confirm-or-correct one, and that every correction makes the next month cheaper — learned patterns absorb the recurring shapes, so model spend falls as a tenant matures rather than scaling with it.
The structural result matters more. Because the AI layer was built as a suggestion engine sitting outside the posting path, we have changed models, added a tier and rewritten the classifier without a single migration to the ledger. The accounting core has never had to trust the model, so it has never had to be corrected for one.
Arthastra is also the reason the TourTripX and UnlistedZone systems exist in the form they do — both are adapters onto this core rather than separate accounting stacks.