diff --git a/src/ClaudeDo.Worker/Usage/TokenTracker/TokenTrackerExportParser.cs b/src/ClaudeDo.Worker/Usage/TokenTracker/TokenTrackerExportParser.cs
new file mode 100644
index 00000000..95afbe73
--- /dev/null
+++ b/src/ClaudeDo.Worker/Usage/TokenTracker/TokenTrackerExportParser.cs
@@ -0,0 +1,98 @@
+using System.Text.Json;
+
+namespace ClaudeDo.Worker.Usage.TokenTracker;
+
+///
+/// Parses tokentracker sessions --format json. The export is not a promised API, so every
+/// field is read defensively: a missing number becomes 0, an unreadable row is skipped, and
+/// unparseable output returns null rather than throwing into the caller's face.
+///
+public static class TokenTrackerExportParser
+{
+ /// The version value this parser was written against (CLI v0.88.4).
+ public const int SupportedFormatVersion = 11;
+
+ public static TokenTrackerExport? Parse(string? json, DateTime fetchedAtUtc)
+ {
+ if (string.IsNullOrWhiteSpace(json)) return null;
+
+ JsonDocument doc;
+ try { doc = JsonDocument.Parse(json); }
+ catch (JsonException) { return null; }
+
+ using (doc)
+ {
+ var root = doc.RootElement;
+ if (root.ValueKind != JsonValueKind.Object) return null;
+ if (!root.TryGetProperty("sessions", out var sessions) ||
+ sessions.ValueKind != JsonValueKind.Array) return null;
+
+ var parsed = new List();
+ var version = 0;
+
+ foreach (var row in sessions.EnumerateArray())
+ {
+ if (row.ValueKind != JsonValueKind.Object) continue;
+
+ version = Math.Max(version, (int)GetLong(row, "version"));
+
+ var hash = GetString(row, "session_hash");
+ if (string.IsNullOrWhiteSpace(hash)) continue;
+
+ var source = GetString(row, "source") ?? "";
+ if (!string.Equals(source, "claude", StringComparison.OrdinalIgnoreCase)) continue;
+
+ if (!DateTimeOffset.TryParse(GetString(row, "started_at"), out var startedAt)) continue;
+
+ var tokens = row.TryGetProperty("tokens", out var t) && t.ValueKind == JsonValueKind.Object
+ ? t
+ : default;
+
+ parsed.Add(new TokenTrackerSession(
+ SessionHash: hash!,
+ Source: source,
+ Model: GetString(row, "model") ?? "",
+ StartedAt: startedAt,
+ InputTokens: GetLong(tokens, "input_tokens"),
+ OutputTokens: GetLong(tokens, "output_tokens"),
+ CacheReadTokens: GetLong(tokens, "cached_input_tokens"),
+ CacheCreationTokens: GetLong(tokens, "cache_creation_input_tokens"),
+ CostUsd: GetDouble(row, "cost_usd"),
+ Turns: (int)GetLong(row, "turns"),
+ RetryTurns: (int)GetLong(row, "retry_turns"),
+ Productive: GetBool(row, "productive"),
+ OneShot: GetBool(row, "one_shot")));
+ }
+
+ return new TokenTrackerExport(version, parsed, fetchedAtUtc);
+ }
+ }
+
+ private static string? GetString(JsonElement obj, string prop) =>
+ obj.ValueKind == JsonValueKind.Object &&
+ obj.TryGetProperty(prop, out var el) &&
+ el.ValueKind == JsonValueKind.String
+ ? el.GetString()
+ : null;
+
+ private static long GetLong(JsonElement obj, string prop) =>
+ obj.ValueKind == JsonValueKind.Object &&
+ obj.TryGetProperty(prop, out var el) &&
+ el.ValueKind == JsonValueKind.Number &&
+ el.TryGetInt64(out var v)
+ ? v
+ : 0;
+
+ private static double GetDouble(JsonElement obj, string prop) =>
+ obj.ValueKind == JsonValueKind.Object &&
+ obj.TryGetProperty(prop, out var el) &&
+ el.ValueKind == JsonValueKind.Number &&
+ el.TryGetDouble(out var v)
+ ? v
+ : 0;
+
+ private static bool GetBool(JsonElement obj, string prop) =>
+ obj.ValueKind == JsonValueKind.Object &&
+ obj.TryGetProperty(prop, out var el) &&
+ el.ValueKind == JsonValueKind.True;
+}
diff --git a/tests/ClaudeDo.Worker.Tests/Usage/TokenTracker/TokenTrackerExportParserTests.cs b/tests/ClaudeDo.Worker.Tests/Usage/TokenTracker/TokenTrackerExportParserTests.cs
new file mode 100644
index 00000000..c0637089
--- /dev/null
+++ b/tests/ClaudeDo.Worker.Tests/Usage/TokenTracker/TokenTrackerExportParserTests.cs
@@ -0,0 +1,100 @@
+using ClaudeDo.Worker.Usage.TokenTracker;
+
+namespace ClaudeDo.Worker.Tests.Usage.TokenTracker;
+
+public sealed class TokenTrackerExportParserTests
+{
+ private static readonly DateTime FetchedAt = new(2026, 8, 24, 9, 0, 0, DateTimeKind.Utc);
+
+ [Fact]
+ public void Parse_ReadsAllSessions()
+ {
+ var export = TokenTrackerExportParser.Parse(TokenTrackerFixtures.SessionsJson, FetchedAt);
+
+ Assert.NotNull(export);
+ Assert.Equal(4, export!.Sessions.Count);
+ Assert.Equal(11, export.FormatVersion);
+ Assert.Equal(FetchedAt, export.FetchedAtUtc);
+ }
+
+ [Fact]
+ public void Parse_MapsTokenFieldsAndCost()
+ {
+ var export = TokenTrackerExportParser.Parse(TokenTrackerFixtures.SessionsJson, FetchedAt)!;
+ var first = export.Sessions.Single(s => s.SessionHash == "b24babcbf2b615730459773f");
+
+ Assert.Equal("claude-opus-5", first.Model);
+ Assert.Equal(100, first.InputTokens);
+ Assert.Equal(400, first.OutputTokens);
+ Assert.Equal(900, first.CacheReadTokens);
+ Assert.Equal(50, first.CacheCreationTokens);
+ Assert.Equal(2.5, first.CostUsd);
+ Assert.Equal(4, first.Turns);
+ Assert.Equal(1, first.RetryTurns);
+ Assert.True(first.Productive);
+ Assert.True(first.OneShot);
+ }
+
+ [Fact]
+ public void Parse_UnsupportedVersion_StillParsesButReportsIt()
+ {
+ var export = TokenTrackerExportParser.Parse(TokenTrackerFixtures.UnsupportedVersionJson, FetchedAt);
+
+ Assert.NotNull(export);
+ Assert.Equal(99, export!.FormatVersion);
+ }
+
+ [Fact]
+ public void Parse_MissingFields_DefaultToZero()
+ {
+ const string json = """
+ { "sessions": [ { "version": 11, "session_hash": "abc", "source": "claude",
+ "model": "claude-opus-5", "started_at": "2026-08-20T10:00:00.000Z" } ] }
+ """;
+
+ var session = TokenTrackerExportParser.Parse(json, FetchedAt)!.Sessions.Single();
+
+ Assert.Equal(0, session.InputTokens);
+ Assert.Equal(0, session.CostUsd);
+ Assert.Equal(0, session.RetryTurns);
+ Assert.False(session.Productive);
+ }
+
+ [Fact]
+ public void Parse_SkipsRowsWithoutHashOrTimestamp()
+ {
+ const string json = """
+ { "sessions": [ { "version": 11, "source": "claude", "model": "m" },
+ { "version": 11, "session_hash": "abc", "source": "claude", "model": "m" },
+ { "version": 11, "session_hash": "def", "source": "claude", "model": "m",
+ "started_at": "2026-08-20T10:00:00.000Z" } ] }
+ """;
+
+ var export = TokenTrackerExportParser.Parse(json, FetchedAt);
+
+ Assert.Single(export!.Sessions);
+ Assert.Equal("def", export.Sessions[0].SessionHash);
+ }
+
+ [Fact]
+ public void Parse_SkipsNonClaudeSources()
+ {
+ const string json = """
+ { "sessions": [ { "version": 11, "session_hash": "abc", "source": "codex", "model": "gpt",
+ "started_at": "2026-08-20T10:00:00.000Z" } ] }
+ """;
+
+ Assert.Empty(TokenTrackerExportParser.Parse(json, FetchedAt)!.Sessions);
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData("not json at all")]
+ [InlineData("[1,2,3]")]
+ [InlineData("{\"available\": false}")]
+ public void Parse_Garbage_ReturnsNull(string json)
+ {
+ Assert.Null(TokenTrackerExportParser.Parse(json, FetchedAt));
+ }
+}