Skip to main content

Data · Methodology

View .md

How ComputePrices collects and normalizes prices

ComputePrices shows listed prices: what a provider publishes on its pricing page, in its API, or in the feed its pricing page renders from. It is not a transaction index and does not estimate what large buyers negotiate. Every number on the site can be traced back to a public source, and the raw value plus the conversion applied to it are stored beside every row. This page describes that pipeline end to end.

What a price row is

A GPU price is one offering: a provider selling a specific GPU at a specific node size under a specific billing model. The identity of an offering is the tuple

(provider, GPU model, pricing_type, gpu_count, commitment_months, variant, region)

and two rows are only ever compared against each other, or replace each other, when every element matches.

  • pricing_type is on_demand, spot or reserved. Reserved rows carry commitment_months; nothing else does.
  • gpu_count is the number of GPUs in the instance. A 1x and an 8x row for the same card are different offerings, because providers price them differently.
  • variant is a provider's concurrent tier for the same card: Runpod's Secure Cloud beside Community Cloud, Gcore's Bare Metal beside Virtual GPU, VoltageGPU's confidential enclave beside standard. Marketplaces that resell other clouds (Shadeform, gpu.ai) store the underlying cloud here, under their own provider record, so a marketplace rate never overwrites the cloud's own listing.
  • region is stored when a provider prices regions differently (AWS us-east-1 against sa-east-1). The comparison tables show the cheapest region per provider; the API returns them all.

An LLM price row is (provider, model, pricing_type), where pricing_type is standard or batch, with separate input, output and cached-input legs per 1M tokens.

Where prices come from

Each provider has exactly one source, in this order of preference:

  1. The provider's official pricing API, when one exists (Lambda, Runpod, Vast, Together, OpenRouter and others).
  2. The public JSON feed the pricing page renders from. Many pricing pages are client-rendered from an unauthenticated endpoint; that endpoint is the source and the page is never scraped. This is where units are stated explicitly, so it is preferred over the rendered page even when both are readable.
  3. Server-rendered HTML parsed directly, for pages that carry their prices in the initial response (Hyperstack, Modal, Mistral, Google Vertex).
  4. AI extraction over the rendered page (Firecrawl), for providers with none of the above. A provider on this path whose page has produced alternating values for one offering is scraped twice per run, and only prices both passes agree on within 1% are stored.
  5. A headless browser, for the few pages that need JavaScript to show anything.

One source per offering is a hard rule. Two collectors writing the same offering key would make the stored price depend on which ran last, and the site would report a change that never happened.

Collectors run daily. Some sources are read every few hours; AI-extracted providers rotate through a schedule and may succeed only every few days.

Normalization

Every stored GPU price is USD per GPU per hour. Every stored LLM price is USD per 1M tokens. The conversion from what the source published is recorded on the row:

  • original_price is the raw value as the source printed it, with its unit and currency ("€2.40/hr", "$57.38/hr (2x node)", "339000").
  • normalization_method names the conversion as steps joined with +, each from a fixed vocabulary. Unit conversions come first, in the order they apply: cents_to_dollars, integer_scale_1e5 (a feed quoting hundred-thousandths of a dollar), per_second_to_hourly (Modal's per-second billing times 3600), per_month_to_hourly, instance_total_divided (an instance total divided by gpu_count), per_token_to_per_1m, currency_converted. Then how the figure was chosen when the source offers several: range_floor (the low end of a published "from X to Y" range), credits_at_par (a credit-denominated rate where the provider fixes credits at one dollar), first_billing_tier (a tiered-billing model's lowest tier, which is what an ordinary request pays), flat_rate_all_tokens (one blended token rate stored on both legs). Then corrections applied at insert: currency_conversion_backfill and undivided_same_scrape (described below), and admin_release (a quarantined row published by hand after review). Rows written before October 2026 carry earlier spellings of the same steps, such as per_gpu_from_instance_total and monthly_to_hourly.
  • currency and exchange_rate_to_usd are stored for non-USD sources, with the rate used at collection time.

Both fields are returned by /api/v1/gpu-prices/history and /api/v1/llm-prices/history, so any published price can be checked against its source.

