Skip to main content

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 userId and an ownerPubkey, 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:

FieldTypeMeaning
ownerPubkeyThe wallet this log belongs to
authorityPubkeyThe key permitted to commit on the owner's behalf
merkle_root[u8; 32]The current commitment over the owner's entire log
memory_countu64Number of leaves the root covers
nonceu64Monotonic ordering counter, incremented per commit
last_updatedi64Unix 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:

DataClientAPIAnchor servicePostgresSolana
Memory plaintext✅ owner only✅ transiently
Ciphertext + IV
Metadata✅ owner only✅ (plain JSON)
Leaf hash
Merkle rootpublic
Master encryption key
Anchoring authority key
Metadata is not encrypted

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:

ServicePortAuthConsumers
apps/api3001SIWS JWT · API keysSDK, dashboard
apps/extension-api3002SIWS session tokensChrome 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.