Баланс через API: как построить свой дашборд расходов
Кабинет показывает баланс, но не даёт агрегировать расход по моделям и дням так, как нужно именно вам. Собираем свой дашборд из трёх источников данных API.
4 мин · · Обновлено · API · разработка
Кабинет на zukko.pro показывает текущий баланс, но не строит график расхода по моделям или дням недели. Если это нужно — например, чтобы понять, какая модель ест бюджет быстрее остальных, или чтобы поставить алерт в Slack при резком росте трат — проще собрать это самому поверх трёх источников: баланса, истории списаний и заголовков ответа.
Три источника данных
GET /v1/balance — текущий остаток на счёте аккаунта (точное значение в центах — поле balance_cents), требует ключ. Хорош для снятия одной точки "сколько денег осталось сейчас".
GET /v1/usage — история списаний по всему аккаунту (все ключи вместе, фильтра по ключу нет) с курсорной пагинацией, требует ключ. Хорош для построения графика по времени и по моделям — это основной источник для дашборда, а не /v1/balance.
Заголовки ответа X-Zukko-Cost-Micros и X-Fetch-Balance-Cents — приходят с каждым обычным (не потоковым) ответом API на вызов модели; у ответов с stream: true их нет. Первый — стоимость именно этого запроса в микродолларах (1 000 000 = $1), второй — остаток баланса сразу после списания, и, несмотря на название, он тоже в микродолларах, а не в центах. Если вы уже логируете вызовы к моделям на своей стороне, эти два заголовка можно писать в лог рядом с остальными полями запроса и не трогать /v1/usage вообще.
Проверка баланса одной командой
curl https://api.zukko.pro/v1/balance \
-H "Authorization: Bearer fetch_ваш_ключ"Для быстрого алерта достаточно cron-задачи раз в час, которая дергает этот эндпоинт и пишет в Telegram, если баланс упал ниже порога.
Постраничный обход истории расходов
/v1/usage отдаёт данные порциями с курсором, а не всё сразу, поэтому для полной выгрузки нужен цикл, который идёт по страницам, пока курсор не закончится:
import requests
API_KEY = "fetch_ваш_ключ"
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",В каждой записи есть поля created_at, model, input_tokens, output_tokens, total_tokens и cost_cents (стоимость в дробных центах), а в ответе — next_cursor для следующей страницы; паттерн курсорной пагинации — идти, пока next_cursor не станет пустым. Rate limit у /v1/usage нет, но полную выгрузку на каждый запуск всё равно лучше заменить инкрементальной (см. ниже).
Агрегация расхода по моделям
Дальше это обычная агрегация в памяти или в pandas:
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 — дробные центы, переводим в доллары
return dict(sorted(totals.items(), key=lambda kv: -kv[1]))
print(spend_by_model(usage))Если данные копятся регулярно, разумнее сохранять их в SQLite или Postgres вместо повторной полной выгрузки при каждом запуске — тянуть с /v1/usage только записи новее последней сохранённой, ориентируясь на время последней записи в локальной базе.
Учёт заголовков прямо во время работы приложения
Если ваше приложение и так дергает /v1/messages или /v1/chat/completions напрямую, дешевле не ходить в /v1/usage отдельно, а читать заголовки из того же самого ответа (для нестриминговых вызовов):
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": "привет"}]},
)
cost_micros = int(resp.headers.get("X-Zukko-Cost-Micros", 0))
# несмотря на название, значение в микродолларах (1 000 000 = $1)
balance_micros = int(resp.headers.get("X-Fetch-Balance-Cents", 0))
print(f"Запрос стоил ${cost_micros / 1_000_000:.6f}")
print(f"Остаток баланса: ${balance_micros / 1_000_000:.2f}")Так расход попадает в лог приложения в момент самого вызова, без отдельного опроса истории; заодно это единственный способ разбить расход по ключам или проектам, потому что /v1/usage общий на аккаунт. Для потоковых клиентов (Cline, Roo, Claude Code, Zed, aider) заголовков нет — их расход смотрите в кабинете, /v1/usage или /v1/balance. В остальном такой учёт удобен, если вы уже пишете структурированные логи и хотите строить дашборд поверх них (например, через ClickHouse или Grafana), а не поверх API истории.
Кешированные запросы не должны искажать дашборд
С заголовком X-Zukko-Cache: true (temperature: 0 тоже включает кэш, но только у моделей, которые принимают этот параметр; Claude-модели — claude-sonnet-5, claude-opus-5-5 — могут отклонить temperature, отличную от 1, с ошибкой 400) запросы с полностью совпадающим телом от одного пользователя кешируются на срок до 7 суток для чата, а повторный запрос возвращается мгновенно и бесплатно — в ответе будет заголовок X-Zukko-Cache: hit. В /v1/usage такие попадания не пишутся, так что дашборд на основе истории их просто не увидит. Если вы считаете не только деньги, но и "сколько раз модель реально вызывалась", стоит отдельно считать запросы с X-Zukko-Cache: hit — в X-Zukko-Cost-Micros для них будет ноль, и они не должны попадать в расчёт средней стоимости запроса, если вы её считаете.
Публичные эндпоинты не требуют ключа
Для дашборда бывает нужен список моделей и их текущие цены, чтобы показывать не только факт расхода, но и по какому тарифу он случился. GET /v1/models отдаёт только список (id, object, created, owned_by) — без цен и контекста, а цены и лимиты есть в GET /v1/models/info; оба работают без ключа:
curl https://api.zukko.pro/v1/models/infoЭто можно кешировать у себя на несколько часов и подтягивать в тот же дашборд рядом с историей расходов — цены и лимиты меняются не каждую минуту.
Что не стоит пытаться сделать через API
Изменение лимитов ключа и списка разрешённых моделей делается только в кабинете на странице ключа, /app/api-keys — отдельного API-эндпоинта для управления настройками ключа в описанной схеме нет, поэтому автоматизировать смену лимита из скрипта, дёргающего этот же API, не получится. Дашборд на основе /v1/balance, /v1/usage и заголовков ответа решает задачу наблюдения за расходом, но не задачу управления лимитами — это по-прежнему ручная операция в кабинете.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.