Skip to main content

Rolter attributes spend along two axes that sit beside the org/team/project hierarchy:

  • Business units collect spend from the virtual keys assigned to them, and from customers that roll up into them, so cost lands on the part of the organisation that incurred it. Teams carry no business unit: until a key is assigned, a unit shows no spend.
  • Customers attribute usage to the people you serve, so it can be billed on or reported per account. A customer can optionally roll up into a business unit.

Both live under Governance in the dashboard.


Slugs are the identity#

Every unit and customer has a name (for humans) and a slug (for everything else). The slug is what spend is recorded against, and it is derived from the name when you leave it blank.

It must match ^[a-z0-9][a-z0-9-]{0,62}$ — lowercase alphanumerics and dashes, starting with a letter or digit.

A slug is immutable by default. Renaming one does not rewrite the attribution already recorded against the old value, so the API refuses a rename unless you pass allow_slug_change: true, and the dashboard makes you tick a box that names the old slug explicitly. This is deliberate friction: a silent rename produces a report that is quietly wrong rather than loudly broken.


Retire, don’t delete#

A unit or customer with history should be retired, not deleted. Retiring sets retired_at, keeps every record that points at it, and stops it being offered for new attribution — the dashboard keeps it visible, dimmed and badged, and drops it from the assignment dropdown unless it is the one already selected.

Delete removes the row outright. Use it for something created by mistake, not for a customer who churned.


Assignment#

A customer’s business_unit_id follows a three-state contract on update:

ValueMeaning
omittedleave unchanged
nullunassign
a UUIDassign to that unit

The unit must belong to the same org as the customer; a cross-org assignment is rejected.


Pointing a key at a unit or a customer#

Spend is attributed by the virtual key that served the request. A key carries an optional business_unit_id and an optional customer_id, and every request it serves is recorded against both.

In the dashboard, open Virtual Keys, press the pencil on a row, and pick a business unit and a customer. The same two controls sit in the Create virtual key sheet, so a key can be attributed from the moment it exists. Each row shows where its spend lands; a key with no attribution shows a dash rather than a badge, because leaving a key unattributed is a legitimate choice, not a warning.

Two rules the editor enforces before you save, because the server rejects them afterwards:

  • The unit and the customer must belong to the key’s own org.
  • A customer already owned by a business unit cannot be paired with a different one. Only customers that fit the unit you picked are offered.

Both fields follow the same three-state contract customers use for business_unit_id:

ValueMeaning
omittedleave unchanged
nullclear the attribution
a UUIDattribute to that unit or customer

The dashboard always sends both fields, so clearing one is something you can actually express.

Note

Creating a key and attributing it are two calls. POST /virtual-keys takes the provider allow-list but not the attribution, so the dashboard follows a create that chose one with the attribution PUT.


Per-key provider allow-list#

Beside the attribution, a key carries a list of upstream provider slugs it may reach. An empty list is the permissive default — the key may reach every provider on a route it is already allowed to use — so ticking nothing does not lock the key out of everything.

The dashboard offers the org’s providers as checkboxes in both the create sheet and the per-row editor, and the self-service Account panel offers the same list plus the response-cache override when you mint your own key.


Reading the spend#

GET /api/v1/analytics/by-attribution rolls request_logs up by one governance dimension over a time window:

ParameterMeaning
since / untilwindow bounds in one of the time window forms; both default to the last 7 days, and a malformed one is a 400
dimensionbusiness_unit or customer (defaults to business_unit)
include_unattributedinclude the bucket for keys that named neither (defaults to false)

Each row carries id, requests, tokens, prompt_tokens, completion_tokens, cost_usd and errors. The unattributed bucket comes back with an empty id.

The Business Units and Customers screens read this over a window you pick beside the New button, and show three figures above the roster: what the window cost in total, how much of it was attributed, and how much was not, with the unattributed share as a percentage. Each card then carries its own spend and request count; a unit with no traffic says so rather than printing a zero, because a zero claims a fact the window does not contain.

WindowWhat it covers
Last 24 hoursthe 24 hours before the figures were read; the default, as on the Dashboard
Last 7 daysthe 7 × 24 hours before the figures were read
Last 30 daysthe 30 × 24 hours before the figures were read
Month to datefrom midnight on the first of the current month up to now
Last monththe whole previous calendar month, from its first midnight to this month’s first

The two month windows follow your browser’s time zone, so “last month” is the month on your own calendar, whatever zone the control plane or ClickHouse runs in, and a chargeback run on the 3rd still covers the previous month in full. The total’s caption gives the first and last date the figures cover, which is where to check that. The rolling windows are worked out again each time the figures are read, so a tab left open does not keep widening its “last 24 hours”.

The picked window is kept in the address as ?window= (24h, 7d, 30d, mtd or last-month), so a reload or a shared link opens on the same window, and an unknown value falls back to the last 24 hours. It also carries from one screen to the other: pick Last month on Business Units, open Customers, and the customers are read over the same month.

To reproduce a figure from a script, send the bounds the screen sends. For last month, read from a UTC+3 browser on any day of October 2026:

The rolling windows and month to date send only since, so the control plane’s clock closes the window.

Amounts are rendered in the deployment’s settlement currency, not a hardcoded $.

Note

The rollup needs ClickHouse. A deployment with no clickhouse_url shows a “Spend figures need ClickHouse” note in place of the spend figures, naming CLICKHOUSE_URL as the setting to add. The note is information about the deployment rather than an error, so it offers no retry and stays until the setting changes. The roster itself keeps working — it is backed by Postgres.


Exporting for chargeback#

Export CSV on Business Units and Customers downloads the rollup for the window on screen, unattributed spend included, as cost-attribution-<dimension>_<from>_to_<to>_<CURRENCY>.csv. A row whose spend leaves out requests with no price is marked cost_is_partial, so a partial total is never read as final. Export CSV on LLM Logs downloads the request log under the filters on screen, metadata only. Both show only the rows you could list yourself. See CSV exports.

Attribution in the request log#

Every row in request_logs carries business_unit_id and customer_id, so LLM Logs can be read per unit or per customer. The filter rail offers both dimensions, and the detail drawer names the unit and the customer a request was charged to.

GET /api/v1/analytics/invocations filters on both dimensions itself:

Both accept a comma-separated set — customer=<uuid>,<uuid> — matching the filter rail, where several can be ticked at once. An id that nothing was attributed to matches nothing: a filter narrows, it never widens.

Because the filtering happens in the database, limit and the page cursor mean what they say under a filter. Paging through a unit’s requests pages through that unit’s requests — see paging the invocation log.

Note

For a whole-window total rather than individual requests, by-attribution is still the cheaper question — it aggregates in ClickHouse instead of returning rows.


API#

Listing needs Viewer on the org; creating, updating and deleting need Admin. Every mutation is audited.

Each row in a listing carries live_key_count, the number of enabled, unexpired virtual keys attributed to it, so a unit or customer with no spend can be told apart from one no key points at. The dashboard says no key assigned on such a card and links to Virtual Keys. See Attribution listings.