10 / 12Prism Systems · FinTech & Banking
A ledger where money can't be lost, doubled or quietly edited
A multi-tenant double-entry ledger with atomic journals, idempotency keys, two-phase holds, reversals, point-in-time balances, a per-tenant SHA-256 hash chain with a Merkle anchor, and bank reconciliation that matches settlement batches net of processor fees.
Three fintechs on one ledger, with money moving live
Kirana Pay, Campus Wallet and Gulf Remit share one ledger but each has its own accounts and hash chain. Watch top-ups, QR payments, payouts and remittances move between accounts, and PSP settlements get matched by reconciliation. Fire a retry storm or a double-spend race, tamper with a posted journal and run a full audit, book breaks, or post your own transfer and send it again with the same key.
What's inside the engine
Real algorithms, not mock-ups. The simulator above runs the exact same code you import.
Double-entry, per currency
Every journal needs at least two legs, and debits must equal credits within each currency. Multi-currency journals (such as FX remittances) balance through position accounts. All amounts are integer minor units, never floats.
Atomic by construction
A journal is fully validated before any change is applied, so applying it can't fail halfway. Batches are all-or-nothing and roll back balances, the chain head and idempotency keys.
Idempotency keys
Retrying with the same key and payload returns the original journal (HTTP 200 with Idempotent-Replayed). The same key with a different body is a 409. Keys expire after a TTL.
Two-phase holds
Funds can be reserved, then captured in full or in part, voided or left to expire. Available balance is balance minus holds, so reserved money can never be spent twice.
Tamper-evident history
Each journal's hash covers its canonical JSON plus the previous hash. Verification re-hashes the chain and re-sums every balance, catching edits, deletions, reordering and direct balance changes.
Point-in-time balances
Each account keeps its balance after every journal, so balances as of any time or sequence number come from a binary search. Statements show running balances, and trial balances can be taken at any point.
Reconciliation
Matches run in order: reference, amount + date, net of an exact processor fee, then settlement batches (a contiguous run first, then a bounded subset sum). The engine also detects a batch where the processor left out exactly one item.
HTTP API + CLI
Tenants, accounts, journals (with an Idempotency-Key header), holds, reversals, trial balance, balance sheet, verify, export and reconcile. The CLI covers demo, simulate, recon, verify, bench and serve.
From request to result
Normalise
Each leg is resolved to an account in this tenant. Sides are checked, string amounts are parsed into exact minor units, and currencies must match the account.
Replay check
If the idempotency key was seen within the TTL, the payload fingerprints are compared: a match replays the original journal, a mismatch is a conflict.
Validate
The engine checks that debits equal credits per currency and that no account is frozen. It also checks optimistic version preconditions, and that no account without overdraft would end up below its holds.
Apply + chain
Balances, versions and per-account history are updated, then the journal is hashed onto the tenant's chain. Nothing in this step can fail.
Reconcile
Expected cash movements are matched to bank lines. Matching a settlement batch posts the bank and fee journal, and leftover lines become categorised breaks an operator can book.
System design
Every box is a module in src/. Data flows left to right.
Ship it in your own stack
Zero dependencies. Works in Node.js 18+ and modern browsers.
import { Ledger, reconcile } from "prism-ledger";
const L = new Ledger();
L.createTenant("acme", { currency: "INR" });
L.openAccount("acme", { code: "bank:hdfc", type: "asset" });
L.openAccount("acme", { code: "wallet:asha", type: "liability" });
L.openAccount("acme", { code: "merchant:chai", type: "liability" });
L.openAccount("acme", { code: "revenue:fees", type: "revenue" });
L.post("acme", { key: "tp-1", entries: [{ account: "bank:hdfc", dr: "2000" }, { account: "wallet:asha", cr: "2000" }] });
// three legs, strings are exact decimals, integers are minor units (paise)
const { journal, replay } = L.post("acme", {
key: "pay-1", memo: "QR payment",
entries: [{ account: "wallet:asha", dr: "120" }, { account: "merchant:chai", cr: "117.60" }, { account: "revenue:fees", cr: "2.40" }],
});
L.post("acme", { key: "pay-1", /* same body */ ... }).replay; // true: retries never double-post
const { hold } = L.hold("acme", { account: "wallet:asha", to: "bank:hdfc", amount: "600" });
L.commit("acme", hold.id, { amount: "500" }); // capture part, release the rest
L.reverse("acme", journal.id); // refund
L.balance("acme", "wallet:asha", { asOf: Date.parse("2026-10-01") });
L.trialBalance("acme").balanced; // true
L.verify("acme"); // { ok, checked, head } or where it broke
L.anchor().root; // Merkle root over every tenant chain
reconcile(expectedLines, bankLines, { settlementBps: 150 }); // matches + categorised breaks$ npx github:krishyaa06/prism-ledger demo
$ npx github:krishyaa06/prism-ledger simulate --minutes 5
$ npx github:krishyaa06/prism-ledger recon examples/expected.csv examples/bank.csv
$ npx github:krishyaa06/prism-ledger export --out ledger.json && npx github:krishyaa06/prism-ledger verify ledger.json
$ npx github:krishyaa06/prism-ledger bench
$ npx github:krishyaa06/prism-ledger serve --port 8788# use the CLI without installing
$ npx github:krishyaa06/prism-ledger --help
# or add the engine to your project
$ npm install github:krishyaa06/prism-ledger
# run the simulator locally
$ git clone https://github.com/krishyaa06/prism-ledger.git && cd prism-ledger
$ npx serve . # then open http://localhost:3000