Turns raw daily notes into a structured, cited wiki — on Fangorn, sealed, with a local model. Nothing leaves the machine except ciphertext.
raw namespace wiki namespace
(written from drive) (written only by this script)
readNamespace ──▶ unseal ──▶ llama3.2:3b ──▶ seal ──▶ commit + push
notes, days, concepts, index
The drive MCP would hand back the same note text, and with less ceremony. Two things it can't give you:
- CIDs. A vertex CID is the only immutable, citable reference to the exact
bytes a page was distilled from.
read_notereturns a string;read_publishedhas a CID but routes through the public endpoint, so private notes come back as a placeholder. Provenance has no other source. - A snapshot. The relay's working tree changes under you mid-run. A published vertex is what it was when you signed for it.
You need ollama running locally and a Fangorn wallet.
pnpm install
ollama pull llama3.2:3b
cat > .env <<'EOF'
AGENT_PRIVATE_KEY=0x... # this agent's own wallet — generate a fresh one
OWNER_ADDRESS=0x... # you: the wallet that owns the raw namespace
READ_SECRET=0x... # from drive's agent panel; opens sealed notes
EOF
pnpm startThe agent never gets your private key. Three credentials, and only the middle one is yours:
AGENT_PRIVATE_KEY— the agent's own wallet. It publishes the wiki and pays the gas. Generate a fresh one; fund it with a little Arbitrum Sepolia ETH. The script registers it as a publisher on first run (free).OWNER_ADDRESS— your address, so the agent knows whose raw notes to read. Public information.READ_SECRET— from drive's agent panel: tick "also hand over the decryption key" when minting a token, and it's shown once, next to the token. It is not a wallet key: it decrypts, and that is all — it cannot spend, publish, or sign. It is bound to one namespace, and it cannot be revoked — re-keying means re-sealing every note.
If you run the agent as yourself, omit READ_SECRET and the script derives it
from AGENT_PRIVATE_KEY — the single-wallet setup, where the agent's wallet
is your drive login wallet. (Privy embedded wallets never expose a private
key, so that mode needs external-wallet login; the three-credential setup above
does not.)
| env | default | |
|---|---|---|
AGENT_PRIVATE_KEY |
— | required; the agent's own wallet |
OWNER_ADDRESS |
— | required; owner of the raw namespace |
READ_SECRET |
derived from the agent key | decrypt-only credential |
RAW_NAMESPACE |
second brain |
where you author |
WIKI_NAMESPACE |
second brain wiki |
written only by this script |
OLLAMA_MODEL |
llama3.2:3b |
any local model that honours a JSON schema |
OLLAMA_HOST |
http://127.0.0.1:11434 |
|
PINATA_GATEWAY |
— | optional read gateway |
node index.js --selfcheck runs the pure parts — date extraction, slugs, the
raw/derived split, concept aliasing, wikilink targets, seal/unseal binding. No
network, no model.
node --env-file=.env index.js --dry runs the whole pipeline and writes the
rendered pages to ./out instead of publishing. Prompt tuning is a dozen
iterations of reading the pages; none of them should cost a tx.
A walkable tree, not a pile of pages:
index.md front page, laid out like Wikipedia's
├── notes.md every note, grouped by kind
│ └── <note>.md one page per source note
├── days.md the timeline
│ └── day-*.md a date, its notes, their key points
└── concepts.md hubs, then the mentioned-once vocabulary
└── concept-*.md a concept, its gloss, and every note it appears in
Note pages are the only ones the model writes: title, kind, one-sentence summary, key points, concepts with a gloss each, open questions, and the source CID they were distilled from. Everything else is derived — no model, so identical mentions produce identical bytes.
index.md borrows Wikipedia's front page: a featured note (the one sharing
the most concepts with the rest — its own words, not a summary of a summary),
did you know (one fact per note, so it's a cross-section rather than the
same note five times), open questions (the loose ends, which nothing else
surfaces), a browse table, recently, and how it connects with the
mermaid map.
Every pick is deterministic — most-connected first, ties broken by path. Wikipedia rotates its front page because it has editors and a clock; a random or dated pick would rewrite this page and its CID on every run while saying nothing new.
The three section pages exist as pages rather than as headings on one long
index because they double as the folders in .tree.json: drive nests its
sidebar under a real note, so a browsable tree and a readable table of contents
are the same three files.
.tree.json is the one vertex published in the clear (tag meta, payload
{path, content, updatedAt} — exactly what drive writes). The browser has to
draw the sidebar before it holds any key, and structure was never the secret:
filenames are already clear on every page. Without it a namespace is sixty loose
pages in no order, all cross-linked and none of it walkable.
One page per note, not per day: a day bucket blends a trail run, a standup and a paper into one flat summary, while a note keeps its own voice, cites exactly one CID, and re-distills on its own when only it changed. Days and concepts fall out of the note pages for free.
Page vertices are tagged doc and .tree.json is tagged meta, matching drive
exactly — the tag becomes the vertex's schemaId, and it is how drive tells a
page from the structure that holds it.
Page payloads are { path, updatedAt, enc, data?, source? }. Clear: identity,
ordering, provenance. Sealed: the prose, plus (on note pages) the structured
data it was rendered from, so a cached note still feeds the derived pages with
no model call. That's the same shape drive publishes, with the same
resourceId = keccak256("<namespace>:<path>"), so the body is the rendered
page — there is no separate output layer to keep in sync.
The wiki is sealed under its own namespace-bound key, so in the single-wallet
setup drive derives it the same way the browser does. With a separate agent
wallet you can't reproduce that derivation, so a handed-over READ_SECRET takes
precedence: it's the one credential both sides hold.
Note that drive can't render the sealed wiki today — the browser only ever
seals, it has no unseal-on-read path, and the namespace belongs to the agent
rather than to you. The tree will draw; the bodies won't open. Reading them back
is --dry's job for now.
One writer per namespace. Drive owns raw; this script owns wiki. Two
writers moving the same namespace head contend for the same compare-and-swap,
and the loser's publish reverts.
The wiki belongs to the agent's address. commitStateRoot keys a namespace
by msg_sender and the registry has no delegation primitive — no addWriter,
no operator approval — so whoever publishes is the publisher. That's fine
here: the wiki is derived, so losing the agent key costs you an address, not
knowledge; re-run with a new agent and it rebuilds from raw. It also means the
publisher address records which agent produced a page, which is the thing
you want once more than one of them is writing.
Never distill your own output. Two ways that happens, both guarded. Same
namespace name under the same wallet is the same timeline, so a replace: true
publish would land the wiki on top of the notes it came from — the script refuses
to start. And a raw namespace that already holds a previous agent's pages
(under a wiki/ folder, or as orphans outside the tree) filters them out via
.tree.json, which is the only thing that knows the folder a flat vertex path
sits in.
Incremental by source CID and prompt. A note page records the content
address of the note it came from, plus distilledBy (<model>/v<n>) — because
editing the prompt doesn't move a source CID, and without that second key every
prompt improvement would apply to new notes only, leaving the wiki a mix of two
eras. Bump PROMPT_VERSION when SYSTEM or SCHEMA changes.
Unchanged input → the prior payload is re-staged verbatim →
identical bytes → identical CID → free in storage, invisible in the diff, and no
model call. Derived pages compare their rendered markdown against the previous
run and reuse the payload when it matches. Run time tracks what changed, not how
long you've been journaling.
Failures skip, they don't stub. A 3B model gets JSON wrong sometimes. The schema is passed as a grammar constraint rather than parsed out of a fenced block, and when a note still fails it is left out so the next run retries it. A placeholder page is a lie that looks finished.
One concept, one page. A small model spells the same thing two ways across
notes — SSE here, Server-Sent Events (SSE) there. A parenthetical is the
model naming both forms, so it seeds an alias table every renderer resolves
through: the note page and the concept page can't disagree about the slug, which
is what a dead wikilink is made of.
Provenance is a payload field, not an edge. Edges may only reference
vertices staged in the same commit, so they cannot point from wiki into raw.
Cross-namespace citation lives in source: {path, cid}. Edges within the commit
(note --mentions--> concept, day --on--> note) are staged alongside the
wikilinks that mirror them.
Snapshot commits, incremental content. Each run stages the whole wiki with
replace: true, so current contents are exactly what the last run produced —
and a note deleted from raw disappears from the wiki, rather than lingering as
an orphan. Staging everything is not the same as rewriting everything: an
untouched page is re-staged as the byte-identical prior payload, which hashes to
the same vertex CID and never enters the commit's diff. History is not lost —
it's the commit chain, walkable with fangorn.log().
Page paths don't depend on their neighbours. A note's page is named from its
own source path, and contested slugs (two notes slugging the same, or a note
slugging onto index.md) are settled by a hash of that same source path. If
arrival order decided it instead, adding a note that sorts earlier would rename
a page that was already published and break every link into it.
Each run prints what it cost:
65 pages: 58 unchanged, 7 rewritten · 1 model call(s)
Bodies are sealed self-hkdf-v1 (your key alone). The paid path is
worker-usdc-v1: sealed to the access worker, released after
SettlementRegistry.isSettled(buyer, resourceId) passes. resourceId is
already one-per-page, which is the unit of sale.
Deferred because it needs the access worker's URL, static X25519 pubkey and
upload token, and nothing is priced yet. It costs nothing to defer: the
plaintext was never public, so switching is a re-seal under the same
resourceId — no leak to undo, no change to the graph.
Publishing a page in the clear is the one-way door. Anything public is content-addressed and permanent; it can never become paid. Sealing everything now and opening summaries later is the reversible direction.
Ollama refusing connections on Linux — try disabling ipv6:
sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1If a run fails inside storage or the metagraph engine, some native deps were
skipped at install: pnpm approve-builds.
index.py is the previous version — same idea, over the drive MCP, without
provenance or incrementality. Kept for reference; delete it once this has run.