# Fleet billing metrics (robotics and games)

> How the robotics and games rate plans are metered, what each unit label counts, and which metrics can be split across tags for chargeback.

The fleet billing screen is one component rendered twice. `/admin/robotics/billing`
and `/admin/games/billing` share the same shell and differ only by a `vertical`
discriminator (`robotics` | `games`). Everything on the page — the KPI strip, the
metered rows, the cost-by-tag split, the period history — is driven by that one
value.

That matters for reading the page: **the rate plan is per vertical.** The metric
list on the robotics screen and the metric list on the games screen come from two
separate server-side rate plans, and neither is a subset of the other. A vertical
whose rate plan is empty renders as *"No billable metrics yet — rate plan is empty
for &lt;vertical&gt;"* rather than as a zero-dollar bill.

## How the rate plan is served per vertical

Three procedures back the screen.

| Procedure                     | Input                          | Used for                                    |
| ----------------------------- | ------------------------------ | ------------------------------------------- |
| `fleetBilling.getUsage`       | `{ orgId, vertical }`          | Current period: rows, totals, projection.   |
| `fleetBilling.costByTag`      | `{ orgId, vertical, key }`     | This period's spend split by one tag key.   |
| `fleetBilling.history`        | `{ orgId, vertical, months }`  | Prior periods from `fleet_usage_period`.    |

`getUsage` returns the rate plan and the metered usage together. There is no
separate "list the rate plan" call — the plan *is* the row set, with `units` and
`cost_usd` filled in for the current period. A metric that exists in the plan but
has not been used this period still comes back as a row, with zero units. The
**Metrics billed** KPI is exactly this distinction: it counts rows where
`units > 0` against the total number of rows in the plan.

The current-period query refetches on a 120-second interval, and the header shows
the last successful aggregation time. Aggregation is computed inline on each query
rather than precomputed, so the **Refresh** button re-runs it. History is cached
with a 5-minute stale time and is requested for 6 months.

## Reading a metered usage row

Each row in **This period · metered usage** is one metric from the vertical's rate
plan. The fields are:

| Field                   | Rendered as                                  | Meaning                                                             |
| ----------------------- | -------------------------------------------- | ------------------------------------------------------------------- |
| `metric`                | Row key; listed in the history table          | The stable machine identifier. This is what you match on in exports. |
| `rate_plan_name`        | Row title                                     | Human label for the line item.                                      |
| `rate_plan_description` | Secondary line under the title                | What the metric counts, as defined by the plan.                     |
| `rate_per_unit`         | `$X/<unit_label>`                             | The unit price the plan charged for this period.                    |
| `unit_label`            | Suffix on both the rate and the quantity      | The unit the meter counts in. See below.                            |
| `units`                 | Right-hand quantity                           | Metered quantity for the period.                                    |
| `cost_usd`              | Right-hand accent figure                      | Server-computed line cost.                                          |

`cost_usd` is computed server-side and rendered as returned; the console does not
multiply rate by units itself. The quantity and the dollar figures are rounded for
display only — quantities below 1 are shown to four decimal places, quantities
between 1 and 100 to two, and larger quantities are progressively rounded. Do not
reconcile a spreadsheet against the rendered strings; reconcile against the
`units` and `cost_usd` values from `getUsage`.

The panel header shows the response's `currency` field, defaulting to `USD`. All
cost fields in the payload are `_usd`-suffixed.

## What each unit label counts and over what window

The `unit_label` is the meter's unit, and it is authoritative in two places at
once: it is the denominator of the price (`$0.02/unit_label`) and the unit of the
quantity (`1,204 unit_label`). Because both come from the same field, a row is
always internally consistent — you never have to guess whether a rate is per hour
while the quantity is in minutes.

What the unit counts for a given metric is stated in that metric's
`rate_plan_description`, which the screen renders verbatim under the row title.
TeleQuick treats that description as the definition of record for the meter;
this page deliberately does not restate per-metric semantics, because the plan is
served from the control plane and can add metrics without a docs change.

The **window** is the billing period, not a rolling window. `getUsage` returns
`periodStart` and `periodEnd`, which the screen prints in the page subtitle, and
every `units` value in the response is the total accumulated inside that period
for the queried `vertical`. The KPI strip additionally surfaces
`projected_end_of_period_usd` as **Projected end-of-period** — a server-side
projection for the same window, not a forecast beyond it.

