MLXDocs
Protocol

Jobs and checks

Markdown

The server hands out every job. Each prompt is drawn from a seed only the server holds, each job goes to several Macs of different wallets, and a result counts only for the job that Mac was handed, once, and no faster than a real Mac could run it. This page also says what these checks do not prove yet.

The code is lib/worker/core.ts (nextJob, saveResult, judge), lib/worker/jobs.ts (the prompts) and lib/compute/verify.ts (the chip checks). The HTTP side is in The worker protocol.

What a job is

{ id, model, modelId, prompt, maxTokens, temperature: 0, kind: "known-answer" }

Every job is decoded greedily (temperature: 0), so Macs running the same model and the same app should produce the same tokens. MLX Node reports a hash of what it generated: SHA-256, as hex, of the generated token ids joined with commas (outHash).

The order work is handed out in

nextJob gives a Mac, in this order:

  1. The job it already holds. A Mac holds one job at a time (wk:cur:<pk>). If it asks again before answering, it gets the same job back with a fresh hand-out time. This is how a job cut short by the owner coming back is offered again. It doesn't get the old job if its seat changed edition (free ↔ dedicated) or its memory no longer fits.
  2. The fixed test jobs it hasn't done (BETA_JOBS). Known-answer prompts on each model in BETA_MODELS, smallest model first, each once per Mac. A model is offered only to Macs with enough memory (minMemoryGB, against the memoryGB the Mac sends).
  3. The next job of the endless stream, shared with other Macs (below).

A Mac not linked to a wallet gets no job.

The stream

All stream jobs run mlx-community/Qwen2.5-0.5B-Instruct-4bit. There are two editions: full jobs of 160 tokens for dedicated seats, and short jobs of 32 tokens for free seats.

IdEditionStatus
qwen05-2s<i>full, v2handed out
qwen05-2f<i>short, v2handed out
qwen05-s<i>, qwen05-f<i>v1no longer handed out; a Mac still holding one can finish it

Fresh prompts (v2)

The prompt of index i in edition e comes from a seed only the server can compute (streamSeed):

key  = HKDF-SHA256(ikm = hotKeySeed(), salt = "mlx-jobs-v2", info = "stream-seed")
seed = HMAC-SHA256(key, "<chain id>:<e>:<i>")
prompt = freshPrompt(seed)

hotKeySeed() is derived from the sequencer's secret. Preview deployments derive their own. freshPrompt draws a template and its words and numbers from the seed: a multiplication, a haiku with a numbered title, sorting words, a sum of four numbers, primes near a number, an explanation with an example number, a translation of a generated sentence, or a seconds conversion. Nobody can know a prompt before it is handed out, and the space of prompts doesn't run out. Every Mac handed the same index gets the same prompt, so their answers can be compared.

The v1 stream built each prompt from its index with small tables. There were few enough distinct prompts that every answer could be computed once and replayed from a lookup table. That is why it was replaced.

Sharing an index

streamIndex picks the index for a Mac:

  • It looks at the edition's open indices for the Mac's GPU group (wk:open2:<e>:<group>, a sorted set by first hand-out).
  • It keeps those not yet handed to this Mac or to any Mac of its wallet (wk:hand:<job> holds d:<pk> and a:<wallet>).
  • It picks one of the oldest at random, so Macs asking together don't all land on the same index.
  • If none is left, it opens a new index (wk:gnext2:<e>).

An index is closed once it has been handed out 5 times, or once 3 Macs have answered it (closeIfAnswered). So each stream job is run by up to 5 Macs, all of different wallets.

Pacing

A Mac gets at most one hand-out every 200 ms (MIN_HANDOUT_GAP_MS). A request that comes sooner is held for the rest of that time, never refused. A free seat also waits out its throttle (see Free seats).

Results

saveResult takes a result only if all of these hold:

CheckRefused with
At most 300 results a minute from this Mac (RESULTS_PER_MINUTE)429 "Slow down"
The result names the job this Mac holds409 "This job wasn't handed to this Mac"
The job id is known400 "Unknown job"
outHash is a SHA-256 hash in lowercase hex400 "Missing output hash"
A complete result came no faster than the timing floor409 "Too fast for this job" (the job is dropped)
A complete result for this hand-out wasn't received before409 "Result already received"

A result is complete when stop is stop or length. Anything else (MLX Node sends cancelled) is stored, but the Mac keeps the job and will be offered it again.

The timing floor (minJobMs) is 1 ms per token, from hand-out to result. A result that ran to its length counts the job's full maxTokens. One that stopped early counts the tokens it reports, within the job's bounds and above a small minimum. Real Macs run this model far slower than that, and also spend the network round trip and the prompt's prefill, so only a result that was never computed falls under it.

Reported token counts are capped at the job's maxTokens.

GPU groups

Greedy decoding gives the same tokens on Macs of one GPU generation, but not always across generations: different GPUs round the same sums in a different order, and once two candidate tokens are nearly tied, the rest of the answer can differ. The network's own counts (GET /api/compute/work) showed it: M5, M6 and M5 Ultra agreed on every shared job, as did M3, M4 and M4 Pro, while Macs of different groups disagreed on 6 to 8% of short jobs and about 22% of full ones.

So each Mac belongs to a group (numericsGroup in lib/compute/verify.ts), from the chip it reports:

GroupChips
m1M1, M1 Pro, M1 Max, M1 Ultra
m2M2 family (kept apart until the network has measured it)
m3M3 and M4 families
m5M5, M6 and newer
xno M chip named