Units are never inferred. A collector that reads a feed matches the feed's unit field literally (perTokens: 1000000, "/ sec", "$ / M tokens"), and a feed that changes its unit fails the run and is reported, rather than storing a figure that is off by a factor of a thousand or a million. Where a source publishes two figures per row, the collector cross-checks them: a per-GPU rate against an instance total, a $/month column against $/hour × 730, an integer in ten-thousandths against a decimal string. Disagreement drops the row and reports it.

Validation before storage

A price is validated before it is written, and a rejected price is written to a quarantine table with its reason instead of to the public table. The layers, in order:

GPU prices

  1. Exact batch invariants, across the rows of one collection run, before anything statistical: a non-USD price that still equals its raw value was never converted, and is converted now (currency_conversion_backfill); a price that is an exact gpu_count multiple of a sibling row's per-GPU price for the same offering is an undivided instance total, and is divided (undivided_same_scrape); two different GPU models carrying a byte-identical high-precision price were mis-extracted, and the one further from its own model's median is rejected.
  2. Hard bounds. A per-GPU price below $0.01 or above $500 per hour is rejected outright.
  3. Cross-provider median. For a multi-GPU row whose source did not confirm the price as per-GPU, the price is compared against the median of every other provider's latest price for that GPU and pricing type within 30 days: one vote per provider, whatever its collection cadence, and never the provider's own rows. A value that lands at the median divided by gpu_count was per-GPU all along, and the division is undone. Spot prices are only ever compared against spot medians.
  4. Historical self-comparison. A price more than 2x, or less than 0.5x, the same provider's own last accepted price for the same offering within 30 days is rejected. A real repricing of that size is rare; a parser reading the wrong column is not. The exception is a repricing confirmed by a dedicated collector (an API, feed or page parser, not AI extraction): when it reports the same new price for the same offering on 3 consecutive collection days, the third day's row is accepted, as long as it is within 3x and 0.33x of the cross-provider median where one exists, and it becomes the new reference. The row is marked confirmed_repricing and reported in the next health digest. A day that returns the old price is accepted as usual and breaks the run, so a price alternating between two values never qualifies. A live market quote is not compared with its own history: a spot price the provider reprices continuously and publishes per region in a structured feed (Nebius, repriced as often as every 15 minutes) moves past 2x in a day without anything being misread. Every other layer still applies to it.
  5. Spot at or below on-demand. A spot price above the provider's own on-demand price for the same GPU, node size and region is rejected. A spot price quoted for one region is not compared with an on-demand price the provider publishes for every region.

Every layer that lacks its reference value (no median, no history, no on-demand row) passes the price through. A missed catch is recoverable; a wrong rejection silently hides a real listing.

LLM prices (every writer, from the hand-written collectors to the AI-extracted pages, goes through the same layer)

  1. Undiscounted batch rows. Within one collection run, a batch row must be below the same provider's standard row for the same model on both legs; one that is not is the standard price duplicated into the batch row, and is rejected.
  2. Currency invariants. A non-USD row without a real exchange rate, or with a rate of exactly 1, was stored as if it were dollars, and is rejected. A non-USD row whose legs still equal the raw figures the source printed was never converted, and is converted now (currency_conversion_backfill).
  3. Hard bounds. Every leg must be a non-negative number. A leg above $1,000 per 1M tokens was published in another unit (per token, per 1K) and is rejected rather than rescaled: the unit is the collector's to declare, never inferred here. A cached-input rate at or above the input rate is rejected too, since a cache read is a discount and a row where it is not has its columns out of order.
  4. Cross-provider median. Input and output are each compared against the median of every other provider's latest price for the same model and pricing type within 30 days (at least two of them, never the provider's own rows), so a batch rate is compared with batch rates; a leg above 3x or below 0.02x the median is rejected. The low bound is far wider than the high one because hosts legitimately undercut the median by ten times or more (resellers, routers): it exists to catch a figure published in another unit (per 1K tokens, per token), not a discount.
  5. Inverted pair. When no median exists, an output price below one tenth of the input price is treated as swapped columns and rejected.
  6. Historical self-comparison. As for GPUs: a leg at 2x and 0.5x against the provider's own last accepted price for the same model and pricing type within 30 days is rejected. A leg that is zero on either side (a model that went free, an embedding's output) is not compared. A repricing a dedicated collector confirms is accepted the same way, with both legs held for the run; the median layer above has already checked it.

