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 Attri has open-sourced OpenLedger, a local-first, double-entry accounting system with an MCP server, designed to provide immutable transactions and an audit log that lives in the same database transaction as the mutation. The system enforces double-entry atomicity, integer minor units, and immutability via contra posting, exposing 15 tools over MCP for AI agents. 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 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. 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.