Search index
Last updated
What it is#
The API has no search endpoint, and query parameters are ignored. Instead, /api/v1/index is one compact file listing every chip, board, chassis, model, motherboard and riser with the fields most searches need. Download it once, cache it, and search it locally. It is the file the website's buyer's guide and Solution Builder, both MCP servers and the desktop app all search.
curl -s https://gpuninja.com/api/v1/indexThe file is at most 1,000,000 bytes, and about 100 KB at launch. Like every API file it is cached for five minutes and revalidates with an ETag.
The envelope#
{
"apiVersion": "v1",
"kind": "search-index",
"formatVersion": "1.0",
"asOf": "2026-10-04T06:12:00Z",
"datasetHash": "sha256:9f2c4e0d1b7a6c5e8f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e",
"speedReference": { "benchmark": "llama-cpp-tg128", "model": "llama-3-1-8b-instruct-q4-k-m" },
"counts": { "chip": 1, "board": 1, "chassis": 0, "model": 0, "motherboard": 0, "riser": 0 },
"entries": [
{
"slug": "ad102",
"kind": "chip",
"name": "NVIDIA GeForce RTX 4090",
"chipSlug": null,
"vendor": "NVIDIA",
"vramGb": 24,
"tdpW": 450,
"lengthMm": null,
"slotWidth": null,
"lowestPriceUsd": null,
"inStock": null,
"status": "active",
"pciVendorId": "10de",
"pciDeviceIds": ["2684"],
"socket": null,
"pcieGen": null,
"speedTokPerSec": null,
"vramType": null,
"powerConnectors": null,
"recommendedPsuW": null,
"widthMm": null
},
{
"slug": "asus-rog-strix-4090",
"kind": "board",
"name": "ASUS ROG Strix GeForce RTX 4090 OC",
"chipSlug": "ad102",
"vendor": "ASUS",
"vramGb": 24,
"tdpW": 450,
"lengthMm": 357.6,
"slotWidth": 3.5,
"lowestPriceUsd": 1999.99,
"inStock": true,
"status": "active",
"pciVendorId": null,
"pciDeviceIds": [],
"socket": null,
"pcieGen": null,
"speedTokPerSec": 128.4,
"vramType": "GDDR6X",
"powerConnectors": [{ "type": "12V-2x6", "count": 1 }],
"recommendedPsuW": 1000,
"widthMm": 149.3
}
]
}formatVersionisMAJOR.MINOR. The major number matches the API version; a minor bump only adds properties.asOfis the latest time any value of an indexed entity was retrieved or verified, ornullfor an empty catalogue. There is no build timestamp.datasetHashchanges if, and only if, some published value changed. Compare it with your cached copy to decide whether anything needs re-reading.speedReferencenames what a board'sspeedTokPerSecmeasures: the llama.cpp text-generation benchmarkllama-cpp-tg128on the modelllama-3-1-8b-instruct-q4-k-m. It is fixed for v1.countsgives the number of entries of each kind.entriesis sorted by kind (chip, board, chassis, model, motherboard, riser), then by slug.
Fields per kind#
Every entry has every field. A field that doesn't apply to a kind is always null, or [] for pciDeviceIds. A field that does apply is also null when the value isn't recorded: a model with no recorded minimum VRAM, for example, stays in the index with vramGb: null.
| Field | chip | board | chassis | model | motherboard | riser |
|---|---|---|---|---|---|---|
slug, kind, name | yes | yes | yes | yes | yes | yes |
chipSlug | null | the board's chip | null | null | null | null |
vendor | vendor | vendor | vendor | the model's publisher | vendor | vendor |
vramGb | maximum VRAM | VRAM fitted | null | minimum VRAM required | null | null |
tdpW | reference TDP | board TDP | null | null | null | null |
lengthMm | null | card length | null | null | null | cable length |
slotWidth | null | card slot width | null | null | null | null |
lowestPriceUsd | null | lowest price | lowest price | null | null | null |
inStock | null | in stock | in stock | null | null | null |
status | status | status | status | always active | status | status |
pciVendorId | PCI vendor ID | null | null | null | null | null |
pciDeviceIds | PCI device IDs, sorted | [] | [] | [] | [] | [] |
socket | null | null | null | null | its socket's slug | null |
pcieGen | null | null | null | null | null | PCIe generation |
speedTokPerSec | null | tokens per second on the speed reference | null | null | null | null |
vramType | null | memory type, such as GDDR7 | null | null | null | null |
powerConnectors | null | each connector's type and count | null | null | null | null |
recommendedPsuW | null | recommended power supply | null | null | null | null |
widthMm | null | the card's height from the slot | null | null | null | null |
lowestPriceUsd and inStock follow the same rules as in the detail documents: discontinued boards and chassis have null and false. Case clearances are deliberately not in the index, so a "length at most" filter can never match a case. Ask fit questions of the compatibility documents instead. A riser's lengthMm is a cable length, so filter by kind before using a length limit meant for cards. Platforms and benchmarks are not in the index. Neither are sockets: /api/v1/sockets lists them all.
A board's speedTokPerSec comes from the board's own result for the speed reference, or, when it has none, from its chip's reference-design result; the most recent run counts. It is null when neither exists, and a discontinued board keeps its figure. powerConnectors keeps the order the card lists them in; [] means the card is powered by its slot alone, and null that its connectors aren't recorded. The detail document gives the same connectors as one display string. A board entry doesn't repeat its chip's vendor and name: read them from the chip entry whose slug is the board's chipSlug.
Motherboard and riser entries carry only what the Solution Builder's part picker needs. A motherboard's PCIe slots, a riser's lanes, and both their offers are in their detail documents.
PCI IDs are four lower-case hex digits with no 0x prefix, as in "10de". A device ID is unique only within its vendor, so match on both.
Searching it yourself#
For simple questions, filter the entries directly. Treat null as "not recorded", and decide whether you want discontinued hardware:
const response = await fetch("https://gpuninja.com/api/v1/index");
const index = await response.json();
// Active graphics cards with at least 16 GB of VRAM, no longer than 320 mm.
const boards = index.entries.filter(
(entry) =>
entry.kind === "board" &&
entry.status === "active" &&
entry.vramGb !== null && entry.vramGb >= 16 &&
entry.lengthMm !== null && entry.lengthMm <= 320,
);To answer "which cards fit this case" or "which cards can run this model", fetch that case's fits or that model's runs-on document and use its slugs as an allowlist over the index.
The open-source query library#
GPU Ninja's own search code is open source under Apache 2.0, in the gpuninja-mcp repository at src/query/, together with the type declarations it uses at src/types/. It is plain TypeScript with no dependencies and no I/O, and runs unchanged in browsers, Workers and Node. You fetch and parse the index yourself, then:
import { loadIndex, search } from "./src/query/index.ts";
const loaded = loadIndex(await (await fetch("https://gpuninja.com/api/v1/index")).json());
if (!loaded.ok) throw new Error(loaded.error.code);
const result = search(loaded.value, {
kind: "board",
text: "rtx 4090",
filter: { op: "range", field: "lengthMm", max: 340 },
limit: 20,
});
if (result.ok) {
for (const hit of result.value.items) console.log(hit.entry.name, hit.score);
}What the library does, so you can match it or simply use it:
- One kind per query.
kindischip,board,chassis,model,motherboardorriser. - Discontinued entries are excluded by default. Set
includeDiscontinued: trueto include them. A filter that asks forstatusdiscontinuedwithout it is rejected withdiscontinued_excluded, rather than silently returning nothing. - Filters are a small tree of
and,orandnotoverrange(inclusive bounds on a numeric field),in(equality onslug,vendor,chipSlug,socket,status,vramType,powerConnectorTypeorchipVendor, ignoring case, accents, symbols and surrounding spaces),is(oninStock),has(the field is notnull) andpci(chips by PCI device ID, optionally with vendor ID, accepting forms such as0x10DE). A comparison against anullfield is false, sonotof it is true. A field that doesn't apply to the kind, such aslengthMmon a chassis query, is a validation error, not an empty result. - Board-only fields. Besides the index's own board fields, the library works out three per board when it loads the index:
pricePerVramGb(lowestPriceUsddivided byvramGb),chipVendor(the vendor of the board's chip entry) andpowerConnectorType(aninfilter matches when any of the card's connectors has that type). They can be filtered, andpricePerVramGbsorted, but they never appear in a result'sentry. A board with no value for a range filter never matches it. - Text matching splits text into runs of letters and runs of digits, so
rtx4090finds "RTX 4090", after folding case and accents. It searchesname,vendor,chipSlugandslug, weighted 4, 2, 2 and 1. Every word in the query must match something. A word matches exactly, or as a prefix when it is at least two characters long. All-letter words of five or more letters also tolerate one typo (an adjacent swap counts as one), and words of nine or more tolerate two. Numbers never match fuzzily:4080never finds a 4090. Two or more words found side by side, in order, in the name earn a phrase bonus, and a query equal to the whole name earns an exact-name bonus. Scores are whole numbers. - Ordering is by relevance then name when there is text, and by name otherwise. Every sort ends with the slug, so results are fully deterministic and never depend on the order of the index. You can give up to three sort keys from the fields sortable for the kind (below);
nullvalues always sort last, ascending or descending. Names compare by their folded form, by character code, never by locale. - Pagination uses an opaque
nextCursor, which isnullon the last page. A cursor records the last item's sort position, not an offset, so it stays valid across a deploy: you won't see an entry twice or miss one whose sort position didn't change. It also survives a change oflimit. A cursor used with a different query, or minted before a change to the library's matching or ordering rules, is rejected withcursor_mismatch; start again from the first page. - Errors are values, not exceptions.
loadIndexreturns oneIndexErrorwith acodeand a JSON Pointerpath.validateQueryandsearchreturn every problem with the query (up to 20) as issues, each with acode, apathand a plain-Englishmessage.
The limits and defaults, as the library defines them:
| Search fact | Value | Meaning |
|---|---|---|
LIMITS.defaultLimit | 20 | Results per page when you don't set limit |
LIMITS.maxLimit | 1000 | The largest limit you can ask for |
LIMITS.maxTextLength | 200 | Characters of text |
LIMITS.maxTextTokens | 16 | Distinct words in text |
LIMITS.maxSortKeys | 3 | Sort keys, not counting the final slug |
LIMITS.maxFilterDepth | 8 | Levels of nesting in a filter |
LIMITS.maxFilterNodes | 64 | Nodes in a filter |
LIMITS.maxInValues | 10000 | Values in one in node |
LIMITS.maxIssues | 20 | Issues returned for an invalid query |
DEFAULT_SORT.withText | [{"by":"relevance","dir":"desc"},{"by":"name","dir":"asc"}] | Order when the query has text |
DEFAULT_SORT.withoutText | [{"by":"name","dir":"asc"}] | Order otherwise |
SORT_FIELDS.chip | ["relevance","name","vramGb","tdpW"] | Sortable fields for chips |
SORT_FIELDS.board | ["relevance","name","lowestPriceUsd","vramGb","tdpW","lengthMm","slotWidth","speedTokPerSec","pricePerVramGb","recommendedPsuW","widthMm","inStock"] | Sortable fields for boards |
SORT_FIELDS.chassis | ["relevance","name","lowestPriceUsd","inStock"] | Sortable fields for chassis |
SORT_FIELDS.model | ["relevance","name","vramGb"] | Sortable fields for models |
SORT_FIELDS.motherboard | ["relevance","name"] | Sortable fields for motherboards |
SORT_FIELDS.riser | ["relevance","name","lengthMm","pcieGen"] | Sortable fields for risers |
includeDiscontinued | false | Discontinued entries are left out unless you ask |
The library accepts any index with the same major formatVersion. A newer minor version loads, with unknown properties ignored.
Or let an assistant search#
The MCP servers expose this same search as tools for AI assistants, with the same matching, filters and ordering. Their page sizes and cursors are their own, and are described there.