Image, video, audio and per-request models are priced per unit rather than per token: per image, per megapixel, per second of video, per minute of audio, per 1M characters of speech, or per 1K requests, restated from whatever unit and quantity the source prints. A unit must fit the model's type (a chat model is never priced per image, a video model only per second), a duration rate is restated in its type's unit, a token rate with a zero leg on an image or video model is rejected as a placeholder, and one provider's price for a model is stored in one unit. A unit price is then checked against a cap per unit, then against the provider's own last accepted price, then against the median of every other provider's latest price for that unit and pricing type within 30 days.

Model matching

Provider names are mapped onto a fixed catalog of GPU models and LLM models before a price is stored, so that "NVIDIA H100 80GB SXM5", "H100-SXM" and "h100_sxm" all land on one row. Rules that matter for correctness:

  • Interconnect and memory decide the model where a provider states them: A100 PCIe and A100 SXM are separate catalog entries, as are H100 PCIe, H100 SXM and H100 NVL.
  • Mobile parts and multi-card buckets ("GTX 1070, 1080, 1080Ti") are never matched. Attributing one price to three cards misprices two of them.
  • Where several of a provider's listings resolve to one catalog model at one node size (a marketplace with many hosts, or H200 NVL beside H200 SXM), they collapse to the cheapest, and the per-model floor shown on the site is the minimum across what survives.
  • Names that resolve to nothing are reported in the collector's run log as unmatched, so a new card shows up as a gap rather than a wrong match.

Marketplaces, resellers and "cheapest"

