GitHub - joshcarter/claude-o-meter: Keep your Claude use at the redline or you're just giving Anthropic free money

GitHub

8 min read Original article ↗

Photo

A Claude tachometer which shows your token use within your session window. The 5-hour window is the main gauge, with its redline where your tokens will be exhaused exactly at the end of the window. The fuel gauge is your 7-day window.

This is a simple Python app designed to run on a Raspberry Pi, but it can also display in a window on your desktop. You'll need to configure your Anthropic session ID (see below). The tach shows a rough window-average burn rate from the first poll; after ~10 minutes of samples the slope switches to a more accurate recent regression.

I've included a 3D printable frame of my own design which fits the Adafruit PiTFT 3.5 Plus. This works fine on the current Raspberry Pi OS (trixie-based) as of 2026.

Quick start (desktop)

python3 -m venv .venv
.venv/bin/pip install -r claude_o_meter/requirements.txt
.venv/bin/python -m claude_o_meter           # DATA_SOURCE defaults to "fake"

A 480×320 window opens. With the default DATA_SOURCE = "fake" the gauges run on a demo cycle — no network or session key needed — so you can watch every state: the tach and fuel gauge sweep their range, the low-fuel and check-engine lights blink near each crest, and the fault message cycles through its variants. Close the window or press Ctrl-C to quit.

To step through every state quickly instead of waiting out the cycle, run the reveal demo — tach, fuel, both lights, then the fault messages:

.venv/bin/python -m claude_o_meter.demo_reveal       # optional ms/step, e.g. 300

Live mode (your real usage)

Live mode polls your account, so it needs your claude.ai session cookie:

  1. Use a separate browser profile (or a private window) to sign in to https://claude.ai, then DevTools → Application → Cookies → https://claude.ai, and copy the sessionKey value (starts sk-ant-sid01-...). Close that window afterwards and don't browse from it again — see Give the meter its own session below for why this matters.
  2. Set DATA_SOURCE = "live" in claude_o_meter/config.toml.
  3. Run with the cookie in the environment:
export CLAUDE_SESSION_KEY=sk-ant-sid01-...
.venv/bin/python -m claude_o_meter

A provisional burn rate is available immediately (utilization ÷ hours elapsed in the window). After ~10 minutes of samples the gauge switches to the tighter recent-regression estimate. The cookie lasts weeks to months; when it expires the check-engine light shows a "needs auth" message — get a fresh sessionKey and restart.

