Book a call
RECIPE14mVERIFIED 2026-08-04 · CLAUDE CODE 2.1.221 · CODEX CLI 0.146.0 · ANTIGRAVITY CLI 1.1.9 · KIMI CODE CLI 0.31.1

Reading your own usage

Where each harness keeps its token counters, which of the four numbers actually moves the bill, and a script that totals a week from the logs on disk.

First work out which meter you're on

The first thing to do when looking at the usage of any tool of this kind is to identify what's the billing model behind it, because the number you see might mean totally different things depending on it. Usually, there are two different types:

  • If you use it with an API key or through some cloud provider — in this case, it's a number of tokens used that is almost directly connected with your invoice, and with every token not used, you save money

  • If it's based on a subscription — here the number shows how much of the rolling window you've consumed; it's not a money figure at all but a share of a time frame

In the second case you don't have an invoice, so the number doesn't reflect what you'll be charged at the end of the month but the share of the window that you used and when it resets. The latter part is actually crucial, because it's the thing that determines when you can work again.

What's more, the figure you see might not even reflect what you'll actually be charged — if you use Claude Code, for instance, have a look at its docs; it says that the number they display is calculated locally based on tokens count at standard prices (so it ignores any promotional pricing or discount you may have), so it's possible it doesn't match your real invoice.

In general, if you have a subscription, the important thing is what's the share of the window you used and when does it reset, not how much money it cost you. And that's very important to understand because often people with a subscription try to optimise a bill they won't ever receive, wasting hours on it. Simultaneously, if you work with an API key, you might be looking at a percentage bar not connected with your invoice at all. That's why the most sensible thing to do is always to check what type of authentication you use before doing anything else, it takes seconds.

Four numbers, and output isn't the one

Whichever meter you're on, there are four different figures that matter here, and the number of output tokens isn't the most important one:

  • input tokens — everything sent to the model for this request, your prompt included

  • output tokens — what the model generated back

  • cache write tokens — the part of that input written into the provider's prompt cache

  • cache read tokens — the part served out of that cache instead of being processed again

These numbers are often referred to by slightly different names across tools, and what's more, some of them might not even be reported separately in your tool of choice: Antigravity, for example, has no separate cache write figure at all, and Cursor reports nothing locally — for that one you'll need its dashboard.

That said, if they are split into separate figures in your tool, we recommend looking at them that way. A single total blurs the only piece of valuable information — the one about the context.

Here's an example of a single headless run of Claude Code on our machine with a prompt asking for a single word; you can see its JSON output:

JSON
{
  "result": "OK",
  "total_cost_usd": 0.12106349999999999,
  "num_turns": 1,
  "usage": {
    "input_tokens": 2,
    "cache_creation_input_tokens": 11278,
    "cache_read_input_tokens": 15185,
    "output_tokens": 4
  }
}

As you can see, we asked for a single word, and it cost a little bit over 12 cents (4 output tokens); the thing is, the reason behind this cost is not what was in the prompt but what was sent to the model before it even got there — 11,278 tokens written to the cache and 15,185 read from it. It's the session's standing context, carried into the request every time, and it's this number that's actually the most important one here.

This is more or less what every agent turn looks like, so in general, there's not much you can do to reduce the cost by shortening the answers. The main drivers are how much context you send per request and how many requests there are — a hundred small tool calls in one session will each happily resend the accumulated history.

You can also see that this run used two different models (the one doing the work and another one doing a small thing in the background), so using a cheaper model is not the whole story; check the per-model breakdown if your tool provides it.

Get the number without a human in the loop

Looking at the number on the screen works for a one-off inspection like this, but it's not something you could script — instead, most of these tools have a headless mode that outputs usage info in a machine-readable form, so you can log it from a wrapper around every run, or from a CI job with nobody watching. Before you build on it though, make sure the following is in place:

  • Claude Code includes a dollar estimate in its payload, but Codex and Antigravity provide only the number of tokens and leave the pricing to you (which is better in our opinion, as prices change, and if you embed a rates table in your script, it becomes outdated after a quarter without notice)

  • If you use Kimi, make sure your script is not set up to assume all tools report their usage, as its headless output doesn't include any usage data — it'll quietly log nothing