An index is shared only within one group, and a Mac is judged only against Macs of its own group. A Mac that claims a newer chip than it has answers like its real group, so it loses quality against the group it claims.

Verdicts

Once a job has answers, the server looks for a majority among the Macs of each GPU group (groupTop): the answer given by more than one Mac of the group, and by more of them than any other answer. Two answers that differ say nothing yet about which one is wrong, so there is no verdict for a Mac until its group has a clear majority.

  • The result reply carries match: true | false once there is a verdict for this Mac (verdictOf). MLX Node counts these as "Checks passed".
  • After the reply, judge gives a verdict to every Mac that answered the job, once each, including the ones that answered before the majority was clear. A Mac first on an index with a made-up answer still loses quality when later Macs settle it.
  • Each verdict adds to the Mac's ok or bad count (noteMatch, cr:q2:<pk>; the counts started again on 27 Oct 2026, when the groups did, because the earlier ones held honest cross-generation differences), which sets q in its score.

Speed measurement

For every full stream job that ran to its length, the server records the Mac's real speed: tokens divided by the seconds from hand-out to result (measuredRate, kept in wk:spd:<pk>). The network round trip counts against it. The Mac's own tokPerSec is shown in apps but never used for pay.

Chip checks

A Mac's class decides what a dedicated seat earns on it (see Classes). The class comes from the chip the Mac reports. checkedClass in lib/compute/verify.ts checks that claim two ways:

  1. Model identifier. MLX Node also reports Apple's model identifier (hw, such as Mac16,10). If the identifier is known and the claimed chip was never sold under it, the Mac counts as the smallest chip that identifier allows. Flag hw.
  2. Speed. If a Mac claims more than a base chip (Pro, Max, Ultra or a Latest chip), has at least 40 measured full jobs, and its median speed is below 60% of the base-Mac baseline, it is carried as a base Mac. Flag speed. The baseline is the median of the medians of Macs reporting a base M chip with at least 40 measured jobs each. It exists only once at least 3 such Macs have been measured (speedBaseline), and is recomputed at most once an hour.

The checks only lower a class, never raise one. With too little data nothing changes. They matter only for seats that need the bigger class: starter seats earn in full on any Mac.

Cross-chip agreement

Greedy decoding is assumed to give the same tokens on every Apple chip. The network measures whether it does. When an index closes, closeIfAnswered compares every pair of answers once and counts, by pair of chip families, how many matched (wk:xchip). The counts are public:

GEThttps://mlx.app/api/compute/work

agreement.sameChip and agreement.crossChip hold the same and diff counts, and agreement.pairs holds them per chip pair, with agree as the share that matched. The same reply has the speed check's baseline (speedCheck) and the share of tokens that served paid jobs (paid). See The work.

Execution checks

Agreement shows that Macs gave the same answer, not that each one ran the model. From MLX Node 0.10 every run commits to its work:

  • While generating, the Mac records three rows of every layer at every decode step: the layer's input x, h after attention and o after the MLP, in the model's dtype.
  • Each (step j, layer l) is a leaf, SHA-256("mlx-co-v1" ‖ u8 l ‖ u32 j ‖ x ‖ h ‖ o), ordered by step, then layer. Pairs hash as SHA-256(0x01 ‖ left ‖ right), an odd node goes up as it is. The root goes with the result, with the token ids.
  • Only then does the server name one slice (j, l), drawn from a key only it holds and the root. The Mac sends those rows with their path in its next job request (open).
  • The server checks the path, then recomputes that layer's MLP from the public weights (fetched from Hugging Face at a pinned revision): o' = h + down(silu(gate(n)) ⊙ up(n)), n = RMSNorm(h). The error ‖o' − o‖, against what the MLP added plus the rounding each value can carry, must be under the tolerance; honest runs measure about 1%, made-up rows about 100%. At layer 0 the input row must also be the embedding of the step's input token.

The Mac can't know which slice will be asked before it commits, so skipping work means guessing. Attention isn't recomputed (it needs every earlier position), so a run that faked attention would still have to run every MLP, about two thirds of the work. The counts per Mac (ok, bad, missed) are in Admin, and the network's totals in execution on GET /api/compute/work.

What this does not prove yet

  • Colluding Macs on one index. Up to 5 Macs of different wallets get the same prompt. If they cooperate, one real run can be copied to the others, hidden states and all, and they all pass the comparison and the execution check. Each answer still has to be computed once, by a real model, after the hand-out.
  • Device keys aren't attested. A Mac's key is made in its Secure Enclave, but macOS gives the server no attestation of that. A Mac without a Secure Enclave (a virtual machine) uses a software key, and a script can make one too. The server can't tell them apart. That is why rewards follow seats, which cost dollars, and why free seats share at most 10% of each release.
  • The majority is the reference. A Mac is judged against other Macs of its GPU group, not against a run the server made itself. The execution check recomputes one layer of one step, not the whole answer.
  • The chip check is statistical. It catches a claim far from the Mac's real speed, once enough jobs are measured. A Mac slowed down by other work can be carried as a base Mac. A Mac that claims a slightly bigger chip than it has, and is fast enough, isn't caught.
  • Delivery counts accepted results. T counts results the server took, before any verdict. A wrong answer counts as delivered, and is caught by q.

Planned: commit-then-open in MLX Node 0.10, which checks a Mac against the server itself, not only against other Macs.