(curl_cffi, used only in live mode, mimics Chrome's TLS fingerprint to get past Cloudflare on claude.ai.)

Give the meter its own session

There is no public API for the 5-hour / 7-day window utilization — the Admin API covers API-key rate limits and API spend, not subscription windows — so this app reads the same private endpoint the claude.ai usage page does, with your session cookie. That means it is, to the server, another client on your session, and it is worth being a considerate one. Sharing a sessionKey between your everyday browser and a machine polling around the clock from a different IP is the exact shape of a leaked cookie, and the usual server response is to invalidate the whole session family — logging you out everywhere.

So: sign in from a separate browser profile, take that profile's sessionKey, and leave the profile alone afterwards. Two sessions that never overlap on a device can't be mistaken for one cookie in two places.

The poller holds up its end:

  • Follows cookie rotation. If claude.ai sends a new sessionKey via Set-Cookie, the poller adopts it and saves it (owner-only, next to the sample DB) so a restart doesn't replay the superseded value — replaying a rotated token is what triggers session-family revocation. Put a fresh key in CLAUDE_SESSION_KEY and the saved chain is discarded automatically.
  • Stops on a rejected cookie. A 401/403 parks the poller — it watches CLAUDE_SESSION_KEY for a new value instead of retrying a dead credential. Export a fresh key and it resumes without a restart.
  • Doesn't poll on a metronome. The interval is jittered ±15%, and the discovered org id is cached to disk so a systemd restart loop doesn't re-hit /api/organizations every few seconds.
  • Sends the browser's own request headers, not just its TLS fingerprint.

Polling gently helps too: POLL_SECONDS = 180 is plenty for a 5-hour window.

Configuration (claude_o_meter/config.toml)

Key Default Notes
DATA_SOURCE "fake" "fake" (offline oscillating values) or "live" (real polling)
POLL_SECONDS 180 Poll cadence; in live mode it sets POLL_INTERVAL_SECONDS (jittered ±15%)
UTC_OFFSET_HOURS 0 Integer offset for displayed clock/date (e.g. -7 for PDT)
DISPLAY_MODE "window" "window" (SDL window on a desktop) or "framebuffer" (Pi TFT)
FB_DEVICE "/dev/fb1" Framebuffer device when DISPLAY_MODE="framebuffer"
DIM_OPACITY 212 Dimming-rectangle opacity 0–255 (212 ≈ 83% ghost)

Any key above can also be set as an environment variable of the same name, which overrides the TOML (the Pi service uses this to set DISPLAY_MODE=framebuffer and DATA_SOURCE=live).

Live-mode environment variables

The session key and a few overrides come from the environment, not the TOML file:

Var Required Default Notes
CLAUDE_SESSION_KEY yes (live) — claude.ai sessionKey cookie value
CLAUDE_ORG_ID no auto-discover Pin a specific org instead of discovering it (skips the /api/organizations call entirely)
POLL_INTERVAL_SECONDS no from POLL_SECONDS Poll cadence override; jittered ±15%
CURL_IMPERSONATE no chrome (newest) curl_cffi impersonation profile, e.g. chrome146 to pin one
DB_PATH no ./samples.db SQLite history file for the 5h/7d windows

Two small state files live in DB_PATH's directory: session-cookie.json (the rotated sessionKey, written 0600) and org-id (the discovery cache). Both are gitignored and safe to delete — the poller rebuilds them from the environment.

Raspberry Pi 3 + PiTFT deployment

The same program runs on a Raspberry Pi 3 driving a 3.5" PiTFT (480×320) panel as a single systemd service. There is no desktop or X server — the renderer writes frames straight to the panel's framebuffer (SDL dummy video driver). Verified on 64-bit Raspberry Pi OS Lite (Trixie).

1. Enable the PiTFT panel (legacy framebuffer driver)

Install Adafruit's PiTFT support, then make sure the panel uses the legacy fb_hx8357d framebuffer driver — not the DRM variant. In /boot/firmware/config.txt the overlay line must not contain ,drm:

dtoverlay=pitft35-resistive,rotate=90,speed=20000000,fps=20

The ,drm flag (which the Adafruit installer may add) forces the DRM/KMS driver, which leaves the SPI panel blank until a modeset and conflicts with vc4-kms-v3d — symptom: a white/blank screen and no console. Remove it and reboot:

sudo sed -i '/pitft35-resistive/ s/,drm,/,/' /boot/firmware/config.txt
sudo reboot

After reboot, confirm the framebuffer:

cat /sys/class/graphics/fb1/name            # -> fb_hx8357d
cat /sys/class/graphics/fb1/bits_per_pixel  # -> 16   (RGB565)
cat /sys/class/graphics/fb1/virtual_size    # -> 480,320

The renderer auto-detects the depth (16bpp RGB565 here, 32bpp as a fallback); numpy packs each RGB565 frame.

2. Clone and install

git clone https://github.com/joshcarter/claude-o-meter.git ~/claude-o-meter
cd ~/claude-o-meter
python3 -m venv .venv
.venv/bin/pip install -r claude_o_meter/requirements.txt

pygame, curl_cffi, and numpy all install from prebuilt aarch64 wheels on 64-bit Raspberry Pi OS — no compiler required.

3. Session key

printf 'CLAUDE_SESSION_KEY=sk-ant-sid01-...\n' > ~/claude-o-meter/.env
chmod 600 ~/claude-o-meter/.env

4. Install the service

deploy/claude-o-meter.service assumes the repo at /home/pi/claude-o-meter, runs as user pi, and sets DISPLAY_MODE=framebuffer / DATA_SOURCE=live via the environment (so no config.toml edit is needed). The pi user must be in the video group to write /dev/fb1 (the default on Raspberry Pi OS). Edit the unit if your path or user differs.

sudo cp ~/claude-o-meter/deploy/claude-o-meter.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now claude-o-meter
systemctl status claude-o-meter --no-pager

samples.db is created under /var/lib/claude-o-meter/ (systemd StateDirectory). Reboot once to confirm the dashboard comes up unattended:

Logs: journalctl -u claude-o-meter -f.

The instrument cluster

What each part of the display shows:

Widget Source Mapping
Horizontal 20-seg tachometer (reveals left→right) 5h redline_ratio burn rate vs sustainable; non-linear (tach_position)
Two-digit 0–99 readout same as tach numeric form of the tach
Vertical 20-seg fuel gauge (drains top→bottom) 7d utilization remaining = clamp(100 − util, 0, 100); linear
Low-fuel light 7d utilization on when remaining ≤ 20% (util ≥ 80%)
Check-engine light + message fault state poller-unreachable / data-stale / needs-auth
Extra use $, Extra limit $, Balance $ extra_usage + /prepaid/credits cents → USD, up to 999.99
7-day reset date / 5-hour reset time window resets_at fmt_date / fmt_hhmm

Tachometer scale (the "redline")

The gauge and the 0–99 readout both come from the 5-hour redline_ratio reported by the poller (burn_rate / sustainable_rate, where 1.0 means you are on track to spend the window's whole budget exactly when it resets). The mapping lives in claude_o_meter/gauges.py.

tach_position() maps the ratio to a continuous gauge position 0.0 .. TACH_FRAMES-1 (20 segments) in two pieces:

  • Below the redline (ratio ≤ 1.0): a concave curve, position = REDLINE_FRAME * ratio ** BLUE_EXPONENT. With BLUE_EXPONENT = 0.5 it is steep at low ratios and flattens toward the redline, so a modest user still sees the needle move through the day.
  • Above the redline (ratio > 1.0): linear from REDLINE_FRAME to the top segment, pegging once the burn rate reaches RED_FULL_RATIO (default 2× sustainable).

redline_ratio == 1.0 lands exactly on REDLINE_FRAME (17), the top of the gauge's yellow band — anything past that is red. The readout is the same position scaled to 0–99 (tach_number()) instead of quantised to segments, so it moves even between segment changes.

redline_ratio segment number zone
0.05 4 19 blue
0.20 8 38 blue
0.50 12 60 blue
0.80 15 75 yellow
1.00 17 84 yellow (redline)
1.60 19 93 red
≥2.00 20 99 red

Tuning knobs in claude_o_meter/gauges.py: REDLINE_FRAME, BLUE_EXPONENT (lower = more sensitive at low use), and RED_FULL_RATIO.

Tests

.venv/bin/pip install pytest
.venv/bin/python -m pytest claude_o_meter/tests/

The display tests run headless via SDL_VIDEODRIVER=dummy (set in the tests), so no window or live cookie is needed.

License