Plug-and-play private memory for AI agents: encrypted to your wallet, stored on 0G Storage
dMemo gives AI agents persistent long-term memory that the agent's host never owns. Today every coding and personal agent either forgets you between sessions or hands your memory to a vendor's cloud. dMemo does neither: memories are extracted locally with mem0 OSS, embedded locally (no embedding API call ever leaves the machine), encrypted client-side with ECIES to your own wallet's public key, and persisted as an append-only chain of blobs on 0G Storage. The only thing that ever crosses the network is ciphertext. There is no dMemo account, no dMemo server, and no API key in the memory path.
It's plug and play. One command — npx @dmemo/cli setup — connects a browser wallet (you sign
one message; you are never asked to paste a private key), writes a 0600 config, offers to fund
the account, then detects and wires up every supported agent already installed on your machine:
Claude Code, Codex, OpenCode, OpenClaw, and Hermes. Developers using the raw OpenAI or Anthropic
SDKs get the same thing through a memory-augmented fetch wrapper instead of a host plugin.
The session loop. At session start dMemo resolves your latest blob pointer from chain logs, downloads the head blob, self-verifies its Merkle root against the on-chain root, decrypts it, and replays it into an in-process memory engine — plaintext exists in RAM only. Each turn, the host's hook injects the relevant memory slice into the prompt; the completed turn is captured, embedded locally, and journaled. The journal is flushed asynchronously as an encrypted delta (one on-chain transaction), with a periodic full checkpoint so restores stay one download deep. At session end the final flush lands and RAM is wiped. Nothing readable persists on disk.
Your memory is a wallet, not an account. The on-chain blob format is the contract, not the SDK — so a conversation captured in Claude Code is restored verbatim by OpenCode, Codex, OpenClaw, or Hermes, on any machine, from nothing but the same key. Switching agents, or laptops, doesn't cost you your memory.
It's measured, not asserted. Everything above runs live against 0G (integration-tested on
Galileo testnet, chain 16602; the CLI defaults to Aristotle mainnet, chain 16661). A memory flush
costs ~0.0012 0G. Cold session restore is P50 3.0s / P95 4.03s — inside every host's hook budget
(Claude Code's SessionStart allows 30s). Writes are off the critical path by construction. 66
timing samples and the raw spend log are committed to the repo.
And it's honest about its limits, in docs/disclosure.md, in plain language: encryption hides
what you said, but your wallet address, blob sizes, and write timing are public on a public chain
forever; losing your key means losing your memory permanently, with no custodian and no reset; v1's
forget() is a durable tombstone plus proof-of-key-derivability, not yet a true per-epoch
crypto-shred; and the optional inference leg is only private if the model you route to is actually
TeeML — as of this writing no TeeML chat model is live on the 0G testnet Router, so we say so
rather than implying a guarantee we can't currently deliver.
A TypeScript pnpm monorepo of nine packages, plus a Python package for Hermes and a Next.js marketing site.
Memory engine — mem0 OSS, not a rewrite. We originally planned to fork supermemory; research (all committed under /research) found its memory engine is closed-source, so we pivoted to mem0 OSS (Apache-2.0), which runs fully in-process and exposes a formal VectorStore adapter seam. The key design decision: rather than replacing mem0's store with a 0G-backed one (impossible — 0G reads are blob downloads, not queries, so search can't hit the chain) we wrote a journaling wrapper that implements mem0's VectorStore interface and delegates to the untouched native store. Search runs locally at full speed; every mutation is additionally appended to a journal as a typed delta op. That one seam is what makes flushing to an append-only chain possible without touching engine internals.
Blob format as the interop contract. Our blob-spec package is a zero-dependency canonical format (dmemo/1) with two record kinds — delta (ordered insert/update/delete/deleteCol/tombstone ops) and checkpoint (full materialized state) — under an envelope carrying wallet address, agent scope, a monotonic sequence number, the previous root hash, and pinned embedder + engine identity. Raw SQLite files are explicitly not the format (that was a Phase-0 stepping stone), which is what lets a Python host read a chain a Node host wrote. The pinned embedder identity also drives automatic re-embed migration: if the embedder resolved at open time differs from the one in the head blob, every stored memory is re-embedded and journaled as update ops.
0G Storage (partner tech), used natively. Via @0gfoundation/0g-ts-sdk: indexer.upload / downloadToBlob, with the SDK's native ECIES encryption option (UploadOption.encryption with the recipient public key) and tryDecrypt — no hand-rolled crypto, and the wallet that pays for storage doubles as the memory key, so there is exactly one secret to manage. 0G's append-only, content-addressed, Merkle-anchored log is what makes the "chain of encrypted deltas plus periodic checkpoint" model work at all, and its free Galileo testnet is why every number in docs/benchmarks.md is a live measurement rather than an estimate. Pointer resolution uses ethers eth_getLogs over the Flow contract's Submit logs. Two things we had to build around: the public RPC caps log ranges (live-measured at about 4.78M blocks), so scans paginate with halve-and-retry; and the SDK's with_proof flag is a no-op, and AES-CTR carries no auth tag, so our core package recomputes each blob's Merkle root from the raw downloaded ciphertext and compares it to the on-chain root before decrypting — always on ciphertext, never on an already-decrypted convenience path.
Local embeddings by necessity and by design. The 0G Compute Router has no /embeddings endpoint (verified against its OpenAPI spec), which turned out to be the right constraint: embeddings are computed on-device with fastembed (bge-small-en-v1.5, 384 dimensions), so plaintext never leaves the machine even for vectorization. The optional inference leg is config-only — the Router is OpenAI-compatible, so it drops into mem0 and both SDK wrappers as a base_url, and we send the private trust-mode header plus filter the model catalog to TeeML-verifiable models.
Durability plumbing. Fire-and-forget flush with an awaitable waitForPendingFlush(); a checkpoint every 2 flushes or on a size threshold; a mandatory 120-second application-level upload timeout because the SDK's waitForLogEntry() retries unboundedly with no timeout of its own; a pointer cache that is a pure soft optimization (never load-bearing) with explicit dangling-pointer accounting so a crashed upload can't strand the chain head; and a graceful-shutdown handler on SIGTERM/SIGINT/SIGHUP so a signal arriving mid-flush still runs the bounded flush-and-close a normal return would.
Five host integrations, two hook binaries. Codex's hook schema is deliberately Claude-Code compatible, so one esbuild-bundled set of dependency-free .cjs scripts serves both: Claude Code gets them through a plugin marketplace repo (SessionStart restore, UserPromptSubmit recall, Stop capture-and-flush, PreCompact persist, PreToolUse recall gating, a /dmemo:status command, and dmemo-save / dmemo-search skills), and Codex gets the identical files via an idempotent merge into ~/.codex/hooks.json. OpenCode uses chat.message, chat.messages.transform, and the compaction hook; OpenClaw uses its exclusive memory slot with before_prompt_build and agent_end. Hermes is a Python MemoryProvider on the same blob spec over a Node bridge — and because Hermes' prefetch budget is 3 seconds while a cold restore is about 6 seconds for a 568KB checkpoint, the provider pays restore once inside initialize() at startup and keeps the engine resident, turning per-turn prefetch into 15-85ms. Every hook fails open: with no key configured, they do nothing rather than break your session.
The notably hacky parts:
Never asking for a private key. Onboarding serves a page on 127.0.0.1, discovers wallets via EIP-6963, and asks for one plain-text signature (no transaction, no gas), then derives a separate account from the signature bytes via HKDF-SHA256. Your wallet's own key is never requested, typed, or transmitted — there is deliberately no prompt in the CLI that asks for one, because that's the exact habit phishing relies on. A --scope flag derives further isolated accounts from the same wallet.
Onboarding a user with no crypto at all. Memory writes cost gas, so dmemo fund serves a local page with three rails: send 0G from a discovered browser wallet; convert ETH or USDC from Base, Arbitrum, Optimism, Polygon, or BNB; or pay by card, Apple Pay, or Google Pay (via an embedded Tokenflight widget) which needs no wallet and no crypto. The destination address is locked into the payment URL and not editable from inside the page, and "funded" is always a fresh eth_getBalance read off the chain — never something the payment page claimed.
Stack: TypeScript, pnpm workspaces, mem0 OSS, @0gfoundation/0g-ts-sdk, ethers, fastembed/onnxruntime, better-sqlite3 and bun:sqlite, esbuild, Node 20+ and Bun, Python for Hermes, and Next.js 16, React 19, Tailwind v4, and Base UI for the site.