Total a week of it from the logs already on disk

If you want to do this analysis for the entire week, you can also have a look at the session logs your tool keeps on disk; Claude Code, Codex and Kimi store the number of tokens there, so you can sum them up. Simultaneously, Cursor and Antigravity don't (their local stores are databases of opaque blobs, with no token column anywhere in the schema), so for these two you'll need to read the tool or the vendor's dashboard — which will give you an answer, but maybe not the one you'd like to hear.

Also, keep in mind where your tool stores its logs (locations are outlined in the per-tool section below) and the following pitfalls:

  • Some tools log every reply as multiple entries in the transcript if they stream the response; these entries have the same message ID and identical usage, so if you naively sum them up, you'll overcount it. For example, in one of our transcripts there were 80 assistant entries but only 29 unique message IDs, so a naive count gave 119,021 output tokens versus 43,007 after removing the duplicates, which is almost 3 times more

  • Some tools write the session's total in a running state on every update, so if you sum up all the events, you'll multiply the session by the number of writes.

  • One tool emits a per-turn usage record and also includes these figures in the internal loop events, so you need to count it once.

To help with it, here's a script that sums everything it finds, handling these pitfalls (feel free to check out its source code); this is what we got for the last three days on our machine:

TEXT
                                input         output     cache read    cache write
----------------------------------------------------------------------------------
2026-08-02 claude-code         19,036      3,031,887    322,642,220     11,685,244
2026-08-02 codex-cli           10,685            330         48,128              0
2026-08-02 kimi-code            5,411            114         36,608              0
2026-08-03 claude-code         11,929      1,984,979    319,760,884      5,478,936
2026-08-03 kimi-code           15,578            355         49,152              0
2026-08-04 claude-code         40,396      1,529,822    277,108,578      7,583,297
2026-08-04 codex-cli            7,160             10         22,016              0
2026-08-04 kimi-code            8,984            533         81,408              0
----------------------------------------------------------------------------------
total                         119,179      6,548,030    919,748,994     24,747,477

As you can see, it's the cache reads that stand out — they're almost four orders of magnitude higher than fresh input, which is what long sessions on heavy days look like. It's also the figure that moves when your working habits change. Another thing that might be useful to know is what the expensive model is actually being used for (--by model gives you that split); usually, it's more actionable than knowing the figure for a day. Last but not least, we're displaying tokens here, not dollars — pricing is the vendor's, and any rates table you put in your script will be outdated within a quarter without notice.

IN YOUR HARNESS

In Claude Code

In the session. /usage, /cost and /stats all point to the same screen. At the top of the Session block you can find the number of tokens and the dollar amount spent in the current session. These get zeroed out when you run /clear to start a new session — before v2.1.211 they kept increasing during the entire life of the process, so if you see a big number on an older screenshot it was probably counting sth else.

If you're on a Pro, Max, Team or Enterprise plan then in the same screen you'll also see plan usage bars and a breakdown of the most recent spend across skills, subagents, plugins and each MCP server separately. You can use d/w to cycle between the last 24h and the last 7 days, and once a behaviour reaches 10% or more of your recent usage it gets pointed out — long context or cache misses — which is the closest thing here to an actionable recommendation.

Don't confuse it with /context, which shows what's in the window right now rather than what you've spent.

Headless. The most detailed view of the costs is the JSON output of a non-interactive run:

BASH
claude -p 'Reply with exactly: OK' --output-format json

You get the same four token counts in the usage section and then total_cost_usd for the entire run. And under the modelUsage key there's another costUSD value for every model that was used — the one you selected and any other that did a background request. Thanks to that, you can actually see the cost of the request made by a model you didn't pick.

On disk. The transcript is saved at ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl. It's one JSON object per line, so the usage data is there in every message whose author is an assistant — under message.usage in that case.

But… if the reply was streamed, it lands as multiple entries sharing one message.id and they all contain the same usage block, so a naive sum counts every request two or three times. Run both of these over one session file and compare:

BASH
f=~/.claude/projects/<encoded-cwd>/<session-id>.jsonl

