Measuring spend through an AI gateway
Unified billing moves your spend off the provider's ledger and onto the gateway's. Here is what that breaks, how the connector keeps two ledgers from ever double-counting one request, and what a promotional rate looks like when it ends.
In short
- A model consumed through a gateway on unified billing does not appear in the OpenAI or Anthropic connectors — that spend is on the gateway ledger.
- The
ai-gatewaysource reads the gateway's reporting endpoints for cost and tokens by model, provider, user and tag, plus the prepaid credits balance. - Every event declares
provider: "ai-gateway"with the vendor asupstream_provider, so running both ledgers can never double-count one request. - Money lives only on
llm.cost_reported; usage events carry tokens. Summing cost is the bill, not a multiple of it. llm.credits_reportedcarries the balance — the failure mode unified billing introduces is a prepaid balance running out and stranding production.- Buckets are read about 90 minutes settled, because a gateway keeps writing into the current one.
What breaks without a gateway source
| Setup | What the provider connectors see | What is missing |
|---|---|---|
| All traffic direct to OpenAI/Anthropic | Everything | Nothing |
| All traffic through a gateway, unified billing | Little or nothing — the spend is on the gateway ledger | The whole bill |
| Some direct, some through a gateway | Only the direct half | The gateway half, silently |
The double-count trap, designed out
The obvious implementation labels gateway traffic with the upstream vendor, because that is who ran the model. It is also how one request gets summed twice the moment a customer runs both paths. The ledger is therefore always the gateway, and the vendor is context:
{
"eventType": "llm.cost_reported",
"attributes": {
"provider": "ai-gateway", // the ledger the money is on
"upstream_provider": "openai", // who ran the model
"model": "gpt-5.6-sol",
"tag": "support-triage"
},
"metrics": { "cost_usd": 4.00 }
}
The AI spend template ships a spend by ledger widget that groups on
attributes.provider for exactly this reason: the two ledgers side by side, summed
on purpose rather than by accident.
Promotions, and the day they end
A discounted model is easy to misread. Three things move at once — the unit price, the volume, and the mix of models — and only one widget separates them:
| What moved | Total spend says | Cost per 1M tokens says |
|---|---|---|
| A discount started | went down, maybe | a clear step down — the rate changed |
| A discount ended | went up | a clear step up on the day, volume unchanged |
| Traffic grew | went up | flat — this is volume, not price |
| Traffic moved to a pricier model | went up | up, with spend by model naming the culprit |
The ratio a promotion does not change. Output tokens cost several times input, and a cache read costs roughly an eighth of an input token. A headline discount that doubles your output volume is a wash; a cache hit rate that climbs ten points is a saving you keep after the promotion ends. Both are widgets in the template.
Credits, and the failure unified billing introduces
Prepaid credits fail differently from an invoice: they run out, and production stops. The
connector reads the credits endpoint on every sync and emits
llm.credits_reported with the balance, so the number is on the dashboard as a
level rather than discovered as an outage. A gateway running in bring-your-own-key mode has no
credits endpoint; that is not an error and the sync continues.
Connect it
curl -X POST https://api.hookget.com/v1/sources \
-H "authorization: Bearer $HOOKGET_KEY" \
-d '{
"provider": "ai-gateway",
"secret": "<read-only reporting key>",
"sync_interval_sec": 900,
"config": { "bucket_width": "1h", "group_by_tag": true }
}'
Config is validated when you save it: an unknown key is refused rather than ignored, a
non-HTTPS base URL is refused, and a bucket width outside 1h or 1d is
refused. A rejected credential backs off and pauses with a readable health message instead of
retrying forever.
Where this is, on 28 August 2026. The gateway connector, the dashboard template, the mapping layer and the per-person and per-department summaries are live: a gateway user is mapped to a named team member and a department, and the API reports spend by person and by department, gated to admins. What is not built yet is the billing — the add-on catalogue is defined in code and nothing charges for it — along with budgets, forecasts and a finance-system export.
Questions
Why does a gateway need its own connector?
Because the money moves. The OpenAI and Anthropic connectors read those vendors' organisation reports. On unified billing — which is what a gateway promotion normally requires — the spend is on the gateway's ledger, not the provider's. Without a gateway source, the bill HookGet shows you is missing that traffic entirely, and nothing says so. That is the hole this closes.
What is the double-count trap?
Running some traffic direct and some through a gateway is normal. If the gateway
connector labelled its events with the upstream vendor's name, one request could be summed
on both ledgers and the total would be quietly, confidently wrong. So every event from the
gateway declares provider: "ai-gateway", and the vendor behind it rides as
upstream_provider. Two ledgers, always separable; a dashboard sums one, the
other, or both — deliberately.
What happens when a promotional rate ends?
Your bill steps up while traffic stays flat, and the widget that shows it is cost per 1M tokens — the rate isolated from volume. Total spend cannot separate a price change from a usage change; that formula can. The step appears the day after the promotion ends, in the same place the discount appeared the day it started.
Do you read the gateway request logs?
No, and that is a design decision worth stating. A gateway's request logs contain
prompts and completions. The connector reads the aggregate reporting endpoints and the
credits endpoint only, which is why these events carry
containsPii: false honestly.
What key does it need?
A read-only reporting key. Nothing here requires a key that can spend, and one should never be issued for it. The key is stored encrypted, used only by the sync worker, and returned by no API route.