A tiny, dependency-free tool that shows all your AI usage, rate limits, and real $ spend in one place on Linux — a native alternative to Pane (Windows-only).
- Web panel at
http://127.0.0.1:8770— spend cards, 30-day chart, per-model table, live limit bars. - waybar badge —
◐ 64%in your bar, colored by how close you are to the wall. - floating widget — the panel as a movable, resizable, always-on-top window.
- guided setup —
./setup.sh, or./setup.sh --uifor the same steps in a browser window. Every step shows a real diff of what it is about to write, and can be undone from the same page.
Built with stdlib Python + Vanilla JS. No pip install, no Node, no Rust. Loopback-only.
🇹🇷 Türkçe: README.tr.md
Demo with sample data; actual spend and limits will reflect your real providers.
If you use Claude Code, Codex, OpenRouter, Ollama etc., your spend and rate-limit status are scattered across dashboards — or invisible. usage-tracker reads what's already on your disk (transcripts) plus the providers' own live endpoints, and surfaces it where you can glance at it.
git clone https://github.com/ihsandeniz/usage-tracker.git
cd usage-tracker
./setup.sh # ★ guided wizard: deps → server → waybar → widget/tray → keys → verifyRather click than type? ./setup.sh --ui runs the same six steps in a browser window —
each one showing a real diff of what it is about to write, with an Undo button next to it.
On Windows? Download the .exe from
Releases and double-click
it — the setup wizard opens in your browser by itself, in English or Turkish. No commands,
no separate installer. Step by step, including what SmartScreen will say:
docs/WINDOWS.md · Türkçe
./setup.sh --ui # the wizard as a page (opens a window; prints the URL either way)
./setup.sh --auto # non-interactive: recommended answer for every question
./setup.sh --uninstall # undo everything the wizard set up (keys and repo stay)
./setup.sh --helpThe terminal wizard stays the default, because it is the one that works over SSH, on a
headless box, and in a dotfiles script. Both faces call the same code — setup.sh probe,
setup.sh do <step>, setup.sh undo <step> — so they cannot drift apart.
What the wizard will and won't do to your system:
- Backs up every file it touches as
<file>.bak-usage-tracker. - Edits your waybar config surgically — comments and formatting survive. It parses the file before and after, and rolls back if the result isn't valid JSONC.
- Refuses to guess. Unparseable config, or a
modules-*list that lives in aninclude? It leaves the file alone and hands you the snippet (on your clipboard) to paste yourself. - Idempotent. Re-run it as often as you like; already-done steps are no-ops.
- Imports API keys you already exported in your shell into the keys file — because shell exports never reach the autostarted service, which used to leave cards silently empty.
- Verifies instead of promising: at the end it queries the server, counts resolved provider cards, and runs the waybar feeder once to show you its real output.
- No root, no network calls, nothing outside
~/.configand this repo.
The long-running server (:8770) is read-only, and that is what lets this tool claim
nothing leaves your machine. Endpoints that edit your waybar config or touch your API keys
do not belong there, because a browser can be tricked into calling a localhost server: any
page you have open can POST to 127.0.0.1 (CORS blocks reading the reply, not sending the
request), and a domain that resolves to 127.0.0.1 becomes same-origin and can read replies
too. So --ui starts a separate server that
- picks an ephemeral port and mints a one-time token, passed in the URL fragment (fragments are never sent to a server, so the token cannot land in a log);
- rejects any request whose
Hostis not127.0.0.1:<port>— this is what stops DNS rebinding; - rejects any request with a foreign
Origin, and any API call without the token; - never returns a key value — you can set or remove a key, never read one back;
- exits when you press Finish, or after ten idle minutes.
Key values reach setup.sh over stdin, never argv, which is world-readable in /proc.
Prefer to do it by hand? Skip the wizard:
./install.sh # checks deps, generates config + keys file, prints snippets
./start.sh # → http://127.0.0.1:8770Requirements: Python 3.9+ (stdlib). curl + jq for the waybar feeder. A Chromium-family
browser (optional, for the floating widget), hyprctl (optional, auto-floats the widget on
Hyprland), wl-copy/xclip (optional, lets the wizard copy the waybar snippet). install.sh
reports what's missing.
So the badge always has data, run the server on login. One command, no root — a systemd user service (works on any systemd distro):
./service.sh install # generate + enable + start on login
./service.sh status # check it
./service.sh uninstall # remove itOnly the server needs autostart; the waybar badge and the floating widget attach to it.
Autostart without systemd
Hyprland (hyprland.conf):
exec-once = /ABS/PATH/start.sh
Generic XDG autostart — create ~/.config/autostart/usage-tracker.desktop:
[Desktop Entry]
Type=Application
Name=usage-tracker
Exec=/ABS/PATH/start.sh
X-GNOME-Autostart-enabled=trueThe setup wizard auto-detects your desktop environment and available panel software, then suggests the right surface for you. Here's the full map:
Install the XFCE genmon plugin:
sudo apt install xfce4-genmon-plugin # Debian / Ubuntu / Kali
sudo dnf install xfce4-genmon-plugin # Fedora
sudo pacman -S xfce4-genmon-plugin # ArchAdd to your XFCE panel: Panel → Add New Items → Generic Monitor, then in its properties:
- Command:
python3 /ABS/PATH/server.py usage --format genmon - Period (s):
30
Install Argos or Executor extension:
# Argos (simple shell script approach)
mkdir -p ~/.config/argos
cat > ~/.config/argos/usage.30s.sh <<'EOF'
#!/bin/bash
exec python3 /ABS/PATH/server.py usage --format argos
EOF
chmod +x ~/.config/argos/usage.30s.shThen install the Argos GNOME extension from https://extensions.gnome.org/extension/1357/argos/
Add a Command Output plasmoid to your panel:
- Right-click the panel → Edit Panel → Add Widget → Command Output
- In the plasmoid settings:
- Command:
python3 /ABS/PATH/server.py usage --format plain - Interval:
30seconds
- Command:
Add a module to your polybar config (~/.config/polybar/config.ini):
[module/usage_tracker]
type = custom/script
exec = python3 /ABS/PATH/server.py usage --format polybar
interval = 30
format = <label>Then add usage_tracker to your bar's modules-right or modules-left.
Add to your i3blocks config (~/.config/i3blocks/config):
[usage_tracker]
command=python3 /ABS/PATH/server.py usage --format i3blocks
interval=30In all cases, replace /ABS/PATH with the actual path to your usage-tracker repo (e.g., /home/user/usage-tracker).
The wizard's verify step suggests the best fit for your system and prints the command to paste:
./setup.sh probe # show the desktop and the recommended surface (touches nothing)
./setup.sh do verify # check server and badge; with no waybar, print your desktop's commandClaude Code, Codex and local runners (Ollama / LM Studio / Jan) need no key — they read files already on your disk. Hosted providers need an API key to show a card. A missing key just hides that card (no dead cards) — nothing breaks.
Keys live in one file, ~/.config/usage-tracker/env (created by install.sh/setup.sh, chmod 600). It's loaded by both ./start.sh and the systemd service, so keys work whether you run the server by hand or on login. Format is KEY=value, one per line — uncomment what you use:
| Provider | Env var | Shows | Endpoint | Get a key |
|---|---|---|---|---|
| OpenRouter | OPENROUTER_API_KEY |
spend + credit + daily limit | ✅ exists | https://openrouter.ai/keys |
| OpenAI | OPENAI_ADMIN_KEY |
Costs API spend — needs an admin key (project key → 401) | ✅ exists | Platform → Admin keys |
| DeepSeek | DEEPSEEK_API_KEY |
balance | ✅ exists | https://platform.deepseek.com |
| ElevenLabs | ELEVENLABS_API_KEY |
character quota + monthly reset | ✅ exists | ElevenLabs → Profile |
| Novita | NOVITA_API_KEY |
credit balance | ✅ exists | https://novita.ai/settings/key-management |
| DeepInfra | DEEPINFRA_API_KEY |
credit balance | ✅ exists | DeepInfra → API keys |
| Hugging Face | HUGGINGFACE_API_KEY / HF_TOKEN |
quota | ✅ exists | https://huggingface.co/settings/tokens |
| Together | TOGETHER_API_KEY |
nothing — see below | 🔴 none published | https://api.together.xyz/settings/api-keys |
After editing keys, restart the server: ./service.sh restart (or re-run ./start.sh).
On 2026-08-11 every endpoint above was probed without credentials. A 401 proves the
URL exists and wants a key; a 404 proves the adapter would never have worked. Everything
marked ✅ came back 401 (or 400 for Novita, which wants a parameter).
Together AI is the exception and it is worth being blunt about. Its API base is real —
/v1/models answers 401 Missing API key — but it publishes no account, balance or usage
endpoint at all: seven candidate paths all returned 404 with an HTML page, on both
api.together.xyz and api.together.ai. The adapter used to scan those non-existent
responses for any field named balance/credit/remaining and print the first number it
found. It now validates your key and says plainly that Together exposes nothing to read.
What is still unverified is the response shape behind those 401s — nobody here has a
key for these services, so the field names inside a successful reply have not been seen. If
an adapter meets a payload it does not recognise it says so and shows no figure; it will
not invent one. Amount scanning is deliberately narrow (usage/providers/_money.py): a
boolean is not an amount, and a remaining buried in some unrelated rate-limit object is
not your balance.
| Dimension | What | Source |
|---|---|---|
| Spend | Today / Yesterday / 30-day $ + per-model + 30-day chart | ~/.claude/projects/**/*.jsonl tokens × real prices |
| Limits | Session (5h) + Weekly usage %, reset countdown | Anthropic's own /api/oauth/usage (real), local estimate as fallback |
| Providers | 16 adapters: real $ (OpenRouter, OpenAI, DeepSeek), credit/quota (ElevenLabs, HuggingFace, Novita, DeepInfra), local (Ollama, LM Studio, Jan), local-log tokens (Codex, Aider, Continue, Cody, Windsurf), key-check only (Together — it publishes no usage endpoint) | each provider's API / local files |
Trust matters, so every number is labelled by source — nothing is silently made up:
- Token counts: 100% real — read from transcripts, not estimated.
- Prices: real — Opus/Sonnet/Haiku from the models.dev catalog (
source: catalog); models not yet in the catalog use official vendor pricing (source: official). Anything unverified is flaggedsource: estimate. - Prices work offline, out of the box — a models.dev snapshot ships inside the repo, so a fresh clone prices your usage on the first run with no network and no other tool installed. Refresh it whenever you like:
python3 -m usage.catalog --update. Runpython3 -m usage.catalogto see which source is in use and how old it is. - A model nobody has priced is not counted as $0. Its tokens are reported separately in
unknownPriceModelsand left out of the totals, so a total that is incomplete says so instead of quietly looking finished. - A scan that stopped early says so — the local-log adapters cap how much they read; when they hit that cap the card reads
partialand explains itself, rather than presenting a truncated number as your usage. - Limits: real, no calibration — pulled from the same endpoint Claude Code's
/usageuses. - If you're on a subscription (Max, ChatGPT Plus…), the $ figure is the API-equivalent cost (what this usage would cost pay-as-you-go) = the value you're getting from the subscription. Your actual bill is the flat fee, not this number.
- Can't be separated: Opus fast-mode and 1M-context premium pricing don't appear in the transcript's model field, so they're computed at standard rates (real cost may be slightly higher).
Everything under surface/ is isolated (it never touches your existing ~/.config/waybar or
~/.config) and the two surfaces are independent — install the waybar badge, the floating
widget, both, or neither. Neither depends on the other.
A compact ◐ NN% module in your bar; the headline is the highest Claude limit %, the tooltip
lists every provider, and clicking it opens the web panel. install.sh prints the ready-to-paste
custom/usage snippet + style.css colors. Standalone — no browser required.
The web panel as a movable + resizable always-on-top window (a Chromium-family --app
window). Unlike a bar widget, you position and size it however you like:
- Move it —
Super+drag (or your compositor's move binding). - Resize it —
Super+right-drag, or drag the edges. The browser remembers your size/position.
surface/usage-widget open # show it
surface/usage-widget close # hide it
surface/usage-widget toggle # show/hide (good for the waybar on-click)
surface/usage-widget status # open / closedOn Hyprland it's auto-floated, pinned (visible on every workspace), and placed top-right at
WIDGET_SIZE — no config edit needed. On other compositors, float/size it with your own controls.
Requires a Chromium-family browser (chromium / chrome / brave / …). Standalone — no waybar required.
The waybar badge works on any systemd-based Linux (Hyprland, GNOME, Plasma, etc.).
The floating widget requires a Chromium-family browser and works on any Linux:
- Hyprland: Auto-float/pin built-in.
- GNOME / Plasma / Cinnamon: Float/pin it with your compositor's native controls (usually right-click title bar → Properties).
- i3 / Openbox: Floating layers work; consult your config for layering.
The core web panel (http://127.0.0.1:8770) runs on every Linux and every desktop environment — no special setup needed.
Autostart (Hyprland): exec-once = /ABS/PATH/surface/usage-widget open.
The other two surfaces are things you look at. This one another program can ask:
python3 server.py guard --threshold 80 || echo "not now — the quota is nearly gone"python3 server.py usage # limits, spend and every provider card
python3 server.py providers # which cards exist, and why one is missing
python3 server.py guard # exit 0 ok · 1 warn · 2 critical · 3 unknown
python3 server.py watch # fire once per threshold crossing (--exec / --notify)
python3 server.py doctor # diagnose an installation without asking for logs
python3 server.py config # visible cards, thresholds, refresh interval
One place moves the alert line for all four surfaces, because the pair travels in the wire:
python3 server.py config --warn 60 --crit 85 # panel · waybar · tray · guardNo server needed (it computes locally when nothing answers on :8770), and no bash, curl or
jq — which is how Windows gets a usable surface while the .sh feeders stay Linux-only.
usage --format waybar is the bar feeder without the shell. The single-file build is the
same binary: usage-tracker guard.
Full reference, including the exit-code contract and the UT_* variables watch --exec
sets: docs/CLI.md.
Nothing is written into the installation directory — it may be read-only (a PyInstaller
bundle, Program Files, /usr/lib, a container). Run python3 -m usage.platform to print
the exact paths on your machine.
| Linux / BSD | Windows | |
|---|---|---|
| view config, settings | ~/.config/usage-tracker/ |
%APPDATA%\usage-tracker\ |
| calibration, live cache | ~/.local/state/usage-tracker/ |
%LOCALAPPDATA%\usage-tracker\State\ |
| fetched price catalogue | ~/.cache/usage-tracker/ |
%LOCALAPPDATA%\usage-tracker\Cache\ |
XDG_CONFIG_HOME / XDG_STATE_HOME / XDG_CACHE_HOME are honoured. Upgrading from an
older version keeps working: a usage_calib.json or view_config.json sitting next to the
code is still read, and the next save moves it. Nobody has to recalibrate.
surface/surface.conf (created by install.sh, git-ignored — machine-specific):
USAGE_URL="http://127.0.0.1:8770"
WIDGET_SIZE="360x520" # floating widget initial size (you can drag/resize after)
WIDGET_CLASS="usage-tracker-widget"The panel's ⚙ Settings section — and config on the command line — change three things:
the warn/critical thresholds, how often the panel polls, and the display currency. They land
in settings.json, and the thresholds travel from there to the top of /v1/usage, which is
why moving them once moves every surface.
A converted amount is derived, not measured. You enter the rate; the app never fetches
one. The result is shown beside the dollars, carrying its rate and the day you entered it:
≈ ₺210,850.89 · rate 1 USD = 41.2 TRY · entered 12.08.2026. A converted number without a
visible source is a number nobody can defend.
Keys are not writable here. The panel and config show the environment variable name and
whether it is set — never the value. Writing keys stays in ./setup.sh --ui (step keys),
which runs on a random port behind a one-time token and shuts itself down. The permanent
panel server has none of those properties and is up all day, so it was not given the power.
Provider adapters live in usage/providers/. Each module exposes collect(days) -> dict | None; returning None means "no card" (a missing/unconfigured provider is silently hidden — no dead cards). To add one, drop usage/providers/<name>.py and register it in _ADAPTERS. OpenRouter reads $OPENROUTER_API_KEY from the environment.
| Endpoint | Returns |
|---|---|
GET /api/spend?days=30 |
Today/Yesterday/30d $ + byModel + byDay |
GET /api/usage |
Claude limit panel (source: live|calibration) |
GET /api/live[?force=1] |
Raw Anthropic live usage (verification) |
GET /api/providers |
Multi-provider cards |
GET /v1/usage |
Stable wire-format (schema: usage/v1) — used by the waybar feeder |
GET /api/settings |
Thresholds, refresh, display currency + key names and set/unset |
POST /api/settings |
Change them (validated; keys are refused by design) |
/v1/usage is a public contract, not an internal detail: four surfaces here read it and so may
your scripts. The fields, the compatibility rules and what changed when are in
docs/WIRE.md.
- Binds
127.0.0.1only — never exposed to the network. - Read-only to your data. OAuth tokens in
~/.claude/.credentials.jsonare only read, never written or refreshed (writing could drop your active session). - No telemetry, no external calls except the providers' own APIs you already use.
- Static file serving is path-traversal protected.
- Writing endpoints validate both
HostandOrigin. A page on the open internet can reach127.0.0.1, but it cannot write: a foreignOriginis refused with 403. - No endpoint ever returns an API key's value, and none writes one.
python3 -m unittest discover -s tests -t . # the test suite — stdlib only, nothing to install
python3 server.py --version # single source of truth, CI checks it against the git tag
python3 -m usage.pricing # price-resolution spot-check
python3 -m usage.engine # limits + $ summary from real transcripts (no server)
python3 -m usage.catalog # which price catalogue is in use, and how oldThe test suite has no test dependencies — same promise as the runtime. CI runs it on Ubuntu
and Windows, on Python 3.9 and 3.13, with no install step. Read tests/README.md before adding
to it; the short version is write the failing test first, and assert the number, not the shape.
- Spend + real prices, live Claude limits (no calibration)
- Multi-provider adapters — 16 across 4 kinds (spend / tokens / local / quota)
- real $: OpenRouter, OpenAI (Costs API, admin key), DeepSeek (balance API — documented, candidate)
- credit/quota: ElevenLabs, HuggingFace, Together, Novita, DeepInfra (some candidate — verify with a live key)
- local ($0): Ollama, LM Studio, Jan
- local-log tokens: Codex, Aider, Continue, Cody, Windsurf
- waybar badge + movable/resizable floating widget
- Per-provider data selection (⚙ View tab →
view_config.json; waybar/widget/panel obey) - Optional native system-tray icon (Qt
QSystemTrayIcon; SNI → waybar tray) - CLI —
usage/providers/guard/watch/doctor/config, with a documented exit-code contract for scripts (docs/CLI.md) - Panel ⚙ settings — thresholds / refresh / display currency, plus read-only key status
- Verify candidate endpoints (Together / Novita / DeepInfra / HuggingFace / DeepSeek) against live keys
Issues and PRs welcome. The design constraints are deliberate: stdlib Python + Vanilla JS, zero runtime dependencies, loopback-only, every number labelled by source. Please keep them.
MIT © 2026 İhsan Deniz