# naive — counts a streamed reply once per transcript entry
jq -s '[.[] | select(.type=="assistant") | .message.usage.output_tokens] | add' "$f"

# deduplicated by message id
jq -s '[.[] | select(.type=="assistant")] | group_by(.message.id)
       | map(.[0].message.usage.output_tokens) | add' "$f"

On a transcript here they printed 119,021 and 43,007 — the same requests, counted two or three times over.

If you were to analyse more files this way, there are a couple of things to keep in mind:

  • The project directories' names encode the working directory with / changed to -, so they all start with a dash. A relative glob would expand into something the next command reads as a flag, so either spell the path out from ~ or pipe find … -print0 into xargs -0

  • -mtime filters files, not individual messages — a directory modified today might contain turns from months ago, so filter on each entry's own timestamp instead, which is what the script does

For a team. At that scale this approach won't work, as you won't have access to everyone's transcript files. What you can do is enable CLAUDE_CODE_ENABLE_TELEMETRY=1 and set an OTEL_METRICS_EXPORTER, which sends claude_code.token.usage and claude_code.cost.usage to your own observability stack — the former documented with dimensions like user, team, model, skill, plugin and agent.

In Codex CLI

In the session. There are actually two different commands for that, answering two different questions:

  • /status — shows the session's setup and its token consumption (the model being used, the approval policy, the directories it can write to, the context left)

  • /usage — an account-level view. It opens a usage menu, and /usage daily, /usage weekly and /usage cumulative lead you to the token activity for these periods directly

That being said, /usage requires Codex account auth first; without it you'll see a sign-in requirement rather than actual figures.

Also, as an alternative you can use the /statusline command, which lets you set up the info to be always visible at the bottom of the terminal instead of asking repeatedly. There are multiple things you can choose from, including context stats and rate limits as well as token counters; it persists to tui.status_line in your config.toml.

Headless.

BASH
codex exec --json 'Reply with exactly: OK'

The last event emitted is turn.completed, with its usage field containing input_tokens, cached_input_tokens, cache_write_input_tokens, output_tokens and reasoning_output_tokens. There are no dollars there, so it's up to you to work them out — and that last field is worth watching, as reasoning tokens are the ones that can move without your prompts changing.

Codex also won't run codex exec outside a trusted directory without the --skip-git-repo-check option, so a scratch directory needs a git init first.

On disk. The sessions are saved under ~/.codex/sessions/ in a structure that looks like this:

TEXT
YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl

These files contain entries like this — you're looking for the ones with a payload type of token_count:

JSON
{"type":"token_count",
 "info":{"total_token_usage":{"input_tokens":44196,"cached_input_tokens":37120,
         "output_tokens":317,"reasoning_output_tokens":43,"total_tokens":44513},
         "last_token_usage":{"input_tokens":15632,"cached_input_tokens":14080,
         "output_tokens":30,"reasoning_output_tokens":0,"total_tokens":15662},
         "model_context_window":258400},
 "rate_limits":{"primary":{"used_percent":2.0,"window_minutes":43200,
                "resets_at":1788214918},"plan_type":"go"}}

total_token_usage is the running total for the whole session, rewritten on every update, so the last one in the file is what you're after and the earlier ones should be disregarded. last_token_usage beside it is how much a single turn added.

There's also that rate_limits field, which is the part people miss. A window_minutes of 43200 is a thirty-day window, used_percent is how much of it has gone, and resets_at is a Unix timestamp. So if you're on a plan, that percentage is the thing that actually constrains you, and it's already sitting in a file on your machine. You can pull it out like this:

BASH
grep -h '"token_count"' ~/.codex/sessions/*/*/*/rollout-*.jsonl \
  | tail -1 | jq '.payload.rate_limits'

In Cursor

The tool doesn't have any native way of reporting what a session cost. There are no such commands as /usage or /cost, the only account-related command is /about, and under the hood it stores the chat history as a local SQLite file with two tables — blobs and meta — holding the actual chat data in an unreadable form. As such there's no way to sum up anything locally. If you came here for a local number, there isn't one.

What it does do is tell you which billing plan you're on, which is step one of the recipe. You can see it if you run the command in JSON format:

BASH
agent about --format json
JSON
{
  "cliVersion": "2026.07.23-e383d2b",
  "model": "Auto",
  "subscriptionTier": "Free",
  "lastRequestId": null
}

That's trimmed — it also reports your OS, shell and the signed-in email. subscriptionTier names the plan you're on, and model echoes the model setting rather than resolving it, so "Auto" stays "Auto" here. /about shows the same thing inside a session, and agent status only confirms you're signed in.

Where the numbers actually live. The dashboard. Its Spending tab shows real-time usage against your plan's allowance and any on-demand charges on top, and plans come with a bundled agent allowance that on-demand usage only starts drawing on once you've passed it. So that's the record itself rather than an estimate of it, which is more than the local files of the other tools can claim.

Tying a run to a line. lastRequestId in the output above holds the id of the last request — null until you've made one — and /copy-request-id copies the same thing from inside a session. When a dashboard line looks wrong, that's how you point at the specific run instead of describing it.

So the weekly read is a dashboard read. Open the Spending tab once a week and compare it against last week, rather than scripting something over local logs that don't hold the answer. And if you need per-run numbers in a pipeline, the practical route is to capture them at the boundary you control — your own wrapper timing and logging every agent -p call — rather than expecting the tool to hand them back afterwards.

In Antigravity CLI

In the session. To find the information about usage and quotas you can run the /usage (or /quota) command, which fetches the model configuration together with the quota state from the backend and shows you a panel with limits and remaining requests or tokens for every supported model. Make sure to check the row for the model you're actually using, as there's a separate bucket for each of them rather than one number for all of them.

Headless. If you set the --output-format json flag on a one-shot prompt you get a tidy JSON with the usage in it, and that's the only place you'll get one:

BASH
agy -p 'Reply with exactly: OK' --output-format json
JSON
{"conversation_id":"...","status":"SUCCESS","response":"OK\n",
 "duration_seconds":1.497004,"num_turns":1,
 "usage":{"input_tokens":10037,"output_tokens":24,"thinking_tokens":20,
          "cache_read_tokens":8141,"total_tokens":10061}}

No costs in currency there, and the thinking tokens are counted separately — that's the one worth watching, because nothing in the prompt you wrote makes it visible.

On disk — the gap. Antigravity CLI stores everything under ~/.gemini/antigravity-cli (not under ~/.antigravity), so the location is the first thing to surprise you. What's much more surprising is that the conversations are stored as SQLite files with binary data in them, which isn't very readable — there are tables for steps, gen_metadata or trajectory_meta, and as the schema doesn't have a token or usage field anywhere, there's no way to do the post-factum sum.

The workaround is to track usage during the run instead, by appending the usage object from every headless run to a file of your own — eg using tee and jq in a pipe with the JSON output:

BASH
agy -p "$PROMPT" --output-format json \
  | tee >(jq -c '{t: now|todate, usage}' >> ~/.agy-usage.jsonl)

That way you'll build your own JSONL log of timestamped usage objects. But it's a substitute rather than a native feature, and you'll only record the runs you put through that pipe — for interactive sessions the panel is what you have.

In Kimi Code CLI

In the session. The /usage command displays the tokens consumed and the context used, along with quota information. It's marked always-available, so it works even during an active stream — which matters exactly when you're deciding whether to stop a running task.

Headless — and a thing not to build on. The stream-json output format doesn't include any usage info. Here's what the end of the stream looks like: the assistant message, a session resumption hint, and no token figures anywhere.

BASH
kimi -p 'Reply with exactly: OK' --output-format stream-json
JSON
{"role":"assistant","content":"OK"}
{"role":"meta","type":"session.resume_hint","session_id":"session_e411c4de-…"}

So if you're writing a wrapper that tracks the cost of every run, parse the session file after it completes rather than waiting for a usage property to appear in the stream.

On disk. This is the strongest part of it. Sessions are stored under ~/.kimi-code/sessions/<workDirKey>/<sessionId>/, where the workDirKey is in the format wd_<slug>_<first 12 characters of a sha256 hash of the working directory>, and the record you want is agents/main/wire.jsonl:

