Skip to content
GPU Ninja

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/index

The 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
    }
  ]
}
  • formatVersion is MAJOR.MINOR. The major number matches the API version; a minor bump only adds properties.
  • asOf is the latest time any value of an indexed entity was retrieved or verified, or null for an empty catalogue. There is no build timestamp.
  • datasetHash changes if, and only if, some published value changed. Compare it with your cached copy to decide whether anything needs re-reading.
  • speedReference names what a board's speedTokPerSec measures: the llama.cpp text-generation benchmark llama-cpp-tg128 on the model llama-3-1-8b-instruct-q4-k-m. It is fixed for v1.
  • counts gives the number of entries of each kind.
  • entries is 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.

Fieldchipboardchassismodelmotherboardriser
slug, kind, nameyesyesyesyesyesyes
chipSlugnullthe board's chipnullnullnullnull
vendorvendorvendorvendorthe model's publishervendorvendor
vramGbmaximum VRAMVRAM fittednullminimum VRAM requirednullnull
tdpWreference TDPboard TDPnullnullnullnull
lengthMmnullcard lengthnullnullnullcable length
slotWidthnullcard slot widthnullnullnullnull
lowestPriceUsdnulllowest pricelowest pricenullnullnull
inStocknullin stockin stocknullnullnull
statusstatusstatusstatusalways activestatusstatus
pciVendorIdPCI vendor IDnullnullnullnullnull
pciDeviceIdsPCI device IDs, sorted[][][][][]
socketnullnullnullnullits socket's slugnull
pcieGennullnullnullnullnullPCIe generation
speedTokPerSecnulltokens per second on the speed referencenullnullnullnull
vramTypenullmemory type, such as GDDR7nullnullnullnull
powerConnectorsnulleach connector's type and countnullnullnullnull
recommendedPsuWnullrecommended power supplynullnullnullnull
widthMmnullthe card's height from the slotnullnullnullnull

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. kind is chip, board, chassis, model, motherboard or riser.
  • Discontinued entries are excluded by default. Set includeDiscontinued: true to include them. A filter that asks for status discontinued without it is rejected with discontinued_excluded, rather than silently returning nothing.
  • Filters are a small tree of and, or and not over range (inclusive bounds on a numeric field), in (equality on slug, vendor, chipSlug, socket, status, vramType, powerConnectorType or chipVendor, ignoring case, accents, symbols and surrounding spaces), is (on inStock), has (the field is not null) and pci (chips by PCI device ID, optionally with vendor ID, accepting forms such as 0x10DE). A comparison against a null field is false, so not of it is true. A field that doesn't apply to the kind, such as lengthMm on 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 (lowestPriceUsd divided by vramGb), chipVendor (the vendor of the board's chip entry) and powerConnectorType (an in filter matches when any of the card's connectors has that type). They can be filtered, and pricePerVramGb sorted, but they never appear in a result's entry. 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 rtx4090 finds "RTX 4090", after folding case and accents. It searches name, vendor, chipSlug and slug, 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: 4080 never 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); null values always sort last, ascending or descending. Names compare by their folded form, by character code, never by locale.
  • Pagination uses an opaque nextCursor, which is null on 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 of limit. A cursor used with a different query, or minted before a change to the library's matching or ordering rules, is rejected with cursor_mismatch; start again from the first page.
  • Errors are values, not exceptions. loadIndex returns one IndexError with a code and a JSON Pointer path. validateQuery and search return every problem with the query (up to 20) as issues, each with a code, a path and a plain-English message.

The limits and defaults, as the library defines them:

Search factValueMeaning
LIMITS.defaultLimit20Results per page when you don't set limit
LIMITS.maxLimit1000The largest limit you can ask for
LIMITS.maxTextLength200Characters of text
LIMITS.maxTextTokens16Distinct words in text
LIMITS.maxSortKeys3Sort keys, not counting the final slug
LIMITS.maxFilterDepth8Levels of nesting in a filter
LIMITS.maxFilterNodes64Nodes in a filter
LIMITS.maxInValues10000Values in one in node
LIMITS.maxIssues20Issues 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
includeDiscontinuedfalseDiscontinued 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.

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.