---
title: How ComputePrices Collects and Normalizes Prices
url: /docs/methodology
type: reference
summary: Where every price on ComputePrices comes from, how it is normalized to per-GPU per-hour and per-1M-token USD, what gets rejected before it is stored, and when a price stops counting as current.
related:
  - /docs/api
  - /docs/api.md
  - /llms.txt
  - /api/v1/gpu-prices/history
---

# 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. The values in use include `per_gpu_from_instance_total` (an instance total divided by `gpu_count`), `per_hour_from_per_second` (Modal's per-second billing times 3600), `monthly_to_hourly`, `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), `currency_conversion_backfill` and `undivided_same_scrape` (corrections applied at insert, described below), and `admin_release` (a quarantined row published by hand after review).
- **`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`](/docs/api#endpoints), 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 current price for that GPU and pricing type. 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.
5. **Spot at or below on-demand.** A spot price above the provider's own on-demand price for the same GPU and node size is rejected.

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**

1. **Unit error.** A price above $1,000 per 1M tokens was published per token, and is divided by 1,000.
2. **Cross-provider median.** Input and output are each compared against the median across providers serving the same model; a leg above 3x or below 0.33x the median is rejected.
3. **Inverted pair.** When no median exists, an output price below one tenth of the input price is treated as swapped columns and rejected.
4. **Historical self-comparison.** As for GPUs: 2x and 0.5x against the provider's own last accepted price within 30 days.

Collectors that read a page with a cached-input column also check that the cached rate is below the input rate. A cache read is a discount, so a row where it is not has its columns out of order, and the collector drops it and reports it rather than storing it.

## 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.

## 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.

## 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.

## 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](mailto:hi@computeprices.com). The row's `original_price` and `source_url` are the fastest way to check it yourself.
