Analytics-Only Flag
Use control.analytics_only when a charge item describes a fact that should be queryable in analytics and audit logs but should never charge the customer’s wallet. Examples include supplier cost, internal amortization, audit rollups, and virtual revenue aggregators that would otherwise double-count the underlying token revenue.
The value must be the string "true". A real boolean is rejected by the rule set validator. Anything other than the string "true" falls back to the default and the charge item is debited normally.
{ "name": "total-tokens-rollup", "metric": "total_tokens", "type": "number", "unit": "token", "quantity_expr": "dec_add(dec_add(dec(items['input_tokens']['quantity']), dec(items['output_tokens']['quantity'])), dec(items['cached_tokens']['quantity']))", "amount_expr": "dec_add(dec_add(dec(items['input_tokens']['amount']), dec(items['output_tokens']['amount'])), dec(items['cached_tokens']['amount']))", "currency": "USD", "control": { "analytics_only": "true" }}What analytics-only items still produce
Section titled “What analytics-only items still produce”An analytics_only=true item is still a complete usage and rating fact. It is written to:
UsageEvent.ChargeItems, so the normalized event carries it forward;- the usage event log and the analytics projection, so the item is queryable in historical analytics;
- the
metrics,group_by, andfiltersresults ofPOST /v1/projects/:project_id/usage/query, so dashboards and reports can summarize it.
If your virtual metric depends on the amount or unit_price of a same-event item, put the dependency first in charge_item_extractors. The items[metric] snapshot is updated in configuration order, so a later item only sees the rated values of earlier items.
What analytics-only items skip
Section titled “What analytics-only items skip”An analytics_only=true item is excluded from the customer money path:
- it does not resolve a currency-bearing realtime debit route;
- it does not produce a
UsageChargeDetailrow with a money amount; - it does not produce a
UsageFundDebitDetailrow; - it does not participate in realtime wallet debit;
- it does not participate in the 5-minute bill reconciliation sweep.
That means the item is still queryable, but it cannot trigger refunds, wallet locks, or overage charges. The remaining currency-bearing items in the same event continue to be rated and debited normally.
How analytics-only differs from allow_voucher
Section titled “How analytics-only differs from allow_voucher”control.analytics_only and control.allow_voucher are independent flags. They answer different questions:
control.analytics_onlydecides whether the item is part of the customer money path at all. When it is"true", no fund debit and no reconciliation ever touch the item.control.allow_voucherdecides the funding source for a still-debited item. It only matters when the item does enter the money path, and it means the debit must be paid with cash rather than a voucher grant.
The two flags do not replace each other. A normal billable item can have allow_voucher="false" to force cash. A pure analytics item uses analytics_only="true" instead, and allow_voucher is ignored because no debit is produced. See Allow Voucher for the funding source side.
How analytics-only interacts with usage limits
Section titled “How analytics-only interacts with usage limits”analytics_only does not configure usage limits. Usage limits are still driven by subject routes and usage-control rules through their measure, selector_type, selector_value, and currency. If you do not want a particular analytics-only metric to count toward a limit, simply do not select that metric in the corresponding usage-control rule.
Do not expect analytics_only to act as a per-metric usage-limit escape. The flag is about whether the item is debited, not about whether the limit engine observes it.
LLM Gateway use case
Section titled “LLM Gateway use case”The same event already carries input_tokens, output_tokens, and cached_tokens from the provider response. Pairing them with cost prices reported through x-billing.pricing_hints lets you compute the supplier cost of each token type and a rolled-up total cost, without changing the revenue charge items that already debit the customer.
This is the finance side of the same event. The revenue items stay rated and debited as before; the cost items are marked analytics_only so they keep full usage and rating facts for analytics but skip the customer money path.
Step 1: report cost prices via x-billing.pricing_hints
Section titled “Step 1: report cost prices via x-billing.pricing_hints”Use a cost.<metric> key in pricing_hints for each cost price. The key shape keeps cost prices separate from the existing revenue prices already keyed by the metric name, so both can coexist in the same event.
{ "x-billing": { "pricing_hints": { "input_tokens": { "unit_price": 0.75, "pricing_unit": 1000000, "currency": "CREDIT" }, "output_tokens": { "unit_price": 3, "pricing_unit": 1000000, "currency": "CREDIT" }, "cached_tokens": { "unit_price": 0.08, "pricing_unit": 1000000, "currency": "CREDIT" }, "cost.input_tokens": { "unit_price": 0.30, "pricing_unit": 1000000, "currency": "CREDIT" }, "cost.output_tokens": { "unit_price": 1.20, "pricing_unit": 1000000, "currency": "CREDIT" }, "cost.cached_tokens": { "unit_price": 0.05, "pricing_unit": 1000000, "currency": "CREDIT" } } }}The cost currency does not have to match the revenue currency. A provider cost in USD can sit alongside a customer charge in CREDIT; the cost items remain analytics-only and never enter the wallet.
Step 2: define per-metric cost items in the extractor
Section titled “Step 2: define per-metric cost items in the extractor”Append three virtual cost items after the revenue items in charge_item_extractors. Each reads the source quantity from the same-event item snapshot and looks up the cost price from pricing_hints.
{ "charge_item_extractors": [ {"name": "input-tokens", "metric": "input_tokens", "path": "$.usage.input_tokens", "type": "number", "unit": "token", "skip_if_missing": true, "currency": "CREDIT", "unit_price_expr": "dec('0')", "amount_expr": "dec_mul(dec_div(dec(item['quantity']), dec(item['pricing_unit'])), dec(item['unit_price']))"}, {"name": "output-tokens", "metric": "output_tokens", "path": "$.usage.output_tokens", "type": "number", "unit": "token", "skip_if_missing": true, "currency": "CREDIT", "unit_price_expr": "dec('0')", "amount_expr": "dec_mul(dec_div(dec(item['quantity']), dec(item['pricing_unit'])), dec(item['unit_price']))"}, {"name": "cached-tokens", "metric": "cached_tokens", "path": "$.usage.input_tokens_details.cached_tokens","type": "number", "unit": "token", "skip_if_missing": true, "currency": "CREDIT", "unit_price_expr": "dec('0')", "amount_expr": "dec_mul(dec_div(dec(item['quantity']), dec(item['pricing_unit'])), dec(item['unit_price']))"}, { "name": "cost-input-tokens", "metric": "cost.input_tokens", "type": "number", "unit": "token", "quantity_expr": "dec(items['input_tokens']['quantity'])", "currency": "CREDIT", "amount_expr": "dec_mul(dec_div(dec(items['input_tokens']['quantity']), dec(pricing_hints['cost.input_tokens']['pricing_unit'])), dec(pricing_hints['cost.input_tokens']['unit_price']))", "control": { "analytics_only": "true" } }, { "name": "cost-output-tokens", "metric": "cost.output_tokens", "type": "number", "unit": "token", "quantity_expr": "dec(items['output_tokens']['quantity'])", "currency": "CREDIT", "amount_expr": "dec_mul(dec_div(dec(items['output_tokens']['quantity']), dec(pricing_hints['cost.output_tokens']['pricing_unit'])), dec(pricing_hints['cost.output_tokens']['unit_price']))", "control": { "analytics_only": "true" } }, { "name": "cost-cached-tokens", "metric": "cost.cached_tokens", "type": "number", "unit": "token", "quantity_expr": "dec(items['cached_tokens']['quantity'])", "currency": "CREDIT", "amount_expr": "dec_mul(dec_div(dec(items['cached_tokens']['quantity']), dec(pricing_hints['cost.cached_tokens']['pricing_unit'])), dec(pricing_hints['cost.cached_tokens']['unit_price']))", "control": { "analytics_only": "true" } } ]}For each cost.<metric>, quantity_expr reads the source metric’s quantity through items['<metric>']['quantity'] so the quantity tracks the original token count one-to-one, and amount_expr multiplies that quantity by the supplier price in pricing_hints['cost.<metric>']. Both reads are filled from the same-event item snapshot written by the extractor that ran earlier in the list. The dependency ordering matters here; see How Extraction Runs for the details. pricing_hints is exposed to every expression as a read-only price book, not only to the metric that originally owns the hint, which is what lets the cost items read a key that was not registered against them.
Step 3: roll the per-metric costs into a total
Section titled “Step 3: roll the per-metric costs into a total”Add a cost.total aggregator that depends on the three per-metric costs and comes after them in the list:
{ "name": "cost-total", "metric": "cost.total", "type": "number", "unit": "event", "quantity_expr": "dec('0')", "currency": "CREDIT", "amount_expr": "dec_add(dec_add(dec(items['cost.input_tokens']['amount']), dec(items['cost.output_tokens']['amount'])), dec(items['cost.cached_tokens']['amount']))", "control": { "analytics_only": "true" }}cost.total aggregates amounts only, so its quantity_expr is dec('0'). All four cost items use analytics_only so they remain queryable for finance dashboards but never reach the customer’s wallet.
Step 4: submit the event
Section titled “Step 4: submit the event”After the cost prices, the per-metric cost items, and the total cost aggregator are in place, the gateway sends one complete event to the ingest endpoint. The body is the full envelope: an outer source, identity, subject, and timestamp, the original provider response under raw_json, and the dynamic billing extension under x-billing carrying both revenue prices and cost prices in the same pricing_hints map.
{ "source": "openai-gateway", "external_event_id": "resp_01JZ8E7K9S3K4N2V9A6Y0B1C2D", "idempotency_key": "openai-gateway:resp_01JZ8E7K9S3K4N2V9A6Y0B1C2D", "subject_type": "organization", "subject_id": "org_001", "occurred_at": 1783008000, "raw_json": { "usage": { "input_tokens": 2400, "output_tokens": 520, "total_tokens": 2920, "input_tokens_details": { "cached_tokens": 800 } } }, "metadata": { "provider": "openai", "model": "gpt-4.1-mini", "type": "response.completed", "membership_tier": "pro" }, "x-billing": { "pricing_hints": { "input_tokens": { "unit_price": 0.75, "pricing_unit": 1000000, "currency": "CREDIT" }, "output_tokens": { "unit_price": 3, "pricing_unit": 1000000, "currency": "CREDIT" }, "cached_tokens": { "unit_price": 0.08, "pricing_unit": 1000000, "currency": "CREDIT" }, "cost.input_tokens": { "unit_price": 0.30, "pricing_unit": 1000000, "currency": "CREDIT" }, "cost.output_tokens": { "unit_price": 1.20, "pricing_unit": 1000000, "currency": "CREDIT" }, "cost.cached_tokens": { "unit_price": 0.05, "pricing_unit": 1000000, "currency": "CREDIT" } } }}POST it to POST /v1/projects/:project_id/extractor-rule-sets/:id/events/ingest. raw_json carries only usage, because that is all the extractor in step 2 reads. The OpenAI response id and the meta.subject_* / meta.project_id / meta.api_key_id fields are dropped: the first duplicates the top-level external_event_id, the others are already pinned by subject_type / subject_id and the request URL. Categorical metadata (provider, model, type, membership_tier) moves to top-level metadata and automatically becomes a dimension on the rated usage event as long as the value is not pure numeric. The numbers in raw_json.usage are the same ones the worked example below uses, so the rated result matches step by step. The full envelope field reference, including how idempotency_key deduplicates retries and how subject_type / subject_id override the extractor’s subject paths, lives on the Usage Events page; the request contract for the endpoint is on Ingest Usage Event.
Worked example
Section titled “Worked example”Suppose the same event has input_tokens: 2400, output_tokens: 520, cached_tokens: 800. With the cost prices in step 1, the runtime produces:
| Metric | Quantity | Amount (CREDIT) |
|---|---|---|
cost.input_tokens |
2400 | 2400 / 1,000,000 × 0.30 = 0.00072 |
cost.output_tokens |
520 | 520 / 1,000,000 × 1.20 = 0.000624 |
cost.cached_tokens |
800 | 800 / 1,000,000 × 0.05 = 0.00004 |
cost.total |
0 | 0.00072 + 0.000624 + 0.00004 = 0.001384 |
The revenue items for the same event still debit the customer as before. The cost items enter UsageEvent.ChargeItems, the usage event log, and the analytics projection, so POST /v1/projects/:project_id/usage/query with metrics=cost.input_tokens,cost.output_tokens,cost.cached_tokens,cost.total returns them grouped by provider, model, or any other dimension.
If you also want a per-event revenue total alongside the cost total, add a total_tokens.amount aggregator the same way, with the same analytics_only flag. Keep margin as a query-time aggregation rather than another charge item so the audit trail stays inside the four rated items:
gross_margin = (sum(total_tokens.amount) - sum(cost.total.amount)) / sum(total_tokens.amount)Once the cost items are in place, the same event shows customer revenue, supplier cost, and gross margin side by side in the Financial Analysis page of the Portal. That page is the live example of this pattern.
Related pages
Section titled “Related pages”- Charge-Item Control — overview of the
controlblock and how keys interact. - Allow Voucher — the funding source side of the same block.
- Create Rules — how to define
charge_item_extractorsand attach pricing. - How Extraction Runs — runtime order and the
items[metric]context used by virtual metrics. - LLM Gateway — provider normalization,
x-billing.pricing_hints, andx-billing.items.