The headline: price per useful GPU-hour
A rented GPU that sits idle still bills. Dividing the market price by the share of capacity actually working gives the effective price — what a buyer pays for compute that runs:Construction
The index composes three layers.1
Occupancy — is the capacity rented at all?
Capacity-weighted share of tracked GPU-hours under an active rental:where
capacity = rented + available on each hourly reading, drawn from
tracked marketplace listings across the providers Ornn observes.The numerator and denominator are summed separately before dividing. This
is not the same as averaging the per-reading ratios: a mean of ratios lets a
4-GPU listing count as much as a 4,000-GPU one, so one tiny provider going
idle moves “market” occupancy as hard as a hyperscaler. Weighting by the
capacity each reading represents is what makes the series a market rate
rather than a average of unrelated fractions.2
Intensity — how hard does it run once rented?
Mean GPU load during rented hours, from fleet heartbeat telemetry.Intensity is live from the fleet’s per-node 1-hour heartbeat rollups:
the GPU-hour-weighted mean of hourly GPU load across node-hours that were
under an active reservation inside the window, reported as
intensitySource: "fleet_rollup_1h". It is one fleet-wide factor per response — the
rollups do not carry a GPU class, so gpuType and region filters slice
occupancy but not intensity.3
Effective Utilization Rate
Derived series
revPag stays on occupancy rather than EUR on purpose: an operator is paid for the
hour the GPU is rented, whether or not the tenant saturates it.
Why it moved: demand and supply effects
A utilization rate that only goes up and down is nearly useless for a trading decision, because two opposite market events produce the same move. Occupancy rising because buyers took more GPUs is a tightening market. Occupancy rising because a provider withdrew capacity is a shrinking one. The headline rate cannot tell them apart, so the index decomposes every step:demandEffect: 0, supplyEffect: 0.25). Nobody bought a single extra
GPU. That is the distinction the index exists to publish.
On the snapshot endpoint the same decomposition runs across the whole window, so
summary.demandEffect and summary.supplyEffect explain where the market ended up
versus where it started.
Attribution only happens like-for-like
The effects carry a causal claim, so they are only published when the two days being compared contain the same set of GPU types. If the basket changed — typically a partial scrape outage — the effects arenull and
decompositionBasis says composition_changed instead of like_for_like.
This matters more than it sounds. With six types requested, a day where five stop
reporting would otherwise decompose into a −49.8 point “demand collapse” on a
day when the one surviving type actually rose a point. The arithmetic balances
perfectly; it is attributing a reporting gap to buyer behaviour. Refusing to
attribute is the only honest answer.
The rate itself still publishes on such a day, alongside reportingTypes and
coverage so the gap is visible on the same row. summary.compositionBreaks
counts how many steps in the window could not be attributed.
decompositionBasis values: like_for_like (attributed), composition_changed
(suppressed), no_prior_day (first row of the series).
Reading the market: the other fields
Data quality
Coverage is published next to every rate. Missing scrape hours are missing information, not idle GPUs, and an index that lets a feed outage read as a demand collapse is worse than no index. Coverage is measured against the requested slice set — asking for a GPU type a region does not carry correctly lowers coverage rather than quietly shrinking the denominator. Thin days are dropped, not published. A day with fewer than 24 readings is excluded from the series and skipped as a decomposition baseline, so the next real day compares against the last real day rather than against a gap. Publishing a confident-looking rate computed from three hours of scrape is how a data product loses its reputation. Impossible readings are clamped.capacity = rented + available, so occupancy
above 1 means the feed disagrees with itself; it clamps to 1 rather than
publishing an impossible rate. Zero-capacity readings carry no information and are
excluded entirely.
Zero never becomes infinity. effectivePrice is null when EUR is zero, not
Infinity.
Levels are fleet sizes, not GPU-hours. The underlying readings are hourly, so
a naive sum would scale with how often the scraper ran — a slow scrape day would
read as a smaller fleet, and the demand/supply decomposition would move even on a
market where nothing happened. Each slice is normalised to a mean over its own
readings before anything is summed, so capacity, rented, capacityMean and
headroomMean are GPU counts and the effects are immune to scrape cadence.
effectivePrice divides matched baskets. If part of the fleet has no settled
price that day, the price and the occupancy it divides into are both computed over
the priced slice only, and priceCoverage reports how much of the fleet that was.
Index level and rebasing
index is 100 x EUR / EUR_base, with baseDate defaulting to 2026-01-01.
The base is a fixed constant rather than “the first day of whatever you asked for”,
because a level rebased to the window start silently changes meaning between two
requests for different windows. If the base date has no usable data the series
rebases on its own first day and reports baseAvailable: false — always check that
flag before comparing levels across responses.
Endpoints
Snapshot
GET /api/utilization-index — window rate, quality signals, decomposition.History
GET /api/utilization-index/history — daily series with per-day effects.gpuType, region, startDate, endDate and baseDate. The window
defaults to the trailing 30 days and is capped at 400 days per request.
Known limitations
Stated plainly, because an index is only as good as its disclosed weaknesses.- Intensity is one fleet-wide factor. It comes from Ornn-fleet heartbeat
rollups and is not sliced by GPU class or region, and internal/test
reservations are not yet excluded — a per-class intensity needs the node
inventory join. It also measures the Ornn fleet while occupancy measures
tracked marketplace listings; the two populations overlap but are not
identical. The
basisfield is authoritative for what any response holds. - Region pairs a regional occupancy with a market-wide price. The occupancy
side is region-filtered;
gpu_daily_metricsis averaged across the regions it carries for a type, matching/api/daily-index. Forregion=globalthese agree; for a narrower region the effective price is approximate. - Occupancy is derived from tracked marketplace listings, so it measures the
market Ornn observes, not every GPU on earth. Capacity coverage widens as more
providers are tracked, which can move
capacityMeanfor reasons that are not market events. - Daily granularity only. The price index settles daily, so a finer utilization series would have no price to divide into.