Because the period is closed at `periodEnd`, the current-period row set is a
moving target and the history table is not. Once a period closes, its totals land
as a row in `fleet_usage_period` and are served by `fleetBilling.history`.

## Which metrics are resource-attributed and which are unattributable

A chargeback split only works for metrics that carry a per-resource dimension —
that is, metrics whose usage the meter can attribute to a specific robot, site,
server, or fleet. Metrics without that dimension cannot be divided by any tag, no
matter how thoroughly you tag.

`fleetBilling.costByTag` makes the division explicit and returns three buckets:

| Bucket                   | Shape                                              | What it is                                                                 |
| ------------------------ | -------------------------------------------------- | -------------------------------------------------------------------------- |
| `groups`                 | `{ value, resources, cost_usd, metrics[] }`         | Spend attributed to resources that carry the selected key, one entry per distinct value. Each entry lists its contributing `metric` and `units`. |
| `unallocated`            | `{ resources, cost_usd }`                           | Resources that ran and were metered, but carry no value for the selected key. Attributable spend with nobody's name on it. |
| `unattributable`         | `{ metric, cost_usd, reason }[]`                    | Metrics with no per-resource dimension. Each entry carries a `reason` explaining why no tag can split it. |

The console renders all three, and the distinction is the point. **Unallocated** is
a tagging gap you can close. **Unattributable** is a property of the metric — the
`reason` field tells you which, and the screen prints it next to the metric name
for any unattributable metric with non-zero cost.

Two further details in the group list:

- Groups arrive ordered by cost, largest first; the bars are scaled against the
  first group.
- A group whose `value` is the empty string is rendered as `(no value)`. This is
  **not** the same as unallocated: it means the tag key is present on those
  resources with an empty value, whereas unallocated means the key is absent
  entirely.

## Reconciling the split against the period total

The panel prints a reconciliation line rather than asking you to trust the bars:

```
allocated_cost_usd + unallocated.cost_usd  ==  total_cost_usd
```

When those agree to within a cent, the screen states that allocated plus
unallocated matches the period total. When they do not, it states that some usage
is not resource-attributed — which is the signal to read the `unattributable`
entries above the line, since that is where the difference lives.

Check this line before you send a chargeback. A split that does not reconcile is
not wrong, but it is incomplete: the residual is real spend that the selected tag
key structurally cannot carry.

## Tagging the objects that make a chargeback split work

The tag key selector is populated by `tags.keys({ orgId })`, which reads the
`resource_tag` table for the organization, counts occurrences per key, and returns
`{ key, count }` sorted by count descending with the key name as tiebreak. The key
list is built from up to 20,000 tag rows. The selector defaults to the first
entry, so **the most-used key in your organization is the default grouping** on
both billing screens.

If the organization has no tags at all, the panel does not render a split. Instead
it tells you what to tag, and the advice differs by vertical:

- **Robotics** — tag a site or a fleet.
- **Games** — tag a region or a server fleet.

In both cases the reason is inheritance: resources inherit the value from the
container they belong to. Tagging the container is what makes the split possible
without tagging every individual robot or every individual server. That is also
the fastest way to shrink **Unallocated**: an untagged container puts every
resource beneath it into the unallocated bucket at once.

Pick the key you intend to charge back on — cost centre, team, customer,
environment — and apply it at container level across the whole organization.
Partial coverage of a key produces a split that looks plausible and under-reports
every group in it.

## Reading the period history

The **Recent months** panel lists closed periods from `fleet_usage_period`, most
recent first, with three columns: the period (month and year), the comma-joined
list of `metric` identifiers billed in that period, and the period total.

The metric list per row is what makes this panel useful beyond the totals: if a
metric appears in one month and not the next, the rate plan for that vertical
changed, or nothing metered against it. A freshly onboarded organization sees *"No
history yet. This month's row lands once usage is aggregated."*

## Current limits of the rate plan

Three constraints are stated on the screen itself and are worth carrying into any
integration you build on these procedures:

- **Rates are platform defaults.** Per-tenant rate overrides are not implemented;
  every organization on a given vertical sees the same `rate_per_unit`.
- **Aggregation is inline.** Usage is computed on each query, not served from a
  precomputed rollup. Re-querying is how you refresh.
- **The period is fixed by the server.** The screen holds a period selector in
  state but does not currently pass it to `getUsage`, so the current-period view
  is always the server's active period.

## Related

- [Telemetry](/platform/telemetry) — the metric and CDR streams that feed usage
- [Authentication](/concepts/authentication) — org-scoped API keys for the control plane
