Skip to content

Gateway

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

HeaderValueNotes
x-lobstack-request-idUUIDThe trace row id. Allocated before anything can fail, so a 401 carries one too.
x-lobstack-modelmodel keyWhat actually served the request. Not necessarily what you sent.
x-lobstack-tiernano … flagshipThe tier the score landed in.
x-lobstack-complexity0–100The heuristic score for the last user message.
x-lobstack-routedtrue | falseTrue whenever you sent "auto", and whenever the served model differs from the one you named.
x-lobstack-modemanaged | byokWhose provider key paid for the tokens.
x-lobstack-principalapi_key | gateway_token | agent_secretWhich credential authenticated the call.
x-lobstack-dropped-paramstemperaturePresent only when a sampling parameter you sent was not forwarded.

Cost

HeaderValueNotes
x-lobstack-cost-usdUSD, 6 decimalsWhat you owe for these tokens. Empty string when the model is unpriced.
x-lobstack-savings-usdUSD, 6 decimalsThe baseline cost minus the cost above. Empty string when there is no baseline.
x-lobstack-baseline-modelmodel keyThe model the saving was measured against. Sent only when a baseline exists.
x-lobstack-baseline-reasonnamed | plan_ceilingWhy that model is the comparison. Read it before rendering the saving.
x-lobstack-baseline-usdUSD, 6 decimalsWhat the baseline model would have charged for these exact token counts.
x-lobstack-savings-pct0–100Tier-multiplier estimate, not dollars. See Token Intelligence.
x-lobstack-pricedtrue | falseFalse when the served model is not in the registry, so cost is null rather than zero.
x-lobstack-meteredtrue | falseFalse when the ledger write failed. You still got your answer; the row is missing.
cost_usd is your number, not oursA request has two costs. What the provider charged us for the tokens, and what you owe. On managed traffic the second is the first times 1.25, the published Lobstack rate, and it is the only one on the receipt. On BYOK and direct you paid your own provider, so nothing is marked up and the ledger row is not billable. The provider figure is written to the ledger as provider_cost_usd and stays there: it appears in no header, no stream frame and no API response.

Quota

Three meters exist, because three different things are 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.

MeterWho is on itWhat the wall is
spendA plan that includes dollars of model spendIncluded allowance, plus purchased top-ups, plus credited savings, minus this period's spend
requestsBYOK — you pay your own provider, so there is no spend of ours to capIncluded requests for the calendar month
legacyA subscription bought on the older messages-per-month tiersThat subscription's own message allowance, counted exactly as it always was
HeaderValueSent on
x-lobstack-quota-meterspend | requests | legacyEvery response with anything to measure against.
x-lobstack-quota-allowance-usdUSD, 6 decimalsspend. Included, plus top-ups, plus credited savings.
x-lobstack-quota-spent-usdUSD, 6 decimalsspend. Consumed so far this period.
x-lobstack-quota-remaining-usdUSD, 6 decimalsspend. Never negative.
x-lobstack-quota-limitintegerrequests and legacy. Included requests or messages.
x-lobstack-quota-usedintegerrequests and legacy. Consumed so far.
x-lobstack-quota-remainingintegerrequests and legacy. Limit minus used, plus credits. Never negative.
x-lobstack-quota-creditsintegerrequests and legacy, and only when greater than zero.
x-lobstack-quota-resetsISO 8601Any meter, when the period end is known.

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.

These are advisory for agent credentials and enforced for API keys. 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.

Streamed responses carry cost in the body, not the headersThe cost headers are absent when stream: true, because headers flush before the provider has reported a single token. The price arrives instead on the final SSE chunk, under x_lobstack, next to the usual usage object — see below. Routing and quota headers are present on both paths.

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 26 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.

