# billing (/docs/reference/api/billing)

> The platform's billing setup for compute: the meters and prices root keeps on the IAM root tenant, so root can bill its sub-tenants for what they use. Every method is decided as a `compute:billing:*` action on `compute-fleet`, which resolves only in the root tenant, so a tenant administrator is turned away before any IAM decision, however broad the tenant's own grants. A root member needs `compute:billing:read` or `compute:billing:manage` on `compute-fleet`.



The platform's billing setup for compute: the meters and prices root keeps on the IAM root tenant, so root can bill its sub-tenants for what they use. Every method is decided as a `compute:billing:*` action on `compute-fleet`, which resolves only in the root tenant, so a tenant administrator is turned away before any IAM decision, however broad the tenant's own grants. A root member needs `compute:billing:read` or `compute:billing:manage` on `compute-fleet`.

These methods call the IAM root tenant's billing API as a root service account. The IAM port must be configured with one: `bridge.embedded(iam, { rootTenantId, serviceCredential })` with `iam:billing:manage` and `iam:billing:read` on the root tenant (a remote bridge takes a `serviceToken`). Without it every method answers `UNSUPPORTED` and names that fix. Amounts are micros of the deployment's currency (1,000,000 micros = 1 unit).

The catalog these methods work from is the same one `better-compute iam setup --meters` and the IAM bridge sync: hosted and node GPU, CPU, memory and volume hours, provider VM meters, reservation-grant and managed-VM meters, the ancillary network, address, disk and snapshot meters, and the `flavor.*` meters root sets on flavors. `vps({ billing: { gpuModels, gpuFamilies } })` names the GPU models whose VM meters exist; name the same models as the bridge so the two agree.

All 7 methods are served by @better-compute/server 0.3.0.