Some providers resell other providers' capacity: GPU marketplaces such as Shadeform, Vast and Lium, and LLM routers such as OpenRouter. Four rules keep their prices comparable with everyone else's, and they apply to every provider.

  1. Every price in a table is a real offer. It is the rate one named seller charges for one offering, as the source printed it. The site does not average sellers, blend input and output rates into one figure, or pair one host's input rate with another host's output rate, because none of those is a price anyone can buy.
  2. The seller is part of the offer when the source names it. A marketplace that says which cloud serves a listing is stored under the marketplace with that cloud as variant, so its rate never overwrites the cloud's own. GPU rows support this today. LLM rows do not have a seller field yet, so an LLM router's row is the one rate its source headlines (#1472).
  3. When the source does not name the seller, the cheapest listing is stored. A marketplace's cheapest listing for an offering is a real offer, so it is kept and the others are dropped. For a GPU that is one number. An LLM price has two legs, and the host with the cheapest input is often not the one with the cheapest output, so there is no single cheapest LLM offer to store, and none is invented. On the anonymous-host GPU marketplaces (Vast, Lium, Omega Gradient, GPU Outlet, QuantaCloud) the dropped listings are not discarded entirely: the stored row records how many listings it was chosen from, their median and the dearest of them (listing_count, listing_median_price, listing_max_price). These describe the market behind the floor, not a price anyone can buy, so they never appear as a price in a table. They are blank on every other row, and on a row the validation layer corrected, because the listings no longer describe the stored figure.
  4. "Cheapest" is decided where it is shown. For GPUs it is the lowest per-GPU-hour rate. For LLMs the site names the leg: the cheapest input rate and the cheapest output rate are shown as two separate facts, each with its seller. Ranking LLM hosts by a single number needs a mix of input to output tokens, so the site does it only where the reader chooses the mix (#1474) or where the page states it (the use-case pages price a typical request of a stated size), and labels the result as an estimate.

A router whose stored rate is one rotating host's headline does not count as a price move. OpenRouter's row changes whenever a different host takes the headline, and when a host it resells reprices, OpenRouter's row follows, so counting it would report moves nobody made and turn one host's decision into two votes. Its current rate stays in the tables, but weekly price changes, consensus moves and daily trends leave it out.

An average is published only as a labelled index, on its own and with how it is computed, never inside a price table. Akash is the source that tests this: its feed publishes a bid-weighted average per GPU, which no provider on the network offers and nobody can rent at. The average is not what is stored — the cheapest live bid is, under rule 3 — and the average and the full bid range are kept in the row's original_price, so a stored rate can still be reconciled against the figure Akash's own pricing page prints.

Availability

Where a source reports stock, the row carries it: true for rentable, false for listed but out of capacity. Providers that publish a catalog with no stock signal (AWS, Azure, Oracle, every AI-extracted page) leave it unset, and only an explicit false is shown as sold out. Marketplaces that only return bookable offers store true, and a sold-out offering on them simply leaves no row.

When a price stops being current

The comparison tables and the API show the most recent accepted row per offering. A row counts as current only if it was collected on one of that provider's own last two distinct collection dates, inside a 30-day backstop.

This is per-provider by design. A provider collected daily drops a delisted offering the day after it disappears. A provider on the AI-extraction rotation that succeeds once every week or two keeps its offerings until two later runs have not seen them. A flat day-count cannot fit both, and would either show delisted offerings for weeks or wipe out a provider whose page merely failed to render for a few days.

Price trends average one pricing type only (on_demand for GPUs, standard for LLMs), so a spot or batch row written seconds after the headline row cannot replace it in a daily series.

Corrections

A price that turns out to be wrong — a parser that read the wrong column, an instance total stored as a per-GPU rate, a row attributed to the wrong GPU or provider — is corrected in two places. The collector is fixed so the error does not recur, and the rows it already wrote are removed or relabelled so they stop counting as current prices and stop distorting validation for the next correct one.

Since 12 October 2026, every such change keeps an audit trail. The full row as it was published, the row it became (for a relabel), the time of the change and the reason are written to a revision log that cannot itself be edited. Nothing is removed from the price history without that record. Corrections made before then removed or rewrote the rows without keeping them. Each of them is recorded in the same log from the migration that made it, and per row where the original value is known exactly. The log is published at /api/v1/corrections.

A row is removed only when it is known to be wrong. A row we keep but cannot vouch for is served with a quality flag instead, on /api/v1/gpu-prices/history, /api/v1/llm-prices/history and in the history backfill. The first is multi_price_run: the collection run that wrote the row stored more than one price for the same offering, so nothing on the row says which of them is its price. Until October 2026 some LLM collectors matched several of a provider's models onto one catalog model in one run (two smaller Qwen models stored beside Qwen 3.5 27B, for instance), and some GPU collectors stored several host configurations of one offering. Where the right row could be told apart, the others were removed with a logged reason; the rest are flagged. Collectors now store one price per offering per run, and a flag is computed when a row is read, so a collector that regresses is flagged without anyone marking it.

Release notes

The corrections log records rows that were changed after they were published. Changes to how future prices are collected and normalized are recorded separately, in a release-notes log, because they change what the daily releases contain from a given day onward: a collector reading a different field, a card filed under a different model, a provider added or removed. Every change of that kind gets an entry, and a check on each change to the code refuses one that has none. Each entry has one of three kinds:

  • Correction: a collector or the stored data brought closer to what the source publishes, such as a relabelled conversion, a refiled card, two listings collapsed onto one offering, or a parser fix. It takes effect from the first collection after it is logged.
  • Coverage: a provider or offering starts or stops being collected because the provider started or stopped selling it. It takes effect the same way.
  • Policy: a deliberate change to the methodology on this page, to normalization or selection rules, to the release schema, or a planned removal from the provider panel. It is announced at least 30 days ahead with an effective date, and nothing it describes reaches a release before that date.

The log is published at /api/v1/release-notes.

Review gate on new providers

A provider that lists itself through the submit form has its prices collected immediately, but nothing public reads them until a person approves the listing. Every public surface (comparison tables, provider pages, the API, weekly movers) waits on that review together.

Data sources and licences

Most prices are facts a provider publishes about its own offerings. One source publishes its data under an open licence that asks for credit, and we credit it here and on every /api/v1 response (meta.attributions):

  • GPU price data from the Aquanode GPU Price Index, licensed under CC BY 4.0. Changes: we store the lowest per-GPU rate for each model, matched to our GPU catalog, as one-GPU on-demand USD rows. In the API these rows have source_url https://www.aquanode.io/gpu-index. Our API terms do not restrict what CC BY 4.0 lets you do with them.

What this data is not

  • It is not a transaction price. Large buyers negotiate rates below list; the site does not see those contracts and does not estimate them. Where a provider publishes a "contact sales" tier, it is reported as unpriced rather than guessed.
  • It is not a survey. No figure comes from asking market participants; every figure comes from a page or feed the provider itself publishes.
  • It is not a single index number. A GPU's price on this site is a distribution across providers, tiers and regions, with the floor and the full set both visible. The spread between the cheapest marketplace listing and the hyperscaler list price is real, and the site shows it rather than averaging it away.

Questions about a specific price, or a source that looks wrong, go to hi@computeprices.com. The row's original_price and source_url are the fastest way to check it yourself.