# Our ledger balanced perfectly and had no audit trail

> Source: <https://dev.to/t49qnsx7qtkpanks/our-ledger-balanced-perfectly-and-had-no-audit-trail-4cbl>
> Published: 2026-11-04 03:42:14+00:00

If an AI agent moves money or changes infrastructure, someone will eventually ask: what exactly happened, and can you prove it? Not "do the totals add up", but "show me this transaction, and show me nobody edited the record afterwards".

MnemoPay is an open-source (Apache 2.0) TypeScript SDK for that layer: policy checks, human approvals, a double-entry ledger, and tamper-evident audit chains for agents that take consequential actions. Version 1.15 is out (current: 1.15.2). The most interesting part of it is a bug in our own code.

`Ledger.compact(n)` trims old entries so memory stays bounded. In 1.14.2, calling it with `n <= 100` also switched entry recording off permanently. The SDK's own `MnemoPayLite` calls `compact(50)`, so after roughly ten settlements (per the changelog) the ledger kept correct running totals but stopped recording entries at all.

Nothing looked wrong. The totals balanced. The hash chain "verified". There was just nothing left to audit. Here is the same loop against both versions (I used `Ledger` directly and called `compact(50)` the way `MnemoPayLite` does):

``` js
import { Ledger } from "@mnemopay/sdk";
const ledger = new Ledger();
ledger.recordFunding("agent-1", 1000, "stripe-topup");
for (let i = 0; i < 20; i++) {
  ledger.recordCharge("agent-1", 10, `tx-${i}`);
  ledger.recordSettlement("agent-1", `tx-${i}`, 10, 0.19, 9.81, "merchant-1");
  if (i % 5 === 4) ledger.compact(50);
}
const s = ledger.verify();
console.log({ balanced: s.balanced, entryCount: s.entryCount,
  entriesForLastTx: ledger.getEntriesForTransaction("tx-19").length,
  auditTrail: s.auditTrail ?? "(field not present)", chainValid: s.chainValid });
sdk 1.14.2   { balanced: true, entryCount: 162, entriesForLastTx: 0, auditTrail: '(field not present)', chainValid: true }
sdk 1.15.0   { balanced: true, entryCount: 162, entriesForLastTx: 8, auditTrail: 'compacted', chainValid: true }
```

On 1.14.2 the last transaction has zero entries behind it, while every check we had said "fine". In 1.15.0, `compact()` only trims, keeps the hash-chain link, and hands dropped entries to an `archiveSink` you can point at durable storage. `verify()` now says how much detail backs the totals (`complete`, `compacted` or `disabled`), and turning the trail off is an explicit `setHighThroughput(true)`. We also widened the entry hash to cover every field, so editing a description or timestamp is now detected, and `recordSettlement()` rejects fee plus net that does not equal gross before it writes any leg.

If you are on 1.14.x, upgrade. (1.15.1 and 1.15.2 are small follow-ups: `require()` for subpath imports, the right version in the MCP server, and scripts that now exit on their own.) Ledgers saved by earlier versions still verify.

The audit chain turns events into a Merkle root. Change one event afterwards and the root no longer matches:

``` js
import { AuditChain, verifyBundle } from "@mnemopay/sdk/governance";
const chain = new AuditChain();
chain.emit("action.proposed",  { agent: "refund-bot", action: "refund", amountUsd: 40 });
chain.emit("approval.granted", { approver: "ops@example.com" });
chain.emit("action.executed",  { txId: "tx-1001", amountUsd: 40 });
const bundle = chain.toBundle({ system: "demo" });
console.log("untouched:", verifyBundle(bundle));
const forged = structuredClone(bundle); forged.events[2].payload.amountUsd = 4;
console.log("edited:   ", verifyBundle(forged));
untouched: { ok: true }
edited:    { ok: false, reason: 'root_mismatch' }
```

The limit is worth saying plainly: whoever holds the bundle can recompute a root, so this proves internal consistency, not that the record existed at a given time.

That gap is what the new `OnChainAnchorSink` interface is for. A provider implements `anchor()` and `status()` in their own package; the SDK hands over only the Merkle root and the metadata you choose, never the events, and stores the returned reference next to the bundle. The SDK ships the interface, helpers and a `MockAnchorSink`, with no chain code, ABIs or gas handling:

``` js
import { AuditChain, verifyBundle, anchorChainBundle, pollAnchor,
         attachAnchorReference, MockAnchorSink } from "@mnemopay/sdk/governance";
const chain = new AuditChain();
chain.emit("action.executed", { txId: "tx-1001", amountUsd: 40 });
const bundle = chain.toBundle({ system: "demo" });
const sink = new MockAnchorSink({ pendingTicks: 2 });
const { anchorId } = await anchorChainBundle(bundle, sink, { producerId: "demo-producer" });
const status = await pollAnchor(sink, anchorId, { delayMs: 0 });
const { state, ...ref } = status;
console.log(status.chain, "| fields sent:", Object.keys(sink.requests[0]).join(", "));
console.log("still verifies:", verifyBundle(attachAnchorReference(bundle, ref)));
mock | fields sent: producerId, merkleRoot, bundleMeta, sealedAt
still verifies: { ok: true }
```

Note `chain` is `"mock"`. That is deliberate, so a mock reference can never pass for a real attestation. What a real anchor proves depends on the provider: the changelog notes that one candidate contract does not authenticate the submitter, so it would show a root existed at block T, not who committed it.

The SDK can export an "Article 12 bundle" for a finished mission: mission metadata, every event, a CSV, a hash chain and a manifest with checksums, with a default retention field of 6 months. That is evidence in a regulator-readable shape. It is not legal compliance, and the repo's own EU AI Act technical file says it is self-prepared with no auditor engaged. Whether your system is high-risk, and whether this satisfies you, is for you and your counsel.

`0xdb8b69be…e213`, block 53374272, `verifyBundle` recomputes for the fixture. It shows that root existed at that block, not who committed it. It is a test fixture, not a customer.`mnemopay` on PyPI, 1.3.0) is a separate, smaller port.

```
npm install @mnemopay/sdk
```

Repo and changelog: [https://github.com/mnemopay/mnemopay-sdk](https://github.com/mnemopay/mnemopay-sdk). Issues welcome, especially from anyone who audits agent systems for a living.
