Building an internal spend dashboard from a usage timeseries

Four free reads give you totals, per-capability breakdown, buckets over time and runway. The panels worth building and the ones that mislead.

A usable internal spend dashboard for Infrai needs four reads, all free and all on the key you already have: GET /v1/account/usage for the period total and per-capability breakdown, GET /v1/account/usage/timeseries for buckets over time, GET /v1/account/balance for what’s left and how long it lasts, and GET /v1/account/budget/get for the ceiling you’re heading toward. No metrics pipeline, no export job, no warehouse.

Because every capability bills to the same account, that single set of reads covers inference, storage, email, SMS, queues and document rendering at once — which is the part that isn’t reproducible when each of those is a separate vendor with its own console. Building the dashboard takes an afternoon; building the right panels is what this page is about.

The four reads

curl -sS "https://api.infrai.cc/v1/account/usage/timeseries" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"

The response carries period, buckets and a next_cursor for paging further back. Each bucket is a slice of spend, which is what a chart wants — you don’t have to derive deltas from a running total.

curl -sS "https://api.infrai.cc/v1/account/usage" \
  -H "Authorization: Bearer ${INFRAI_API_KEY}"
{
  "ok": true,
  "data": {
    "period": "30d",
    "period_start": "2026-08-22T00:00:00+00:00",
    "period_end": "2026-09-21T02:45:30+00:00",
    "total_cost": 147.77267529,
    "total_calls": 2787232,
    "total_failed_calls": 0,
    "cache_hits": 610,
    "cache_savings": 0.0,
    "breakdown": [
      {"key": "storage.object.put", "label": "storage.object.put", "cost": 71.8075, "calls": 718075, "failed_calls": 0},
      {"key": "ai.chat", "label": "ai.chat", "cost": 58.58761455, "calls": 5872, "failed_calls": 0},
      {"key": "pdf.generate", "label": "pdf.generate", "cost": 8.745, "calls": 583, "failed_calls": 0}
    ]
  }
}

Two numbers in there are worth a panel each on their own. total_failed_calls is spend you got nothing for. cache_savings is spend you avoided — a zero next to a non-zero cache_hits is a hint that your cacheable traffic isn’t shaped the way you think.

One collector, four panels

import os

import requests

API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}"})


def read(path: str, **params) -> dict:
    resp = SESSION.get(f"{API}{path}", params=params or None, timeout=25)
    resp.raise_for_status()
    return resp.json()["data"]


def snapshot() -> dict:
    """Everything the dashboard needs, in four free reads."""
    usage = read("/v1/account/usage")
    balance = read("/v1/account/balance")
    budget = read("/v1/account/budget/get")
    series = read("/v1/account/usage/timeseries")

    breakdown = sorted(usage.get("breakdown", []), key=lambda r: -r["cost"])
    top = breakdown[0] if breakdown else {"key": "-", "cost": 0.0}
    total = usage.get("total_cost") or 0.0
    calls = usage.get("total_calls") or 0

    return {
        "spend_this_period": round(total, 2),
        "cap": budget.get("hard_cap_usd"),
        "cap_used_pct": round(100 * total / budget["hard_cap_usd"], 1) if budget.get("hard_cap_usd") else None,
        "runway_days": balance.get("runway_days"),
        "failure_rate_pct": round(100 * (usage.get("total_failed_calls") or 0) / calls, 4) if calls else 0.0,
        "concentration_pct": round(100 * top["cost"] / total, 1) if total else 0.0,
        "top_capability": top["key"],
        "buckets": len(series.get("buckets", [])),
    }


if __name__ == "__main__":
    for name, value in snapshot().items():
        print(f"{name:>20}: {value}")

Seven derived figures from four requests. That’s the whole backend of the dashboard.

The panels that earn their space

PanelSource fieldWhy it’s worth a slot
Spend vs captotal_cost / hard_cap_usdthe only panel that predicts an outage
Runway in daysrunway_daysmoves before the balance does
Cost concentrationlargest breakdown row sharetells you where an optimisation would pay
Failed-call costtotal_failed_callsspend with nothing to show for it
Daily bucketstimeseries.bucketsshows the step change a total hides

Spend-vs-cap is the one to put top left. Everything else is diagnostics; that one is the alarm.

The panels that mislead

Total calls is a vanity metric. Nearly three million calls sounds like scale, but in the breakdown above 718,075 of them are object writes costing a fraction of a cent while 5,872 inference calls cost nearly as much in total — so a chart of call volume will point your optimisation effort at exactly the wrong place, and a month where you halve your call count while doubling your bill looks like a win on that panel.

Month-to-date spend without a projection is similarly weak: on the 3rd of the month everything looks fine. Multiply daily_avg_spend by the days in the period and plot that against the cap instead.

Don’t poll it every ten seconds

These are free reads, but they’re also aggregations, and hammering them gives you nothing a one-minute refresh wouldn’t. Cache the snapshot in your dashboard process for 60 seconds and you’ll never think about rate limits.

If you want the numbers alongside your own application metrics rather than in a separate tab, POST /v1/metrics/report takes them on the same key, and GET /v1/metrics/query reads them back next to your latency and error series — so the cost chart lives beside the traffic chart that explains it. That’s the argument for keeping this on one account: the spend, the metrics, the alert email and the cron job that collects them are one credential and one invoice, not four.

The honest limitations

The breakdown is by capability, not by key, tenant or environment. If you need cost per customer, capture metadata.cost_usd from each response yourself — every call returns it — and aggregate on your side. There’s also no push for usage: no “spend changed” event, so the dashboard polls. The wallet events (wallet.low_balance and friends, via POST /v1/account/webhooks/register) are the closest thing, and they fire on thresholds rather than continuously.

All four reads report billing_class: free in discovery, so the dashboard costs nothing to run. The rates it’s charting are live — GET /v1/account/balance returns an affordable_uses_hint with the current per-capability price and how many calls your credit buys (verified 2026-09-21) — and those rates move downward as vendor contracts improve, which is exactly why the dashboard should read them rather than store them.

References

Browse more account developer guides