Overview
Haddon is an open-source double-entry ledger for AI agents. An agent loads bank statements, books invoices and accruals, files the documents behind them, reconciles each account and closes each period over an HTTP API. It comes with a web interface where people can review the books, correct entries and close periods. If you know Xero, think of its ledger, reconciliation and close, with an agent doing the data entry.
The API assumes the agent will sometimes be wrong. Every write is a named command, validated before it lands and recorded with who made it and why. Posted entries are never edited, only reversed or corrected, and a closed period refuses changes until someone reopens it. Each legal entity’s books are one SQLite file, and they import from and export to hledger journals, checked by a pinned hledger binary.
Haddon is pre-1.0. The schema and API are stable enough to build on, but we don’t make compatibility promises yet.
Try it
Section titled “Try it”You need Python 3.12 or later.
git clone <repository-url> haddon && cd haddonpython3 -m venv .venv && .venv/bin/pip install --upgrade pipmake install
# One set of books for one entity, with a starter chart of accountsbin/haddon-init var/acme.db --id acme --name "Acme Ltd" --legal-form Ltd \ --country US --base-currency USD --base-precision 2
# A configuration naming that file and one agent credentialTOKEN=$(python3 -c 'import secrets; print(secrets.token_hex(24))')cat > var/haddon.json <<EOF{ "entities": {"acme": "acme.db"}, "tokens": {"$TOKEN": {"actor": "agent:bookkeeper", "entities": ["acme"]}}, "cursor_keys": [{"id": "k1", "secret": "$(python3 -c 'import secrets; print(secrets.token_hex(32))')"}]}EOF
bin/haddon-serve --config var/haddon.json # serves on 127.0.0.1:8080In another shell, with the same TOKEN, post an invoice:
curl -s -X POST http://127.0.0.1:8080/v1/entities/acme/entries \ -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: invoice-0041" \ -H 'Content-Type: application/json' -d '{ "dt": "2026-04-01", "narration": "Invoice 0041, services for April", "reason": "the April invoice was raised on the 1st", "legs": [ {"account": "assets:receivables", "commodity": "USD", "amount": "7316.67"}, {"account": "income:sales", "commodity": "USD", "amount": "-7316.67"} ]}'The reply (201 Created, trimmed) has the posted entry, attributed to the token’s actor. Amounts go in as
decimal strings and come back as integer millionths:
{ "entry": { "id": "e-b3528913b5d84e7db6b3d25403632652", "status": "posted", "created_by": "agent:bookkeeper", "postings": [ {"seq": 1, "account_path": "assets:receivables", "commodity": "USD", "amount_micros": "7316670000"}, {"seq": 2, "account_path": "income:sales", "commodity": "USD", "amount_micros": "-7316670000"} ] }}Send it again with the same Idempotency-Key and you get the same body back with Idempotency-Replayed: true,
not a second invoice. Send "100.001" and it is refused rather than rounded, with an error a model can act on:
{"error": {"code": "precision", "message": "legs[0].amount: '100.001' has 3 decimal places; USD supports 2", "retryable": false, "details": [{"path": "legs[0].amount", "reason": "unsupported_precision", "places": 3, "commodity": "USD", "precision": 2, "value": "100.001"}], "request_id": "req-ac4bf1119ea24f4398b6af0147c7cfa7"}}Quickstart carries on from here: correcting the entry, loading a bank statement, explaining its line and closing the month.
Giving it to an agent
Section titled “Giving it to an agent”Each write in /openapi.json has its command name as its operationId, so tool definitions come straight out
of the schema. examples/haddon_tools.py does that with the standard library and
is meant to be copied into your harness:
from haddon_tools import load_tools, call
tools, routes = load_tools("http://127.0.0.1:8080", allow={"post_entry", "correct_entry", "book_line", "ignore_line"})
# pass `tools` to the model; then, for each tool call it makes:result = call(routes, tool_call.name, tool_call.input, entity="acme", token=AGENT_TOKEN, call_id=tool_call.id)It uses the tool call’s id as the Idempotency-Key, so an HTTP retry can’t post twice, and it hands refusals
back to the model instead of raising them. allow is how you keep close_period for a person if that’s your
process. GET /v1/entities/{id}/capabilities tells an agent what this installation serves, how to write amounts
and dates, and the page limits. Haddon doesn’t ship an MCP server; the
agent guide covers reads, errors and the guardrails we recommend.
What it does
Section titled “What it does”Haddon keeps books for any number of legal entities, each with its own chart of accounts, currencies and period lock.
- Bank reconciliation. The agent reads a statement in whatever format the bank provides and submits its lines. Each line is booked as a new entry, linked to entries already in the books (splits included), or ignored with a reason, and Haddon reports whether each account agrees with the bank.
- Journal entries for invoices, bills, accruals, payroll, depreciation and adjustments, posted singly or in all-or-nothing batches checked against the caller’s own counts and totals.
- Documents, registered by hash and filed against the entries they support. A booking made on incomplete evidence can be marked as awaiting a document.
- Month-end and year-end close. Before a period closes Haddon checks that every statement is loaded and every line explained, and a check that can’t tell counts as a failure. You can close over a failing check only with a recorded override.
- Tax preparation. Accounts and postings carry tax codes in the entity’s own scheme, and a separate register records recoverable tax (VAT, GST and the like) deliberately not reclaimed, with the reason and the last date a claim is possible.
- Trial balance, income statement and balance sheet with a comparative period (JSON or PDF), account registers, and an append-only audit log of every change.
The write commands are all named, and there is no generic update endpoint:
| Area | Commands |
|---|---|
| Chart | create_account, archive_account, restore_account |
| Journal | post_entry, post_batch, correct_entry, reverse_entry |
| Period | close_period, reopen_period |
| Statements | load_statement, replace_statement, book_line, link_line, ignore_line, reexplain_line |
| Documents | register_document, file_document, await_document, withdraw_expectation |
| Tax not recovered | record_unrecovered_tax, resolve_unrecovered_tax |
| Export | run_export |
| Books | initialize_entity (POST /v1/entities) |
Each one runs in a single transaction and writes an audit event with the actor, the reason and a hash of the
input. The rule against editing posted entries is enforced by database triggers as well as the API. A
correction posts an exact mirror of the original plus a replacement, so the history of what was believed and
when stays readable. Nothing in the data model tells an agent from a person: a credential is an actor string
such as agent:bookkeeper or ui:alice, and the same rules apply to both.
Concepts and the accounting model covers the accounting model in full.
How it compares
Section titled “How it compares”Haddon covers the ledger, reconciliation, close and reporting parts of a product like Xero. It doesn’t try to be the rest of it.
| Haddon | Xero and similar | |
|---|---|---|
| Data entry | an AI agent, through an API built for it | people, with rules and suggestions |
| Bank data | statements the agent reads and submits (CSV, PDF, anything) | live bank feeds |
| Ledger | posted entries immutable, every change attributed and explained | editable transactions with an audit history |
| Close | checked month-end and year-end close with recorded overrides | lock dates |
| Hosting | self-hosted, one SQLite file per entity, MIT licence | hosted SaaS |
| Invoicing, bills, payments | recorded as entries, not sent or paid | yes |
| Payroll | booked as journal entries | in some countries |
| Tax returns | keeps codes and figures, knows no country’s rules, files nothing | VAT/GST/sales tax in many countries |
| Multi-currency | per-currency balances, no conversion or revaluation | conversion, revaluation, gain/loss |
| Users and roles | a bearer token per agent, a reverse proxy for people, no roles | multi-user with roles |
| Contacts, inventory, projects | no | yes |
If you need those things, Haddon won’t replace your accounting system. It’s for when you want an agent to keep the books, with every step recorded and checkable by whoever needs to. For how it sits next to hledger, Beancount, Ledger and GnuCash, see Haddon and other open-source accounting tools.
Web interface
Section titled “Web interface”Haddon comes with a web interface where you can browse the books, review and correct entries and close
periods, if you want to. It is a React app served by the same process (make web builds it). It has no logic
of its own and goes through the same API, so anything it shows an agent can query, and everything a person does
in it is recorded under their own actor name.
The Review page (/e/{entity}/month) takes one month and shows whether the bank agrees with the books and what
still needs a decision, all of which can be finished on the page. The Inbox (/e/{entity}/review) collects
everything unfinished across periods, and Closing (/e/{entity}/close) shows each close check and is where a
period is closed or reopened. There are also pages for entries with their correction chain, accounts and
registers, and the reports.
When someone changes something already posted, the interface asks for one sentence on why the earlier booking was wrong, because that is the part nobody can reconstruct later. See The web interface.
hledger import and export
Section titled “hledger import and export”bin/haddon-import brings in existing books in hledger’s journal format, and bin/haddon-export (or
POST /exports) writes any date range back out. Exports are deterministic and must pass
hledger check --strict with the pinned binary before they are published. The oracle (bin/haddon-oracle)
loads a corpus of journals and compares the source, the store and the re-exported journal for every book. See
hledger import, export and the oracle.
Running it
Section titled “Running it”The service reads one JSON file, given by --config or $HADDON_CONFIG:
| Key | Purpose |
|---|---|
entities |
entity id to database file, one entity per file |
tokens |
bearer token to {actor, entities}; the actor is recorded on everything the token writes |
cursor_keys |
signing keys for pagination cursors, newest first |
proxy_auth |
lets an authenticating reverse proxy pass a person’s name in a header (ui:<name>) |
entity_root, blob_root, export_root |
directories for books created over the API, document bytes and exports; each feature is off until set |
HADDON_RECONCILIATION=off switches off the statement extension, its routes and its close checks.
A deployment is one Python process on loopback behind a reverse proxy. deploy/ has an example systemd unit,
service configuration and nginx server block. Operating a Haddon deployment covers adding books,
exports, backups that don’t lose WAL data, rehearsing a restore, and a checklist for before you deploy.
Development
Section titled “Development”src/haddon/ commands.py journal commands: one function per write, shared by HTTP and Python callers reconciliation_commands.py statement and line commands (an optional extension) api/ FastAPI app, routes, schemas, auth, idempotency, pagination repository/ database reads and writes, close checks, corrections db/migrations/ the schema: STRICT tables, CHECKs and triggers reports/ statements/ balances, registers, trial balance, income statement, balance sheet, PDF importing/ exporting/ hledger journal import and deterministic export oracle/ the source / store / export cross-checkweb/ the reviewer UI (React, TypeScript, Vite)corpus/ a fictional three-company sample corpus and its generatordocs/design/ the decision record kept while Haddon was builtmake test-fast # about a minute: everything except the slow testsmake test-gate # the full suite and the oracle over the sample corpus (about ten minutes)make web-check # type-check the reviewer UImake site # build the documentation site into site/dist (Node 22; see site/README.md)A change has to pass make test-gate. The import, export and oracle tests need hledger 1.52.1 at
vendor/hledger, checked against the hashes in vendor/hledger.sha256:
# Linux x86_64 (macOS arm64: use hledger-mac-arm64.tar.gz)curl -sSLf https://github.com/hledgerorg/hledger/releases/download/1.52.1/hledger-linux-x64.tar.gz \ | tar -xz -C vendor hledgerPDF statements use WeasyPrint, which needs Pango installed on the system.
Documentation
Section titled “Documentation”- Quickstart: from an empty directory to a closed month, over HTTP
- Using Haddon from an agent: the contract an agent works under, on one page
- Connecting an LLM agent: connecting an LLM agent, tool definitions, errors, guardrails
- Concepts and the accounting model: the accounting model and the rules the ledger enforces
- API conventions: API conventions and the route list
- The web interface: the web interface
- hledger import, export and the oracle: import, export, the oracle and corpora
- Operating a Haddon deployment: configuration, deployment, backup and restore
- Architecture: the journal core, the extensions and the dependency rule
- Haddon and other open-source accounting tools: Haddon next to hledger, Beancount and Fava, Ledger and GnuCash
Contributing
Section titled “Contributing”Bug reports, questions and pull requests are welcome. CONTRIBUTING.md has the development setup and what a change needs before it can be merged. Please report vulnerabilities as described in SECURITY.md. Participation is covered by the code of conduct.
License
Section titled “License”MIT. See LICENSE.