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.
The bug: balanced, and unauditable
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):
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.
Tamper evidence you can check
The audit chain turns events into a Merkle root. Change one event afterwards and the root no longer matches:
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.
New: the OnChainAnchorSink seam
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:
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.
Article 12, carefully
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.
What this does NOT do yet
-
No production MnemoPay deployment anchors on chain yet. One third party has used the seam: on 2026-11-01 AgentAudit AI anchored a MnemoPay audit-chain test bundle on Base mainnet (tx
0xdb8b69be…e213, block 53374272, Basescan). I checked it: the transaction succeeded and the vault's event contains the root thatverifyBundlerecomputes for the fixture. It shows that root existed at that block, not who committed it. It is a test fixture, not a customer. - Live settlement is only verified against mocks. There is an opt-in Stripe test-mode test, skipped unless you supply a test key.
- Hardware signers: the SDK takes an injected signer; it has only been tested with software keys.
- Memory merging across processes is not a lock. 1.15.0 stops concurrent writers wiping each other's memories, but a true guarantee needs file locking.
-
The Python package (
mnemopayon PyPI, 1.3.0) is a separate, smaller port. It has no ledger, persistence or anchor sink. Its audit chain now serializes exactly like the TypeScript one, and a bundle built in either runtime verifies in the other (tested both ways, including accents, emoji and exponent-form floats). - It is not a bank, a money transmitter or a compliance product.
npm install @mnemopay/sdk
Repo and changelog: https://github.com/mnemopay/mnemopay-sdk. Issues welcome, especially from anyone who audits agent systems for a living.
Top comments (0)