Why Cost Tracking Matters
Claude Code cost tracking was the very first feature I wanted when building this dashboard. It was time to tackle the cost black-box problem raised in EP1. I pay $200/month for Claude MAX, but every Gemini, xAI, and Qwen API call incurs separate charges, and 7 MCP servers execute hundreds of tool calls daily. I wanted all of that visible on a single screen.
Claude MAX vs Pure API — Pricing Structure Comparison
Designing a cost tracking dashboard starts with understanding the pricing structure. Claude MAX ($200/month) provides unlimited Opus and Sonnet usage, while other providers charge per token.
| Provider | Model | Input ($/1M) | Output ($/1M) | Source |
|---|---|---|---|---|
| Anthropic | Claude Opus 4 | $15.00 | $75.00 | Anthropic Docs |
| Anthropic | Claude Sonnet 4 | $3.00 | $15.00 | Anthropic Docs |
| Gemini 2.5 Pro | $1.25 | $10.00 | Google AI Pricing | |
| xAI | Grok 3 | $3.00 | $15.00 | xAI Docs |
| Alibaba | Qwen-Max | $1.60 | $6.40 | Qwen Platform |
This rate table maps directly to pricing.json in the dashboard. Per-model unit costs — Opus input/output, Sonnet input/output, and so on — are managed in this single file. Claude tokens cost $0 under the MAX subscription, but these rates are needed to calculate “what if we were paying API prices?” Gemini, xAI, and Qwen costs are handled by separate Collectors that poll each provider’s billing API for actual charges, rather than relying on pricing.json. This idea led to the third metric on the CostOverview card — MAX Savings.
CostOverview Cards — The Core of Claude Code Cost Tracking
Three cards sit side by side at the top of the dashboard’s main screen. Each answers one of the three most important questions in Claude Code cost tracking.
| Card | Question | Calculation |
|---|---|---|
| Real Spend | How much did I actually pay? | Gemini + xAI + Qwen + MCP costs |
| If All API | What if everything were API? | Claude estimate + Real Spend |
| MAX Savings | How much did MAX save me? | Claude estimated cost = savings |
Here is the frontend code. This is the core logic in CostOverview.tsx that computes the three metrics.
// CostOverview.tsx — core cost calculation
const providers = stats.by_provider ?? []
const realCostByProvider = providers
.filter((p) =>
["gemini","xai","qwen"].includes(p.provider)
)
.reduce((sum, p) => sum + p.cost_usd, 0)
const mcpCost = mcpStats
.reduce((sum, s) => sum + s.total_cost_usd, 0)
// Claude tokens converted to API pricing (estimate)
const claudeProvider =
providers.find((p) => p.provider === "claude")
const claudeEstimated =
claudeProvider?.cost_usd ?? 0
const liveEstimated = stats.live?.cost_usd ?? 0
const totalClaudeEstimated =
claudeEstimated + liveEstimated
// Three metrics
const totalRealSpend =
realCostByProvider + mcpCost
const totalIfApi =
totalClaudeEstimated + totalRealSpend
const maxSavings = totalClaudeEstimated
totalRealSpend sums only charges where actual payment occurs. totalIfApi adds the estimated Claude cost on top — a “hypothetical invoice.” And maxSavings is simply the amount saved by the MAX subscription. One clean subtraction line is all it takes to quantify the value of $200/month.
token_daily — SQLite UPSERT Pattern
All cost data lives in a single SQLite file. No external database server — just one binary. A perfect fit for a personal dashboard.
CREATE TABLE IF NOT EXISTS token_daily (
date TEXT NOT NULL,
host TEXT NOT NULL DEFAULT '',
model TEXT NOT NULL DEFAULT '',
provider TEXT NOT NULL DEFAULT 'claude',
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
cache_read INTEGER NOT NULL DEFAULT 0,
cache_creation INTEGER NOT NULL DEFAULT 0,
session_count INTEGER NOT NULL DEFAULT 0,
cost_usd REAL NOT NULL DEFAULT 0.0,
estimated INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (date, host, model, provider)
);
The primary key spans four columns: (date, host, model, provider). Even if the same model is used across multiple sessions on the same host on the same day, everything accumulates into a single row via UPSERT.
INSERT INTO token_daily
(date, host, model, provider,
input_tokens, output_tokens,
cache_read, cache_creation,
session_count, cost_usd, estimated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(date, host, model, provider)
DO UPDATE SET
input_tokens = input_tokens
+ excluded.input_tokens,
output_tokens = output_tokens
+ excluded.output_tokens,
session_count = session_count
+ excluded.session_count,
cost_usd = cost_usd
+ excluded.cost_usd;
ON CONFLICT ... DO UPDATE is the key. This query runs every time a Claude session ends, and the excluded keyword adds the new values to the existing totals. The FastAPI backend designed in EP2 handles this UPSERT asynchronously.
Provider Cost Collectors — External Cost Aggregation
Claude tokens are automatically tallied at session end, but Gemini, xAI, and Qwen require polling each provider’s management API. Three async Collectors run in the background every 10 minutes.
# gemini_collector.py — Google Cloud Monitoring
async def collect(
self, since: datetime | None = None
) -> list[dict]:
end = datetime.now(timezone.utc)
start = since or (end - timedelta(days=1))
points = await self._fetch_metric(
METRIC_INPUT_TOKENS, start, end
)
return self._aggregate_to_daily(points)
The Gemini Collector reads the generate_content_paid_tier_input_token_count metric and aggregates it to daily maximums. Output tokens are estimated at 1.5x the input count, since Google does not expose a separate output metric.
The xAI Collector pulls actual USD amounts directly from the Management API’s /v1/billing/teams/{team_id}/usage endpoint. Token counts are back-calculated from the rate table — 60% of the cost is attributed to input, 40% to output.
The Qwen Collector calls the DashScope API, but gracefully skips collection if the API key is missing. It stores cost_usd=0.0 so that token counts are still recorded.
MCP Telemetry — Fire-and-Forget Instrumentation
The other axis of cost tracking is MCP server telemetry. Seven MCP servers (nanobanana, terryxai, terryvideogen, terrychadesign, terry-research, terry-bookstack, knowledge-rag) fire events to the dashboard on every tool call.
The guiding principle is “never block.” The primary function of each MCP server — image generation, search, and so on — must never slow down because of telemetry.
# mcp_telemetry_template.py
async def report_tool_call(
server: str,
tool: str,
duration_ms: int = 0,
estimated_cost_usd: float = 0.0,
success: bool = True,
error: str | None = None,
caller: str = "",
) -> None:
"""Fire-and-forget POST. NEVER blocks."""
client = _get_client()
if client is None:
return
payload = {
"event": "mcp_tool_call",
"timestamp": datetime.now(
timezone.utc
).isoformat(),
"server": server,
"tool": tool,
"host": HOST_NAME,
"caller": caller,
"duration_ms": duration_ms,
"estimated_cost_usd": estimated_cost_usd,
"success": success,
"error": error,
}
try:
await client.post(
DASHBOARD_URL, json=payload
)
except Exception:
pass # Telemetry failures are silently ignored
Drop this function into every MCP server and you are done. Set the environment variable DASHBOARD_TELEMETRY_URL (default: http://localhost:8420/api/mcp-event) and instrumentation starts automatically.
The OpenClaw bridge follows the same pattern. A launchd agent on the Mac mini (com.claudie.openclaw-bridge) polls the OpenClaw CLI every 10 seconds, fetches the session list, and relays it to the dashboard via WebSocket. That is how the multi-host view can show agent status across Windows, MacBook, and Mac mini on a single screen.
mcp_events and mcp_daily — Dual-Layer Aggregation
The dashboard backend stores MCP events in two stages.
-- Raw events (for debugging and auditing)
CREATE TABLE IF NOT EXISTS mcp_events (
id INTEGER PRIMARY KEY
AUTOINCREMENT,
timestamp TEXT NOT NULL,
server TEXT NOT NULL,
tool TEXT NOT NULL,
host TEXT NOT NULL DEFAULT '',
caller TEXT NOT NULL DEFAULT '',
duration_ms INTEGER NOT NULL DEFAULT 0,
estimated_cost_usd REAL NOT NULL DEFAULT 0.0,
success INTEGER NOT NULL DEFAULT 1,
error TEXT,
created_at TEXT NOT NULL
DEFAULT (datetime('now'))
);
-- Daily aggregation (for dashboard display)
CREATE TABLE IF NOT EXISTS mcp_daily (
date TEXT NOT NULL,
server TEXT NOT NULL,
tool TEXT NOT NULL,
call_count INTEGER NOT NULL DEFAULT 0,
total_cost_usd REAL NOT NULL DEFAULT 0.0,
avg_duration_ms REAL NOT NULL DEFAULT 0.0,
error_count INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (date, server, tool)
);
The UPSERT on mcp_daily uses an interesting pattern: incremental moving average for response time.
ON CONFLICT (date, server, tool) DO UPDATE SET
call_count = call_count + 1,
total_cost_usd = total_cost_usd
+ excluded.total_cost_usd,
avg_duration_ms =
(avg_duration_ms * call_count
+ excluded.avg_duration_ms)
/ (call_count + 1),
error_count = error_count
+ excluded.error_count;
Multiply the existing average by the existing count to recover the running total, add the new value, then divide by (call_count + 1). An incremental average update with no extra sum column required.
Backend API — The /api/mcp-event Endpoint
The telemetry receiving endpoint is deliberately simple. It returns 200 OK even on failure, and the MCP server side never waits for the response.
# mcp_endpoint.py
@router.post("/api/mcp-event")
async def receive_mcp_event(
event: dict,
) -> dict:
"""Receive MCP tool call telemetry."""
if _db:
try:
await _db.save_mcp_event(event)
except Exception as exc:
logger.warning(
"MCP event save failed: %s", exc
)
return {"status": "ok"}
Unlike the authentication system covered in EP3, this endpoint has no auth. It is only accessible within the internal network (Tailscale), and telemetry data contains nothing sensitive.
Frontend Visualization — Framer Motion Animation
The CostOverview cards use Framer Motion to animate numbers counting up. The visual impact is far stronger than plain text.
The three cards are arranged side by side, each with a distinct color accent. Real Spend is red (actual spending alert), If All API is yellow (hypothetical scenario), and MAX Savings is green (savings highlight). Color alone conveys meaning — intuitive even without reading the labels.
MCP server call counts and costs are displayed in a table, with a success-rate badge next to each server name. The classification logic is straightforward.
// McpUsage.tsx — success rate badge
function getSuccessBadge(rate: number) {
if (rate >= 0.99)
return "bg-green-500"
if (rate >= 0.95)
return "bg-yellow-500"
return "bg-red-500"
}
// rate = (call_count - error_count) / call_count
A single badge reveals which MCP server is unstable at a glance. When a high error rate appears, it is time to check server logs or review rate limits.
SD Emoji — Status at a Glance Through Claudie’s Expressions
Every SessionCard on the dashboard displays an SD (Stable Diffusion-generated) emoji that reflects the agent’s current state. Claudie and Siwol each have five expressions mapped to different states.
| Agent | State | Emoji (filename) | Meaning |
|---|---|---|---|
| Claudie | avatar (default) | wink | Idle avatar |
| running | cx02 (focused) | “Working hard on it~” | |
| completed | excited | “오빠, all done!” | |
| error | embarrassed | “Oops, something went wrong…” | |
| idle | thinking | / sleeping: cx04 | |
| Siwol | running | fighting | “Giving it my best!” |
| completed | ok (OK pose) | “All done!” | |
| error | sorry | “Sorry about that…” | |
| idle | heart | “Standing by.” | |
| sleeping | please | “Taking a short break.” |
Technically, these are Telegram 512×512 PNGs that a React component swaps based on state. Every time a session state changes, the SessionCard emoji updates — so on a dashboard with multiple agents running concurrently, you can instantly tell what is active and what is finished without reading any text. This small touch is what makes the dashboard feel like “my own tool.”
The cost screens use Claudie’s emoji too — excited when savings are high, embarrassed when spending spikes, and thinking during aggregation loads.
JSONL session history scanning follows the same philosophy. Completed Claude Code sessions are stored as JSONL files under ~/.claude/projects/. Parsing all of them could mean thousands of files, so I implemented a lightweight scan strategy: 30-second TTL cache, reading only the first 8KB and last 2KB of each file. The session list loads instantly, and hitting “Resume” runs claude --resume <session-id> to pick up exactly where you left off.
Real Data — Transparent Calculation Logic
Let me show you the actual numbers the cost tracking system produces, using two days of real measured data.
Over two days — 94 sessions, 26.3M input + 9.6M output tokens — the headline numbers don’t tell the full story. The key is cache tokens.
# pricing.json — per 1M tokens (Anthropic official rates)
# Claude Opus 4.6:
# input: $15, output: $75
# cache_read: $1.50 (90% discount vs input)
# cache_creation: $18.75 (25% premium vs input)
def estimate_cost(model, input_tokens, output_tokens,
cache_read=0, cache_creation=0):
p = PRICING[model]
return (
input_tokens * p["input"] / 1_000_000
+ output_tokens * p["output"] / 1_000_000
+ cache_read * p["cache_read"] / 1_000_000
+ cache_creation * p["cache_creation"] / 1_000_000
)
# Actual data (2 days, Opus 4.6):
# input: 1.49M, output: 4.85M
# cache_read: 973M, cache_creation: 198M
# = $22 + $364 + $1,460 + $3,706 = $5,552
Claude Code sends the entire conversation history via prompt caching on every turn. That’s why cache_read balloons to 973M tokens and cache_creation to 198M tokens. Without caching, all of those tokens would have been charged at the regular input rate ($15/1M), totaling $17,951. Caching saved $12,399, but cache_creation alone still costs $3,706.
⚠️ An honest confession: We’re still verifying whether this calculation is 100% accurate. Whether the token parsing from Claude Code JSONL is correct, whether this volume of cache tokens is normal — there’s more to validate. If anyone reading this is well-versed in Claude API billing, we’d be truly grateful if you could let us know in the comments whether this logic checks out. 🙏
One thing is certain: without the MAX subscription, calling the API directly would have generated at least hundreds to thousands of dollars in costs over just two days. The exact figure may vary, but the value of MAX at $200 is beyond doubt.
Operational Results — 3-Month Claude Code Cost Tracking Report
After running the Claude Code cost tracking dashboard for three months, the estimated API cost against the $200/month Claude MAX subscription ranged from $600 to $1,200 per month. Months with heavy Opus usage exceeded $1,000. Real Spend (Gemini + xAI + Qwen) stayed at $15 to $40 per month, and MCP server costs were dominated by image generation (xAI, Gemini).
The numbers tell a clear story: the MAX subscription delivers 3x to 6x its value. Being able to verify this on the dashboard every day — that is the real value of Claude Code cost tracking. It turns “I have a feeling MAX is worth it” into “the data proves it.”
Wrapping Up the Series
Across four episodes, we completed the journey of “remote-controlling Claude Code without RDP.” Looking back, each episode solved one problem while surfacing the next.
EP1 was about “why build this at all.” Three pain points — RDP’s inconvenience, cost opacity, and multi-host fragmentation — kicked off the dashboard project. EP2 laid the real-time communication foundation with a minimal stack of FastAPI + WebSocket + React. EP3 completed “secure remote access from anywhere” with JWT auth, TOTP 2FA, and an xterm.js terminal. And here in EP4, we finally solved the cost black-box problem raised in EP1 — with real data.
Before the dashboard, I had no idea how much I was spending until the monthly bill arrived. Now I can see in real time how many tokens I used today, which MCP server consumed how much, and how many dollars the MAX subscription saved this month. When Claudie’s SD emoji flashes an excited expression to report the savings — that is the payoff for this entire series.
The appeal of a personal dashboard is building “only what you need, exactly the way you want it.” Commercial tools become complex for the sake of generality, but my dashboard only needs to track 7 MCP servers and 3 providers. One SQLite file, one FastAPI server, one React frontend — this minimal stack was enough.
Claudie’s dashboard drew inspiration from open-source tools like claude-code-dashboard, but MCP telemetry and multi-provider cost tracking are custom-built. Build a dashboard tailored to your own workflow. The code is simpler than you think, and the impact is greater than you expect.
Three lessons as I close out the series.
- The launcher is harder than the app. Windows Task Scheduler and process management proved far more challenging than the FastAPI server code itself. The VBS-to-bat-to-python chain had different failure modes at every stage. Designing a single Python entry point (
launcher.py) from the start would have saved two days of debugging. - Design caching first. When JSONL session files number in the thousands, scanning all of them every time is not feasible. A 30-second TTL cache and partial reads (head/tail) should be part of the initial design. Adding them later means rewriting the entire API interface.
- Treat the dashboard like a product. Thinking “it is just a tool for myself” tempts you to skip tests. Without 173 tests, I would have missed a session aggregation bug during the auth refactor. The smaller the project, the more tests shine.
Series: Remote-Controlling Claude Code Without RDP