DEV Community

t49qnsx7qt-kpanks
t49qnsx7qt-kpanks

Posted on

How to give your Gemini agent persistent memory and a tamper-evident audit log in 3 lines

I shipped middleware in @mnemopay/sdk last week that wraps a Gemini agent and gives it three things it can't get from the Google SDK alone:

  1. Persistent memory that survives process restarts.
  2. A SHA-256 hash-chained audit log of every call.
  3. A payment rail it can charge against (Stripe, Lightning, Paystack, x402, or AP2 — pick one).

This post walks through the first two with runnable code. If you've ever built an agent that "forgot everything after the page reloaded" or had to prove to a compliance reviewer that your agent didn't fabricate a tool call, this is for you.

The setup

npm install @mnemopay/sdk @google/generative-ai
Enter fullscreen mode Exit fullscreen mode

The middleware pattern is intentionally small: it intercepts the agent's chat method, writes every turn into a recall store, and appends every call to an AuditChain JSONL file.

The 3 lines

Start with a vanilla Gemini agent:

import { GoogleGenerativeAI } from "@google/generative-ai";

const genAI = new GoogleGenerativeAI(process.env.GOOGLE_API_KEY);
const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
const chat = model.startChat();
Enter fullscreen mode Exit fullscreen mode

Now wrap it:

import { wrapGemini } from "@mnemopay/sdk/middleware/gemini";
import { RecallEngine, localEmbed } from "@mnemopay/sdk/recall";
import { AuditChain } from "@mnemopay/sdk/governance";

const recall = new RecallEngine({ embed: localEmbed });
const audit = new AuditChain({ path: "./audit.jsonl" });

const wrapped = wrapGemini(chat, { agentId: "support-bot-1", recall, audit });
Enter fullscreen mode Exit fullscreen mode

That's it. Everything wrapped.sendMessage(...) does is now:

  • Embedded and stored in recall so the next call can pull related history without re-stuffing the context window
  • Logged to ./audit.jsonl as an append-only hash-chained event
  • Tagged with agentId so a multi-tenant deployment can isolate memory per agent

What the audit log looks like

After three turns the file looks like:

{"ts":"2026-05-22T13:01:11.221Z","agentId":"support-bot-1","kind":"chat.in","payload":{"text":"Hi, how do I reset my password?"},"prev":"GENESIS","hash":"3f0a..."}
{"ts":"2026-05-22T13:01:12.490Z","agentId":"support-bot-1","kind":"chat.out","payload":{"text":"Sure — go to..."},"prev":"3f0a...","hash":"7c14..."}
{"ts":"2026-05-22T13:02:08.110Z","agentId":"support-bot-1","kind":"chat.in","payload":{"text":"Still doesn't work"},"prev":"7c14...","hash":"a921..."}
Enter fullscreen mode Exit fullscreen mode

Each hash is SHA-256(prev + ts + agentId + kind + JSON.stringify(payload)). Drop a single byte in any record and the chain breaks at that record. You can verify it later:

const isValid = await audit.verify();
console.log(isValid); // true
Enter fullscreen mode Exit fullscreen mode

verify() walks the chain once and recomputes every hash. On my 5-year-old laptop it does ~50k records/second. There's a eventJsonCache parallel list so verify() doesn't pay for double serialization — that landed in 1.10.

Why bother

Three reasons depending on who you are:

Building for EU customers. The AI Act's Article 12 ("Record-Keeping") wants automatic logs of high-risk AI system events that allow traceability. A JSONL file with a hash chain is the simplest viable evidence trail. You don't need a vendor for this. You do need the file.

Selling into banks or healthcare. "Where is the audit log?" is the second question in every security review. "It's in Datadog" is not a satisfying answer when Datadog can edit it. A hash-chained local file is.

Debugging your own agent. You wrote a prompt three weeks ago and your agent is doing something weird this week. The audit log is the single source of truth for what actually went in and what came out, in order.

Why recall is more than just a vector DB

@mnemopay/sdk/recall does cosine over a local embedding store by default (localEmbed is a deterministic 384-dim embed; no network). You can swap in any embedder. The thing that's different from a vector DB is that it's keyed on agentId and has a forget() method that respects scope. So when a GDPR delete request comes in for one user, you call recall.forget({ agentId, userId }) and the rows actually leave the disk, not just the index.

Picking a payment rail

I skipped over the third thing the wrapper adds — a payment rail. Short version: if your agent needs to charge a user, your code looks like this regardless of which rail you pick:

import { StripeRail } from "@mnemopay/sdk/rails/stripe";
const rail = new StripeRail({ apiKey: process.env.STRIPE_SECRET });

await wrapped.charge({ rail, amount: 999, currency: "usd", description: "API call" });
Enter fullscreen mode Exit fullscreen mode

Swap StripeRail for LightningRail, PaystackRail, X402Rail, or GoogleAP2Rail and the agent's code doesn't change. The 1.9.0 release added AP2 verifiable credentials. The python port (mnemopay-python) has the same rails behind the same interface — useful if your agent backend is Python and your frontend SDK is TypeScript.

The repo

github.com/mnemopay/mnemopay-sdk — Apache 2.0, npm @mnemopay/sdk, current stable 1.11.0. The Gemini middleware is @mnemopay/sdk/middleware/gemini; OpenAI, Anthropic, and LangGraph wrappers ship alongside it under the same pattern.

Good first issues are tagged. PRs welcome.

— Jeremiah (@mnemopay)

Top comments (0)