1import type { Metadata } from 'next';2import Link from 'next/link';3import { DataTable, Td, Th } from '@/components/ui/data-table';4import { Container, Note, PageHeader, Section } from '@/components/ui/section';5import { Unavailable } from '@/components/ui/unavailable';6import { api, safe } from '@/lib/api';7import { PUBLIC_API_BASE, routes } from '@/lib/site';8import type { EntityDetail, Stats } from '@/lib/types';910export const metadata: Metadata = { title: 'Developers — public API', description: 'The AI Atlas public API: JSON over HTTPS, no key required for public routes, OpenAPI docs, live example responses and curl recipes.', alternates: { canonical: '/developers' } };11export const revalidate = 3600;1213const ENDPOINTS: { path: string; returns: string }[] = [14 { path: 'GET /health', returns: 'Service status (db, redis, llm) and version' },15 { path: 'GET /stats', returns: 'Live counters: entities per type, sources, documents, claims, events, prices, archive size — always computed from the database' },16 { path: 'GET /stats/history?days=90', returns: 'Daily counts' },17 { path: 'GET /search?q=&type=&limit=&offset=', returns: 'Natural-language or keyword search → { query: compiled filters, items: ranked entities (+rank), total }' },18 { path: 'GET /search/suggest?q=', returns: '≤ 8 prefix suggestions { id, entity_type, slug, name, organization_name }' },19 { path: 'GET /entities/{slug}', returns: 'Full entity detail: attributes, provenance per attribute, aliases, identifiers, relations (grouped), sources, timeline and type-specific blocks (prices, price_history, results, lineage, hardware_fit, models, papers, repositories)' },20 { path: 'GET /models/{slug} · /companies/{slug} · /papers/{slug} · /providers/{slug} · /benchmarks/{slug} · /hardware/{slug} · /frameworks/{slug} · /datasets/{slug} · /tools/{slug}', returns: 'Same detail, type-checked (404 if the slug is another type)' },21 { path: 'GET /entities/{slug}/timeline?limit=&before=', returns: '{ items: ChangeEvent[], next_before } — the entity’s own events (and, for companies, those of the models it develops)' },22 { path: 'GET /entities/{slug}/history?property=', returns: '{ items: Claim[] } — every claim ever recorded, newest first, with status current | superseded | conflicting | retracted, valid_from → valid_to, source and tier' },23 { path: 'GET /entities/{slug}/asof?date=YYYY-MM-DD', returns: '{ existed, first_seen_at, date, attributes, claims } — the entity as it was known on that date' },24 { path: 'GET /entities/{slug}/graph?depth=1|2&limit=80', returns: '{ root, nodes: { id, slug, name, entity_type, organization_name }[], edges: { source, target, predicate }[] } — capped neighbourhood' },25 { path: 'GET /entities/{slug}/sources · /related?limit=', returns: 'Documents describing the entity (deduped by URL) · related entities (same org / family / shared relations)' },26 { path: 'GET /models?q=&org=&family=&openness=&modality=&status=&min_params=&max_params=&min_context=&year_from=&year_to=&license=&sort=&order=&facets=1', returns: 'Paged models (+facets: organizations, openness, modalities, families, years, licenses, status)' },27 { path: 'GET /companies?q=&country=&kind=&sort=', returns: 'Paged organizations with model_count and paper_count (+facets)' },28 { path: 'GET /papers?q=&category=&org=&since=&until=&sort=', returns: 'Paged papers' },29 { path: 'GET /providers · GET /prices?model=&provider=&sort=¤t=1 · GET /prices/history?model=|provider=', returns: 'Providers with min prices · current price rows (USD / 1M tokens) · every price row incl. closed ones, oldest first' },30 { path: 'GET /prices/index?days=180', returns: '{ days, series: { day, median_input, median_output, min_input, max_input, models, offers }[], movers: ChangeEvent[] (PRICE_CHANGED), note }' },31 { path: 'GET /benchmarks · GET /benchmarks/{slug}/results?config=&history=1&limit=&offset= · GET /benchmarks/{slug}/history?model=', returns: 'Benchmarks with result_count, model_count and current top · leaderboard sorted by score (respecting higher_is_better), current rows only unless history=1, filterable by config key=value · one model’s results over time' },32 { path: 'GET /hardware?kind=&manufacturer=&min_memory=&sort= · GET /hardware/fit?memory_gb=&quant=4bit|8bit|fp16&context=8192&openness=&limit=', returns: 'Hardware listing · ESTIMATED fit: { inputs, estimated: true, assumptions[], counts, items: { model, parameter_count, estimated_memory_gb, headroom_gb, fits, quantization, note }[] }' },33 { path: 'GET /explore/types · GET /explore/{type}?q=&org=&sort=', returns: 'Entity types with live counts; generic paged listing for any type' },34 { path: 'GET /changes?category=&type=&entity_type=&importance_min=&since=&until=&q=&limit=&before= · GET /changes/daily?date= · GET /changes/categories?days=', returns: 'Change-event feed (cursor paged, next_before) · daily digest with sections · category counts' },35 { path: 'GET /timeline?entity=&year=&category=&limit=', returns: '{ items: { month, count, events[] }[], total } — grouped by month' },36 { path: 'GET /compare?ids=a,b[,…6]', returns: '{ entity_type, dimensions: { key, label, unit, kind: number|text|list|bool|date, source }[], items: { entity, values, provenance, prices?, results? }[] } — 2–6 entities of one type; models add best prices and the benchmarks shared by all' },37 { path: 'GET /diff?a=YYYY-MM-DD&b=YYYY-MM-DD&scope=all|models|org:<slug>|family:<name>', returns: '{ a, b, scope, new_entities[], gone_entities[], property_changes[], price_changes[], benchmark_changes[], counts }' },38 { path: 'GET /sources · GET /methodology · GET /trending?days=&limit=&type= · GET /sitemap?type=&limit=&offset=', returns: 'Transparency page data, vocabularies and tiers, most viewed entities, sitemap feed' },39];4041/** Compact, honest projection of /stats for the example block (the real payload has more counters). */42function statsExample(s: Stats) {43 return {44 entities: s.entities,45 entities_total: s.entities_total,46 sources: s.sources,47 connectors: s.connectors,48 claims_current: s.claims_current,49 change_events_24h: s.change_events_24h,50 benchmark_results: s.benchmark_results,51 prices_current: s.prices_current,52 last_event_at: s.last_event_at,53 computed_at: s.computed_at,54 '…': `${Object.keys(s).length} keys in total`,55 };56}5758function modelExample(d: EntityDetail) {59 const attrKeys = Object.keys(d.attributes ?? {}).slice(0, 8);60 const attributes: Record<string, unknown> = {};61 for (const k of attrKeys) attributes[k] = d.attributes[k];62 const provKey = attrKeys.find((k) => d.provenance?.[k]) ?? Object.keys(d.provenance ?? {})[0];63 const rest = Object.keys(d.attributes ?? {}).length - attrKeys.length;64 return {65 id: d.id,66 entity_type: d.entity_type,67 slug: d.slug,68 name: d.name,69 organization: d.organization,70 status: d.status,71 attributes: rest > 0 ? { ...attributes, '…': `${rest} more` } : attributes,72 provenance: provKey ? { [provKey]: d.provenance[provKey], '…': `one entry per attribute` } : {},73 quality: d.quality,74 counts: d.counts,75 relations: `${d.relations?.length ?? 0} groups`,76 prices: `${d.prices?.length ?? 0} current rows`,77 results: `${d.results?.length ?? 0} benchmark results`,78 timeline: `${d.timeline?.length ?? 0} events`,79 updated_at: d.updated_at,80 };81}8283function Code({ children }: { children: string }) {84 return (85 <pre className="scrollbar-thin mt-3 overflow-x-auto border border-rule bg-surface p-4 text-[12.5px] leading-relaxed text-ink">86 <code>{children}</code>87 </pre>88 );89}9091export default async function DevelopersPage() {92 const [health, stats, top] = await Promise.all([safe(api.health()), safe(api.stats()), safe(api.models({ limit: 2, sort: 'quality' }))]);93 const slugA = top?.items[0]?.slug;94 const slugB = top?.items[1]?.slug;95 const model = slugA ? await safe(api.entityOfType('models', slugA)) : null;96 const exA = slugA ?? '<model-slug>';97 const exB = slugB ?? '<other-slug>';98 const today = new Date().toISOString().slice(0, 10);99 const weekAgo = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10);100101 return (102 <Container>103 <PageHeader eyebrow="Developers" title="Public API" lede="Everything on this site comes from the same JSON API. Public routes need no key; responses are cached for 1–10 minutes and search is rate-limited per IP." aside={health ? <p className="text-sm text-ink-3">API <span className={health.status === 'ok' ? 'text-positive' : 'text-warning'}>{health.status}</span> · v{health.version}</p> : <p className="text-sm text-ink-3">API status unavailable</p>} />104105 <Section eyebrow="Base URL" title={<span className="mono text-lg md:text-xl">{PUBLIC_API_BASE}</span>} hairline={false}>106 <p className="text-sm text-ink-2">107 Interactive OpenAPI documentation: <a href="/api/v1/docs" className="link mono">/api/v1/docs</a>. JSON, UTF-8, ISO-8601 UTC timestamps. Errors are <span className="mono">{'{ "detail": "…" }'}</span> with 400 / 404 / 429 / 503 (422 for parameters rejected by validation).108 </p>109 <Code>{`# Live counters110curl -s ${PUBLIC_API_BASE}/stats | jq '.entities'111112# Natural-language search → compiled filters + ranked results113curl -s "${PUBLIC_API_BASE}/search?q=open+models+over+100B+released+in+2026" | jq '.query, .items[0:3]'114115# A model with provenance, prices, results, lineage116curl -s ${PUBLIC_API_BASE}/models/${exA} | jq '{name, attributes, provenance: (.provenance | keys)}'117118# Compare two models on every shared dimension (+ shared benchmarks, best prices)119curl -s "${PUBLIC_API_BASE}/compare?ids=${exA},${exB}" | jq '.dimensions[].key, .items[].values'120121# What changed between two dates (scope: all | models | org:<slug> | family:<name>)122curl -s "${PUBLIC_API_BASE}/diff?a=${weekAgo}&b=${today}&scope=models" | jq '.counts, .new_entities[0:3]'123124# Claim history and the state of a record as of a date125curl -s "${PUBLIC_API_BASE}/entities/${exA}/history?property=context_length" | jq '.items[] | {value, status, valid_from, valid_to, source_url}'126curl -s "${PUBLIC_API_BASE}/entities/${exA}/asof?date=${weekAgo}" | jq '{existed, attributes}'127128# Neighbourhood graph, price index, leaderboard by config, estimated hardware fit129curl -s "${PUBLIC_API_BASE}/entities/${exA}/graph?depth=1&limit=80" | jq '{nodes: (.nodes|length), edges: (.edges|length)}'130curl -s "${PUBLIC_API_BASE}/prices/index?days=90" | jq '.series[-1], (.movers|length)'131curl -s "${PUBLIC_API_BASE}/benchmarks/<benchmark-slug>/results?config=variant=v2.1&limit=20" | jq '.items[] | {model: .model.name, score, config}'132curl -s "${PUBLIC_API_BASE}/hardware/fit?memory_gb=64&quant=4bit&context=32768&limit=20" | jq '.assumptions, .items[0]'133134# What changed today135curl -s "${PUBLIC_API_BASE}/changes?importance_min=2&limit=20" | jq '.items[] | {event_type, summary, source_url}'`}</Code>136 </Section>137138 <Section eyebrow="Live examples" title="Example responses" lede="Fetched from the API when this page was rendered (revalidated hourly) and trimmed for display — not sample data.">139 <div className="grid gap-8 lg:grid-cols-2">140 <div className="min-w-0">141 <p className="mono text-sm text-ink">GET /stats</p>142 {stats ? <Code>{JSON.stringify(statsExample(stats), null, 2)}</Code> : <Unavailable what="Live /stats example" className="mt-3" />}143 </div>144 <div className="min-w-0">145 <p className="mono text-sm text-ink">GET /models/{exA}</p>146 {model ? <Code>{JSON.stringify(modelExample(model), null, 2)}</Code> : <Unavailable what="Live model example" className="mt-3" />}147 {model && (148 <Note className="mt-2">149 Trimmed: the full record has every attribute, a provenance entry per attribute, grouped relations, sources and the latest 30 events — <Link href={routes.entity(model)} className="link">see it rendered</Link> or <a href={`/api/v1/models/${encodeURIComponent(model.slug)}`} className="link">fetch the JSON</a>.150 </Note>151 )}152 </div>153 </div>154 </Section>155156 <Section eyebrow="Endpoints" title="Public routes">157 <DataTable caption="Public endpoints" compact>158 <thead><tr><Th>Route</Th><Th>Returns</Th></tr></thead>159 <tbody>160 {ENDPOINTS.map((e) => (161 <tr key={e.path}>162 <Td primary className="mono text-[12.5px] break-words">{e.path}</Td>163 <Td label="Returns" wide className="text-ink-2">{e.returns}</Td>164 </tr>165 ))}166 </tbody>167 </DataTable>168 <Note className="mt-3">Shapes are documented in the OpenAPI schema. Admin routes (<span className="mono">/admin/*</span>) require the <span className="mono">x-aia-admin-token</span> header and are not public.</Note>169 </Section>170171 <Section eyebrow="Conventions" title="Reading responses">172 <ul className="max-w-3xl list-disc space-y-1.5 pl-5 text-sm leading-relaxed text-ink-2">173 <li>Numeric aggregates may arrive as <span className="mono">string</span> (Postgres decimals): treat every numeric field as <span className="mono">number | string | null</span> and coerce.</li>174 <li><span className="mono">null</span> means the sources did not state it. Never read a missing field as zero, and never average two conflicting claims — the API returns both, flagged.</li>175 <li>Timestamps are ISO-8601 UTC. Dates without time (<span className="mono">release_date</span>, <span className="mono">knowledge_cutoff</span>) may be <span className="mono">YYYY</span>, <span className="mono">YYYY-MM</span> or <span className="mono">YYYY-MM-DD</span>, exactly as published.</li>176 <li>Units: tokens, USD per 1M tokens (<span className="mono">*_per_mtok</span>), GB, GB/s, W. Openness vocabulary: <span className="mono">open-weights | open-source | proprietary | restricted</span>.</li>177 <li><span className="mono">provenance</span> is an object keyed by attribute: <span className="mono">{'{ source_id, url, observed_at, tier (1 official → 4 unverified), confidence, extractor: deterministic | llm, unit? }'}</span>.</li>178 <li>Cursor feeds (<span className="mono">/changes</span>, <span className="mono">/entities/{'{slug}'}/timeline</span>) return <span className="mono">next_before</span>; pass it as <span className="mono">before=</span> for the next page (<span className="mono">null</span> at the end). Listings otherwise use <span className="mono">limit</span>/<span className="mono">offset</span>, capped at 200 (sitemap: 5 000).</li>179 <li>Parameters rejected by validation (bad type or bounds) return <span className="mono">422</span> with <span className="mono">{'{ detail, errors }'}</span>; semantic errors (unknown sort, bad date, wrong scope, ids count) return <span className="mono">400</span>.</li>180 <li>Anything marked <span className="mono">estimated</span> (hardware fit) is derived from published specs by a stated formula — the <span className="mono">assumptions</span> array is part of the response.</li>181 </ul>182 </Section>183184 <Section eyebrow="Terms" title="Fair use">185 <ul className="max-w-3xl list-disc space-y-1.5 pl-5 text-sm leading-relaxed text-ink-2">186 <li>Public routes are free to use without a key. Please cache responses and identify your client with a User-Agent that includes a contact address.</li>187 <li>Search is rate-limited per IP (HTTP 429 when exceeded). Higher limits and developer keys (<span className="mono">x-api-key</span>) will be available on request — see <Link href={routes.about()} className="link">contact</Link>.</li>188 <li>Attribution: “Data: AI Atlas (www.ai-atlas.co)” with a link. Every record carries its own upstream sources; please keep them when you redistribute.</li>189 <li>Never treat a missing field as zero. <span className="mono">null</span> means the sources did not state it.</li>190 </ul>191 </Section>192 </Container>193 );194}195