Architecture
toda has five actors: the client, the API, the anchor service, the commitment program, and the chain. Each has exactly one job, and the boundaries between them are the security boundaries of the protocol.
Client
Anything holding a bearer token. Three exist today:
@toda/sdk- what an agent imports. Authenticates with a server-issued API key (toda_…).- Dashboard - Next.js, authenticates with a Solana wallet via SIWS and holds a short-lived JWT.
- Chrome extension - captures conversations from ChatGPT, Claude, Gemini, and Grok and writes them as memories. See the capture layer.
Clients never hold the master encryption key and never sign anchoring transactions. A client that wants to verify a memory needs nothing from toda but the proof bundle - verification runs against a Solana RPC the client chooses.
API
A Hono server. It is the only component that sees plaintext, and it sees it only in memory, only for the duration of a request.
Its responsibilities:
- Authenticate the caller to a
userIdand anownerPubkey, from either a session JWT or an API key. - Encrypt on write and decrypt on read, using the caller's per-user data key unwrapped from their wrapped key blob.
- Append the leaf to the caller's log, assigning the next
leafIndex. - Assemble proofs - rebuild the sibling path for a leaf and fetch the live on-chain root.
- Prepare staking transactions - unsigned, for the user's own wallet to sign. The API never holds a key that can move staked funds.
The API does not anchor. It cannot: it has no reason to hold the anchoring authority keypair on a request-serving process.
Anchor service
A separate long-running process with one loop:
every ANCHOR_INTERVAL_MS (default 300s):
for each user with pending leaves:
if seal policy fires:
advance the persisted incremental Merkle tree
read the on-chain nonce
submit commit_root(root, count, nonce)
confirm, mark the batch confirmed
It is the only holder of the anchoring authority keypair. It is idempotent under crash: a batch that fails is marked failed and re-attempted on the next sweep; a tree save that loses an optimistic-concurrency race is skipped and retried. See the anchor service for the full state machine.
Commitment program
programs/toda-commit, an Anchor program on Solana. It stores one account per user - the commitment account, a PDA at ["user", owner_pubkey] - containing:
| Field | Type | Meaning |
|---|---|---|
owner | Pubkey | The wallet this log belongs to |
authority | Pubkey | The key permitted to commit on the owner's behalf |
merkle_root | [u8; 32] | The current commitment over the owner's entire log |
memory_count | u64 | Number of leaves the root covers |
nonce | u64 | Monotonic ordering counter, incremented per commit |
last_updated | i64 | Unix timestamp of the last commit |
Two instructions: init_user and commit_root. The program stores and gates the root; it never recomputes it. The byte format of the root is defined entirely by @toda/crypto, and the program's only job is to make sure the wrong party cannot write one and the right party cannot write one out of order.
See the program reference for accounts, errors, and instruction signatures.
Chain
Solana holds the commitment accounts. It is the one component toda does not operate and cannot rewrite, which is the only reason the whole construction means anything.
Nothing else goes on-chain. No ciphertext, no metadata, no memory count per batch, no leaf hashes. A validator sees 32 bytes of high-entropy data, a counter, and a nonce.
The confidentiality boundary
Drawn precisely, because it is the thing people get wrong:
| Data | Client | API | Anchor service | Postgres | Solana |
|---|---|---|---|---|---|
| Memory plaintext | ✅ owner only | ✅ transiently | ❌ | ❌ | ❌ |
| Ciphertext + IV | ❌ | ✅ | ❌ | ✅ | ❌ |
| Metadata | ✅ owner only | ✅ | ❌ | ✅ (plain JSON) | ❌ |
| Leaf hash | ✅ | ✅ | ✅ | ✅ | ❌ |
| Merkle root | ✅ | ✅ | ✅ | ✅ | ✅ public |
| Master encryption key | ❌ | ✅ | ❌ | ❌ | ❌ |
| Anchoring authority key | ❌ | ❌ | ✅ | ❌ | ❌ |
metadata is stored as plain JSONB and is part of the leaf preimage. It is visible to anyone with database access and it affects the leaf hash. Do not put secrets in metadata. Put them in content, which is encrypted.
Two API surfaces
toda runs two HTTP services against the same database and the same crypto primitives:
| Service | Port | Auth | Consumers |
|---|---|---|---|
apps/api | 3001 | SIWS JWT · API keys | SDK, dashboard |
apps/extension-api | 3002 | SIWS session tokens | Chrome extension, capability grants |
They share @toda/crypto and @toda/db, so a memory written through either surface produces the same leaf and lands in the same log under the same ownerPubkey. They differ in their session model and in what they expose: capability grants (agent cases, memory drops) live only on the extension API.
Deployment shape
The whole system is four processes and a database:
apps/api :3001 HTTP, stateless, scale horizontally
apps/api worker - anchor service, exactly one instance
apps/extension-api :3002 HTTP, stateless
apps/dashboard :3000 Next.js
postgres :5432 the only stateful component
Run more than one anchor service and the nonce-mismatch guard will make one of them lose every race - correct, but wasteful. Run exactly one.
Next: toda fundamentals - the vocabulary the rest of these docs use without further explanation.