Building a Ledger for AI Agents: OpenLedger's 15 MCP Tools, Immutable Transactions, and Why the Audit Log Lives in the Same DB Transaction as the Write
TL;DR Today Attri is open-sourcing OpenLedger - a local-first, double-entry accounting system with an MCP server on top. Python 3.11+, SQLite, aiosqlite. 15 tools exposed over MCP. Immutable transactions. Corrections via contra posting. Every mutation writes an audit-log row in the same DB transaction as the mutation itself. Apache 2.0. Below: the architecture, the four invariants we enforce in code, the MCP tool surface, a working walk-through, and the tradeoffs we deliberately made (single-currency v1, no ORM, no admin UI). Repo: https://github.com/Attri-Inc/open-ledger Why this exists If you've ever tried to put an AI agent on a financial workflow, you know the shape of the problem. The agent works fine. The agent gets fast. And then someone with an auditor hat on shows up and asks "prove what the agent actually did." The audit trail in most accounting stacks lives in a different system from the write. Datadog. Splunk. A Postgres logs table nobody reads. When the write succeeds and the log fails - or vice versa - you have a gap that a real auditor will find in five minutes. OpenLedger's whole design is organized around one guarantee: you cannot have a successful mutation without a matching audit-log row, because both live inside the same SQL transaction. Either both commit or both roll back. There is no other outcome. Architecture at a glance โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ SQLite ledger โ โ accounts ยท transactions ยท โ โ entry_lines ยท audit_log ยท settings โ โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโ โ โผ MCP server (stdio / SSE :8791) 15 tools - reads + safe writes โ โผ Claude Desktop, Claude Code, agent frameworks Layering: src/ โโโ domain/ # pure constants + typed errors (no I/O) โโโ infrastructure/ # DB connection, Unit of Work, id/clock helpers โโโ repositories/ # protocols.py - narrow Reader/Writer contracts โ # sqlite.py - the only code that writes SQL โโโ services/ # accounts ยท ledger ยท reports ยท audit ยท query โโโ serialization.py # response/error envelope helpers โโโ container.py # composition root โโโ mcp_server.py # thin MCP transport adapter run_mcp.py # stdio entry point The four invariants (enforced, not documented) Double-entry, atomic, at the write path Every transaction has โฅ 2 entry lines. sum(debits) == sum(credits). Enforced inside a single DB transaction. If the balance check fails, the whole insert rolls back. There is no "post now, validate later" mode. Integer minor units (cents). No floats. Every amount is an integer. 25000 is $250.00. 500 is $5.00. If you've ever debugged floating-point rounding in a general ledger, you know why this matters. Decimal types are fine in principle; integer minor units are unarguable in practice. Immutability. Corrections via contra posting. Transactions and entry lines are never updated or deleted. If a transaction is wrong, you post a contra transaction that reverses it. reverse_transaction(txn_id) is the tool. UPDATE and DELETE on transactions or entry_lines do not exist in the API. Audit log in the same DB transaction as the mutation async with unit_of_work: await ledger.post_transaction(...) # mutation await audit.append(...) # audit-log row await unit_of_work.commit() # both or neither Either both writes commit, or both roll back. The audit log cannot lag behind reality. The mutation cannot slip past the audit trail. This is the SOX-grade guarantee. The MCP tool surface (15 tools) โข Accounts: list_accounts, get_account, get_balance, get_account_ledger, create_account โข Journal: get_transaction, search_transactions, post_transaction, transfer_funds, reverse_transaction โข Reports: get_trial_balance, get_profit_loss, get_balance_sheet โข Audit: get_audit_log โข Escape hatch: run_query (SQL SELECT only) - for questions we didn't anticipate Walk-through: from clone to your first query git clone https://github.com/Attri-Inc/open-ledger cd open-ledger python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt python scripts/seed.py # bootstrap fresh dev DB Connect it to Claude Desktop / Code: python run_mcp.py # stdio transport (default) # for Claude Code: claude mcp add openledger -s user -- \ /absolute/path/to/open-ledger/.venv/bin/python \ /absolute/path/to/open-ledger/run_mcp.py Once connected, ask Claude questions in plain English: โข "How much cash do we have right now?" โข "Show me the P&L for January." โข "Are the books balanced?" โข "Post a $250 cash sale for today." โข "Move $500 from Wallet A to Wallet B." โข "What was reversed recently, and why?" Design decisions worth naming (and defending) Why not an ORM? Considered SQLAlchemy. Passed. Two reasons: (1) The invariants (debits == credits, atomic audit-log write) are enforced inside a single SQL transaction that's much easier to reason about without an ORM's implicit session semantics. (2) Swapping SQLite for Postgres should be a repository-file change, not a full ORM migration. Why SQLite, not Postgres from day one? SQLite is embedded, zero-config, most-deployed database in history. For a single-tenant, single-node ledger - which is the shape of most finance-team-owned deployments - it is not a compromise. It is the right choice. The repository layer is designed so that swapping in Postgres touches two files. Why Apache 2.0, not MIT or AGPL? โข AGPL scares enterprise legal reviewers. Makes commercial embedding legally fraught. โข MIT is fine but weaker on patent grants. โข Apache 2.0 gives enterprise-legal acceptance without the AGPL copyleft trap. Why MCP, given how young the spec is? MCP is early. Anthropic-originated. There is real risk. We bet on it anyway because it's the first serious attempt at a standard for how an agent talks to a system it doesn't own. If MCP turns out to be a dead-end in 18 months, the ledger and its business rules survive; the MCP server is a ~100-line adapter. What's not in v1 (deliberately) โข Multi-currency. Single-currency only for now. โข Postgres backend. SQLite is day-one; Postgres is roadmap. โข Admin UI. Everything is MCP or SQL. โข Document ingestion pipeline. Bring your own invoice-parsing / OCR / receipt-extraction. โข Tax / GAAP / IFRS modules. Not core. Layer on top. Try it Four commands to a running ledger. Two commands to connect it to Claude. Ten seconds to your first "how much cash do we have?" question. Repo: https://github.com/Attri-Inc/open-ledger If you build something interesting on top of it, please share. If you find a bug, please file. If the invariants feel wrong, please argue with us in an issue - we'd rather have that conversation now than after ten teams have built on top of a bad primitive. Top comments (0)
Comments
No comments yet. Start the discussion.