Skip to content
warfield2016Public

About

Wallet-owned memory for AI agents, on Arkiv. ETHNS x Arkiv Challenge submission.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Mnemos

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

What the schema buys you

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.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│  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.

Entity schemas

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 days

A 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.


Setup

Requirements

  • Bun 1.3+ (or Node 22.10+). The Arkiv SDK needs a modern runtime.
  • A modern browser.

Install + run

git clone <this repo>
cd mnemos
bun install
bun run dev

Open http://localhost:3000. No .env required; the Braga RPC and chain config are baked in.

Get test GLM

  1. On the home page, click Generate new wallet (or import an existing private key).
  2. Click Get test GLM ↗ in the wallet panel; this opens the Braga faucet.
  3. Paste your address. Wait a few seconds for the drip.
  4. The in-app balance polls every 15s. Once GLM lands you can start writing memories.

Seed sample data

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.ts

This avoids leaving the key in shell history.


How an agent integrates

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}`;

Code map

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.


Notes on design

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.


License

MIT

About

Wallet-owned memory for AI agents, on Arkiv. ETHNS x Arkiv Challenge submission.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages