Gateway API
Metering & cost
Every request is priced at the moment it is served and the arithmetic comes back on the response. Where the honest answer is null, the header is empty rather than zero.
Two things are written for every request that reaches the Gateway. A priced ledger row in token_usage, which carries the price that was applied at request time, and a trace row in gateway_requests, which carries the status code, the latency and the failure class. Both are best-effort by design: neither may fail a completion a provider has already charged for.
The response headers
Routing
| Header | Value | Notes |
|---|---|---|
| x-lobstack-request-id | UUID | The trace row id. Allocated before anything can fail, so a 401 carries one too. |
| x-lobstack-model | model key | The exact named model, or the model selected by auto. |
| x-lobstack-tier | nano … flagship | The exact model's tier, or the tier selected by auto. |
| x-lobstack-complexity | 0–100 | The heuristic score for the last user message. |
| x-lobstack-routed | true | false | True for a request routed by "auto". Named model requests are false because they are exact. |
| x-lobstack-mode | managed | byok | Whose provider key paid for the tokens. |
| x-lobstack-principal | api_key | gateway_token | agent_secret | Which credential authenticated the call. |
| x-lobstack-dropped-params | temperature | Present only when a sampling parameter you sent was not forwarded. |
There is no x-lobstack-requested-model. Nothing on a buffered response echoes the model key you sent, and that is a stated absence rather than a gap in this table — you already hold what you sent, so what is worth a header is what came back instead. Compare it against x-lobstack-model, and read x-lobstack-routed for the comparison already made: it is true when auto selected a model. A named model is exact, so it is always false on a successful exact call.
When you need it back from us rather than from your own call site, it is recorded rather than headered: requested_model on the streamed response's x_lobstack frame, on the token_usage ledger row, and on the trace row that GET /api/v1/requests, the usage API and Console → Logs all read. On an auto request every one of those says auto, which is the honest answer and not a missing value.
Cost
| Header | Value | Notes |
|---|---|---|
| x-lobstack-cost-usd | USD, 6 decimals | What you owe for these tokens. Empty string when the model is unpriced. |
| x-lobstack-provider-cost-usd | USD, 6 decimals | The provider's list price for the same tokens. On managed traffic the cost above equals it inside the plan's included usage and on Free, and is provider price + 50% past it. |
| x-lobstack-savings-usd | USD, 6 decimals | The baseline cost minus the cost above. Empty string when there is no baseline. |
| x-lobstack-baseline-model | model key | The model the saving was measured against. Sent only when a baseline exists. |
| x-lobstack-baseline-reason | named | plan_ceiling | Why that model is the comparison. Read it before rendering the saving. |
| x-lobstack-baseline-usd | USD, 6 decimals | What the baseline model would have charged for these exact token counts. |
| x-lobstack-savings-pct | 0–100 | Tier-multiplier estimate, not dollars. See Nex. |
| x-lobstack-priced | true | false | False when the served model is not in the registry, so cost is null rather than zero. |
| x-lobstack-metered | true | false | False when the ledger write failed. You still got your answer; the row is missing. |
| x-lobstack-routing-share-usd | USD, 6 decimals | Lobstack's share of a routing saving on a model you named, already inside cost-usd. Only sent on calls where one was billed. |
Quota
Three meters exist, because three different things have been sold, and the counters mean different things on each. x-lobstack-quota-meter names the one that applied, and it is the header to branch on before reading any of the others.
| Meter | Who is on it | What the wall is |
|---|---|---|
| spend | A plan that includes dollars of model spend | Included allowance, plus purchased top-ups, plus credited savings, plus any trial credit, minus this period's spend |
| requests | The BYOK plan, no longer sold | Included requests for the calendar month |
| legacy | A subscription bought on the older messages-per-month tiers | That subscription's own message allowance, counted exactly as it always was |
| Header | Value | Sent on |
|---|---|---|
| x-lobstack-quota-meter | spend | requests | legacy | Every response with anything to measure against. |
| x-lobstack-quota-allowance-usd | USD, 6 decimals | spend. Included, plus top-ups, plus credited savings, plus any trial credit. |
| x-lobstack-quota-spent-usd | USD, 6 decimals | spend. Consumed so far this period. |
| x-lobstack-quota-remaining-usd | USD, 6 decimals | spend. Never negative. |
| x-lobstack-quota-credit-usd | USD, 6 decimals | spend, and only when greater than zero. Free trial credit still unused, already inside allowance-usd. |
| x-lobstack-quota-weekly-credit-usd | USD, 6 decimals | spend, on Free, only while a weekly grant issued before 13 October 2026 is unexpired. What is left of it, already inside allowance-usd. No new weekly grants are made. |
| x-lobstack-quota-limit | integer | requests and legacy. Included requests or messages. |
| x-lobstack-quota-used | integer | requests and legacy. Consumed so far. |
| x-lobstack-quota-remaining | integer | requests and legacy. Limit minus used, plus credits. Never negative. |
| x-lobstack-quota-credits | integer | requests and legacy, and only when greater than zero. |
| x-lobstack-quota-resets | ISO 8601 | Any meter, when the period end is known. Not sent on Free: its credit does not reset. |
A spend meter sends dollars and no request counters. A zero in x-lobstack-quota-limit reads as "no allowance" to a client that cannot tell which meter it is on, so the counters are absent rather than zero. The rule runs the other way too: a legacy subscriber who bought messages is never sent a dollar figure they did not agree to be measured in.
They are enforced for every credential: at the limit a call gets a 402. Errors & retries covers the 402 and the retry-after that comes with it. All of them are omitted when there is nothing to measure against — no agent row and no org.
Only requests that returned below 400 count against a request or legacy org allowance, so a provider outage on our side does not consume your quota.
Cost on a streamed response
The last chunk before [DONE] carries token counts in the standard usage object and the money beside it under x_lobstack. An SDK that does not know us parses the usage object unchanged and ignores the rest.
Read the price from here rather than pricing the token counts against your own copy of our rate card. A local copy drifts: we serve 18 models today and retire or reprice some of them every few weeks, and a client that cannot price the model our router chose will show $0.00 for a request that is on your bill.
{
"id": "chatcmpl-2f1c…",
"object": "chat.completion.chunk",
"model": "gemini-3.8-flash",
"choices": [],
"usage": { "prompt_tokens": 1000, "completion_tokens": 500, "total_tokens": 1500 },
"x_lobstack": {
"request_id": "2f1c…",
"served_model": "gemini-3.8-flash",
"requested_model": "claude-opus-5",
"routed": true,
"cost_usd": 0.0039375,
"provider_cost_usd": 0.002625,
"rate": "provider price + 50%",
"savings_usd": 0.0144375,
"priced": true,
"baseline_model": "claude-opus-5",
"baseline_reason": "named",
"baseline_cost_usd": 0.018375
}
}cost_usd is null, never 0, when the served model is not in the registry or the stream ended before it could be metered — priced tells you which case you are in. Render a null as unknown; rendering it as free writes off a real charge.
rate says how cost_usd was produced from provider_cost_usd: provider price inside the plan's included usage, provider price + 50% past it, and provider price, then provider price + 50% on a call that crossed the line. The sample above is a call past it.
The three baseline fields are sent together or not at all, and savings_usd never travels without baseline_reason. What the two reasons mean, and why they are not interchangeable, is the next section.
The baseline rule
A saving is a subtraction, and a subtraction is honest only when the reader can see both operands and where the first one came from. So a baseline never travels without the reason it exists. The saving is always what these exact token counts would have cost on the baseline model, minus what they actually cost, priced on the same basis so the subtraction is between two comparable numbers.
| baseline_reason | You sent | The baseline is | What the saving means |
|---|---|---|---|
| named | An explicit model key | The model you named (legacy routed rows) | Historical savings against a named request; new exact calls have no named baseline. |
| plan_ceiling | auto | The most expensive managed model your plan may reach | What you would have paid had you asked for the best one. Nobody asked for it. |
| null | Anything with no honest comparison | None | No saving is reported and savings-usd is empty. |
| You sent | Served | baseline_reason | savings-usd |
|---|---|---|---|
| auto | a cheaper model, on a priced plan | plan_ceiling | the difference against the plan's ceiling model |
| auto | the plan's ceiling model itself | plan_ceiling | 0.000000 — a real zero, not a null |
| auto | anything, on a legacy messages tier | null | empty — that contract has no ceiling to compare against |
| claude-opus-5 | claude-opus-5 | null | empty — exact selection has no routing saving |
| a key not in the registry | request refused | null | no inference or saving is produced |
x-lobstack-savings-pct follows a different rule and is present on every response. It is one minus the selected tier's cost multiplier over the ceiling tier's: an estimate about tiers, not a measurement about dollars. It is not a rounded version of the dollar figure and the two should never be added together.
Savings credit
The credit share is currently 0%: a verified saving shows on the receipt and the ledger and extends no allowance. The rule below is what verified means, and it still decides what the receipt reports. Verified is the whole of the rule, and every one of these must hold or the saving is zero.
| Condition | Why |
|---|---|
| The plan credits savings, and meters spend | There is no dollar allowance to extend otherwise. |
| The request ran on our provider key | On your own provider key you paid your provider; there is nothing of ours to give back. |
| The row is not an error row | A failed round saved nobody anything. |
| Both models priced | A credit computed from a null is an allowance handed out by a gap in the catalogue. |
| The baseline is strictly above the actual cost | Equal or below is not a saving. |
Routing does not always find something cheaper, and on the calls where it does not there is no saving and no credit. Nothing here promises a saving of any size; the credit is a share of whatever is actually measured, which is sometimes nothing.
The credit is computed on every qualifying request, reported in the meter result, and accrued into spend_allowances — one row per payer per billing period — through an atomic SQL function, so concurrent requests cannot lose a credit to a read-modify-write. The row survives plan changes and subscription churn, because a balance the customer earned should not be stranded on a cancelled subscription mid-period. What each plan credits, and which plans meter spend at all, is on Pricing & plans.
What a ledger row records
One row per inference round in token_usage, including rounds that failed with zero tokens, so an error rate stays computable from the ledger alone.
| Column | What it holds |
|---|---|
| prompt_tokens, completion_tokens, total_tokens | The provider's own counts. |
| model | The Lobstack key that served the request. |
| provider | Resolved from the registry. Null when the model was unpriced. |
| cost_usd | What the customer owes, frozen at write time. Null when unpriced. |
| provider_cost_usd | The provider's list price for the same tokens. Also on the receipt since 2026-10-08. |
| rate_multiplier | The spread applied to reach cost_usd, so the row reproduces its own arithmetic. |
| price_in_per_m, price_out_per_m | The provider's per-million list rates that were in force. |
| requested_model | What you asked for — "auto", or a key. |
| routed | True when "auto" selected a model through Nex. Exact named requests are false. |
| baseline_cost_usd | What the baseline model would have charged. Null when there is no baseline. |
| baseline_model, baseline_reason | Which model, and why it was the comparison. |
| savings_credit_usd | The share of a verified saving credited back to the allowance. Zero when not credited. |
| plan_id | The priced plan in force. Null on a legacy tier. |
| mode | managed or byok — who paid the provider. Only managed is billable to us. |
| source | Which surface produced it: gateway, chat, messages or bridge. |
| request_id, session_id | Links to the trace row, and your own grouping key. |
| agent_id, org_id, api_key_id | Whose spend this was. agent_id is null for an org-level key. |
| latency_ms, error | Round-trip time and whether the round failed. |
The row carrying its own price is the point. Editing the model registry later cannot retroactively move a historical invoice, because nothing recomputes an old row from the current catalog.
Unpriced requests
When the served model key is not in the registry, the price is recorded as null, not zero. A missing price is a gap in the catalog; recording it as free would silently write off real spend, which is the most expensive way to be wrong here. The response reports x-lobstack-priced: false and the header value for cost is empty.
Every one of these is logged loudly on the server and counted in the usage rollup as an unpriced request. If you see one, the fix is a registry entry or an alias, not a default. This is also why aliases resolve for pricing and not only for routing: a live agent once ran on a spelling the resolver did not know, and every request it made metered free.
The usage API
Org-scoped usage, traces and latency. Two credentials work: a browser session, which is what the Console reads, and an API key carrying the usage:read scope. Both get identical numbers from identical arithmetic rather than each computing their own.
Query parameters: range (7d, 14d, 30d, 90d; default 7d), group_by (day, model, key, agent; default day), agent_id and key_id.
curl "https://www.lobstack.ai/api/v1/usage?range=30d&group_by=model" \
-H "Authorization: Bearer $LOBSTACK_API_KEY"{
"enabled": true,
"org_id": "…",
"authenticated_via": "api_key",
"range": "30d",
"group_by": "model",
"summary": {
"requests": 18412,
"errors": 96,
"error_rate": 0.0052,
"errors_by_class": { "provider": 71, "validation": 19, "timeout": 6 },
"prompt_tokens": 9140233,
"completion_tokens": 1882901,
"total_tokens": 11023134,
"cost_usd": 41.283914,
"p50_latency_ms": 812,
"p95_latency_ms": 3140,
"p99_latency_ms": 7602,
"streamed": 11208
},
"groups": [
{
"key": "claude-sonnet-5",
"requests": 6120,
"errors": 21,
"total_tokens": 5011882,
"cost_usd": 24.117,
"p50_latency_ms": 940,
"p95_latency_ms": 3402
}
],
"truncated": false
}Percentiles are true percentiles over the raw latency values, not an average of per-bucket averages — which is the usual way a dashboard ends up reporting a p95 nobody experienced.
truncated is true when the row cap bound, at which point the sums are a floor rather than a total and the response says so instead of handing you a confident wrong number. Narrow the range or filter by key.