Operations
Memories are interacted with through four operations: write, read, recall, and prove. This page enumerates exactly what the client and server do for each one, in order.
Write
const receipt = await toda.remember("The user always deploys on Fridays.", {
kind: "preference",
});
When you call remember, these steps execute:
-
Validate. The body is parsed against
memoryInputSchema-contentmust be a non-empty string;metadatamust be a plain object if present. A malformed body is rejected with400before anything is stored. -
Resolve the caller. The bearer token is resolved to a
userIdand anownerPubkey, from either a session JWT or an API key. The user's wrapped data key is loaded in the same step. -
Unwrap the data key. The user's independent 32-byte data key is decrypted from its wrapped blob using a KEK derived from the master key and bound to the
userId. See encryption. -
Encrypt.
contentis encrypted with AES-256-GCM under the data key, using a fresh random 96-bit IV. Only{ciphertext, iv}is retained. The GCM authentication tag is carried inside the ciphertext. -
Compute the leaf.
contentandmetadataare canonicalized and hashed per the leaf encoding. This uses the plaintext, not the ciphertext - the proof must be checkable by anyone holding the memory, and nobody can be expected to reproduce a random IV. -
Append. The row is inserted with the next
leafIndexfor that user.batchIdisnull. The memory is now durable and unanchored. -
Return a receipt.
{ "id": "6c1f…", "leafHash": "9ab3…", "leafIndex": 41 }
metadata is written to Postgres as unencrypted JSONB, and it is hashed into the leaf. Anyone with database access reads it. Anyone holding the memory can recompute the leaf from it. Secrets belong in content.
The write path performs no chain interaction. anchored is false until the anchor service seals a batch covering this leaf - typically within one sweep interval.
Read
const memories = await toda.recall(""); // all
const one = await toda.getProof(id); // one, with proof
Reading reverses the write path:
- Resolve the caller to a
userId; load their wrapped data key. - Fetch the rows scoped to that
userId. Ownership is enforced by query scope, not by a post-hoc check - a memory belonging to another user is never loaded in the first place. - Unwrap the data key; decrypt each
{ciphertext, iv}under it. - Return the decrypted view, including
leafHash,leafIndex,batchId, andanchored.
Recall
const hits = await toda.recall("deployment schedule");
const context = await toda.recallForPrompt("deployment schedule", 10);
recall performs a substring match over the caller's decrypted memory bodies and returns the matching set. recallForPrompt is the same call, truncated to limit hits and formatted for injection into a system prompt:
What you remember about this user:
- The user always deploys on Fridays.
- The user prefers TypeScript.
It returns the empty string when nothing matches, so it can be concatenated into a prompt unconditionally without a branch.
Because memory bodies are encrypted at rest under a per-user key, the database cannot index them. Matching therefore happens after decryption, in the API process, over the caller's own memories. This bounds the work to one user's log - but it is linear in that log, and it is the reason recall quality is a function of the distillation step, not the search step.
The right shape for a large log is to make memories few and dense: ingest() exists to force that discipline, extracting only atomic durable facts rather than transcribing conversation.
Prove
const bundle = await toda.getProof(id);
Steps:
- Resolve the caller and load the memory, scoped to their
userId. A memory that is not theirs returns404. - Load all of the user's leaf hashes, in
leafIndexorder. - Build the inclusion proof for
memory.leafIndexagainst that leaf set. - Load the batch this memory was anchored under, if any. Its
merkleRootis the root as it stood at anchor time; itstxSignatureis the anchoring transaction. - Read the user's live on-chain tip - the root currently in their commitment account.
- Return all four.
{
"proof": { "leaf": "9ab3…", "leafIndex": 41, "siblings": ["c40f…"], "directions": [true] },
"batchRoot": "7de2…",
"txSignature": "5Kx…",
"onchainRoot": "7de2…"
}
Verifying
toda.verify(id) fetches the bundle and checks two things:
verifyProof(bundle.proof, hexToBytes(bundle.batchRoot)) // the proof is internally sound
&& bundle.batchRoot === bundle.onchainRoot // the root is the one on-chain
Both must hold. The first is arithmetic and needs nobody's cooperation. The second is the claim that costs trust - and toda supplying onchainRoot in the bundle does not discharge it.
A proof bundle is data from toda. If toda is the adversary in your threat model, the onchainRoot field is worthless. Fetch the commitment account yourself:
import { readUserCommit } from "@toda/solana";
const tip = await readUserCommit(myConnection, ownerPubkey);
bytesToHex(tip.merkleRoot) === bundle.batchRoot;
See verifying a memory for the full independent path.
Delete
There is no delete operation, by construction.
Removing a row from Postgres removes the ability to produce the memory, but the leaf remains folded into every root that ever covered it, and the on-chain memory_count still acknowledges it. The absence is therefore detectable: rebuild the log, and either the count is short (the chain rejects a shorter commit) or the recomputed root disagrees with the tip.
This is a feature. A memory layer where deletion is invisible cannot serve as a record of anything.
If you need a memory to stop influencing recall, write a superseding memory and let your application's resolution logic prefer the newer one - the event-sourced pattern. The history stays intact and stays provable.
Next: The anchor service.