Getting Started
From a cold start to an anchored, proven memory.
Prerequisites
| Tool | Version | Install |
|---|---|---|
| Bun | 1.3.0 - pinned in packageManager | curl -fsSL https://bun.sh/install | bash |
| Postgres | 15 or any modern release | brew install postgresql@15, or use Docker |
| Anchor CLI | 1.1.2 - pinned in Anchor.toml | cargo install --git https://github.com/solana-foundation/anchor avm && avm install 1.1.2 && avm use 1.1.2 |
| Solana CLI | 4.1.1 (Agave) | sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)" |
| Rust | 1.89.0 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
| Anthropic API key | - | Only for ingest(). The store → anchor → prove loop needs no LLM |
You do not need to deploy anything. toda-commit is already live on devnet at 5SwaBH3pXzEKhmL6pbqZ1JgeDiGaf6i5oVj8951RX9sm.
Install
bun install
Database
createdb toda
cd packages/db && bun x drizzle-kit migrate && cd ../..
This creates seven tables: users, memories, commit_batches, merkle_trees, siws_nonces, api_keys, stake_lots.
drizzle.config.ts reads DATABASE_URL, defaulting to postgres://postgres:postgres@localhost:5432/toda. On Homebrew, your Postgres superuser is usually your own username - export the right URL:
export DATABASE_URL="postgres://$(whoami)@localhost:5432/toda"
Environment
cp .env.example .env
Three values must be real. The rest have working defaults.
# Fail-fast at startup if any of these are missing or malformed.
DATABASE_URL="postgres://…/toda"
JWT_SECRET="$(openssl rand -hex 32)" # ≥ 32 chars
MASTER_ENCRYPTION_KEY="$(openssl rand -base64 32)"
TODA_PROGRAM_ID="5SwaBH3pXzEKhmL6pbqZ1JgeDiGaf6i5oVj8951RX9sm"
MASTER_ENCRYPTION_KEY unwraps every user's data key. JWT_SECRET mints sessions for any user. Anyone holding either reads plaintext. Generate them fresh, keep them in a secrets manager, and never commit them. There is no rotation path.
Anchoring authority
The anchor service needs a funded devnet keypair to pay transaction fees.
bin/dev-keypair.sh # idempotent; generates and funds ./authority-keypair.json
The file is gitignored. Never commit it. If devnet's faucet is dry, fund it from faucet.solana.com.
Run
bun run dev
| Service | URL |
|---|---|
| Dashboard | http://localhost:3000 |
| API | http://localhost:3001 |
| Extension API | http://localhost:3002 |
| Docs | http://localhost:3003 |
bun run dev does not start the anchor service. It is a distinct long-running process, and you want exactly one of it:
bun run apps/api/src/worker/anchor.ts
Or bring up Postgres, migrations, the API, the anchor service (worker), and the dashboard together:
docker compose up
The extension API and these docs are not in the compose stack - run them with bun run dev.
Your first memory
Open the dashboard, connect a Solana wallet, and sign the SIWS message. Then mint an API key from the dashboard, or curl for one after authenticating.
import { Toda } from "@toda/sdk";
const toda = new Toda({
apiUrl: "http://localhost:3001",
apiKey: process.env.TODA_API_KEY!,
});
const { id, leafHash, leafIndex } = await toda.remember(
"The user always deploys on Fridays.",
{ kind: "preference" },
);
console.log({ id, leafHash, leafIndex });
The memory is now encrypted, hashed, and appended to your log. It is not yet anchored:
await toda.verify(id); // → false
That is correct. The seal policy fires when 64 leaves accumulate or the oldest pending leaf turns 5 minutes old. With one memory, you wait for the timeout - or force a sweep:
import { anchorUser } from "./apps/api/src/worker/anchor.ts";
await anchorUser(userId, { force: true });
Then:
await toda.verify(id); // → true
Your memory's leaf is folded into a Merkle root, and that root now sits in a PDA on Solana that toda cannot rewrite.
Look at the chain
import { Connection, PublicKey } from "@solana/web3.js";
import { readUserCommit } from "@toda/solana";
import { bytesToHex } from "@toda/crypto";
const connection = new Connection("https://api.devnet.solana.com");
const tip = await readUserCommit(connection, new PublicKey(myWallet));
console.log({
root: bytesToHex(tip.merkleRoot), // 32 bytes committing to your whole log
count: tip.memoryCount, // 1n
nonce: tip.nonce, // 1n - one commit has landed
});
This read goes to Solana, not to toda. It is the only fact in the system that toda cannot fabricate.
Give an agent memory
cd examples/chat-agent
TODA_API_URL=http://localhost:3001 \
TODA_API_KEY=toda_… \
ANTHROPIC_API_KEY=sk-ant-… \
bun run src/agent.ts "I prefer TypeScript and I deploy on Fridays."
Run it again in a fresh process, ask what it knows about you, and it remembers. The memory did not live in that process.
Build the programs
Only needed if you change Rust, or want to run the staking tests.
anchor build # emits target/idl/*.json and target/types/*.ts
@toda/solana consumes committed copies of the IDL in packages/solana/src/idl/. Refresh them after any IDL change, or the typed client will drift from the deployed program.
bun test programs/toda-commit/tests/ programs/toda-staking/tests/
A cold anchor build compiles the full Solana dependency graph and takes several minutes. Incremental rebuilds are ~12 seconds. The staking suite loads target/deploy/toda_staking.so, so build before you test.
Where to go next
- The commitment log - the byte formats every proof depends on.
- Verifying a memory - prove a memory without trusting toda at all.
- Data security - what the guarantees rest on, and what they do not cover.
- Production readiness - read before you deploy.