postledger
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@postledgerPost a journal entry: debit Office Supplies 45.50, credit Cash 45.50"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Postledger
Double-entry bookkeeping that assumes the bookkeeper is not trustworthy.
A ledger built for one situation: an AI agent is doing the writing. That premise decides everything here, and it comes down to three things.
Idempotent. An agent retries — after a timeout, a resumed session, a compacted context. The network drops the response, not the write, and from the caller's side those are indistinguishable. Every write carries a key derived from the real-world event, so replaying it returns the original entry and posts nothing.
Auditable. Nothing is ever edited or deleted; corrections are reversing entries. A hash chain covers
every entry and its postings. Balance assertions record what you confirmed against a bank statement, and
verify re-checks all of it — because the chain proves nobody altered the books, and only an
assertion catches something that was never written down.
MCP-native. 25 tools shaped so a model has little room to get it wrong: amounts are strings (a JSON number is already an imprecise float), unknown accounts come back with suggestions, an unbalanced entry is diagnosed by the classic bookkeeper's checks, and every write can be previewed without consuming its key.
Underneath, the invariants are SQLite triggers rather than application code — because a rule in a trigger outlives the code path that was meant to enforce it. Balance, immutability, chain continuity and period locks are all refused by the database itself.
467 tests. Zero runtime dependencies. One book is one SQLite file: backup is cp, isolation is
chmod. Runs on Node 22.13+, needs no server, no daemon, and no account.
npx postledger --helpWhat it is not
Not a general accounting package, and not trying to become one. No invoicing, no AR/AP workflow, no budgets, no payroll, no tax engine, no multi-user. There is no hosted version and there will not be one — this project is structurally incapable of holding your data, and therefore incapable of leaking it or walking away with it. That is not a promise about intentions; it is a property of where the file lives.
Foreign amounts are converted at the rate that applied and recorded with the original alongside, which is all "multi-currency" means at the point of entry. What it deliberately does not do is exchange gain/loss or period-end revaluation — those are about rate movement, and belong in ordinary entries against an FX account rather than in the engine.
60 seconds, no signup
postledger init books/demo.db --name "Demo Co" --currency SGD
export POSTLEDGER_BOOK=books/demo.db
postledger account open Assets:Bank:Checking --type asset
postledger account open Income:Sales --type income
postledger post --key inv-001 --date 2026-08-08 --desc "Invoice 001" \
--leg "Assets:Bank:Checking debit 5000.00" \
--leg "Income:Sales credit 5000.00" \
--expect-total 5000.00Now try to break it:
# Replay the same key — returns the original entry, posts nothing
postledger post --key inv-001 ... # "replayed": true, still 1 entry
# Off by one cent — rejected, exit code 2
postledger post --key x --leg "Assets:Bank:Checking debit 100.00" \
--leg "Income:Sales credit 99.00" --expect-total 100.00
# Edit the books behind postledger's back — the database itself refuses
sqlite3 books/demo.db "UPDATE postings SET amount = 1"
# Error: postledger: postings are immutable
sqlite3 books/demo.db "DELETE FROM entries"
# Error: postledger: entries are append-only; correct with a reversalRelated MCP server: accounting-mcp-server
Where this sits
There are several local-first double-entry MCP servers now. They mostly compete on how much your agent can do — budgets, reconciliation, VAT, securities, cash-flow forecasting. Postledger competes on a different axis: whether you can trust what the agent did.
Feature comparison, from reading the source of each project on 2026-08-08. Facts only; every project listed is doing something legitimate and several are more feature-rich than this one.
Postledger | A | B | C | D | |
Storage | SQLite | SQLite | SQLite | PostgreSQL | JSONL file |
Money as integer minor units | ✅ | ✅ | ✅ | — | ✅ |
General idempotency key on writes | ✅ | — | — | — | ✅ |
Immutability enforced by DB triggers | ✅ all tables | partial | ✅ postings | — | — |
Hash chain over entries | ✅ | — | — | — | — |
External anchoring | ✅ | — | — | — | — |
Bulk revert by actor | ✅ | — | — | — | — |
Statistical fraud indicators | ✅ | — | — | — | — |
Bookkeeper's error diagnostics | ✅ | — | — | — | — |
Document archive + fingerprint check | ✅ | ✅ | ✅ | ✅ | — |
Breadth of features | moderate | very high | high | moderate | minimal |
A = cloviscomputing/clovis · B = erikvankempen/bukio-cli · C = yuens1002/bookie · D = themusashimaru/ledgerkit-mcp
Worth knowing: "idempotent" means different things across these projects. In several it refers to
import deduplication (re-importing a bank file doesn't duplicate rows, keyed on a natural key) or to
MCP's idempotentHint protocol metadata. Postledger uses it in the strict sense: a caller-supplied key
on every write, claimed atomically before any work happens, where replay returns the original result.
The seven guarantees
Guarantee | Enforced by | Where |
Debits equal credits |
|
|
Retries never double-post | Key claimed before the work — no check-then-act window |
|
Nothing is edited or deleted |
|
|
No floating point, ever |
|
|
The caller's own total must match |
|
|
Source documents stay verifiable | Content-addressed; |
|
Tampering is detectable | Hash chain over entries and their postings |
|
Each row has a test that goes red if you remove the mechanism. npm test runs 467 of them across eight
suites, including one that drives the real CLI and speaks real MCP over stdio.
Why the database and not the application layer
debits == credits in application code protects you from today's callers. In a trigger it protects you
from every future one — a migration script, a cron job, a helpful contributor, an agent with direct SQL
access. The rule outlives the code path that was meant to enforce it.
SQLite has no deferred constraints, so the write protocol is inverted to make that stop mattering:
1. INSERT all postings — the entry is unsealed, invisible to every read path
2. INSERT the entry header — a BEFORE INSERT trigger validates the whole entry at this instantThere is no window in which an unbalanced entry is visible, and appending a leg after sealing is rejected.
Use it from Claude, ChatGPT, or any MCP client
{
"mcpServers": {
"postledger": {
"command": "npx",
"args": ["-y", "postledger", "mcp", "--book", "/absolute/path/to/books/demo.db"]
}
}
}The tool surface is shaped so the model has little room to get it wrong:
postledger_post_entryrequires anidempotency_keyand anexpected_totalthe caller computed itself. A hallucinated line item rarely arrives with a total that happens to balance.Amounts are strings, never JSON numbers.
JSON.parseturns125.50into an imprecise double before any validator could see it, so it is refused at the boundary with an explanation.Unknown account? The error carries
did_you_meancandidates rather than leaving the model guessing.Unbalanced? The error runs the classic bookkeeper's checks and names the likely mistake:
debits 54.00 != credits 45.00 (off by 9.00)— the difference is divisible by 9, the classic signature of a transposition error — two digits swapped somewhere (e.g. 54 typed as 45). Re-read each amount against the source document.It also catches the two other classics: a difference that is exactly twice one leg (that leg is on the wrong side) and a difference that equals one leg exactly (its counterpart is missing).
Every write returns the current chain head. In an MCP session that value lands in the conversation transcript — a copy of your ledger's fingerprint that lives outside the machine holding the ledger.
Look at the books in a browser
postledger serve # http://127.0.0.1:7777A single self-contained page: overview, chart of accounts, balance sheet, income statement, journal, and
the forensics panel. No build step, no framework, no CDN — the HTML you can read is the HTML that runs,
and a CSP of default-src 'none' means the page cannot reach the network even if something got into it.
Two deliberate limits: it is read-only (writing stays with the CLI and MCP, so there is no form to
CSRF and no session to steal), and it binds 127.0.0.1 unless you explicitly pass --host. Your
books should not become reachable because you left a tab open.
When an agent goes wrong
Every entry is signed with its author and nothing is ever deleted, so one actor's entire footprint can be undone:
postledger revert-actor agent:rogue --key cleanup-1 --reason "malfunction" --dry-run
# → matched: 3, and exactly what each balance would become
postledger revert-actor agent:rogue --key cleanup-1 --reason "malfunction"
# → 3 reversing entries posted; balances back to where they wereIt reverses, it does not delete. The books end up as if that actor never wrote, while the record of what happened — what was posted, by whom, when it was undone and why — stays intact. Deleting would defeat the point of keeping an audit trail.
Re-running with the same batch key is safe: already-reversed entries are recognised and skipped, so an interrupted cleanup resumes rather than double-reverting.
Statements
postledger balance-sheet --table
postledger income-statement --from 2026-01-01 --to 2026-03-31The balance sheet asserts the accounting identity rather than assuming it:
assets = liabilities + equity + (income − expenses)If that does not hold exactly it returns ok: false, prints the exact gap, and exits 5. There is no
rounding tolerance to hide behind — money is integer minor units, so a difference of one cent is a real
difference and means something is wrong. Profit for the period is shown as its own line inside equity
rather than folded in silently, so retained earnings and this period's result stay distinguishable.
A report you can send to your accountant
postledger export --format html > audit-report.htmlOne self-contained file. It opens from file://, makes no network request at all (verified in CI by
intercepting every request), and carries the chain head plus the integrity result in a banner at the top
— so whoever receives it can run postledger verify against the original book and compare hashes.
It reuses the exact page postledger serve renders, so there is no second reporting engine to drift out
of sync with the first. A report nobody can check is decoration; this one states what it is (a
point-in-time snapshot, not a live view) and how to check it.
Your data is not held hostage
postledger export --format journal > books.journal # hledger/ledger format
hledger -f books.journal balancesheet # someone else's tool, your data
postledger import books.journal --dry-run # see what would happen
postledger import books.journalRound-trip is lossless. Postledger's own facts (entry id, idempotency key, actor) ride along in tag comments, which ledger-likes preserve and ignore — so an export re-imports without inventing a dialect.
Direction is the one real difference between the formats and it is handled explicitly: Postledger uses an
explicit side with a strictly positive amount; ledger-likes use a sign. Positive is debit, negative is
credit, and the export writes that convention into the file header.
Import goes through the same post() path as everything else — an import is not a back door, and the
same invariants apply. Idempotency keys are derived from the file and position, so re-running an import
is a no-op rather than a duplicate. Anything Postledger does not model (virtual postings, multi-commodity
legs, automated transaction rules) is rejected with the line number, never silently dropped: a tool
that quietly discards part of your file is worse than one that refuses it.
Statistical forensics
postledger auditBenford first-digit distribution, round-number density, duplicate amounts, clustering just below approval thresholds, and outliers by modified Z-score. Fabricated numbers have a fingerprint — people and language models both favour uniform leading digits, round figures, and amounts sitting just under a limit. Real ledgers do not.
These are indicators, not evidence. Deviation is not fraud and conformity is not innocence: a careful fabricator can match Benford on purpose, and plenty of honest ledgers fail it (fixed contract prices, a natural floor or cap, or simply too few entries). Below 100 samples the tool refuses to draw a conclusion at all. The output repeats this caveat every time. It tells you which entries to pull the source document for. Nothing more.
A second axis, and what happened at the time
postledger post --key inv-88 --date 2026-08-08 --desc "Team lunch, Amsterdam" \
--leg "Expenses:Meals debit 100.00" \
--leg "Liabilities:VAT debit 21.00" \
--leg "Assets:Bank credit 121.00" \
--expect-total 121.00Two things a ledger can never recover after the fact, so both are recorded at write time:
Tax and original currency, pinned to the leg. One expense account can carry legs at four different
VAT rates, so the rate cannot be reconstructed from the account later — and postings are immutable, so a
column added next year would be permanently blank for everything before it. tax_code, tax_amount,
fx_currency and fx_amount are audit columns: they record what was true, they never compute. A tax
engine can be built on top whenever one is needed; the facts it would need are being kept now.
Tags, orthogonal to the chart of accounts. {"project": "apollo", "client": "acme"} instead of
forking the tree into Expenses:Meals:ProjectA. Every mature system in this space has a second axis,
because the chart alone cannot carry it.
Both are covered by the entry hash — altering a tax code after the fact breaks the chain, and there is a test that does exactly that. The consequence is that neither can be applied retroactively. That is deliberate: a tag you can add later is a tag you can change later, and this ledger has no change operation.
Finding entries again
An audit signal or an ageing bucket is only useful if you can pull up the entries behind it:
postledger entries --tag project --tag-value apollo --min 1000.00 --since 2026-01-01
postledger entries --actor agent:rogue --describes refund
postledger balance Assets:Bank --as-of 2026-07-31 --subtreePaging is by cursor, not offset: each response carries next_before_seq, and seq is monotonic, so the
window stays stable even while new entries are being written. null means the end — the whole history
can be walked without guessing when to stop.
Preview anything
postledger post --key inv-99 --dry-run ...The dry run performs the real write — every trigger, every constraint — and then rolls it back.
Simulating the checks instead would mean a second implementation of them, and the second implementation
is the one that drifts. It also never touches the idempotency table, so previewing does not consume the
key the real call still needs. The response says ids_are_preview_only, because the entry id it shows
belongs to a transaction that no longer exists.
Closing the books
postledger close 2026-03-31 --name "FY2026 Q1" --note "reviewed with accountant"
postledger close reopen 1 --reason "a supplier invoice arrived late"
postledger periodsA close is an event, not a setting: it has a name you can say out loud, it can be listed, and reopening it is recorded with a reason rather than being a silent rewind of a number. In a ledger where every correction is visible, closing the books should not be the one operation that leaves no trace. The period table is append-only too — the date and name of a close cannot be edited, and a close can be reopened exactly once.
Bank statements
postledger read-statement march.csv --date Date --desc Description --amount Amount --ref ReferenceNothing is posted. A statement line tells you money moved and roughly why; it does not tell you which account the other side belongs to. That is a judgement call, and here the caller making it is usually a model — which does it better than any rules table. So this parses, normalises and fingerprints, then hands the rows back.
Each candidate carries a suggested_key (use it as the idempotency key, so re-importing the same file
is a no-op) and an already_posted flag. Handles European and Anglo decimals, accounting parentheses,
currency symbols, split debit/credit columns, and card statements where a purchase is printed positive.
It refuses to guess exactly two things, because guessing either one silently corrupts the whole file:
the date "03/04/2026" on line 2 could be either day-first or month-first— both parts are 12 or less, so there is no way to tell. Pass date_format as "dmy" or "mdy".
An operating manual for the model
postledger_manual — mounted three ways so a model finds it however it looks: as the MCP instructions
handed over at initialize, as postledger://manual/* resources, and as a plain read-only tool. Eight
topics covering posting safely, correcting mistakes, checking against reality, importing statements,
closing periods, reading the forensic signals — and one titled what this cannot do, which forbids
describing this ledger as tamper-proof or blockchain-backed. A test asserts that topic still says so.
The check that looks outward
The hash chain proves nobody altered what is written down. It cannot tell you something was never written down at all — and that is the most common bookkeeping error there is.
postledger assert Assets:Bank:Checking 4820.15 --note "July statement"That records a confirmation permanently. A figure that disagrees with the books is refused, with the gap stated: recording an assertion you know to be false is not a checkpoint, it is a note saying the books are wrong, and that belongs in a correcting entry.
From then on postledger verify re-checks every confirmation. Back-date an entry into a month somebody
already confirmed and it goes red, naming the assertion and the divergence.
Why this matters, concretely — on a book that is missing one entry:
check | result |
hash chain | ✅ passes |
trial balance | ✅ balances |
accounting identity | ✅ holds |
balance assertion | ❌ caught it |
There is a test asserting exactly that table. Assertions are anchored to a business date, not to a position in the chain — anchoring to chain position would make them vacuous, since a new entry always lands after an old checkpoint and could never disturb it.
postledger assert --generate # snapshot every asset and liability, once reconciled
postledger stale-assertions # asserted once, moved a lot since — reconcile these nextDeliberately not copied from the prior art in this space: beancount's pad (invents an entry to
absorb a discrepancy) and hledger's balance assignment (works backwards from a balance to an amount).
hledger's own documentation argues against the latter — it hides errors and weakens the audit trail. A
gap has to be explained by a human, not absorbed by a tool.
Threat model, honestly
What the audit chain detects
Accidental corruption
Any modification that did not go through Postledger
Entries deleted from the middle or the end of the chain
An archived source document swapped for a different file
What it cannot do
Stop someone who owns the machine. With write access to the file, an attacker can drop the triggers, rewrite history, and recompute the chain so it verifies clean. There is a test in this repository that does exactly that and asserts local verification passes — because claiming otherwise would be the dishonest choice.
Prove who did anything. Over stdio there is no authenticated identity. The
actorfield is self-declared, and the schema column is calledclaimed_actorso nobody mistakes it for proof. Good for tracing accidents, useless against an adversary.
What actually raises the bar
postledger anchor --line >> anchors.log # after each session
git -C anchors commit -am "anchor" && git push
postledger verify-anchors anchors.log # check the book against those witnessesAn attacker can rewrite what is on your disk. They cannot rewrite the copy that already left it. The same test that proves local verification is defeatable also proves the anchor check catches it. Anchor somewhere you do not control — a remote repo, a colleague, another host — and the more places, the higher the cost of forgery.
Design decisions
One book per file. A book is a file you can cp, tar, rsync, chmod, and sha256sum. Backup is
copy. Isolation is file permissions — which matters, because with no trustworthy identity in the
application layer, the filesystem is the only real access control there is. Reporting across books is a
separate read-only command, not a reason to put five companies in one file.
No account tree table. Hierarchy lives in the name (Expenses:Meals:Team) and reports aggregate by
prefix. That removes parent ids, closure tables, and subtree moves in one stroke.
One currency per book. Multi-currency drags in rates, translation, and revaluation — half a project. Need another currency? Open another book.
Zero runtime dependencies. The only thing the published package imports is node: builtins —
node:sqlite for storage, node:http for the web view, node:crypto for the chain. TypeScript is a
build-time dependency and nothing else. The MCP server is ~350 lines of newline-delimited JSON-RPC rather
than an SDK, because a financial tool people are asked to audit should be readable end to end.
(During development Node runs the .ts sources directly, so there is no build step in the loop. The
package is compiled for publication because Node deliberately refuses to strip types from anything under
node_modules — shipping .ts files would install cleanly and then crash on first run. CI installs the
real tarball into a clean directory and drives the binary, so that failure mode cannot come back.)
Deliberately not in v1: multi-currency, invoice/AR/AP state machines, period-close automation, a web UI, bank imports. None of them change whether an AI can keep books safely, which is the only thing this is trying to be good at.
Install
Requires Node 22.13+ — that is where node:sqlite became available without a flag (measured, not guessed). Contributors running the TypeScript sources directly need 22.18+, where type stripping is on by default.
npx postledger --help # no install
npm install -g postledger # or install itDocker — nothing to install, and the image runs the full test suite at build time, so an image that exists is an image whose invariants held. Built for amd64 and arm64:
docker run -v "$PWD/books:/books" ghcr.io/shuaige121/postledger \
init /books/demo.db --name "Acme Co" --currency USDFrom source — requires Node 22.18+ (running the TypeScript sources needs type stripping):
git clone https://github.com/shuaige121/postledger && cd postledger
npm test # 467 assertions
node src/cli.ts --helpA book is one SQLite file, so the entire deployment story is that one -v. No database service, no
migrations to run, nothing to back up except a directory.
As a library:
import { Ledger } from 'postledger';
const book = Ledger.open('books/demo.db');
book.post({
idempotencyKey: 'stripe_evt_1P9x…', // the real-world event id
date: '2026-08-08',
description: 'Stripe payout',
legs: [
{ account: 'Assets:Bank:Checking', side: 'debit', amount: '4820.15' },
{ account: 'Expenses:Fees', side: 'debit', amount: '179.85' },
{ account: 'Income:Sales', side: 'credit', amount: '5000.00' },
],
expectedTotal: '5000.00',
actor: 'agent:stripe-sync',
});Exit codes
0 ok · 1 error · 2 validation failed · 3 idempotency conflict · 4 book problem ·
5 integrity check failed
postledger verify || echo "the books need attention"Deeper reading
Idempotency — the retry that posts twice, and the four ways an "idempotency" claim turns out to mean nothing
Append-only accounting — why there is no UPDATE and no DELETE, and how inverting the write order makes SQLite's lack of deferred constraints stop mattering
Threat model — what the chain proves, what it cannot, and what would have to change for that list to shrink
Tests
npm test467 assertions across eight suites: schema invariants, money arithmetic, the engine, forensics, reports and journal interop, and an end-to-end pass that drives the real CLI and speaks real MCP over stdio.
License
MIT
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- Alicense-qualityAmaintenanceA Model Context Protocol (MCP) server that keeps the books for your personal and business finances using double-entry accounting — driven entirely from an LLM.378MIT
- -license-quality-maintenanceA personal accounting MCP server that enables AI assistants to record and query financial transactions through natural language, supporting income/expense tracking, balance inquiry, and monthly summaries.
- Alicense-qualityAmaintenanceDouble-entry accounting ledger MCP server for autonomous agents that enables creating accounts, posting journal entries, and generating financial reports.MIT
- Flicense-qualityBmaintenanceAn MCP server that enables AI agents to safely interact with a double-entry payments ledger, enforcing idempotency, policy-based access control, and human-in-the-loop approval for high-value actions.
Related MCP Connectors
Hosted MCP server for Mini Accountant: invoices, expenses, customers, analytics, tax estimates.
Remote MCP for A2A failure replay MCP, structured receipts, audit logs, and reviewer-ready evidence.
A paid remote MCP for hosted MCP server, built to return verdicts, receipts, usage logs, and audit-r
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/shuaige121/postledger'
If you have feedback or need assistance with the MCP directory API, please join our Discord server