◆PRISMLedger

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.

Zero dependenciesInteger minor unitsIdempotency keysSHA-256 hash chainReconciliation engine
Live simulator

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.

paused
money flow · hash chain
Journals / s posted · replayed · rejected
Cash vs owed A = L + E + P
Journal inspector latest
Post a transfer POST /journals
Reconciliation every 5 s
Trial balance —
Events
Features

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.

How it works

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.

Architecture

System design

Every box is a module in src/. Data flows left to right.

settleCLIENTSHTTP APIIdempotency-KeyApps / CLIpost · hold · reverseLEDGER COREValidatebalance · funds · versionIdempotencyfingerprint · TTLSTATEAccountsbalance · holds · historyHash chainSHA-256 · Merkle anchorASSURANCEReconciliationref · fee · batchVerifyre-hash · re-sum
Use it

Ship it in your own stack

Zero dependencies. Works in Node.js 18+ and modern browsers.

example.js
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
terminal
$ 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
terminal
# 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