Balanced at the write
One rule core decides every entry — over a database, inside the store's
write transaction under the ledger's lock. Every entry balances per
currency and layer, and audit() decides each one again.
Rust · accounting · cryptographic audit
An immutable, tamper-evident double-entry bookkeeping engine. One rule core decides every write, money is exact integer arithmetic, and every sealed period commits to balances a third party can verify without seeing your books.
cargo add doubleentry
One rule core decides every entry — over a database, inside the store's
write transaction under the ledger's lock. Every entry balances per
currency and layer, and audit() decides each one again.
Money is an i128 at a compile-time scale, each currency
declared with its exponent. No floats, no operators, explicit rounding,
and every overflow a Result.
An entry's id is derived from its idempotency key, so a retry replays instead of booking twice. The rule core reads no clock and no randomness.
Every record is a leaf of one RFC 6962 SHA-256 log. An auditor holding a
tree head gets O(log n) proofs that an entry is included and
that the log only grew.
The log head is a C2SP signed checkpoint that stock witnesses — somebody else's — cosign. The crate ships no witness and no HTTP client: requests and replies are bytes.
audit() replays every record through the rule core and
compares every table a store keeps, byte for byte. A superuser's edit is
detected, not prevented.
Balances keep debit and credit totals, so a zero balance after heavy turnover is told apart from no activity.
Accounts are a tree of paths and only leaves take postings. A rollup folds a trial balance up the tree and infers the grouping nodes.
No chart of accounts and no opinion about your domain: paths, classes and reporting axes are yours. In memory, or over SQLite and PostgreSQL.
use doubleentry::prelude::*;
let mut books = Journal::new(LedgerId::new("example.com/acme")?).as_actor("setup");
books.declare_currency(EUR, 2, date!("2026-01-01"))?;
books.open("Assets:Cash", AccountKind::Asset, date!("2026-01-01"))?;
books.open("Income:Sales", AccountKind::Income, date!("2026-01-01"))?;
books.define_period("2026-03", date!("2026-03-01"), date!("2026-03-31"))?;
let rec = books.record(
Entry::new("invoice-2026-0001", date!("2026-03-15"))
.debit("Assets:Cash", amount!("1190.00"), EUR)
.credit("Income:Sales", amount!("1190.00"), EUR),
)?; // Ok(Recorded) | Err(Error::Rejected(r))
let proof = books.prove_entry(rec.id)?; // verifies against books.head()
assert_eq!((proof.index, proof.size), (rec.position, books.head().size));
A ledger is one append-only RFC 6962 log of typed records — master data,
periods, entries, clearings, seals. One pure rule core decides every write,
returning the records to append or one Rejection with a stable code. Journal
runs it in memory; Ledger runs it inside a store's write transaction (SQLite,
PostgreSQL or memory), with the same calls .awaited. The idempotency key is the entry's
identity, so recording the same draft again returns the original receipt.
Not “does this balance?” — most systems manage that — but “how do I know this record wasn't changed after the fact?”
This entry sits at this index under this root, in O(log n) hashes, revealing nothing else.
The earlier log is a prefix of the later one. Not merely linked — provably append-only.
This account held this balance when the period closed, proven against a tree head and named — without disclosing any other account.
And the one no proof answers alone — “is this the history you showed everybody else?” — is answered by independent witnesses, who refuse to cosign a second history at a size they already vouched for.
Install the crate, record your first entry, and prove it was recorded.
Define a contiguous calendar, seal a period, and prove one closing balance to an auditor.
The split-view attack proofs cannot see, the witnesses that refuse to take part in it, and offline proofs for auditors.