~/ emre.cavunt_
Tools

The Token Economy of Your Coding Agent

Auditing the token economy of your coding agent: 3,004 tokens on a codebase graph vs 67,428 for disciplined grep — and what the README hides.

You meter everything else. CPU requests, egress, the NAT gateway that quietly costs more than the dev cluster. Then you hand a coding agent your repository and let it spend tokens — the one resource that bills you twice, once in dollars and once in context-window capacity — with no meter at all.

That spend is the token economy your coding agent operates in, and nobody audits it. I spent an evening doing exactly that. The subject is codebase-memory-mcp, an MCP server that parses your repos into a persistent knowledge graph so the agent queries structure instead of reading files. Its README promises "99.2% fewer tokens". The project's own arXiv preprint promises ten times fewer. Those are not the same number, and the gap between them turned out to be the most interesting measurement of the night.

Every figure below is counted from real command output with a real tokenizer, on repos you can clone, with commands you can replay.

The meter

Version 0.9.0 on an Apple Silicon MacBook. Two Google repos chosen because they look like work:

RepoContentSize
google/adk-python1,709 Python files, 465K LOC54MB
GoogleCloudPlatform/microservices-demo363 files, 89K LOC, five languages plus K8s YAML22MB

One methodological upgrade before any numbers. The standard trick for estimating token cost is bytes divided by four. I counted actual tokens instead (tiktoken, cl100k_base), and the rule of thumb turns out to be biased in a specific direction: the graph's JSON responses tokenise at 3.8 bytes per token, but Python source tokenises at 2.6. Code is dense with identifiers and punctuation, so bytes÷4 understates the cost of reading files by more than a third. Every "we saved N tokens" claim you've read that was measured in bytes flattered the wrong side of the comparison.

With that settled, the bill for one honest question.

One question, three bills

What calls BaseAgent.run_async, what does it call, and show me the code. A structural question any senior engineer asks on day one in a new codebase. Three ways to answer it, metered:

PathTool callsTokens
Graph (search_graph + trace_path + get_code_snippet)33,004
Disciplined agent (grep with line numbers, read the 8 hottest files)967,428
Naive agent (read all 69 files grep names)692,189,555

The graph beats the disciplined agent by 22x and the naive one by 729x. The README's 120x sits inside that band; the preprint's 10x sits just below it. Both are defensible, neither is the whole story — the multiplier is a function of how badly the baseline behaves, and baselines in the wild behave very badly.

Against a 200K context window, the graph's answer costs 1.5% of the window. The disciplined answer costs 34% — one question, and a third of the session's working memory is gone. The naive answer is eleven windows' worth: that agent doesn't give a worse answer, it never finishes. It truncates, hallucinates a summary of files it half-read, or dies mid-thought. Token spend is capacity spend.

In dollars, at a generic 3permillioninputtokens:3 per million input tokens: 0.009 versus 0.20versus0.20 versus 6.57. Per question.

Grep's dirty secret: generated code is bait

The second question was cross-service, against microservices-demo: who calls CartService/GetCart, and where is it handled? Eleven services, five languages, gRPC everywhere — the shape of a real platform.

