The static-facts ledger: pricing a reference file
TL;DR. "Memory" and "persistent facts" stop being confusing the moment you separate two
questions that sound like one: where a fact lives (disk, free) and when a fact is loaded
(into the window, billed). A static reference file, say a repos.md describing your
open-source projects, costs nothing to keep and plenty to carry: loaded always, it is re-sent
on every turn of every session, and the bill scales with the file times your total turn count.
This chapter builds that file for real, prices four loading designs with the book's cost model,
and lands the ladder: always-loaded loses to an index above a tiny usage threshold, and
targeted retrieval beats both by orders of magnitude once the file grows. The output side barely
matters (writes are one-time); the input side is the whole game, and caching softens it by 10x
without changing its shape.
Contents
- Memory and persistent facts, untangled
- The scenario: your repos.md
- The lab: four loading designs, priced
- Reading the tables
- The loading ladder
- Where Serena fits
- Doing this in Claude Code today
- Further reading
- Takeaways
Chapter 9 built a memory store, and Chapter 18 mapped where Claude Code's layers live on disk. What neither chapter did is price the design you are most likely to build first: a hand-maintained file of durable facts, always loaded. This chapter does that, because the pricing is what resolves the memory-versus-persistent-facts confusion for good.
Memory and persistent facts, untangled
Every persistence mechanism in this book, and every one in Claude Code, is the same two-step: a fact lives somewhere durable, and some rule decides when it enters the window. The mechanisms differ only in who writes the fact and when it loads:
| Mechanism | Who writes it | When it loads | Billed |
|---|---|---|---|
CLAUDE.md hierarchy (Ch 18) | you | every session, in full, at launch | every turn (cached) |
@path imports in CLAUDE.md | you | every session, expanded at launch | every turn (cached) |
Auto memory MEMORY.md index | Claude | every session (first 200 lines / 25KB) | every turn (cached) |
| Auto memory topic files | Claude | only when Claude reads one | that session, from the read on |
A file you point to ("read docs/repos.md when...") | you | only when the agent reads it | that session, from the read on |
| Serena memories (Ch 38) | the agent, on request | read_memory on demand | that session, from the call on |
| MCP memory servers (Ch 41) | the agent, per exchange | a retrieved slice per query | per query, slice only |
The transcript (--resume) | the tool | when you resume it | every turn of the resumed session |
Don't be confused. Persistent describes where the fact lives; loaded describes whether it is in the current window. A fact can be persistent and never loaded (a topic file nobody reads: free, and useless today), loaded but not persistent (something said mid-session: gone tomorrow), or both (the
CLAUDE.mdline that loads every session). Disk space is free. Window space is billed per turn and competes with the work. All memory engineering is deciding which facts earn the loaded column, and for how long.
The confusion between "memory" and "persistent facts" dissolves here: they are the same thing at
different rungs of the loading rule. A CLAUDE.md is memory with the rule "always." A memory
system like mem0 or Letta is a fact file with the rule "the slice
retrieval picks." Everything else is tuning.
The scenario: your repos.md
Take the concrete case this chapter prices. You track dozens of open-source projects: what each
repo is, where it lives, why you care. You want Claude Code to know this ledger in any session
that touches your projects. The obvious first design is to paste it into CLAUDE.md (or
@-import it, same effect: Chapter 18 showed imports expand at launch).
The obvious design has a hidden multiplier. Chapter 2 established that input is re-sent every turn, and Chapter 6 that caching makes re-sends cheaper (0.1x) but not free, with every edit re-paying the 1.25x write. So the ledger's real cost is not its token count; it is its token count times every turn of every session it rides in, at the blended cache rate, whether or not the session ever mentions your repos.
The output side, for contrast, is nearly irrelevant here. Writing a 60-token update to the ledger costs 60 output tokens once (at the 5x output price, still a fraction of a cent). Facts are written once and carried forever; the carrying, not the writing, is the bill. That asymmetry is why every design question below is an input-side question.
The lab: four loading designs, priced
The script builds the ledger from a real entry template (so sizes are measured, not assumed), then prices a working month (40 sessions of 25 turns) under four designs: A always loaded in the prefix, B a short index always loaded with the full file read on demand, C targeted retrieval of only the entries used, and D always loaded with no caching, the naive mental model most people price in their heads.
"""The static-facts ledger: what a reference file really costs.
This is a COST MODEL in the style of optimization_lab.py, not a live run. The
scenario: you maintain a personal reference file, repos.md, that describes the
open-source repositories you care about (name, URL, what it is, how you use it).
The questions: what does that file cost per month under each way of loading it,
where is the break-even between "always in context" and "fetched on demand",
and what happens as the ledger grows?
The file itself is BUILT by this script (a realistic entry template), so the
token counts are measured from real text with the chars/4 estimate used
throughout this book, not invented.
Prices, US dollars per million tokens (chapter 2):
opus-4-8 input $5 output $25
Prompt caching (chapter 6): cache write 1.25x input, cache read 0.1x input.
Standard library only. Run: python3 static_facts_cost.py
"""
OPUS_IN, OPUS_OUT = 5.0, 25.0 # $/Mtok
CACHE_WRITE, CACHE_READ = 1.25, 0.10 # multipliers on input price
M = 1_000_000
WINDOW = 200_000 # the window the prefix competes for
def tokens(text):
"""The book's standing estimate: one token per four characters."""
return len(text) // 4
# ---------------------------------------------------------------------------
# 1. Build the reference file, for real, so its size is measured, not assumed.
# ---------------------------------------------------------------------------
SEED_REPOS = [
("serena", "oraios/serena", "MCP server; symbol-level retrieval and editing over a language server; project memories in .serena/memories/"),
("multilspy", "microsoft/multilspy", "uniform Python client over language servers; the library under Monitor-Guided Decoding"),
("mem0", "mem0ai/mem0", "long-term memory layer; extracts atomic facts from exchanges into a searchable store"),
("letta", "letta-ai/letta", "MemGPT lineage; self-editing core memory blocks plus archival store, agents as services"),
("graphiti", "getzep/graphiti", "temporal knowledge graph memory; facts carry validity intervals, contradictions expire edges"),
("llmlingua", "microsoft/LLMLingua", "prompt compression, coarse-to-fine token pruning before the call"),
("vllm", "vllm-project/vllm", "inference server; PagedAttention KV-cache paging"),
("sglang", "sgl-project/sglang", "inference server; RadixAttention prefix sharing"),
("gptcache", "zilliztech/GPTCache", "semantic response cache keyed on embedding similarity"),
("repomix", "yamadashy/repomix", "pack a repository into one prompt-sized file with tree-sitter compression"),
]
def build(n):
"""A ledger with n entries, and its short-index variant."""
repos = list(SEED_REPOS)
for i in range(n - len(SEED_REPOS)):
repos.append((f"tool-{i:03d}", f"example/tool-{i:03d}",
"supporting project tracked for reference; notes on what it does and when it was last evaluated"))
header = "# My open-source reference ledger\n\nOne entry per repo: name, org/repo, what it is, why it is here.\n\n"
body = "".join(f"- **{name}** ({slug}): {what}\n" for name, slug, what in repos)
index = "# Repo index (read repos.md for detail)\n\n" + "".join(
f"- {name}: {what.split(';')[0]}\n" for name, _, what in repos)
full = header + body
return tokens(full), tokens(index), tokens(full) // n
F, I, E = build(40)
print(f"repos.md at 40 entries: ~ {F:,} tokens index: ~ {I} tokens one entry: ~ {E} tokens\n")
# ---------------------------------------------------------------------------
# 2. The workload: a working month of Claude Code sessions.
# ---------------------------------------------------------------------------
SESSIONS = 40 # sessions per month
T = 25 # turns per session
K = 2 # entries actually consulted when the file is needed
TOOL_OVERHEAD = 80 # tool_use + tool_result framing for one retrieval call
CHURN = 0.20 # share of sessions in which you edit the file (invalidates cache)
def dollars(write_toks, read_toks, out_toks=0):
return (write_toks * CACHE_WRITE + read_toks * CACHE_READ) * OPUS_IN / M \
+ out_toks * OPUS_OUT / M
def month(p, design, f, i, e):
"""Monthly dollars for one loading design, at file size f / index i /
entry e. p = share of sessions that actually consult the file."""
need = SESSIONS * p
if design == "A": # always loaded: file sits in the stable prefix
# One cache write per session, cache reads after; a churn session
# edits the file mid-way and re-writes the prefix once more.
cost = SESSIONS * dollars(f * (1 + CHURN), f * (T - 1))
cost += SESSIONS * CHURN * 60 * OPUS_OUT / M # the edit itself, output-priced
elif design == "B": # index in prefix, full file read on demand
cost = SESSIONS * dollars(i, i * (T - 1))
cost += need * dollars(f + TOOL_OVERHEAD, f * (T // 2))
elif design == "C": # targeted retrieval: only the entries used
slice_ = TOOL_OVERHEAD + K * e
cost = need * dollars(slice_, slice_ * (T // 2))
elif design == "D": # always loaded, NO caching (the naive mental model)
cost = SESSIONS * f * T * OPUS_IN / M
return cost
# ---------------------------------------------------------------------------
# 3. Sweep 1: how often do you actually use the file?
# ---------------------------------------------------------------------------
print(f"=== Monthly cost, 40-entry file, {SESSIONS} sessions x {T} turns, "
f"edited in {CHURN:.0%} of sessions ===")
print(f"{'consulted in':>13}{'A always':>12}{'B index+read':>14}{'C targeted':>12}{'D no cache':>12}")
print("-" * 63)
for p in (0.10, 0.25, 0.50, 0.75, 1.00):
row = [month(p, d, F, I, E) for d in "ABCD"]
print(f"{p:>12.0%} {row[0]:>11.2f}{row[1]:>14.2f}{row[2]:>12.2f}{row[3]:>12.2f}")
print("-" * 63)
lo, hi = 0.0, 1.0
for _ in range(60):
mid = (lo + hi) / 2
if month(mid, "B", F, I, E) < month(mid, "A", F, I, E):
lo = mid
else:
hi = mid
print(f"Break-even A vs B: always-loaded wins only above p = {hi:.0%}, "
f"and D shows what caching is silently absorbing.\n")
# ---------------------------------------------------------------------------
# 4. Sweep 2: the ledger grows. Same habits, one year later.
# ---------------------------------------------------------------------------
P = 0.25 # a realistic consult rate: one session in four touches it
print(f"=== The same file as it grows (consulted in {P:.0%} of sessions) ===")
print(f"{'entries':>8}{'file tok':>10}{'A always':>12}{'B index+read':>14}{'C targeted':>12}{'% of window':>13}")
print("-" * 69)
for n in (40, 100, 200, 400, 800):
f, i, e = build(n)
a, b, c = (month(P, d, f, i, e) for d in "ABC")
print(f"{n:>8}{f:>10,}{a:>12.2f}{b:>14.2f}{c:>12.2f}{f / WINDOW:>12.1%}")
print("-" * 69)
f800, i800, e800 = build(800)
a = month(P, "A", f800, i800, e800); c = month(P, "C", f800, i800, e800)
print(f"""
At 800 entries the always-loaded design costs ${a:.2f}/month and occupies
{f800 / WINDOW:.0%} of a {WINDOW // 1000}k window on every turn of every session; targeted
retrieval costs ${c:.2f} and occupies nothing until asked ({a / c:.0f}x apart).
Lesson: a static reference file is cheap to OWN and expensive to CARRY.
The cost is not the file, it is the file times every turn of every session
it rides along in, plus the window share it takes from the work. Caching
divides the dollars by ten but not the occupancy; an index changes the
slope; retrieval changes the shape.""")
The output, verified on this machine:
repos.md at 40 entries: ~ 1,272 tokens index: ~ 522 tokens one entry: ~ 31 tokens
=== Monthly cost, 40-entry file, 40 sessions x 25 turns, edited in 20% of sessions ===
consulted in A always B index+read C targeted D no cache
---------------------------------------------------------------
10% 1.00 0.45 0.01 6.36
25% 1.00 0.54 0.02 6.36
50% 1.00 0.70 0.03 6.36
75% 1.00 0.86 0.05 6.36
100% 1.00 1.02 0.07 6.36
---------------------------------------------------------------
Break-even A vs B: always-loaded wins only above p = 97%, and D shows what caching is silently absorbing.
=== The same file as it grows (consulted in 25% of sessions) ===
entries file tok A always B index+read C targeted % of window
---------------------------------------------------------------------
40 1,272 1.00 0.54 0.02 0.6%
100 3,222 2.53 1.36 0.02 1.6%
200 6,472 5.06 2.73 0.02 3.2%
400 12,972 10.13 5.46 0.02 6.5%
800 25,972 20.27 10.92 0.02 13.0%
---------------------------------------------------------------------
At 800 entries the always-loaded design costs $20.27/month and occupies
13% of a 200k window on every turn of every session; targeted
retrieval costs $0.02 and occupies nothing until asked (1149x apart).
Lesson: a static reference file is cheap to OWN and expensive to CARRY.
The cost is not the file, it is the file times every turn of every session
it rides along in, plus the window share it takes from the work. Caching
divides the dollars by ten but not the occupancy; an index changes the
slope; retrieval changes the shape.
Reading the tables
The first table fixes the file at 40 entries and varies how often you actually consult it:
- A is flat because the waste is flat. Always-loaded costs the same whether you use the ledger every session or once a quarter; you pay for the carrying, not the consulting. That is the defining property of the design, and the argument against it.
- B tracks usage. The index rides along cheaply (522 tokens instead of 1,272), and the full file is only paid for in sessions that pull it. The break-even the script computes says always-loaded only wins when you consult the file in more than 97% of sessions, and even then barely. Almost no reference file clears that bar.
- C barely registers. Pulling two entries through a tool call, then carrying just that slice, is pennies at any usage rate. The 80-token tool overhead is real but small next to the 1,192 tokens of file it displaces per consult.
- D is the mental model to delete. Without caching the same design costs 6x more. When your intuition says "a 1,300-token file is nothing," it is pricing D at A's bill; caching is silently absorbing the difference, and it stops absorbing when the file churns (Chapter 24 showed exactly how edits reshape the hit).
The second table is the one that decides the architecture, because ledgers grow. At 800 entries (a few years of diligent note-taking) the always-loaded design is $20 a month and, worse, 13% of the window on every turn, which is not a cost line but a quality line: Chapter 33 measured how accuracy degrades as competing context grows. Design C does not appear in that fight at all; its cost is pinned to what you use, not what you know.
Remember. Own as much as you like; carry as little as you can. The store can grow without bound because disk is free; the window cannot because attention and dollars are not. The whole design question for persistent facts is the loading rule, and "always" is the right rule only for the few hundred tokens of instructions that genuinely apply to every turn of every session.
The loading ladder
The four designs generalize past this one file. Any body of durable facts can be mounted at one of four rungs, and the right rung is set by usage rate and size:
- Always loaded (
CLAUDE.md,@-imports, theMEMORY.mdindex). For facts needed on most turns of most sessions: house rules, build commands, the conventions this book's own repo keeps in itsCLAUDE.md. Budget it like rent; every line is billed forever. - Index always, body on demand (a pointer line plus a file the agent reads when relevant).
This is exactly the auto-memory pattern Chapter 18 described: the
MEMORY.mdindex loads each session, topic files load when read. Yourrepos.mdbelongs here at minimum: one line inCLAUDE.md("my OSS ledger isdocs/repos.md; read it when we discuss my projects"), the body loaded only in the sessions that need it. - Targeted retrieval (grep the file, or a memory tool that returns matching entries). The
agent never loads the ledger, only the entries a question touches.
rg serena docs/repos.mdis a retrieval system; so is a Serenaread_memorycall; so is an MCP memory server's search. This rung wins as soon as the file outgrows a few hundred lines. - Embedded retrieval (Chapter 31). When entries stop being greppable by name and you need similarity search, the ledger becomes a corpus and retrieval becomes the chunk-and-embed pipeline that chapter built.
Move a body of facts down the ladder (toward retrieval) as it grows or as its usage rate falls; move it up only when you catch yourself fetching it in nearly every session, and even then move the hot slice, not the file.
Where Serena fits
Chapter 38 covered Serena's internals; here is its place in this chapter's frame, because Serena implements two rungs of the ladder at once:
- Its memories are rung 3 by construction.
.serena/memories/files persist per project,list_memoriesexposes only the names (an index, a few tokens each), andread_memoryloads one body on demand. Nothing rides in the prefix. It is the index-plus-retrieval design with the agent, not you, doing the writing, and it is why a project with twenty Serena memories costs no more per turn than a project with none. - The language server replaces a whole class of static facts. The strongest move on the
ladder is off it: facts you never write down because a tool can look them up live. Nobody
should keep "the
Accountclass is inbank/models.pyand has methods X, Y" in a reference file;find_symbolanswers that in 18 tokens, current as of this second, with no staleness risk. Before a fact earns a line in any ledger, ask whether a language server,git log, orrgalready knows it. The best static fact is the one you never store.
The division of labor that falls out: code facts to the language server, decisions and
preferences to memories, per-turn rules to CLAUDE.md. Each store holds what nothing else
can recompute, and the loading rule matches how often each is needed.
Doing this in Claude Code today
The migration for an always-loaded ledger, in the order you would actually do it:
- Measure the rent. Run
/contextin a fresh session and note where yourCLAUDE.mdplus imports land (Chapter 21 reads the panel). That number is paid at every turn's start, cached or not. - Split rule from reference. Keep in
CLAUDE.mdonly lines that change behavior on most turns (conventions, build commands, prohibitions). Move reference bodies (your repos ledger, API notes, vendor docs) into plain files under the repo or~/.claude/. - Leave a pointer, not a body. One line per moved body: what it is, where it lives, when to read it. The pointer costs ~20 tokens per session; the body now costs zero until used. This is rung 2, and for most personal ledgers it is the final resting place.
- Let grep be the memory system. For a ledger with clean entry names, teach the pointer to
say "grep it first" ("look up a single repo with
rg <name> docs/repos.md"), which promotes the file to rung 3 with no new tooling. - Re-measure.
/contextagain in a fresh session, then compare a real week's bill inccusage(Chapter 26) or your ledger script (Chapter 25). Chapter 42 adds the other half of the verdict: proving the agent still finds the facts after the move.
The failure mode to avoid is the inverse ladder: reference bodies pinned in CLAUDE.md "so it
never forgets," while durable decisions get said once in chat and lost at session end. That
design pays maximum rent on facts it rarely needs and zero rent on facts it needs every session.
Further reading
- Chapter 18: the layer map this chapter prices, including
@pathimport mechanics and the auto-memory index cap (200 lines / 25KB), which is Claude Code institutionalizing rung 2. - Chapter 6 and Chapter 24: why the write/read multipliers in the model are 1.25x/0.1x, and how churn re-triggers writes.
- Chapter 33: the quality cost of occupancy, which dollars do not capture; a 13%-of-window ledger competes with the task for attention.
- Claude Code memory docs (
code.claude.com/docs/en/memory): the authoritative reference forCLAUDE.md, imports, and auto memory.
Takeaways
- Persistent is where a fact lives (disk, free); loaded is whether it enters the window (billed, every turn). All memory design is choosing the loading rule per body of facts.
- A 40-entry ledger costs about $1/month always-loaded and $0.02 retrieved; the break-even for always-loaded is a 97% consult rate, which nothing clears. At 800 entries always-loaded is $20/month and 13% of the window, a quality problem as much as a cost problem.
- Writes are output-priced but one-time; carrying is input-priced and perpetual. The input side decides every design question about static facts.
- The ladder: always-loaded, index-plus-read, targeted retrieval, embedded retrieval. Grow down the ladder; promote only hot slices up.
- Serena's memories are the index-plus-retrieval design built in, and its language server deletes the class of static facts a tool can recompute; the best fact is the one never stored.
👉 The ladder's lower rungs assumed a "memory system" that stores, retrieves, and updates facts for you. The next chapter opens those systems up: MemGPT's self-editing core memory, mem0's extract-and-update pipeline, the knowledge-graph and file-based stores, and what each one puts in your window per turn, applied to Claude Code. Continue to The memory systems tour.