Preview build — figures marked “✓” or in green are sourced (benchmarks by Artificial Analysis, MTEB and Open ASR); treat everything else (value scores, “Est.” figures, unmarked prices and changes) as illustrative. Methodology
API
One request, the whole price book.
Every price, benchmark and provenance column the site renders is available as JSON and CSV, from the same serializer that builds the pages — so the API cannot disagree with what you see in the tables. No key, no signup, no tracking. 10 requests per day per IP, for reasons set out below.
- Rows
- 4325
- Providers
- 224
- Markets
- 6
- Snapshot
- 2026-08-03
Endpoints
The full catalogue in one object: a documented envelope plus one row per priced offering.
The same rows, flat. A leading # comment line carries the snapshot id and the required attribution — pass comment='#' to your reader.
GET only, over HTTPS. CORS is open (access-control-allow-origin: *), so you can call this straight from a browser. Responses are gzipped, which is the figure that matters: the JSON is 3.0 MB of text but 93 KB over the wire. There is no pagination, no per-model endpoint and no query filtering — splitting a payload that size would only cost you requests.
Quick start
Fetch it:
curl -s https://modelpricebook.com/api/v1/modelscurl -sL https://modelpricebook.com/api/v1/models.csv -o model-price-book.csvLoad it into pandas and rank the cheapest sourced language models:
import pandas as pd
df = pd.read_csv("https://modelpricebook.com/api/v1/models.csv", comment="#")
# Only quote what is sourced — the rest is illustrative.
sourced = df[df.price_sourced]
# Cheapest sourced LLM by blended token price.
llm = sourced[sourced.category == "llm"].copy()
llm["blended"] = llm.price_input_per_1m + llm.price_output_per_1m
print(llm.nsmallest(5, "blended")[["name", "provider", "blended"]])Or from JavaScript:
const res = await fetch("https://modelpricebook.com/api/v1/models");
const book = await res.json();
// Cache on snapshot_id: unchanged id means byte-identical data.
console.log(book.snapshot_id, book.model_count);
const cheapestVideo = book.models
.filter((m) => m.category === "video" && m.price_sourced)
.sort((a, b) => a.price_per_second - b.price_per_second)[0];What you get
The dataset answers one question across six markets: what does this model cost, who is actually selling it, how good is it, and how do we know. Today that is 4325 priced offerings from 224 providers, 882 of 1133 tracked models carrying a sourced price and 425 carrying a sourced benchmark score.
The envelope
JSON responses wrap the rows in the terms you need to use them safely:
datasetstringConstant identifier for the dataset.
snapshot_idstringThe build this payload came from. Key your cache on it: an unchanged snapshot_id means byte-identical data.
generated_atstringISO 8601 timestamp of the data run that produced the snapshot.
licensestringLicence of the compilation, plus the benchmark attribution you must retain.
citationstringThe citation line to reproduce.
attributionobjectBenchmark sources and price feeds, spelled out with URLs so credit can be passed on mechanically.
notesstring[]What the prices exclude — taxes, batch discounts, volume and context-length tiers — and which columns are estimates. Read this before comparing.
model_countnumberNumber of rows in `models`.
modelsobject[]The rows, documented below.
The columns
Every row carries 26 columns. The CSV has the same names in the same order.
Identity
Which model, sold by whom, in which market.
idstringStable row key. One model sold by two providers is two rows with two ids — that duplication is the cross-provider comparison.
namestringDisplay name as the provider markets it.
providerstringWho you actually buy from — the first party, a cloud, a host or a gateway. Cheapest means little if the seller routes your request elsewhere on different terms.
category"llm" | "video" | "tts" | "image" | "embeddings" | "stt"Which of the six markets the row belongs to.
unitstringThe billing unit the populated price column is measured in, e.g. "per 1M tokens". Read it before comparing two rows from different categories.
Price
Exactly one price shape is populated per row, matching the category's billing unit. Every other price column is null — that is the schema working, not missing data.
price_input_per_1mnumber | nullUSD / 1M input tokensInput leg for language models; embeddings are billed on this leg too, since they meter exactly like an LLM's input.
price_output_per_1mnumber | nullUSD / 1M output tokensOutput leg for language models.
price_cached_input_per_1mnumber | nullUSD / 1M cached input tokensCache-read rate where the provider publishes one. Null where feeds disagree on the leg — a contested cache rate is never exported as fact.
price_per_secondnumber | nullUSD / second of outputVideo generation.
price_per_imagenumber | nullUSD / generated imageImage generation.
price_per_1k_charsnumber | nullUSD / 1K input charactersSpeech synthesis.
price_per_audio_minutenumber | nullUSD / minute of audioTranscription.
Price provenance
Where the number came from and when. These columns are the reason the dataset exists: a price you cannot trace is a rumour.
price_sourcedbooleanTrue where the price came from real feeds or a provider's own page. False marks an illustrative sample — filter on this column before you quote anything.
price_sourcesstringSemicolon-separated feed names behind the number, e.g. "models.dev; LiteLLM". Empty where the row is not sourced.
price_retrieved_atstring | nullISO 8601 timestampWhen the primary feed snapshot was retrieved.
price_provider_verified_urlstring | nullThe provider's own pricing page, present where the figure was additionally read there by hand. The provider page outranks every aggregator feed.
price_notestring | nullPricing caveat the number is only true under, e.g. "base tier ≤200K context". Where this is set, the bare price is not the whole story.
Quality benchmark
Sourced measurements only. Where no published score exists the columns are null — a gap, never an invented number.
benchmark_labelstring | nullWhich scale the score is on — "Intelligence index", "Arena Elo", "MTEB mean" or "Avg WER". The units differ per category and are not comparable across them.
benchmark_scorenumber | nullThe measurement. Higher is better on every scale except Avg WER, which is an error rate: lower is better.
benchmark_ci95number | null95% confidence-interval half-width for arena Elo scores. Two models whose intervals overlap are not meaningfully ranked.
benchmark_sourcestring | nullWho measured it. Attribution is required wherever the score is republished — see the licence below.
benchmark_task_setstring | nullThe exact task set a mean is taken over, e.g. "MTEB(eng, v2)". A mean is only comparable within one task set, so the set travels with the number.
benchmark_retrieved_atstring | nullISO 8601 timestampWhen the leaderboard was read.
Derived estimate
Computed by us, named so you can tell. Not a citable measurement.
value_score_estnumber | null0–100Benchmark-per-dollar, normalised against the live catalogue's median. An estimate, carried under an _est name on purpose: it moves when the catalogue moves, so it is not a stable figure to cite. Null where a row has no score or no positive price.
Model facts
Everything else worth carrying in a row.
context_window_tokensnumber | nulltokensContext window, language models only.
release_datestring | nullISO 8601 datePublic availability date, where one is known.
What is deliberately not in it
- Invented numbers. Where no benchmark is published the score columns are null. Synthetic scores render on the site with an est tag and are never exported.
- Contested figures. A price its sources disagree on is not shipped as fact — it is held under review and listed on the methodology page.
- Price history. The API is a snapshot of current list prices. The history chart on the site is still an illustrative sample, so it is not exported — archive the daily snapshots yourself if you need a series.
- Negotiated or effective prices. These are provider list prices in USD, excluding taxes, batch discounts, volume tiers and context-length tiers except where
price_notesays otherwise. What you actually pay depends on your contract. - Latency, throughput and rate limits. Not measured here, so not claimed here.
Rate limit
The rule
10 requests per rolling 24 hours, counted per source IP address, across both endpoints combined.
It applies to /api/v1/* and to nothing else. Pages, charts, the sitemap and the OG images are never limited, so readers and crawlers never meet it.
Why 10 is not stingy
- A complete integration costs one request. There is no pagination, no per-model endpoint and no auth round-trip: request one returns all 4325 rows, across all 6 markets, with every provenance column attached. The limit is ten times what a correct client needs.
- The data changes once a day. A daily run rebuilds the catalogue and stamps it with a new
snapshot_id. Requests two through ten in the same day return byte-identical content. Polling faster cannot get you fresher numbers — it can only get you the same numbers again. - It is a fairness device, not a paywall. There is no paid tier to upsell you to, and no key to make you register for. The limit exists so one script cannot spend the bandwidth that thousands of readers share.
- The site is one small server. This runs on a single low-cost VM in the single digits of dollars a month, with no ads, no trackers and no sponsor. Each JSON response is 93 KB on the wire, so the quota still allots every address around 928 KB a day — generous per caller, survivable in aggregate. One unbounded scraper on a loop would move more than the readers do.
- Copying is encouraged; hammering is not. The compilation is CC BY 4.0. If you need the data constantly, take it once and serve it from your own infrastructure — that is explicitly allowed, and it is better for both of us than ten thousand requests a day.
What happens when you exceed it
You get 429 Too Many Requests with a JSON body naming the limit and a Retry-After header in seconds. The window is rolling, not a midnight reset: a slot frees exactly 24 hours after it was used, so a client that backs off recovers on its own. Nothing is blocked, banned or blacklisted.
Staying under it
- Fetch once a day and cache the result. Key your cache on
snapshot_id: if it has not moved, nothing in the payload has. - Send
If-None-Matchwith theETagyou were given. A304still spends a slot, but costs neither of us the payload. - Fetch server-side, not from every visitor's browser — otherwise your users share your quota and your tenth reader gets an error.
- Do not retry a
429immediately. HonourRetry-After; retrying in a loop just keeps the window full.
If 10 a day genuinely is not enough
Email ilyasubach@gmail.com and say what you are building. Raises are free. This matters most for shared addresses — an office, a university, a CI runner or a corporate NAT can burn ten requests between colleagues who have never met, and that is a flaw in per-IP counting rather than anything you did wrong.
Licence and attribution
The compilation is CC BY 4.0. Use it commercially, modify it, redistribute it — keep the credit. Cite as:
Model Price Book, snapshot 2026-08-03 — https://modelpricebook.comBenchmark scores are third-party measurements used under their own attribution requirements, and that credit travels with the data: every row names its measurer in benchmark_source, and the payload's attribution object spells out the sources in full. If you republish a score, you must carry that attribution too — the methodology page explains each source's terms.
Found a wrong number? Every figure here is meant to be checked — report it and it gets fixed in public.