JSON
{"type":"usage.record","model":"kimi-code/k3",
 "usage":{"inputOther":2464,"output":30,"inputCacheRead":18944,"inputCacheCreation":0},
 "usageScope":"turn","time":1785875741650}

One such record per turn, tagged with the model that served it. Sum them:

BASH
find ~/.kimi-code/sessions -name wire.jsonl -mtime -7 \
  | xargs -n1 jq -c 'select(.type=="usage.record") | {model, usage}' 2>/dev/null \
  | jq -s 'group_by(.model)
      | map({model: .[0].model,
             out: (map(.usage.output) | add),
             read: (map(.usage.inputCacheRead) | add)})'

Just be mindful of two pitfalls:

  • The same figures are also present inside context.append_loop_event lines carrying a step.end event. If you were to filter on the presence of the usage property rather than on .type=="usage.record", you'd get every turn twice

  • Sub-agents create their own files in agents/agent-0/wire.jsonl and so on. The command above finds them, but anything pointed at agents/main alone would quietly under-report every fan-out you ran

Lastly, keep in mind that relocating the data root with KIMI_CODE_HOME moves all of these paths with it.

What your own numbers can't see

These local figures describe one machine, so if you want to show them to someone, make sure you're aware of the following before you do:

  • If you have a laptop and a desktop, these are separate machines, so they keep their own logs — in Claude Code's case, it says its panel is based on the local session history on the machine it's installed on, excluding other devices and claude.ai

  • If you use the chat app, it's a separate surface on your account that draws on the same subscription as the CLI but doesn't appear in the CLI's figures — Claude Code explicitly lists claude.ai as excluded.

  • Team-level spend is not a question of local files — use the dashboard or set up OpenTelemetry to bring it into your own stack.

  • If you have a subscription, there's no dollar figure that could be recovered; the window is what you've got, and the only thing that matters is its remaining capacity.

The weekly read, in about a minute

Okay, so if you know all of this, the weekly analysis takes about a minute: run the script across the last seven days and ask yourself three questions:

  • Is anything changing its shape? — an increase in cache reads faster than the rest usually means sessions are longer before they're cleared, which is the same habit the vendor guidance blames for unexpectedly high spend; if output is increasing, it's worth checking what you're making your agent do

  • Is the mix changing? — has an expensive model quietly become the default? Or maybe you assumed you were on a cheap one and actually weren't

  • Has anything new arrived? — a fan-out, a scheduled job or a subagent that started running and is still in motion; these tend to be visible as a step change in the daily line, rather than a slope.

And then act on one finding. It's not about delivering a report but identifying a single thing you'll do differently.

THE FILEbin/agent-usage.py
PYTHON
#!/usr/bin/env python3
"""Total up the tokens your coding agents actually spent, from their own session logs.

Reads what's already on disk — nothing is sent anywhere and nothing is written. Three of the
harnesses keep a machine-readable record we can add up:

    Claude Code   ~/.claude/projects/*/*.jsonl
    Codex CLI     ~/.codex/sessions/*/*/*/rollout-*.jsonl
    Kimi Code     ~/.kimi-code/sessions/*/*/agents/*/wire.jsonl

Tokens only, no dollars: pricing lives with the vendor and a table baked in here would go stale
without telling you. For money, read the figure the tool itself prints.
"""

import argparse
import json
import os
import time
from collections import defaultdict
from pathlib import Path

FIELDS = ("input", "output", "cache_read", "cache_write")


def blank():
    return dict.fromkeys(FIELDS, 0)


def read_claude(root, cutoff):
    """Claude Code writes one JSONL per session, one line per transcript entry.

    A streamed reply lands as several entries carrying the same `message.id` and the *same* usage
    block, so summing every line counts the same request two or three times. Key by message id.
    """
    for path in sorted(root.glob("*/*.jsonl")):
        seen = {}
        for entry in lines(path):
            if entry.get("type") != "assistant":
                continue
            message = entry.get("message") or {}
            usage = message.get("usage") or {}
            day = entry.get("timestamp", "")[:10]
            if not day or day < cutoff:
                continue
            seen[message.get("id") or entry.get("uuid")] = (
                day,
                message.get("model", "?"),
                {
                    "input": usage.get("input_tokens", 0),
                    "output": usage.get("output_tokens", 0),
                    "cache_read": usage.get("cache_read_input_tokens", 0),
                    "cache_write": usage.get("cache_creation_input_tokens", 0),
                },
            )
        yield from seen.values()