The disciplined baseline first. rg -n GetCart costs 9,234 tokens, and ranking files by hit count tells the agent to read, in order: six generated genproto/*.pb.go files. 122,696 tokens of protobuf boilerplate before a single line of business logic. Grep cannot tell a hand-written handler from a generated stub, so the most disciplined agent in the world still walks into the bait.

The graph path, metered call by call:

search_graph  name_pattern=".*GetCart.*"            9,120 tokens
query_graph   MATCH (f)-[e]->(r:Route) ...             81 tokens
query_graph   MATCH (h)-[:HANDLES]->(r:Route) ...      65 tokens
search_graph  name_pattern=".*GetCart.*"            9,120 tokens
query_graph   MATCH (f)-[e]->(r:Route) ...             81 tokens
query_graph   MATCH (h)-[:HANDLES]->(r:Route) ...      65 tokens

The two precise Cypher queries return the answer — frontend.getCart and checkoutservice.getUserCart, resolved to a Route node — for 146 tokens combined. The broad name search that preceded them cost 62x more than both answers together. Same product, same index, and the difference between an expensive tool and a cheap one is purely query discipline. If you take one operational lesson from this post: teach your agent to reach for query_graph with a tight MATCH before it reaches for anything broad.

The HANDLES edges dead-end at vendored proto declarations — the adservice and currencyservice copies of demo.proto — instead of the actual C# handler in CartService.cs. The graph walks you to the service boundary and stops. For the question "which deployment consumes this secret", keep kubectl close.

The taxes nobody quotes

The headline multiplier assumes well-scoped queries. The tool has three quiet taxes that erode it.

The orientation tax

get_architecture with aspects: ["all"] returns 14,185 tokens. An agent that fires it reflexively at session start has spent a small essay before your first question. aspects: ["overview"] covers the same ground for 3,095. Scope it, or pay 4.6x for the same orientation.

The pollution tax

adk-python ships a minified Angular bundle for its dev UI, and the indexer ate it whole: 102 .js files contributing 8,714 Function nodes — 18% of the entire 48,378-node graph — with names like Kt and zA sitting alongside real Python symbols. Every broad search wades through that. The fix exists and I verified it works: a .cbmignore file (gitignore syntax) at the repo root. Ignoring helm-chart/ and docs/ in microservices-demo dropped the index from 3,699 to 3,539 nodes. Write the ignore file before the first index, not after you notice minified callees in a trace.

The discoverability tax

The README advertises 15 tools; --help documents 14. A spec-compliant MCP client calling tools/list — I drove the server over raw stdio JSON-RPC — gets eight. The hidden set includes detect_changes, which maps a git diff to a blast radius of affected symbols and is the feature I'd wire into a PR pipeline tomorrow. Some hidden tools, like list_projects, work fine when called directly. They're simply invisible to any client that builds its tool list from tools/list, which is every client that follows the spec. You cannot save tokens with a tool your agent doesn't know exists.

What the marketing leaves off the invoice

The README and the preprint describe the same product with numbers twelve times apart, and the discrepancy is instructive.

The preprint (evaluating v0.5.5, 31 repos, both agents on the same frontier model) reports 10x fewer tokens and 2.1x fewer tool calls per question. It also reports the figure the README never mentions: answer quality of 0.83 for the graph agent versus 0.92 for the file-reading agent, with the file reader winning full-source-context questions in 16 of 31 languages. That's the honest shape of the trade. The graph is cheaper and slightly worse, because it stores relationships rather than lines — the discount is funded by abstraction. For structural questions ("what calls this", "what breaks if I change it") abstraction is exactly what you want. For "show me the error handling in this function", read the file.

The README's 120x comes from five structural queries against a file-by-file baseline — real, but chosen from the graph's strongest category. My 22x against a disciplined baseline is the number I'd plan a budget around. Treat 10x as the floor, 25x as the expectation, 120x as the pitch.

Two smaller claims, quickly metered. "Sub-millisecond queries" refers to execution inside the server; a full CLI round trip — process start, query, JSON out — is 11.8ms median across seven warm runs. Immaterial either way. And "the Linux kernel in three minutes": my machine indexed 552K LOC in 19.8 seconds, about 28K LOC/s, which extrapolates to roughly 17 minutes for 28M LOC. Their benchmark was an M3 Pro in a controlled run; mine is a laptop running a life. Quote the kernel number with an asterisk.

The meter also runs before the first question: a 273MB binary, and a 158MB SQLite index for a 54MB repo. Call it 3x source size on disk.

The compounding ledger

Single questions are where blog posts stop. Sessions are where the economics bite. A full-time agent pair-programming with you asks a structural question every few minutes; call it twenty a day, conservatively:

GraphDisciplined grep+read
Per question3,004 tok67,428 tok
Per day (20 questions)60K tok · $0.181.35M tok · $4.05
Per month, per engineer~1.3M tok · ~$4~30M tok · ~$89

The dollar gap is real but modest. The capacity gap is not: the disciplined path spends the equivalent of more than six full 200K context windows per day on retrieval alone — context that could have held the actual problem. This is why agent sessions degrade by mid-afternoon on big repos. They aren't getting dumber; they're getting full.

Against that spend, the graph charges once: 19.8 seconds of indexing and 158MB of disk, amortised across every question after. Break-even is the first morning.

Where it doesn't pay: the repo you touch twice a year, one-off questions, macro-heavy C (the preprint's weakest result, 0.58 quality, because macros never reach the AST), and anything needing exhaustive line-level reads. Meter before you mandate.

The UI is a demo, not a workflow

Run with --ui=true and a dashboard on localhost:9749 renders the whole graph as a WebGL force-directed hairball:

3D force-directed graph of adk-python: 48,378 nodes coloured by type, with filter and overlay controls

You don't navigate 48,000 nodes visually, and you don't need to — the agent queries it; the picture is for you. The one view with daily value is the dead-code overlay:

Dead-code overlay on the same graph: 3,882 of 48,378 nodes unreachable from entry points, highlighted in red

3,882 nodes unreachable from any entry point, 8% of the graph, in red. My own Cypher equivalent on microservices-demo — functions with zero inbound CALLS, excluding tests and entry points — found 318, and cost 17 tokens. The overlay is the same answer with a rendering budget. Nice for showing a new joiner the shape of the system; the query is what you'll actually use.

Run the numbers yourself

Fifteen minutes, fully replayable. The binary doubles as a CLI, which is what makes the accounting honest — no agent in the loop, just bytes on stdout:

git clone --depth 1 https://github.com/google/adk-python.git
codebase-memory-mcp cli index_repository \
  '{"repo_path":"'$PWD'/adk-python","name":"adk-python","mode":"full"}'
 
QN='adk-python.src.google.adk.agents.base_agent.BaseAgent.run_async'
codebase-memory-mcp cli search_graph \
  '{"project":"adk-python","query":"agent run async execution","limit":10}' | wc -c
codebase-memory-mcp cli trace_path \
  '{"project":"adk-python","function_name":"'$QN'","depth":3}' | wc -c
git clone --depth 1 https://github.com/google/adk-python.git
codebase-memory-mcp cli index_repository \
  '{"repo_path":"'$PWD'/adk-python","name":"adk-python","mode":"full"}'
 
QN='adk-python.src.google.adk.agents.base_agent.BaseAgent.run_async'
codebase-memory-mcp cli search_graph \
  '{"project":"adk-python","query":"agent run async execution","limit":10}' | wc -c
codebase-memory-mcp cli trace_path \
  '{"project":"adk-python","function_name":"'$QN'","depth":3}' | wc -c

Then the baseline the graph competes against, and the tokeniser that keeps both sides honest:

cd adk-python
grep -rl run_async src/ | wc -l        # 69 files the naive agent reads
 
pip install tiktoken                   # count tokens, not bytes
python3 - <<'EOF'
import subprocess, tiktoken
enc = tiktoken.get_encoding("cl100k_base")
files = subprocess.run(["grep","-rl","run_async","src/"],
                       capture_output=True, text=True).stdout.split()
total = sum(len(enc.encode(open(f, encoding="utf-8", errors="replace").read())) for f in files)
print(f"{len(files)} files, {total:,} tokens")   # 69 files, 2,189,555 tokens
EOF
cd adk-python
grep -rl run_async src/ | wc -l        # 69 files the naive agent reads
 
pip install tiktoken                   # count tokens, not bytes
python3 - <<'EOF'
import subprocess, tiktoken
enc = tiktoken.get_encoding("cl100k_base")
files = subprocess.run(["grep","-rl","run_async","src/"],
                       capture_output=True, text=True).stdout.split()
total = sum(len(enc.encode(open(f, encoding="utf-8", errors="replace").read())) for f in files)
print(f"{len(files)} files, {total:,} tokens")   # 69 files, 2,189,555 tokens
EOF

Two warnings before you start. delete_project sits in the hidden tool set, and the README documents no confirmation step for it — don't experiment against an index you care about. And indexing is content-hashed and incremental, so an unchanged repo re-indexes almost instantly: time a fresh project name or you're benchmarking the no-op path.

Verdict

Install it for the repos you work in daily — but plan your budget around the honest multiplier, not the headline. The measured reality is 22x fewer tokens on structural questions against a disciplined agent, with a documented quality haircut on source-level ones, and both numbers are more useful than the README's 120x precisely because they survive contact with a sceptic.

The deeper point is the economy itself. Tokens are the only resource in the agent loop that constrain capability and cost at once, and the difference between a cheap answer and an expensive one turned out to be less about the tool than about how it was asked — 146 tokens for a tight Cypher query, 9,120 for a lazy search against the same index. Give the agent a map, scope the queries, ignore the marketing. Then meter your own setup, because the most expensive line item in your agent's month is probably a question you never thought to count.