← Notes
A ledger you can't quietly rewrite
architecture·audit·security

A ledger you can't quietly rewrite

Every capital flow, every approval, every mode change lands in an audit log with a hash chain. Why this matters in trading software, and how to verify integrity in under a minute.

· Mikhail Savchenko

There’s one class of bug in financial software that’s worse than the rest: not a crash, a quiet break. The algorithm keeps trading, the dashboard keeps drawing the curve, and in the background something — a bug, a race, a serialisation error — rewrote a past row in place. You can’t catch a break like that a week later: you don’t know which numbers were real and which were the rewrite.

The audit ledger in inite.fund is built to rule out exactly this failure mode. Not to flag it after the fact, but to make rewriting the past structurally impossible.

Append-only by construction

Every audit table — audit_log, capital_flows, mode_history, hil_decisions — runs as append-only. UPDATEs are blocked by database triggers. DELETE is allowed only on the most recent row, and only while it’s marked pending. Anything more than a day old can’t be modified by anyone, admin included.

It sounds like a restriction; it’s actually a freedom. Once a row is in the ledger, it’s there forever. No “we patched yesterday’s value because we spotted a typo.” The typo stays next to the correction, and both are visible.

The hash chain

Every ledger row carries two extra fields alongside its data: prev_hash and row_hash. row_hash is the SHA-256 of all the significant fields concatenated with prev_hash of the previous row. Each row signs not only itself but everything that came before in the same ledger.

The arithmetic from there is simple. Change one row in the middle and its row_hash breaks. To hide that, you’d have to recompute every row_hash after it. To recompute, you’d have to disable the database triggers. To disable them, you’d need postgres superuser. And every such elevation lands in postgres’s own audit, which we also keep.

The chain can be verified by any external tool in a single pass. Take the first row, recompute row_hash by hand, compare to what’s written. Take the next, recompute with prev_hash, compare. Repeat to the end. If even one disagrees, the ledger was touched. The script fits in twenty lines; we run it inside every nightly backup.

Idempotency via audit_ref

Every capital operation — move_to_long, move_to_trading, approve_trade, set_mode — requires an audit_ref. It’s a unique string the caller generates before the request. The database checks that no row with that ref exists yet, and only then applies the op.

Why this matters. A network blip mid-call is routine. Client sends a request, the database applies it, the response is dropped, the client retries. Without audit_ref, the second call goes through and the same rebalance happens twice. With audit_ref, the second call comes back “already applied” with the same row_hash as the first, and the client sees there’s nothing new to do.

In practice this means capital operations in inite.fund are safe to retry at any layer. You can retry from the network stack, from the MCP client, from a manual operator script — the ledger stays consistent.

What it gets the operator

Three things. First, a complete history of capital movement broken out by cause and initiator. For any figure on the dashboard you can walk back to the specific audit row that produced it — when, by whom, with what reasoning.

Second, external verifiability. If we ever need to show a regulator, an auditor, or a partner that the data wasn’t fudged after the fact, we have a script and an export format for it. This isn’t “trust us, we’re the good guys.” It’s a mathematical property of the ledger that can be checked independently.

Third, and to us the most important — psychological relief. When you know that every operation left a trace and the trace can’t be forged, a whole class of anxiety disappears. You don’t have to wonder whether the system quietly decided for you and hid the decision. If the ledger is empty, the action didn’t happen. If the ledger has a row, it’s exactly the row that was written at the moment.

What it costs

Each ledger write is one extra INSERT, one SHA-256, one uniqueness check on audit_ref. On our measurements that’s 40-60 microseconds per operation. For a trading engine ticking every 200 milliseconds, it’s noise. For a high-traffic broker API it would be critical, but we’re not a broker.

Storage grows by roughly 4-6 KB per trade. A year of live trading strategy is 20-40 MB of ledger. The portfolio side is an order of magnitude smaller because the rebalance runs once a week. Free on any modern-disk scale.

Net: the operational cost is near zero, and the payoff is the inability to quietly rewrite the past. It’s the rare architecture choice where you don’t have to trade one for the other.

— inite team

Related notes
All notes →