A live token counter for any OpenAI-compatible API.
You point your AI agent at a local port instead of at your provider. The meter passes every request straight through and counts what comes back. A full screen dashboard shows the number growing in real time, what it cost, how much of your input was served from cache, how today compares to yesterday, and a stacked chart of every day since you started.
One file, no dependencies, no build step, no account, no telemetry. Node 18 or newer is the only requirement.
If you run agents all day, you have no idea what they are actually burning until the bill lands. Provider dashboards are delayed, they lump everything together, and they will not tell you that one background agent has quietly been eating 60 percent of your spend.
This sits in the middle and tells you. Per agent, per day, live.
It also tells you where the money actually goes. On a long running agent most input is served from cache at a fraction of the price, so the raw token count can be a hundred times bigger than the bill. The meter splits every request into cached input, uncached input and output, and prices each part separately, so you can see which one you are really paying for.
git clone https://github.com/Suma-Udd/token-meter.git
cd token-meter
node setup.js
node meter.jssetup.js asks a few questions and writes meter.config.json. Then open http://localhost:8125 and you have the screen.
Last step, the one that actually connects it. Wherever your agent is configured, change its base URL from your provider to the local port the meter printed on startup:
https://api.deepseek.com -> http://127.0.0.1:8123
Leave the API key exactly as it is. The meter never needs it and never stores it.
That is the whole setup. Send one message to your agent and the number moves.
Prefer to skip the wizard? Copy meter.config.example.json to meter.config.json and edit it by hand. The wizard writes the same file.
The change is always the same: replace the provider URL with the meter port. Only the name of the setting differs.
Environment variable, works for most SDKs and CLI tools
export OPENAI_BASE_URL="http://127.0.0.1:8123"OpenAI Python or Node SDK
client = OpenAI(base_url="http://127.0.0.1:8123", api_key=YOUR_KEY)A YAML or JSON config file
Look for a key named baseUrl, base_url, api_base, endpoint, or host, and put the meter port there.
Two agents, two ports
Give each agent its own port in meter.config.json and they show up as separate colours on the chart. That is the entire point of the split. One agent per port.
If your client insists on appending /v1 itself, that is fine. Both http://127.0.0.1:8123 and http://127.0.0.1:8123/v1 work.
The main screen is one big number: tokens today. It rolls up while a response streams, then settles on the exact official count the moment the provider reports it. The roll is timed to the size of the change, so a small correction glides in under half a second and a big batch takes a couple of seconds instead of jumping. If the official count differs meaningfully from the live estimate, the number gets a brief green or red halo. Tiny corrections just ease in.
Under the number sits a 14 day strip and a "vs yesterday" chip that counts and turns its arrow as today catches up. Four tiles carry cost today, request count, tokens per minute, and your account balance if your provider exposes one.
The cache cards split today's usage three ways: cache hits, cache misses and output. Each card shows the tokens, its share of your input, what it cost, and the rate in force right now. This is usually the most useful part of the screen, because it shows that a huge token count can be mostly cheap cached input.
The history sheet, opened by tapping History or the strip, has 7 day, 30 day and all time views, five summary cards, a stacked bar chart with a tooltip on every day, and a per day list going back to the day you started.
Reset takes two taps, so a stray touch cannot wipe your count.
Everything lives in data/usage-YYYY-MM-DD.json, one small file per day. Plain JSON, yours, on your disk, going nowhere.
{
"ui": {
"port": 8125,
"host": "127.0.0.1",
"title": "Live Tokens"
},
"upstream": {
"url": "https://api.deepseek.com"
},
"pricing": {
"name": "deepseek-flash",
"default": { "inputMiss": 0.15, "inputCached": 0.003, "output": 0.60 },
"models": {
"v4-pro": { "inputMiss": 0.66, "inputCached": 0.022, "output": 1.98 }
},
"peak": { "utcHours": [[1, 4], [6, 10]], "days": [1, 2, 3, 4, 5], "multiplier": 2 }
},
"balance": {
"path": "/user/balance",
"apiKeyEnv": "TOKEN_METER_API_KEY"
},
"agents": [
{ "id": "coder", "label": "Coding agent", "short": "CO", "color": "#0a84ff", "port": 8123 },
{ "id": "writer", "label": "Research agent", "short": "RE", "color": "#ff9f0a", "port": 8124 }
]
}| Key | What it does |
|---|---|
ui.port |
Where the screen is served. |
ui.host |
127.0.0.1 keeps it on this machine. 0.0.0.0 lets other devices on your network open it. Read the security note first. |
ui.title |
The text in the corner and in the browser tab. |
upstream.url |
Your provider's base URL, the one you are replacing. A path like /v1 on the end is fine. |
pricing |
What each part of a request costs. See below. |
balance |
Optional. Set to null to hide the balance tile. |
agents[] |
One entry per agent. Each needs a unique id and a unique port. label and color are what you see. short is the two letter tag in the footer. hosts is optional, see below. |
Up to eight agents. Colours are assigned automatically if you leave color out.
All prices are dollars per one million tokens, the unit every provider quotes.
| Key | What it does |
|---|---|
name |
The model name shown next to the rates on screen. |
default |
inputMiss, inputCached and output prices for any model not listed in models. |
models |
Optional. Different prices for specific models. Each key is matched against the model name in the request, ignoring case, so "v4-pro" matches deepseek-v4-pro. The longest matching key wins. |
peak |
Optional. For providers that charge more at certain hours. utcHours lists the peak windows in UTC, days lists the peak weekdays (0 is Sunday), and multiplier is how much more peak costs. When peak is set, the prices above are the off peak ones. |
A request is priced at the rate in force when it was sent, not when the reply finished, which matters for a long reply that crosses into a different band.
The example config carries DeepSeek's list price, checked against their pricing page in November 2026. DeepSeek also treats Chinese public holidays as off peak, which the meter does not model, so on those days it slightly overstates the cost. For any other provider, put in the prices of the model you actually run. The setup wizard asks for them.
A flat block from version 1, { "inputMiss": ..., "inputCached": ..., "output": ... }, still works and is read as default.
Say the same agent runs on your laptop and on a server, and you want one counter for both. Give that agent an extra address to listen on, the one the other machine can reach, typically a VPN address like Tailscale's:
{ "id": "coder", "label": "Coding agent", "port": 8123, "hosts": ["127.0.0.1", "100.64.0.12"] }Then point the other machine's agent at http://100.64.0.12:8123. Both machines land in the same counter.
Rules for hosts. Name exact addresses. 0.0.0.0 and other wildcards are refused, because a proxy port should never answer a whole network. An address that is not up yet when the meter starts, for example a VPN that connects after boot, does not stop the meter. It retries every 30 seconds until the address exists.
Only some providers expose an account balance, and none of them agree on the response shape. The meter understands DeepSeek's /user/balance and OpenRouter's /credits. To use it, put your API key in an environment variable before starting:
export TOKEN_METER_API_KEY="your-key-here"
node meter.jsThis is the only situation in which the meter reads your key, and it is entirely optional. Set "balance": null and the tile disappears.
Linux, as a user service
mkdir -p ~/.config/systemd/user
cp install/token-meter.service ~/.config/systemd/user/
# open the file and fix the paths if you cloned somewhere other than ~/token-meter
systemctl --user daemon-reload
systemctl --user enable --now token-meter
loginctl enable-linger "$USER" # so it survives a logoutLogs with journalctl --user -u token-meter -f.
macOS
Same idea with a launchd plist, or just run it in a terminal tab you leave open.
Windows
Easiest is a shortcut to node meter.js in your Startup folder. Right click the shortcut, set "Run" to minimized.
This is what the layout was built for. An old phone on a stand, plugged in, showing the number all day.
- Set
"host": "0.0.0.0"inmeter.config.jsonand restart. - Find your computer's local address, something like
192.168.1.40. - Open
http://192.168.1.40:8125on the phone and tap Fullscreen.
The page requests a screen wake lock, so it will not dim on browsers that support it. The layout is height aware and has been checked on wide phone screens, tablets in both orientations, and desktop.
The meter has no authentication of any kind. That is deliberate for something meant to run on your own machine, and it is the one thing to understand before you change a setting.
- The default binding is
127.0.0.1, which means only your own computer can reach it. Leave it there unless you have a reason not to. - The agent proxy ports listen on
127.0.0.1unless you list more addresses in an agent'shosts. Wildcards like0.0.0.0are refused there, so a proxy port can only reach the exact networks you name. Anyone who can reach such an address can send requests through it, but only with their own key, because the meter holds none. - Setting
"host": "0.0.0.0"puts the screen, including the reset button, on your local network with no password. Do that on your home network, not on a cafe network, and never forward the port to the internet. - The meter does not need your API key for chat traffic. It forwards the caller's own
Authorizationheader untouched and never writes it anywhere. - Nothing leaves your machine except the requests your agent was making anyway.
For each configured agent the meter opens a local port. A request arriving there is forwarded to your provider unchanged, with one exception: on a streamed chat completion it merges stream_options.include_usage into the body so the provider appends an exact usage chunk at the end.
While the response streams back, the meter tees it. It parses the data: lines as they pass, adds up the characters in content, reasoning_content and tool call arguments, and multiplies by roughly 0.3 to grow a live estimate. That estimate is only there so the number on screen moves while you wait.
When the stream ends, the official usage chunk lands. The meter throws the estimate away and records the exact count. The number you end up with is never a guess.
Non-streamed responses are simpler: it reads usage from the JSON body and records it.
From that usage it takes the cached part of the prompt, the uncached rest, and the output, and prices each at the rate for that model at the time the request was sent. Providers report the cached part under one of two names, prompt_cache_hit_tokens or prompt_tokens_details.cached_tokens, and the meter reads both. If your provider reports neither, every input token is counted as uncached.
Totals are written to disk every ten seconds and on every completed request, so a crash or a restart costs you nothing. At midnight UTC the day rolls over: the day that just ended is written out first, then it becomes a row in history and every connected screen drops to zero together. A reply that started before midnight and finished after it is counted on the new day.
If your tool talks to a /chat/completions endpoint and lets you set a base URL, yes. That covers DeepSeek, OpenAI, OpenRouter, Groq, Together, Fireworks, DeepInfra, Mistral, local vLLM, LM Studio, Ollama, and most agent frameworks built on the OpenAI SDK.
It does not currently understand Anthropic's native Messages API, which uses a different endpoint and a different event shape. Route Claude through an OpenAI-compatible gateway and it works.
Requests that are not chat completions pass straight through, uncounted. Nothing breaks.
If you use Claude Code, Cursor, Codex, Aider, Copilot or anything similar, the repo ships an AGENTS.md written for them rather than for you.
Point your agent at this repository and say:
Read AGENTS.md and set up Token Meter for me.
It will read your existing agent config, work out which base URL to change, write meter.config.json with the right ports and prices, start the meter, verify a real request is counted, and hand you the URL. It will also do the parts this repo cannot ship, because they depend on your machine: the service file for your init system, the edits to your specific agent's config file, and a pricing block that matches the model you actually run.
AGENTS.md is a plain markdown file. You can read it yourself and follow it by hand.
The number never moves. Your agent is still talking to the provider directly. Check the base URL actually changed, and watch the meter's terminal output. Every counted request prints a line. No lines means no traffic is arriving.
"port is already in use". Something else holds that port. Change it in meter.config.json, or stop the other process.
Requests work but nothing is counted. Your provider may strip stream_options, in which case there is no usage chunk to read. Try a non-streamed request. If that counts and streaming does not, that is the cause.
The cost looks wrong. The pricing block is plain multiplication. Check the prices are per one million tokens, that the model you run is matched by the right models key (the meter log prints rate=<key>/<band> on every request), and that peak is only set if your provider really has peak hours.
The cache cards say "Share of tracked input". Part of today was counted by an older version that did not record the split. The note under the cards says how many requests are covered. It fixes itself from tomorrow.
The cache hit share is 0%. Your provider does not report cached tokens, or there were none. Nothing to fix in the meter.
The screen says "cannot reach the meter". The page loaded from a stale tab or from a file, not from the meter. Open http://localhost:8125 directly.
Nothing shows in history. History is built from data/usage-*.json. It fills in as days pass. On day one you will only see today.
Pull, then restart the meter. Nothing else is required.
git pull
node meter.jsYour config keeps working, including the old flat pricing block. Your history keeps working too: day files from 1.0 have no cache split, so for those days, and for the part of today counted before the upgrade, the cache cards say how much they cover. Prices are applied to new requests only. Old days keep the cost they were recorded with.
If you were on DeepSeek, the old example prices are out of date. Copy the pricing block from meter.config.example.json into your config to get the current table with peak hours.
Issues and pull requests are welcome. The whole thing is two files worth reading, meter.js and ui/index.html, both plain and commented. Keep it dependency free.
MIT. Do what you like with it.

