COMPARE INFERENCE
2026A price index for the three things AI actually costs: GPUs, tokens and power.

Compare Inference is a daily median price index for AI infrastructure, across GPU rental, LLM tokens and wholesale electricity. It crawls what venues publish, takes a median across them, and shows the dispersion behind that median. Free, no account, with a public JSON API and a CSV export.
Every venue that rents an H100 publishes a price. None of them publish the same price. On the day this launched an H100 hour cost $1.95 at one provider and $4.15 at another, a spread of 112 percent, and there was no neutral place to see it.
Compare Inference crawls what venues publish, takes a median across them, and shows the dispersion behind that median. Three markets, one method.
| Market | Unit | Coverage today |
|---|---|---|
| Compute | USD per physical GPU-hour | 15 accelerator types, up to 26 providers each |
| Tokens | USD per 1M tokens, blended 3:1 input to output | 37 labs, 2,532 models |
| Power | USD per MWh, on-peak | 7 US trading hubs |
Free, no account, no login wall. There is a public JSON API and a CSV export, because a price index you cannot pull into a spreadsheet is a screenshot.
These are list prices across venues, not transacted prices, and the site says so on every surface that shows a number. Power is the exception: those are reported trades, published weekly, roughly eight days behind. Missing days stay missing. Nothing is interpolated.
The 25 percent price collapse that never happened
Backfilling 159 days of history from a third-party collection sounds like a data-loading task. It is not. That collection covered four venues quoting an H100. This one covers twenty-two.
Measured on the same snapshot, the median across neoclouds is $3.29, mixed $4.04, and across hyperscalers $15.16. A naive backfill writes 158 days near $4 to $5, then drops to $3.20 on the first self-measured day. That is a 25 percent collapse in the price of compute that never happened, and every type checker and test in the repository would have passed it without comment.
So the tier is a constant rather than a column, which makes the two collections identical in composition by construction. An automated gate compares each accelerator's last inherited day against its first self-measured one. Above 10 percent the two are not describing the same market, and that accelerator's history is withdrawn. On the real data three of nine broke the rule, at +25.8, -13.7 and -11.2 percent. Six kept 158 days each.
Inherited history is drawn as a separate dashed line in the same colour, with a note under the chart saying what the dash means and how far the two collections agree where they overlap.
The rest of the data pipeline
Eight adapters across three markets, each normalising to one shape. Fetching is the easy half. Every source lies in a different way.
- Prices arrive per instance, per node, in cents, or per GPU. Everything normalises to one physical GPU-hour, and no source's own per-GPU field is trusted.
- One historical dataset had 38,620 of 46,589 rows flagged CPU-only. Without that filter the median is zero.
- The same configuration appears once per region. One provider's H100 PCIe showed up twelve times at an identical price, so without deduplication the median is region-weighted and a single hyperscaler supplies half the rows.
- Implausible observations are quarantined rather than dropped, with the reason recorded. One catalogue error moved a part from $0.2253 to $3.5642 overnight.
- Unknown hardware names are skipped, never guessed. A mapper that guesses corrupts a series. One that skips leaves a gap that is visibly a gap.
Architecture
Four deployables, one repository, no servers to patch.
apps/web Vite SPA on Cloudflare Workers static assets
apps/api Hono on Cloudflare Workers, six JSON endpoints
apps/crawler Cloudflare Worker on a cron trigger, twice daily
packages/db Drizzle schema and the domain constants both sides share
The SPA and the API share one origin. Cloudflare matches the most specific route first, so cmpinf.com/api/* reaches the API worker and everything else falls through to the static assets. No CORS preflight on the site's own requests, and the session cookie for API tokens can stay SameSite=Lax.
Postgres runs on private infrastructure and is not reachable from the internet. The workers get to it through Cloudflare Tunnel with a Zero Trust Access policy in front, and Hyperdrive holding the pool. Both workers use the same binding, and not for symmetry: a raw Postgres socket cannot present an Access service token, and Hyperdrive can.
Stack
Frontend. React 19, TypeScript strict, Vite 7. TanStack Router for file-based routing with automatic code splitting, TanStack Query for the data layer, TanStack Table for the analytics tables. Tailwind v4, CSS-first, no JavaScript config. shadcn/ui over Radix primitives. Then one library per job: cmdk for the command palette, @number-flow/react for rolling price digits, d3-scale and d3-shape under a small custom chart kit, zod for every boundary the app does not control.
Backend. Hono on Workers, Drizzle ORM, postgres.js, Postgres 17. Tooling. pnpm workspaces and Turborepo, Biome, oxlint with a local plugin, Vitest and Playwright.
Two choices worth explaining.
Everything is in the URL. Domain, range, asset, model, comparison overlays, table sort, filter chips. A filtered, sorted view of 2,532 models is what someone wants to paste into a chat, and it survives a reload. Sort clicks push a history entry; typing in a filter replaces one, so six keystrokes do not cost six presses of Back.
The animation library is fetched, not imported. It arrives on the visitor's first pointer move or keystroke. It weighs 41.7 kB gzipped and every surface using it is a hover card or a dropdown, nothing that can be on screen before someone touches something. Naming the two exports instead of holding the namespace object took the chunk from 188.4 kB to 126.8 kB raw, because Rollup cannot drop an export something might read off the namespace later.
Performance
Measured on the deployed site rather than on a build server, three runs, median reported.
| Lighthouse | Score |
|---|---|
| Performance | 100 |
| Accessibility | 100 |
| Best Practices | 100 |
| SEO | 92 |
| Largest contentful paint | 589 ms |
| Total blocking time | 0 ms |
| Cumulative layout shift | 0.002 |
The eight missing SEO points are not the site's doing. Cloudflare writes a Content-Signal line into robots.txt, and Lighthouse flags the directive it does not recognise as an error. Those signals reserve rights under Article 4 of the EU copyright directive, which is worth more than the eight points, so they stay.
Two changes carried the paint timings, and the second only became visible once the first was fixed. The loader used to skip the price series entirely when the URL named no accelerator, which is every cold visit to the landing page: the catalogue had to land, the component had to render, an effect had to write the default into the URL, and only then did the chart ask for anything. It now guesses that default from the same preference list the component resolves against. Being wrong costs one unused request. Being late cost a serialised round trip on every visit.
Then the document itself preloads the four calls the landing page makes, so they leave with the HTML rather than after the bundle has parsed. Those hints carried crossorigin only when the API sat on another origin, on the reasoning that a same-origin fetch sends credentials an anonymous preload entry would not match. That is wrong: as="fetch" describes a request fetch() will make, and fetch() is CORS mode by default. Only production could show it, because development serves the API from another port and took the other branch, so the test passed while all four hints reported "preloaded but not used" on the live site and the whole optimisation was inert.
Prerendering all twelve routes was built and measured before either of those, and dropped. FCP improved by 390 ms, TBT by 23 ms, the score moved by one point, and LCP did not move at all, because prerendered HTML supplies no LCP candidate when there is no content in it yet. The measurement is written down so it is not rediscovered later.
Accessibility 100 took the most work. The focus ring shadcn ships composites to 1.54:1 against white where WCAG 2.2 asks for 3:1, so the token was recomputed to 7.4:1 in light and 5.6:1 in dark. The ticker's links carry an sr-only suffix rather than an aria-label, because any label explaining what the numbers mean puts words between them, and then the visible text is no longer a contiguous run of the accessible name, which breaks speech control. Every animation ships two variants, and under reduced motion the marquee becomes a horizontally scrollable price bar rather than a strip whose content is stuck behind overflow: hidden.
Quality gates
Four stages, each answering a different question.
- typecheck, lint, test, build. Does it compile and bundle.
- anti-slop. A local oxlint plugin with fifteen rules against the patterns unreviewed model output falls into: chained type assertions, widening to
unknownand asserting back, runtimetypeofguards for things the type already knows, conditional empty-object spreads. Any assertion that survives needs a// SAFETY:comment saying why it holds. - Review. Independent readers on the diff.
- Browser probe. Every route driven in a real browser, asserting zero error pages, zero console errors, zero 4xx and 5xx.
Only the last stage asks about the product. This project inherited that rule from a post-mortem where a starter kit shipped fourteen releases with every gate green while a fresh clone could not start, every page behind the login crashed, and a sign-in form put the password in the URL.
Three gate defects turned up while building:
- Biome was linting
test-results/.last-run.json, so lint went red after every Playwright run, on a file no human wrote. - Playwright's default worker count was killing wrangler's dev proxy. The default 8 workers on a 16-core machine killed it in 3 of 3 runs, response times climbing from 60 ms to 890 ms before every later request got ECONNREFUSED. Capped at 4, it survived 3 of 3. The suite had been reporting a machine limit as product failure.
- The browser suite declared no dependency on the app it tests, so its cache key never included the frontend. Editing a component and running the tests printed "FULL TURBO, 43 passed" without executing anything. It went green on a broken chart and on its fix alike.
The third one is why the SOP behind this project says a gate you have never seen fail is untested. The fix was verified by making the same edit twice and watching the task hash change.
How it was built
Solo, and that covers product decisions, data model, crawler, API, interface and operations. Agent-assisted, with the structure that makes that work rather than the structure that makes it fast.
Plan before code. Seventeen numbered plan documents, from the MVP through compute sources, enrichment and API tokens, each written and reviewed before implementation.
Delegated investigation, single-threaded implementation. Specialist agents produced the research: a TanStack review against the framework's own best practices, a performance report, an animation pass, an end-to-end audit, a design-engineering audit, a search and answer-engine review, three data-quality reports on the crawler, two production-readiness audits, and a deploy report. Fifteen such reports and audits sit in the repository. The implementation stayed in one place, because parallel agents editing the same files produce a merge nobody can review.
Skills as the review standard. The animation and interaction work was done against named skill sets rather than taste alone. The design-engineering audit is a read-only pass against those same rules, which makes it checkable instead of an opinion.
Cross-model review. Investigations and reviews ran across model families, so the reviewer of a change was not the model that wrote it.
What is next
- Spot pricing on the landing page. The data is already collected and it is 10 to 30 percent cheaper, but five of the tracked types have exactly one spot provider, and a median across providers over one provider is that company's price wearing the word median. It belongs where a provider count can sit beside every number.
- More power hubs. The current source has no ERCOT hub and only PJM's Western Hub. Those are the two regions where data centres are actually being built, so the site says that in as many words rather than quietly omitting it.
- Memory. Daily DRAM spot prices sit behind licences that forbid redistribution. A monthly index is possible, and it is a different, smaller claim.
Highlights
8 adapters across three markets, 5 sources with every licence recorded
110 unit tests and 43 end-to-end tests across 8 suites
Lighthouse 100 for performance, accessibility and best practices
3 Cloudflare Workers, no server to patch