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 | read | compute:billing:read @ compute-fleet | v1.0 | registered |
syncMeters | mutation | compute:billing:manage @ compute-fleet | v1.0 | registered |
prices | read | compute:billing:read @ compute-fleet | v1.0 | registered |
setPrices | mutation | compute:billing:manage @ compute-fleet | v1.0 | registered |
proposePrices | read | compute:billing:read @ compute-fleet | v1.0 | registered |
margins | read | compute:billing:read @ compute-fleet | v1.0 | registered |
leaks | read | compute:billing:read @ compute-fleet | v1.0 | registered |
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
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
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
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 }. WithouttargetTenantIdit is the list price; with one it is a negotiated price for that tenant's subtree.effectiveFromis a billing period (YYYY-MM), andspecis a better-iam price (per-unit,graduated,volumeorpackage) in micros. - Meters. Every
metermust be in the catalog (meters) or be a flavor meter; one that is not refuses the whole request withINVALID_INPUTbefore any price is applied. - Tenants.
targetTenantIdis not checked here: better-iam validates it, and a tenant it refuses lands infailed. - Partial failure. A price better-iam refuses does not stop the others:
appliedlists the prices that took effect, in request order, andfailedlists{ 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
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.
accountIdis 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, isNOT_FOUND, and a provider without prices isUNSUPPORTED.marginPct(0 to 1000) is the percentage added.regionlimits 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 tosetPrices. -
Result.
{ proposals, unpriced, currency };currencyis the deployment currency every proposal is in. A proposal is{ meter, spec, basis }:specisper-unitatround(unitPriceMicros x (1 + marginPct / 100))micros, andbasislists the provider prices (sku,unitPriceMicros) it came from. A meter the provider's prices cover is proposed; the provider meters that remain areunpriced. -
vm.vcpu.hoursandvm.memory.gib.hourstake 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>.hoursand the.spotmeters) 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 spelledzone/machine-type). Archived flavors are ignored. A flavor's GPU model is its pool'sgpuModelas the deployment's GPU models name it (vps({ billing: { gpuModels } }), as metering does), so a model the deployment does not name isotherand has no price. Flavors that share a meter must share a shape, else the meter isunpriced. -
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 } }), defaultUSD). 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
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.
fromandtoare epoch milliseconds (toafterfrom, at most 400 days apart);groupByis required. - Revenue comes from the IAM port's spend report on the root tenant (
IamPort.spend, by tenant, by meter, or by thepooltag). better-iam sums whole billing periods, so the answer covers every period the range touches, and costs are summed over exactly that range:fromandtoin the answer are the range both cover. Withoutspend,revenueMicros,marginMicrosandmarginPctarenulland costs cover the query's range. better-iam has no provider-account dimension, sogroupBy: 'account'always hasnullrevenue. - Revenue is before contract terms. It is better-iam's spend: usage rated at list or negotiated prices (
setPrices), before the contract discounts ofsetTerms, 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.costsworker'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 } }), defaultUSD; rows in another currency are excluded). Rows attributed to no tenant are not inrowsbut inunattributedCostMicros. 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 isnull. Provider compute cost has no meter dimension (a provider bills a VM's cores and memory, not compute's meters), so withgroupBy: 'meter'all compute cost is under(none)while its revenue is under thevm.*meters: read the meter view for pass-through margins, and the tenant or pool view for compute. Pass-through usage carries the instance'spooltag, 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 }.marginPctis the margin over revenue in percent (2 decimals),nullwhen 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
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.
fromandtoare 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 }:tenantIdis the instance's tenant,nullonce the instance is gone;providerHoursis what the provider billed andmeteredHourswhat 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:readoncompute-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 |
Better Compute is created by Sean Filimon
Last updated
API
Every method of the compute API, by group: 291 registered, 289 in PLAN.md §9.2.
capacity
The capacity methods report what the pools a tenant can see hold, book and have free, without booking anything. `summary` and `byPool` read the per-pool rollup, which the booking transactions update and a periodic rebuild corrects. `preview` asks the placer whether a request fits, and `timeline` counts booked GPUs over time. All four are decided as `compute:capacity:read`. See [Capacity](/docs/guides/capacity).