Wallet-owned memory for AI agents, on Arkiv. Submitted to the ETHNS × Arkiv Challenge (AI theme).
Every Mnemos memory is an Arkiv entity signed by the user's wallet. Any compatible client can read them. Sign into a different app and the context comes with you, because it lives on the ledger.
- Live demo: https://mnemos-psi.vercel.app
- Network: Arkiv Braga testnet (chain id
60138453102) - Project attribute:
app = mnemos:v1 - Entity kinds:
memory,conversation
| Property | What it gives you |
|---|---|
$creator (immutable) |
Proves which agent wrote which memory. |
$owner (mutable) |
User can transfer or burn a memory. |
| Shared attribute keys | A memory references its parent conversation by storing the conversation's entity key. Arkiv has no JOINs. |
| Numeric attributes | importance and createdAt support range queries (gte, lte). "High-importance memories from the last 24h" is one query. |
expiresIn |
Lifetime per memory; Arkiv garbage-collects when the time is up. |
┌─────────────────────────────────────────────────────────────────┐
│ Browser (Next.js 16 App Router, all client-side) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌──────────────┐ │
│ │ Wallet (in │ │ Arkiv SDK │ │ React UI │ │
│ │ localStorage, │──▶│ (Viem-based) │──▶│ (Tailwind) │ │
│ │ generated │ │ │ │ │ │
│ │ client-side) │ └────────┬────────┘ └──────────────┘ │
│ └─────────────────┘ │ │
└────────────────────────────────┼──────────────────────────────┘
│ HTTPS JSON-RPC
▼
┌─────────────────────────────────────────────────────────────────┐
│ Arkiv Braga testnet (DB-Chain on Ethereum L2) │
│ │
│ memory ┐ │
│ memory ┼──── shared attr: { conversation: <entity key> } │
│ memory ┘ │
│ conversation ┐ │
│ conversation ┼──── all tagged with { app: "mnemos:v1" } │
│ conversation ┘ │
└─────────────────────────────────────────────────────────────────┘
No backend. The user's wallet signs every write; reads use a public RPC client. The server is the Arkiv DB-Chain.
Memory (kind = "memory")
attributes: [
{ key: "app", value: "mnemos:v1" }, // PROJECT_ATTRIBUTE
{ key: "kind", value: "memory" },
{ key: "agent", value: "claude" }, // claude | gpt | gemini | ...
{ key: "topic", value: "preferences" }, // free-form
{ key: "importance", value: 80 }, // numeric — range-queryable
{ key: "createdAt", value: 1716480000000 }, // numeric — range-queryable
{ key: "conversation", value: "0xabc..." }, // optional — parent entity key
]
payload: { content: string, tags: string[], source?: string } // JSON
contentType: "application/json"
expiresIn: 30 days (configurable per memory)Conversation (kind = "conversation")
attributes: [
{ key: "app", value: "mnemos:v1" },
{ key: "kind", value: "conversation" },
{ key: "agent", value: "claude" },
{ key: "messageCount", value: 0 }, // numeric
{ key: "createdAt", value: 1716480000000 }, // numeric
{ key: "lastUsedAt", value: 1716480000000 }, // numeric
]
payload: { title: string, summary?: string, agent: string } // JSON
contentType: "application/json"
expiresIn: 90 daysA Memory stores its parent Conversation's entity key under the conversation attribute. Arkiv has no foreign keys; shared attribute equality is the only relationship mechanism.
- Bun 1.3+ (or Node 22.10+). The Arkiv SDK needs a modern runtime.
- A modern browser.
git clone <this repo>
cd mnemos
bun install
bun run devOpen http://localhost:3000. No .env required; the Braga RPC and chain config are baked in.
- On the home page, click Generate new wallet (or import an existing private key).
- Click Get test GLM ↗ in the wallet panel; this opens the Braga faucet.
- Paste your address. Wait a few seconds for the drip.
- The in-app balance polls every 15s. Once GLM lands you can start writing memories.
A Bun script writes ~10 sample memories so the UI is not empty on first load:
# Put your funded private key in a local file (gitignored by default)
echo 'PRIVATE_KEY=0xYOUR_KEY' > .env.local
bun run --env-file=.env.local scripts/seed.tsThis avoids leaving the key in shell history.
Mnemos is a schema, not a service. Any agent integrates by reading and writing Arkiv entities that follow the schema. The user controls the data because they hold the key.
See the For agents page in the app, or scripts/agent-example.ts. Quick version:
import { createPublicClient, http } from "@arkiv-network/sdk";
import { braga } from "@arkiv-network/sdk/chains";
import { eq, gte } from "@arkiv-network/sdk/query";
const client = createPublicClient({ chain: braga, transport: http() });
// Recall before generating
const memories = await client
.buildQuery()
.where([
eq("app", "mnemos:v1"),
eq("kind", "memory"),
eq("topic", "preferences"),
gte("importance", 50),
])
.ownedBy(userAddress)
.withPayload(true)
.orderBy("createdAt", "number", "desc")
.limit(20)
.fetch();
const context = memories.entities.map((e) => e.toJson().content).join("\n");
const prompt = `User preferences from prior sessions:\n${context}\n\nNow: ${userInput}`;src/
├── app/
│ ├── layout.tsx Root layout, fonts, providers
│ ├── providers.tsx Wallet + toast contexts (client)
│ ├── page.tsx Landing
│ ├── memories/page.tsx Memory CRUD + filter bar
│ ├── conversations/page.tsx Conversation CRUD
│ └── docs/page.tsx Integration guide
├── components/
│ ├── TopBar.tsx Nav + wallet chip
│ ├── WalletPanel.tsx Generate / import / export / sign out
│ ├── MemoryComposer.tsx Write memory form
│ ├── MemoryCard.tsx Read view with $creator / $owner links
│ ├── ConversationComposer.tsx
│ └── ConversationCard.tsx
└── lib/
├── arkiv.ts SDK clients + PROJECT_ATTRIBUTE constants
├── wallet.ts In-browser keypair (generate / import / export)
├── balance.ts GLM balance via publicClient
├── entities.ts All Arkiv reads + writes (the only file
│ that imports createEntity / buildQuery)
├── types.ts Memory + Conversation domain types
└── format.ts truncateHex, relativeTime, formatGlm
scripts/
├── seed.ts Seeds sample memories from PRIVATE_KEY
└── agent-example.ts Standalone "external agent recall" demo
The file you most want to read is src/lib/entities.ts — it's the entire Arkiv-facing layer in ~180 lines.
Keys in localStorage. Generated on first visit. localStorage is XSS-readable, which is acceptable for testnet GLM the user can rotate freely; asking judges to add a custom network in MetaMask would lose most of them before they see a memory get written. A real deployment would WebCrypto-encrypt the key under a passphrase.
Numeric vs. string attributes. importance and createdAt are stored as numbers so Arkiv indexes them for range queries. Everything else (topic, agent, kind, app) is a string because we only need equality. Once you write an entity with a string attribute, it stays a string; see src/lib/entities.ts.
PROJECT_ATTRIBUTE is the only scoping mechanism. Arkiv has one shared entity store; there are no separate databases. Every Mnemos query filters on eq("app", "mnemos:v1"). Without it, results would include every other app on the chain.
No backend. Every Arkiv call runs in the browser. The user has to hold their own key; that is the trade-off for not having a database to leak.
Expiry per memory. expiresIn defaults to 30 days (1–365 configurable). Conversations default to 90. Arkiv garbage-collects after that.
MIT