MLXDocs
AI App Store

Catalogue API

Markdown

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.

GEThttps://mlx.app/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

FieldTypeNotes
payloadstringThe catalogue as a JSON string. Parse it after you check sig.
sigstringEd25519 signature, hex, over SHA-256 of "mlx-store-v1\n" + payload.
keystringThe sequencer's public key, hex: the same key that signs every block (sequencer in GET /api/log).
revstring16 hex characters that change when a listing is added, changed, flagged or removed.
atnumberWhen the response was built, in ms.

payload, parsed:

FieldTypeNotes
revstringThe same as the outer rev.
entriesarrayEvery 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.
scoresobjectslug → MLX Score, 1.0–5.0 to one decimal. A listing with no public signal yet has no score. See MLX Score.
iconsobjectslug → 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:

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:

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.

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);

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.