# Catalogue API

GET /api/store/catalog returns every listing, its MLX Score and its picture as one JSON string signed by the sequencer's Ed25519 key, so an app can check the catalogue came from mlx.app.

```endpoint try
GET /api/store/catalog
```

No key, no parameters. The route is `app/api/store/catalog/route.ts`; it reads the live catalogue (`liveStore` in `lib/store/runtime.ts`): the listings in git, plus listings approved in Admin since and any flags, with the scores worked out from the signals read so far.

## Response

| Field | Type | Notes |
| --- | --- | --- |
| `payload` | string | The catalogue as a JSON string. Parse it after you check `sig`. |
| `sig` | string | Ed25519 signature, hex, over SHA-256 of `"mlx-store-v1\n" + payload`. |
| `key` | string | The sequencer's public key, hex: the same key that signs every block (`sequencer` in `GET /api/log`). |
| `rev` | string | 16 hex characters that change when a listing is added, changed, flagged or removed. |
| `at` | number | When the response was built, in ms. |

`payload`, parsed:

| Field | Type | Notes |
| --- | --- | --- |
| `rev` | string | The same as the outer `rev`. |
| `entries` | array | Every listing shown in the store (status `listed` or `flagged`), sorted by name. The fields are in `lib/store/types.ts` (`StoreEntry`). A `flagged` entry carries `flag` {reason, url?, since}: a security advisory or a broken download, shown as a banner, installs paused. |
| `scores` | object | slug → MLX Score, 1.0–5.0 to one decimal. A listing with no public signal yet has no score. See [MLX Score](/store/score). |
| `icons` | object | slug → the picture the store read for it (App Store or Mac App Store artwork, else its GitHub organisation's picture), when there is one. |

The response is cached for a minute in the browser and five minutes at the edge. MLX Node's heartbeat reply carries `storeRev`, so a Mac fetches the catalogue again only when it changes.

## Example

The response from production on 1 Nov 2026, with `payload` left out:

```json title="Response"
{
  "sig": "018ff95d8870506b97361f4596b3a649dbb4a2d66b6a376d79a2b9ebb526f4e34c433396f3511e0f324dce1afea9129b24e188561ae37aad34f4c3f3f7f7910e",
  "key": "92114fe09a90d1a3aced63f35463e4120969d1b323c41070a9ee3ca3cf6ae9a3",
  "rev": "da93e9c6660e759d",
  "at": 1793561537882
}
```

Its `payload`, parsed and trimmed to one of 138 entries (Jan), with two of its six sources and one of its three paragraphs, and `scores` and `icons` trimmed to that entry:

```json title="payload"
{
  "rev": "da93e9c6660e759d",
  "entries": [
    {
      "slug": "jan",
      "name": "Jan",
      "maker": "Menlo Research",
      "kind": "mac-app",
      "category": "run-local",
      "sub": ["app", "chat", "server"],
      "tags": ["gguf", "mlx", "local-api", "openai-compatible", "mcp"],
      "pricing": { "model": "free" },
      "license": "Apache-2.0",
      "data": "both",
      "platforms": { "macos": "13.6", "ios": null },
      "needs": { "memGB": 8, "chip": "M1" },
      "install": [
        { "via": "brew-cask", "id": "jan" },
        { "via": "web", "url": "https://www.jan.ai/download" }
      ],
      "detect": { "bundleIds": ["jan.ai.app"] },
      "links": { "site": "https://www.jan.ai/", "github": "https://github.com/janhq/jan", "docs": "https://www.jan.ai/docs/desktop" },
      "signals": { "github": "janhq/jan", "brewCask": "jan" },
      "summary": "An open-source chat app that runs models on your Mac with a bundled llama.cpp engine, with a local API and optional cloud models.",
      "body": ["Jan is free and open source (Apache 2.0). Chats, models and settings are stored on your Mac; a prompt only leaves it if you choose a cloud provider."],
      "sources": [
        { "url": "https://github.com/janhq/jan/blob/main/LICENSE", "what": "licence Apache-2.0", "checked": "2026-11-01" },
        { "url": "https://formulae.brew.sh/api/cask/jan.json", "what": "Homebrew cask jan, bundle id jan.ai.app", "checked": "2026-11-01" }
      ],
      "checked": "2026-11-01",
      "status": "listed"
    }
  ],
  "scores": { "jan": 1.9 },
  "icons": { "jan": "https://avatars.githubusercontent.com/u/102363196?v=4&s=128" }
}
```

## Check the signature

Hash the exact `payload` string, as received, with the prefix `mlx-store-v1` and a newline in front, and verify `sig` over the 32-byte digest with `key`. Then compare `key` with the sequencer key you already trust (from `GET /api/log`, or pinned the first time). Parse `payload` only after both checks pass.

```js tab=JavaScript
import { createHash, createPublicKey, verify } from "node:crypto";

const r = await (await fetch("https://mlx.app/api/store/catalog")).json();
const digest = createHash("sha256").update("mlx-store-v1\n" + r.payload).digest();
const key = createPublicKey({ key: Buffer.concat([Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(r.key, "hex")]), format: "der", type: "spki" });
if (!verify(null, digest, key, Buffer.from(r.sig, "hex"))) throw new Error("bad signature");
const catalog = JSON.parse(r.payload);
```

```python tab=Python
import hashlib, json, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey

r = json.load(urllib.request.urlopen("https://mlx.app/api/store/catalog"))
digest = hashlib.sha256(("mlx-store-v1\n" + r["payload"]).encode()).digest()
Ed25519PublicKey.from_public_bytes(bytes.fromhex(r["key"])).verify(bytes.fromhex(r["sig"]), digest)  # raises if bad
catalog = json.loads(r["payload"])
```

## Errors

If the catalogue can't be built, the answer is `503` with `{ "error": "<message>" }` and `Cache-Control: no-store`. Keep the last good copy and try again later; MLX Node keeps its last good copy on disk.