json
{
  "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.003281,
    "savings_usd": 0.018594,
    "priced": true,
    "baseline_model": "claude-opus-5",
    "baseline_reason": "named",
    "baseline_cost_usd": 0.021875
  }
}

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.

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_reasonYou sentThe baseline isWhat the saving means
namedAn explicit model keyThe model you namedA measurement against your own request.
plan_ceilingautoThe most expensive managed model your plan may reachWhat you would have paid had you asked for the best one. Nobody asked for it.
nullAnything with no honest comparisonNoneNo saving is reported and savings-usd is empty.
A plan-ceiling saving is not a named savingBoth are real subtractions between two real prices, and they are not the same claim. named compares against a model the caller chose. plan_ceiling compares against one they never mentioned, chosen because it is the most expensive their plan can reach — which is also the most flattering number available to us. Render baseline_reason next to the figure, or do not render the figure. A client that shows a plan-ceiling saving as though the customer had asked for that model is publishing a number they never chose to be measured against.
You sentServedbaseline_reasonsavings-usd
autoa cheaper model, on a priced planplan_ceilingthe difference against the plan's ceiling model
autothe plan's ceiling model itselfplan_ceiling0.000000 — a real zero, not a null
autoanything, on a legacy messages tiernullempty — that contract has no ceiling to compare against
claude-opus-5gemini-3.1-flash-litenamedthe measured difference
claude-opus-5claude-opus-5nullempty — nothing was routed away from
a key not in the registrythe fallback modelnullempty — no price for the baseline

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

On a plan that carries it, 50% of a verified saving is added back to that period's allowance. Verified is the whole of the rule, and every one of these must hold or the credit is zero.

ConditionWhy
The plan credits savings, and meters spendThere is no dollar allowance to extend otherwise.
The request ran on our provider keyOn BYOK you paid your own provider; there is nothing of ours to give back.
The row is not an error rowA failed round saved nobody anything.
Both models pricedA credit computed from a null is an allowance handed out by a gap in the catalogue.
The baseline is strictly above the actual costEqual 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.

ColumnWhat it holds
prompt_tokens, completion_tokens, total_tokensThe provider's own counts.
modelThe Lobstack key that served the request.
providerResolved from the registry. Null when the model was unpriced.
cost_usdWhat the customer owes, frozen at write time. Null when unpriced.
provider_cost_usdWhat we paid the provider for the same tokens. Server-side only; never returned.
rate_multiplierThe spread applied to reach cost_usd, so the row reproduces its own arithmetic.
price_in_per_m, price_out_per_mThe provider's per-million list rates that were in force.
requested_modelWhat you asked for — "auto", or a key.
routedTrue when the served model differs from the requested one.
baseline_cost_usdWhat the baseline model would have charged. Null when there is no baseline.
baseline_model, baseline_reasonWhich model, and why it was the comparison.
savings_credit_usdThe share of a verified saving credited back to the allowance. Zero when not credited.
plan_idThe priced plan in force. Null on a legacy tier.
modemanaged or byok — who paid the provider. Only managed is billable to us.
sourceWhich surface produced it: gateway, chat, messages or bridge.
request_id, session_idLinks to the trace row, and your own grouping key.
agent_id, org_id, api_key_idWhose spend this was. agent_id is null for an org-level key.
latency_ms, errorRound-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.

Rows written before 10 September 2026 are shorter than thisprovider_cost_usd through plan_id arrived with 20260910_spend_metering.sql, which is applied: every column above exists and is written today. Rows written before it carry a customer cost and no margin columns, and rows older than the priced ledger carry no cost at all. Nothing backfills them, because the prices and the plan in force at the time are not recoverable — the gaps stay visible as gaps rather than being filled in with a number nobody can reconstruct. The Gateway still detects a missing column and writes the row without it rather than dropping the row, so a deployment on an older schema loses the margin columns and not the ledger.

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

GEThttps://www.lobstack.ai/api/v1/usage

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.

Requestbash
curl "https://www.lobstack.ai/api/v1/usage?range=30d&group_by=model" \
  -H "Authorization: Bearer $LOBSTACK_API_KEY"
Responsejson
{
  "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.

Two answers mean two different thingsThe usage API reads the trace table, so a deployment that has not applied the request-tracing migration gets enabled: false with reason: "request_tracing_not_migrated" rather than a 500 that reads like an outage. An API key without the usage:read scope gets a 403 naming the missing scope.
Lobstack

One key over every frontier model, a receipt on every call, and an agent that waits before it changes anything.

© 2026 LobstackAll rights reserved  Status