| Method                            | Kind     | Authorization                              | Phase | Status     |
| --------------------------------- | -------- | ------------------------------------------ | ----- | ---------- |
| [`meters`](#meters)               | read     | `compute:billing:read` @ `compute-fleet`   | v1.0  | registered |
| [`syncMeters`](#syncmeters)       | mutation | `compute:billing:manage` @ `compute-fleet` | v1.0  | registered |
| [`prices`](#prices)               | read     | `compute:billing:read` @ `compute-fleet`   | v1.0  | registered |
| [`setPrices`](#setprices)         | mutation | `compute:billing:manage` @ `compute-fleet` | v1.0  | registered |
| [`proposePrices`](#proposeprices) | read     | `compute:billing:read` @ `compute-fleet`   | v1.0  | registered |
| [`margins`](#margins)             | read     | `compute:billing:read` @ `compute-fleet`   | v1.0  | registered |
| [`leaks`](#leaks)                 | read     | `compute:billing:read` @ `compute-fleet`   | v1.0  | registered |

## meters [#meters]

`POST /api/compute/billing/meters`

Returns the catalog compute defines, `catalog`, and `missing`: the keys of the catalog the IAM root tenant has no meter for yet. Usage recorded on a missing meter is refused by IAM; `syncMeters` creates the missing ones.

An archived meter still exists on the root tenant, so it is not listed as missing. `UNSUPPORTED` when the IAM port cannot list meters.

* **Kind:** read
* **Authorization:** `compute:billing:read` @ `compute-fleet`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

Takes an empty object: `{}`.

## syncMeters [#syncmeters]

`POST /api/compute/billing/syncMeters`

Creates the catalog's meters that the root tenant lacks, and returns `{ created, existing }` (meter keys). A meter that exists is left as it is, whatever its name, unit or pricing mode; calling it again creates nothing.

Decided as `compute:billing:manage` on `compute-fleet`. The IAM call happens outside any compute transaction.

* **Kind:** mutation
* **Authorization:** `compute:billing:manage` @ `compute-fleet`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

Takes an empty object: `{}`.

## prices [#prices]

`POST /api/compute/billing/prices`

Returns the rate-card price entries of one meter, or of every rate-card meter, newest first, as `{ meter, targetTenantId, effectiveFrom, spec }`. The list price is the entry whose `targetTenantId` is the root tenant; a negotiated price names the tenant (and its subtree) it applies to. `targetTenantId` filters.

`meter` and `targetTenantId` are optional. `reported` meters carry their cost on each usage event and have no prices.

* **Kind:** read
* **Authorization:** `compute:billing:read` @ `compute-fleet`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field            | Type   | Required | Constraints                                                |
| ---------------- | ------ | -------- | ---------------------------------------------------------- |
| `meter`          | string |          | at most 64 characters; matches `^[a-z][a-z0-9_.-]{0,63}$`  |
| `targetTenantId` | string |          | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$` |

## setPrices [#setprices]

`POST /api/compute/billing/setPrices`

Sets up to 100 prices on the root tenant's rate card and returns `{ applied, failed }`.

* **Each price** is `{ meter, targetTenantId?, effectiveFrom, spec }`. Without `targetTenantId` it is the list price; with one it is a negotiated price for that tenant's subtree. `effectiveFrom` is a billing period (`YYYY-MM`), and `spec` is a better-iam price (`per-unit`, `graduated`, `volume` or `package`) in micros.
* **Meters.** Every `meter` must be in the catalog (`meters`) or be a flavor meter; one that is not refuses the whole request with `INVALID_INPUT` before any price is applied.
* **Tenants.** `targetTenantId` is not checked here: better-iam validates it, and a tenant it refuses lands in `failed`.
* **Partial failure.** A price better-iam refuses does not stop the others: `applied` lists the prices that took effect, in request order, and `failed` lists `{ index, meter, targetTenantId?, code, message }` for the rest. Setting a price is idempotent, so a retry of the same request is safe.

Decided as `compute:billing:manage` on `compute-fleet`.

* **Kind:** mutation
* **Authorization:** `compute:billing:manage` @ `compute-fleet`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field                                    | Type                                                | Required | Constraints                                                |
| ---------------------------------------- | --------------------------------------------------- | -------- | ---------------------------------------------------------- |
| `prices`                                 | list of object                                      | yes      | 1-100 items                                                |
| `prices[].meter`                         | string                                              | yes      | at most 64 characters; matches `^[a-z][a-z0-9_.-]{0,63}$`  |
| `prices[].targetTenantId`                | string                                              |          | at most 128 characters; matches `^[A-Za-z0-9._:-]{1,128}$` |
| `prices[].effectiveFrom`                 | string                                              | yes      | matches `^\d{4}-(0[1-9]\|1[0-2])$`                         |
| `prices[].spec`                          | object                                              | yes      |                                                            |
| `prices[].spec.model`                    | one of `per-unit`, `graduated`, `volume`, `package` | yes      |                                                            |
| `prices[].spec.unitAmountMicros`         | integer                                             |          | at least 0                                                 |
| `prices[].spec.tiers`                    | list of object                                      |          | 0-50 items                                                 |
| `prices[].spec.tiers[].upTo`             | integer,null                                        | yes      | at least 1                                                 |
| `prices[].spec.tiers[].unitAmountMicros` | integer                                             | yes      | at least 0                                                 |
| `prices[].spec.tiers[].flatAmountMicros` | integer                                             |          | at least 0                                                 |
| `prices[].spec.packageSize`              | integer                                             |          | at least 1                                                 |
| `prices[].spec.packageAmountMicros`      | integer                                             |          | at least 0                                                 |
| `prices[].spec.includedQuantity`         | number                                              |          | at least 0                                                 |
| `prices[].spec.minimumAmountMicros`      | integer                                             |          | at least 0                                                 |
| `prices[].spec.maximumAmountMicros`      | integer                                             |          | at least 0                                                 |

## proposePrices [#proposeprices]

`POST /api/compute/billing/proposePrices`

Reads the list prices of one of the platform's provider accounts, adds a margin and returns prices root can publish. It sets nothing: `prices` is the same before and after, and root publishes the proposals it wants through `setPrices`. Decided as `compute:billing:read` on `compute-fleet`. It needs no billing member on the IAM port, only the provider.

* **Input.** `accountId` is a provider account of the root tenant whose provider lists prices (Compute Engine reads the Cloud Billing Catalog): another tenant's account, or one that does not exist, is `NOT_FOUND`, and a provider without prices is `UNSUPPORTED`. `marginPct` (0 to 1000) is the percentage added. `region` limits the prices read to one region; without it the cheapest basis across the regions the provider lists is used. `effectiveFrom` (`YYYY-MM`) is the period root means to publish the proposals from; it is checked and left to `setPrices`.

* **Result.** `{ proposals, unpriced, currency }`; `currency` is the deployment currency every proposal is in. A proposal is `{ meter, spec, basis }`: `spec` is `per-unit` at `round(unitPriceMicros x (1 + marginPct / 100))` micros, and `basis` lists the provider prices (`sku`, `unitPriceMicros`) it came from. A meter the provider's prices cover is proposed; the provider meters that remain are `unpriced`.

* **`vm.vcpu.hours` and `vm.memory.gib.hours`** take the cheapest general-purpose machine family (E2, N1, N2, N2D, N4, T2D, C3, C3D, C4, by one vCPU and four GiB of on-demand prices) in the region. Per-family accuracy is what flavor meters are for.

* **GPU meters** (`vm.gpu.<model>.hours` and the `.spot` meters) take the cheapest GPU SKU of that model, on demand or Spot. **Ancillary meters** (`ip.public.hours`, `disk.<type>.gib.hours`) take the matching SKU; monthly storage prices become GiB-hours at 730 hours a month.

* **Flavor meters** (`flavor.<name>.hours`) are composed for the flavors of the account's pools that carry them: the flavor's vCPUs, memory and GPUs at the prices of its offer's machine family in the offer's region (an offer id spelled `zone/machine-type`). Archived flavors are ignored. A flavor's GPU model is its pool's `gpuModel` as the deployment's GPU models name it (`vps({ billing: { gpuModels } })`, as metering does), so a model the deployment does not name is `other` and has no price. Flavors that share a meter must share a shape, else the meter is `unpriced`.

* **Left `unpriced`:** instance, reserved, reservation-grant and managed-VM meters, network and snapshot meters, GPU models the provider has no price for, and any meter whose price is in another currency than the deployment's (`vps({ billing: { currency } })`, default `USD`). Hosted, node and sandbox meters are the platform's own and are not listed.

* **Kind:** read

* **Authorization:** `compute:billing:read` @ `compute-fleet`

* **Module:** `vps`

* **Phase:** v1.0 (registered)

**Input**

| Field           | Type   | Required | Constraints                                                        |
| --------------- | ------ | -------- | ------------------------------------------------------------------ |
| `accountId`     | string | yes      | at most 58 characters; matches `^pacct_[0-9a-hjkmnp-tv-z]{26,52}$` |
| `marginPct`     | number | yes      | 0-1000                                                             |
| `region`        | string |          | at most 63 characters; matches `^[a-z0-9][a-z0-9._-]{0,62}$`       |
| `effectiveFrom` | string | yes      | matches `^\d{4}-(0[1-9]\|1[0-2])$`                                 |

## margins [#margins]

`POST /api/compute/billing/margins`

Returns the platform's margin over a range: revenue (what better-iam billed) minus provider cost (what the providers billed the platform, D12), grouped by `tenant`, `pool`, `account` (provider account) or `meter`, as `{ rows, unattributedCostMicros, currency, from, to }`.

* **Input.** `from` and `to` are epoch milliseconds (`to` after `from`, at most 400 days apart); `groupBy` is required.
* **Revenue** comes from the IAM port's spend report on the root tenant (`IamPort.spend`, by tenant, by meter, or by the `pool` tag). better-iam sums whole billing periods, so the answer covers every period the range touches, and costs are summed over exactly that range: `from` and `to` in the answer are the range both cover. Without `spend`, `revenueMicros`, `marginMicros` and `marginPct` are `null` and costs cover the query's range. better-iam has no provider-account dimension, so `groupBy: 'account'` always has `null` revenue.
* **Revenue is before contract terms.** It is better-iam's spend: usage rated at list or negotiated prices (`setPrices`), before the contract discounts of `setTerms`, which better-iam applies when it closes the period into a statement. Margins of a discounted tenant are overstated by its discount.
* **Root's own usage is not revenue.** better-iam never invoices the platform. By tenant, the root tenant's row keeps its provider cost and has `revenueMicros: 0`, so its margin is minus that cost. By pool or meter, the spend report cannot be scoped to the root tenant without the tenants below it, so pool and meter revenue include root's internal usage at its rated price.
* **Cost** is the sum of cost plus credits of the `vps.costs` worker's rows (`provider_costs`) of the platform's own provider accounts (the root tenant's; a tenant's own account is that tenant's cost, not the platform's) whose usage started in the range, in the deployment currency (`vps({ billing: { currency } })`, default `USD`; rows in another currency are excluded). Rows attributed to no tenant are not in `rows` but in `unattributedCostMicros`. By pool, a row counts toward its instance's pool; by meter, toward the reported meter it was passed through on. Attributed cost with no such pool or meter is grouped under the key `(none)`, whose revenue is `null`. Provider compute cost has no meter dimension (a provider bills a VM's cores and memory, not compute's meters), so with `groupBy: 'meter'` all compute cost is under `(none)` while its revenue is under the `vm.*` meters: read the meter view for pass-through margins, and the tenant or pool view for compute. Pass-through usage carries the instance's `pool` tag, so revenue by pool includes it.
* **Rows.** One per key that has revenue or cost, sorted by key with `(none)` last: `{ key, revenueMicros, costMicros, marginMicros, marginPct }`. `marginPct` is the margin over revenue in percent (2 decimals), `null` when revenue is not positive.

Decided as `compute:billing:read` on `compute-fleet`, so a tenant cannot read it. The IAM call happens outside any compute transaction.

**Leak detection** (reported as events, listed by `leaks`) — at the end of every `vps.costs` pass over a platform provider account, each instance's provider-billed compute is compared with what compute metered for it, per UTC day. The provider hours of an instance-day are the **distinct hour slots** the provider billed compute for it (regular rows of the provider's compute SKUs attributed to the instance, quantity above 0), not the sum of the rows' quantities: a provider bills a VM's cores and memory as separate rows in vCPU-hours and GiB-hours, so summed quantities are no count of hours. The metered hours are how many of those slots a `running` usage interval of the instance overlaps. When the provider hours exceed the metered ones by more than `vps({ costs: { leakToleranceHours } })` (default 1), `billing.leak` is published, at most once per instance and day. Instances with `billing: none` and tenants' own accounts are not checked.

A provider whose cost rows are period aggregates (`billing.periodRows`: a DigitalOcean invoice item is a Droplet's month so far) is compared differently: the row's billed hours against the instance's metered hours over the row's span, with a gap over `max(leakToleranceHours, 5%)` of the billed hours, raised once per instance and period; a provider that bills stopped machines (`billing.billsStopped`) has the instance's stopped hours counted as metered.

* **Kind:** read
* **Authorization:** `compute:billing:read` @ `compute-fleet`
* **Module:** `vps`
* **Phase:** v1.0 (registered)

**Input**

| Field     | Type                                        | Required | Constraints |
| --------- | ------------------------------------------- | -------- | ----------- |
| `from`    | integer                                     | yes      | at least 0  |
| `to`      | integer                                     | yes      | at least 1  |
| `groupBy` | one of `tenant`, `pool`, `account`, `meter` | yes      |             |

## leaks [#leaks]

`POST /api/compute/billing/leaks`

Lists the leaks raised on the platform's (root's) provider accounts whose day is in `[from, to)`, newest day first, at most 500: `{ items }`.

* **Input.** `from` and `to` are epoch milliseconds; a leak's day is its UTC day (for a period row, the period's first day).

* **Items.** `{ accountId, accountName, provider, instanceId, tenantId, day, providerHours, meteredHours, raisedAt }`: `tenantId` is the instance's tenant, `null` once the instance is gone; `providerHours` is what the provider billed and `meteredHours` what compute metered. A tenant's own accounts are never checked, so they have no leaks; without a root tenant the list is empty.

* **IAM.** Decided as `compute:billing:read` on `compute-fleet`, so a tenant cannot read it. A read of the database: no provider is called.

* **Kind:** read

* **Authorization:** `compute:billing:read` @ `compute-fleet`

* **Module:** `vps`

* **Phase:** v1.0 (registered)

**Input**

| Field  | Type    | Required | Constraints |
| ------ | ------- | -------- | ----------- |
| `from` | integer | yes      | at least 0  |
| `to`   | integer | yes      | at least 1  |
