Balance via API: building your own spend dashboard
The dashboard shows a balance, but it will not aggregate spend by model and day the way you actually need. Here is how to build one from three API data sources.
4 min · · Updated · API · development
The zukko.pro dashboard shows the current balance, but it will not chart spend by model or by day of the week. If you need that — say, to see which model is eating the budget fastest, or to fire a Slack alert on a sudden spend spike — it is straightforward to build on top of three data sources: the balance, the usage history, and the response headers.
Three data sources
GET /v1/balance — the current account balance (the exact value in cents is the balance_cents field), requires a key. Good for a single snapshot of "how much money is left right now."
GET /v1/usage — the spend history for the whole account (all keys together, no per-key filter) with cursor pagination, requires a key. Good for a time series broken down by model — this is the main source for a dashboard, not /v1/balance.
The X-Zukko-Cost-Micros and X-Fetch-Balance-Cents response headers — attached to every regular (non-streaming) API response to a model call; responses with stream: true don't carry them. The first is the cost of that specific request in micro-dollars (1,000,000 = $1); the second is the remaining balance right after that charge, and despite its name it is also in micro-dollars, not cents. If you already log model calls on your side, these two headers can be written to the log alongside the rest of the request fields, and /v1/usage need not be touched at all.
Checking the balance with one command
curl https://api.zukko.pro/v1/balance \
-H "Authorization: Bearer fetch_your_key"For a quick alert, an hourly cron job that hits this endpoint and posts to Telegram when the balance drops below a threshold is enough.
Paging through the spend history
/v1/usage returns data in chunks with a cursor rather than all at once, so a full export needs a loop that keeps paging until the cursor runs out:
import requests
API_KEY = "fetch_your_key"
BASE = "https://api.zukko.pro/v1"
def fetch_all_usage():
records = []
cursor = None
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
resp = requests.get(
f"{BASE}/usage",Each record has the fields created_at, model, input_tokens, output_tokens, total_tokens, and cost_cents (cost in fractional cents), and the response carries next_cursor for the next page; the cursor-pagination pattern is to keep going until next_cursor is empty. /v1/usage has no rate limit, but it is still better to replace a full export on every run with an incremental one (see below).
Aggregating spend by model
From there it is ordinary in-memory or pandas aggregation:
from collections import defaultdict
def spend_by_model(records):
totals = defaultdict(float)
for r in records:
totals[r["model"]] += r["cost_cents"] / 100 # cost_cents is fractional cents, convert to dollars
return dict(sorted(totals.items(), key=lambda kv: -kv[1]))
print(spend_by_model(usage))If data keeps accumulating, it makes more sense to store it in SQLite or Postgres than to re-export everything on every run — pull from /v1/usage only records newer than the last one saved, based on the timestamp of the latest local row.
Reading headers while your app is already running
If your app already calls /v1/messages or /v1/chat/completions directly, it is cheaper to read the headers off that same response (for non-streaming calls) than to hit /v1/usage separately:
import requests
resp = requests.post(
"https://api.zukko.pro/v1/chat/completions",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
},
json={"model": "claude-sonnet-5", "messages": [{"role": "user", "content": "hello"}]},
)
cost_micros = int(resp.headers.get("X-Zukko-Cost-Micros", 0))
# despite the name, the value is in micro-dollars (1,000,000 = $1)
balance_micros = int(resp.headers.get("X-Fetch-Balance-Cents", 0))
print(f"Request cost: ${cost_micros / 1_000_000:.6f}")
print(f"Remaining balance: ${balance_micros / 1_000_000:.2f}")This way spend lands in your application log at the moment of the call itself, with no separate history poll; it's also the only way to split spend by key or project, since /v1/usage is shared across the account. Streaming clients (Cline, Roo, Claude Code, Zed, aider) get no such headers — check their spend in the dashboard, /v1/usage, or /v1/balance. Otherwise this approach is convenient if you already write structured logs and want to build a dashboard on top of them (through ClickHouse or Grafana, say) rather than on top of the usage API.
Cached requests should not skew the dashboard
With the X-Zukko-Cache: true header (temperature: 0 also enables the cache, but only on models that accept that parameter; Claude models — claude-sonnet-5, claude-opus-5-5 — may reject a temperature other than 1 with a 400), requests with a fully identical body from the same user are cached for up to 7 days for chat, and a repeat request comes back instantly and free — the response carries an X-Zukko-Cache: hit header. Such hits aren't written to /v1/usage, so a dashboard built on the history simply won't see them. If you are tracking not just money but "how many times the model was actually invoked," count requests with X-Zukko-Cache: hit separately — their X-Zukko-Cost-Micros will be zero, and they should not factor into an average cost-per-request calculation if you are computing one.
Public endpoints need no key
A dashboard often needs the model list and current prices, so it can show not just what was spent but at what rate. GET /v1/models only returns the list (id, object, created, owned_by) — no prices or context — while prices and limits live in GET /v1/models/info; both work without a key:
curl https://api.zukko.pro/v1/models/infoThis can be cached locally for a few hours and pulled into the same dashboard next to the spend history — prices and limits do not change minute to minute.
What not to try building through the API
Changing a key's limits or its list of allowed models is only done in the dashboard, on the key's page at /app/api-keys — there is no separate API endpoint for managing key settings in the current schema, so automating a limit change from a script that calls this same API is not possible. A dashboard built on /v1/balance, /v1/usage, and response headers solves the observation problem, not the limit-management one — that stays a manual step in the dashboard.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Your Own Neural Network Proxy on Node.js: Keep the API Key Off the FrontendA small Express server between your frontend and the model API: the key stays on the server, and models, request size and rate are capped.
- DeepSeek API in Python: Requests, Streaming, JSON and Error HandlingReady code for DeepSeek V4.1 Flash and V4 Pro on the OpenAI SDK: a request, streaming, JSON parsing and careful error handling.
- Prompt Engineering Examples: 12 Ready-Made Templates for WorkTwelve prompt templates for everyday tasks: what each one says, why it is built that way, and what a single API call costs.