Developers
Read our lab results as JSON. Same numbers as the boards, taken every hour on our lab machine. No key, no account.
Before you start
- Plain GET, open to all
- Every path below answers a normal GET. There is no key, no token, and no rate-limit header. Nothing here writes anything.
- Open and uncached
- Every data route sends
Access-Control-Allow-Origin: *andCache-Control: no-store. We skipOPTIONSpreflight, so keep request headers simple. Our numbers change every hour — cache on your side and do not poll faster. - Windows and rows
- Most routes take
?hours=(bad values fall back to the default; anything over 720 is clamped)./api/runstakes?limit=instead. Typical speed summaries use completed, correct current or raw-recovered compatible speed rows, including a served rate-limit warning; raw samples hold everything, fails included.
Your first call
No setup step comes before this. Paste either one and you have our newest numbers.
curl -s "https://benchalert.com/api/status"
const res = await fetch("https://benchalert.com/api/speed?hours=24");
const { models } = await res.json();
// Models with enough data are sorted by shortest typical total wait.
console.log(
models[0]?.model,
models[0]?.publishedLatencyMs,
models[0]?.publishedVisibleWallTokensPerSec,
);Data endpoints
Six JSON routes. Every board on this site is built from these and nothing else. Each entry folds a trimmed real response under Sample response.
- /api/today
The daily card as JSON: who wrote fastest in the last 24 hours, the featured ranking, a one-line hook, and ready-made share text.
- Query
- None.
- Returns
- generatedAt, timezone, harnessVersion, dateLabel, weather, fastest, fastestOverall, ranking, deltas, pingLeader, hook, shareText, shareUrl
- Example
- /api/today
Sample response
{ "generatedAt": "2026-08-19T09:55:15.025Z", "harnessVersion": 5, "dateLabel": "Aug 19, 2026", "weather": { "level": "many", "label": "Lots of limits", "count": 14 }, "fastest": { "provider": "claude", "model": "fable", "publishedLatencyMs": 4200, "publishedVisibleWallTokensPerSec": 61.4 }, "ranking": [ "… one entry per model, fastest first …" ], "hook": "Faster than every featured model.", "shareText": "… ready-made text you can post as-is …" } - /api/speed
The speed board plus every sample behind it. models holds one summary per pinned model, sorted the same way the board is. samples holds the raw rows.
- Query
- hours — how far back to look. Default 24, max 720.
- Returns
- generated_at, timezone, prompt, window_hours, models, samples
- Example
- /api/speed?hours=24
Sample response
{ "window_hours": 24, "prompt": "Count from 1 to 120 in plain text …", "models": [{ "provider": "claude", "model": "fable", "effort": "high", "harnessVersion": 0.2, "publishedLatencyMs": 4200, "publishedVisibleWallTokensPerSec": 61.4, "latencyIntervalLowMs": 3900, "latencyIntervalHighMs": 4700, "served": 23, "scheduled": 24, "coverage": 1, "availability": 0.96, "medianTtftMs": 4839, "publishable": true, "modelVerified": true }], "samples": [ "… every raw row, fails included …" ] } - /api/probes
Our ping log: one row per tool per round, with how long the tiny ok check took and whether it worked. This is the data behind the day strip on the home board.
- Query
- hours — how far back to look. Default 24, max 720.
- Returns
- generated_at, timezone, probe, window_hours, samples
- Example
- /api/probes?hours=24
Sample response
{ "probe": "Reply with exactly: ok", "window_hours": 24, "samples": [{ "started_at": "2026-08-19T09:00:04.786Z", "agent": "claude", "latency_ms": 5300, "answer_text": "ok", "status": "ok", "error_class": "ok" }] } - /api/runs
The run log: every time our lab started a round, what it aimed at, what came back, and the tokens it spent. Newest first. Every run and every result also carries cost_usd (the app's own figure, null when it reports none) and api_dollars (our recomputation at list price).
- Query
- limit — how many runs to return. Default 100, max 2000. This route takes no hours.
- Returns
- generated_at, timezone, total_runs, runs
- Example
- /api/runs?limit=100
Sample response
{ "total_runs": 41, "runs": [{ "run_id": "2026-08-19T09:42:03.296Z-57988", "kind": "pulse", "harness_version": 5, "recorded": 1, "skipped": 0, "failed": 0, "output_tokens": 282, "cost_usd": 0.0412, "api_dollars": 0.0412, "results": [ "… one line per target …" ] }] } - /api/quota
The quota benchmark: one row per plan, per usage window, per UTC day, saying what 100% of that window is worth at the vendor's published API list price. Rows come back oldest first. providers names each plan and carries the vendor's own figure where one exists.
- Query
- days — how many trailing days to return, counted from the newest day in the file. Default 90, max 730. provider — claude, codex, or grok. window — one window key, e.g. seven_day.
- Returns
- generated_at, published_at, timezone, version, quota_method_version, pricing_version, harness_version, window_days, total_days, providers, days
- Example
- /api/quota?days=90
Sample response
{ "quota_method_version": 1, "pricing_version": "2026-08-21", "providers": { "codex": { "label": "ChatGPT Pro ($100, 5×)", "price_usd_month": 100, "official_usd_week": 600, "bundled": [] } }, "days": [{ "day": "2026-08-20", "provider": "codex", "window": "primary", "window_seconds": 604800, "reset_at": "2026-08-25T09:00:00.000Z", "Q_api_usd": 592.4, "ci_low": 568.7, "ci_high": 627.9, "ci_level": 0.95, "n_clean_intervals": 27, "estimator": "upper-envelope-p95", "quality_grade": "A", "lower_bound_only": false, "spend_lab_usd": 0.63, "util_eod": 0.42, "util_readings": 92, "burn_lab_pct_of_window": 0.00106, "official_usd": 600, "flags": [] }] } - /api/status
Lab health: the last pulse, whether the login check passed, recent rate limits, how old the pulse is, and when the next round is due.
- Query
- None.
- Returns
- generatedAt, lastPulse, preflight, recentRateLimits, lastErrorClass, pulseAgeMs, nextSlotAt
- Example
- /api/status
Sample response
{ "generatedAt": "2026-08-19T09:55:14.763Z", "preflight": { "ok": true, "claude": { "loggedIn": true, "subscription": "max" }, "codex": { "ok": true, "method": "chatgpt" }, "grok": { "loggedIn": true } }, "recentRateLimits": [ "… newest first …" ], "pulseAgeMs": 779000, "nextSlotAt": "2026-08-19T10:00:00.000Z" }
Fields on a sample
The rows in /api/probes, /api/speed and /api/runs gained these in August 2026. They are additive: nothing that was there before moved or changed meaning, and an older row simply does not carry them.
- model_verification_method
- How the model identity was confirmed. Codex rows require an exact CLI pin plus an exact match in the authenticated account's model catalog before timing. If model_verified is not true, that row cannot publish a speed.
- visible_wall_tokens_per_sec
- Visible answer tokens divided by the full CLI start-to-finish time. This is the public writing-speed unit. wall_tokens_per_sec remains in raw rows as a diagnostic that includes hidden reasoning and must not be used for a public rank.
- publishedLatencyMs / publishedVisibleWallTokensPerSec
- The typical public figures in a model summary. Both stay null until 12 distinct scheduled checks completed correctly. Models with figures are sorted by publishedLatencyMs, shortest first.
- coverage / availability
- Coverage is tried scheduled slots divided by all scheduled slots. Availability is completed correct answers divided by all scheduled slots. A skipped app therefore cannot look perfect.
- sequence_position / sequence_total
- Where that target ran in the round. The order rotates each hour so one model is not always first or last.
- api_dollars
- What this request is worth at the vendor's published API list price, recomputed by us from the token counts. Null when our price table has no row for the model — never 0. This is the only dollar field the three apps share, and it is what the quota board sums.
- cost_usd
- The cost the app itself reported with the answer. Claude and Grok send one; Codex sends none, so the field is null on every Codex row. Null means not measured, not free.
- rate_limit
- The plan's usage gauge as the app reported it on this request: status, rate_limit_type (which window), utilization (0–1 of the window used), resets_at, surpassed_threshold, and the overage fields. Optional, and the three apps report different subsets.
- service_tier
- Which service tier served the request, when the app names one. It matters because some tiers draw on the quota at a different rate than standard.
- usage_incomplete
- True when the app's own usage report was missing a field we needed. Rows like this are kept, and the totals on /logs say how many there were.
Community reports
Opinion data from the Share your take button. It is a separate module and it can be switched off.
The visitor-reports module is switched off right now, so /api/sentiment answers 404 with { "enabled": false, "summaries": [] }. Those votes are opinion and never mix into our lab numbers.
Files, feeds, and images
Not JSON, but public and live in the same way.
- /badgeimage/svg+xml
A live SVG badge, 20 pixels tall. With no model it shows the current fastest. Add ?model= with a slug from the table below to pin one row.
- /feed.xmlapplication/rss+xml
RSS 2.0. One item, rebuilt on every request, holding the current daily card and its share text. Poll it like a status feed, not an archive.
- /llms.txttext/plain
A plain-text summary written for AI agents: what we measure, the current test kit version, every page worth reading, the machine endpoints, and what each unit means.
- /today/opengraph-imageimage/png
The share image for the daily card, drawn with live numbers. No parameters.
- /api/ogimage/png
A compare share image for two models over the last 24 hours. Takes a and b only, defaulting to fable and sol. There is no c here, even though the compare page takes one.
The live badge
Paste it in a README or a status page. It redraws itself with our newest number every time it loads.
Markdown:
 
Model slugs
Use any of these in ?model= on the badge, in ?a=, ?b=, and ?c= on Compare, and in the /models/ address.
Featured
- opus
Opus 5 · claude · we ask for claude-opus-5
- fable
Fable 5 · claude · we ask for claude-fable-5
- sol
GPT-5.6 Sol · codex · we ask for gpt-5.6-sol
- grok-4.6
Grok 4.6 · grok · we ask for grok-4.6
Other models
- haiku
Haiku 4.5 · claude · we ask for claude-haiku-4-5
- sonnet
Sonnet 5 · claude · we ask for claude-sonnet-5
- luna
GPT-5.6 Luna · codex · we ask for gpt-5.6-luna
- terra
GPT-5.6 Terra · codex · we ask for gpt-5.6-terra
- gpt-5.5
GPT-5.5 · codex · we ask for gpt-5.5
- grok-4.5
Grok 4.5 · grok · we ask for grok-4.5
The full model id works too — ?model=claude-fable-5 lands on the same row as ?model=fable — and an unknown slug is not an error: the page just renders with no data. Retired models keep their slug and their old samples, but no new rows arrive.
If you republish our numbers
- Say the number came from BenchAlert and link the page or endpoint you took it from.
- Name the test kit version and the window, for example: BenchAlert, test kit v0.2, 24-hour window.
- Do not present our numbers as official vendor figures, and do not edit a number and keep our name on it. The rest of the rules are on Terms and Brand.
How to read this
These feeds carry our own measurements from one lab machine on one home connection, not the official speed of any model. There is no uptime promise: the lab can be down and a window can be empty. Write your client so an empty array is a normal answer, not a failure. Test kit bumps are announced on Changelog — watch it before you pin a parser.