SDK
@toda/sdk is the client an agent imports. It is a thin, dependency-light wrapper over the HTTP API plus the two things worth doing on the client: prompt formatting and proof verification.
bun add @toda/sdk @toda/llm
Constructing a client
import { Toda } from "@toda/sdk";
import { ClaudeMemoryProvider } from "@toda/llm";
const toda = new Toda({
apiUrl: process.env.TODA_API_URL!, // your deployment
apiKey: process.env.TODA_API_KEY!, // toda_… - see /data-security/authentication
memoryProvider: new ClaudeMemoryProvider(), // optional; enables ingest()
});
| Option | Required | Purpose |
|---|---|---|
apiUrl | ✅ | Base URL of your toda API. Trailing slash is stripped |
apiKey | ✅ | Server-issued API key, sent as Authorization: Bearer |
memoryProvider | - | An LLM that distills conversation into memories. Required only for ingest() |
fetch | - | Inject a custom fetch - for retries, tracing, or tests |
The SDK holds no keys beyond the API key, opens no sockets, and keeps no state. Constructing one is free.
The memory loop
The whole point of the SDK is two calls, one on each side of your model:
// ── Recall ───────────────────────────────────────────────
const context = await toda.recallForPrompt(userTurn);
const reply = await anthropic.messages.create({
model: "claude-opus-4-8",
max_tokens: 1024,
system:
"You are a helpful assistant with long-term memory.\n" +
(context || "You have no memories about this user yet."),
messages: [{ role: "user", content: userTurn }],
});
// ── Capture ──────────────────────────────────────────────
await toda.ingest(`User: ${userTurn}\nAssistant: ${replyText}`);
Kill the process, start a new one, and the agent remembers. The memory does not live in the process; it lives in a log whose root is on Solana.
Writing
remember(content, metadata?)
Store a pre-distilled memory. Returns immediately - anchoring is asynchronous.
const { id, leafHash, leafIndex } = await toda.remember(
"The user always deploys on Fridays.",
{ kind: "preference" },
);
metadata is not encryptedIt is stored as plain JSONB and hashed into the leaf. Anyone with database access reads it. Secrets go in content.
ingest(conversation)
The capture half of the loop. Hands a slice of conversation to the configured memoryProvider, which extracts only durable facts, then stores each one.
const receipts = await toda.ingest(
"User: I always deploy on Fridays.\nAssistant: Noted!",
);
// → [{ id, leafHash, leafIndex }]
Before extracting, ingest() calls recall(conversation) and passes the existing memories to the provider as a do-not-repeat list. The provider is instructed to return an empty list when nothing is worth storing, so ingest() returning [] is the common, correct case - most conversational turns contain no durable facts.
Throws if no memoryProvider was configured.
Reading
recall(query)
Substring match over the caller's decrypted memory bodies. Returns full Memory objects.
const hits = await toda.recall("deployment");
// [{ id, content, metadata, leafHash, leafIndex, batchId, anchored, createdAt }]
An empty query returns everything.
recallForPrompt(query, limit = 10)
The same call, sliced and formatted for prompt injection:
What you remember about this user:
- The user always deploys on Fridays.
- The user prefers TypeScript.
Returns "" when nothing matches, so it concatenates into a system prompt unconditionally. No branch, no if (memories.length).
Proving
getProof(id)
const bundle = await toda.getProof(id);
// { proof, batchRoot, txSignature, onchainRoot }
verify(id)
const proven = await toda.verify(id); // → boolean
Fetches the bundle, verifies the inclusion proof against batchRoot, and checks batchRoot === onchainRoot.
verify() trusts the API for the on-chain rootonchainRoot arrives in the bundle, from the API. If toda is in your threat model - and if it is not, you did not need toda - this is circular. Fetch the tip yourself:
import { readUserCommit } from "@toda/solana";
import { verifyProof, hexToBytes, bytesToHex } from "@toda/crypto/merkle";
const bundle = await toda.getProof(id);
const tip = await readUserCommit(myOwnConnection, ownerPubkey);
const sound = verifyProof(bundle.proof, hexToBytes(bundle.batchRoot));
const landed = bytesToHex(tip.merkleRoot) === bundle.batchRoot;
See verifying a memory.
verify() returns true only when the memory's batch root equals the current tip - that is, when the memory was covered by the most recent anchor. A memory anchored under an earlier root is still in the log the tip commits to; establishing that requires reconstructing the current root over the full leaf set rather than comparing against the batch root. Applications that need to prove older memories should verify the inclusion proof against a root they recompute, and check that root against the tip.
Staking
getStakeStatus()
const status = await toda.getStakeStatus();
// { live: false, staked: "0", mature: "0", … } ← before token launch
Returns live: false and zeros while the stake mint is unconfigured. See Staking.
Memory providers
A MemoryProvider is one method:
interface MemoryProvider {
extract(input: { conversation: string; existing?: string[] }): Promise<ExtractedMemory[]>;
}
interface ExtractedMemory {
content: string;
kind?: "preference" | "fact" | "decision" | "event" | "identity";
}
@toda/llm ships ClaudeMemoryProvider, which calls Claude with a JSON-schema-constrained output and a system prompt that enforces four rules:
- Each memory is one atomic, self-contained fact, phrased to make sense with no surrounding context - pronouns resolved, subject named.
- Only durable facts: preferences, stable facts, decisions, commitments, identity.
- Skip small talk, acknowledgements, transient state, and anything in the
existinglist. - Return an empty list rather than inventing memories.
Rule 1 is the one that determines whether recall works at all. "He said he'd do it Friday" is useless six sessions later. "Marcus commits to shipping the pricing page on Fridays" is a memory.
new ClaudeMemoryProvider({
apiKey: process.env.ANTHROPIC_API_KEY, // or read from the env
model: "claude-opus-4-8", // default
});
Bring your own key. toda's core loop - store, anchor, verify - runs without any LLM at all; the provider seam exists so that distillation is your choice of model, prompt, and cost.
Implement the interface against any model:
class MyProvider implements MemoryProvider {
async extract({ conversation, existing }) {
return [{ content: "…", kind: "fact" }];
}
}
Errors
Every method throws on a non-2xx response:
Error: Toda /memories: 401
There is no retry, no backoff, and no circuit breaker in the SDK. Inject a fetch that provides them if you need them:
new Toda({ apiUrl, apiKey, fetch: fetchWithRetry });
Types
All request and response shapes are zod schemas in @toda/types - the API contract, shared by the server, the SDK, and the dashboard. Memory, MemoryInput, MemoryReceipt, ProofBundle, MerkleProof, StakeStatus, and ApiKey are re-exported from the SDK.