def read_codex(root, cutoff):
    """Codex records a `token_count` event whose totals are cumulative for the whole session, so
    only the last one in each rollout file counts."""
    for path in sorted(root.glob("*/*/*/rollout-*.jsonl")):
        last = None
        for entry in lines(path):
            payload = entry.get("payload") or {}
            if payload.get("type") != "token_count":
                continue
            totals = (payload.get("info") or {}).get("total_token_usage")
            if totals:
                last = (entry.get("timestamp", "")[:10], totals)
        if not last or not last[0] or last[0] < cutoff:
            continue
        day, totals = last
        cached = totals.get("cached_input_tokens", 0)
        yield day, "codex", {
            # `input_tokens` here is the whole input, cache included — subtract to line the
            # columns up with the other two tools.
            "input": totals.get("input_tokens", 0) - cached,
            "output": totals.get("output_tokens", 0),
            "cache_read": cached,
            "cache_write": 0,
        }


def read_kimi(root, cutoff):
    """Kimi writes a `usage.record` line per turn. The same numbers also appear inside the
    `step.end` loop events — take the records, or you'll count each turn twice."""
    for path in sorted(root.glob("*/*/agents/*/wire.jsonl")):
        for entry in lines(path):
            if entry.get("type") != "usage.record":
                continue
            usage = entry.get("usage") or {}
            day = time.strftime("%Y-%m-%d", time.localtime(entry.get("time", 0) / 1000))
            if day < cutoff:
                continue
            yield day, entry.get("model", "?"), {
                "input": usage.get("inputOther", 0),
                "output": usage.get("output", 0),
                "cache_read": usage.get("inputCacheRead", 0),
                "cache_write": usage.get("inputCacheCreation", 0),
            }


def lines(path):
    with path.open(encoding="utf-8", errors="replace") as handle:
        for line in handle:
            try:
                yield json.loads(line)
            except ValueError:
                continue


SOURCES = (
    ("claude-code", "~/.claude/projects", read_claude),
    ("codex-cli", "~/.codex/sessions", read_codex),
    ("kimi-code", "~/.kimi-code/sessions", read_kimi),
)


def main():
    parser = argparse.ArgumentParser(description="Token totals from local agent session logs.")
    parser.add_argument("--days", type=int, default=7, help="how far back to look (default 7)")
    parser.add_argument("--by", choices=("day", "model", "tool"), default="day")
    args = parser.parse_args()

    cutoff = time.strftime("%Y-%m-%d", time.localtime(time.time() - args.days * 86400))
    totals = defaultdict(blank)
    missing = []

    for tool, where, reader in SOURCES:
        root = Path(os.path.expanduser(where))
        if not root.is_dir():
            missing.append(f"{tool}: nothing at {where}")
            continue
        for day, model, usage in reader(root, cutoff):
            if not any(usage.values()):
                continue
            key = {"day": (day, tool), "model": (tool, model), "tool": (tool,)}[args.by]
            for field in FIELDS:
                totals[key][field] += usage[field]

    if not totals:
        print(f"No sessions since {cutoff}.")
    else:
        width = max(len(" ".join(key)) for key in totals)
        header = f"{'':{width}}" + "".join(f"{f.replace('_', ' '):>15}" for f in FIELDS)
        print(header)
        print("-" * len(header))
        summed = blank()
        for key in sorted(totals):
            row = totals[key]
            print(f"{' '.join(key):{width}}" + "".join(f"{row[f]:>15,}" for f in FIELDS))
            for field in FIELDS:
                summed[field] += row[field]
        print("-" * len(header))
        print(f"{'total':{width}}" + "".join(f"{summed[f]:>15,}" for f in FIELDS))

    for note in missing:
        print(f"\n{note}")
    print("\nNo readable local token log for Cursor or Antigravity — read those two on their own surfaces.")


if __name__ == "__main__":
    main()
j / k to move between lessons