# mPAKT Grid > A volunteer compute grid exposed as one virtual accelerator. Browsers visiting participating sites contribute WebGPU or CPU compute; approved accounts load models by name, submit graphs of tensor operations, keep KV caches resident in sessions, and decode tokens on the server. Everything is JSON over HTTPS on the host that served this file. All paths below are relative to that host. Responses are JSON; errors are `{"error": "..."}` with an HTTP status that means what it says (400 bad request, 401 no or bad credentials, 403 not allowed, 404 not found or not yours, 409 conflict, 422 the graph failed, 429 rate limited, 503 no volunteers connected). Machine-readable companions: `GET /api/ops` (every operation, inputs, parameters, features), `GET /api/versions` (versions, limits, build), `GET /api/health`. ## Access Compute endpoints need an API key in the `x-api-key` header (or `Authorization: Bearer mpakt_...`). Keys belong to accounts. Accounts are by invitation: there is no public sign-up. Contact the owner of the grid you are using; they send you a temporary username and password, good for 7 days. 1. `POST /api/auth/login` `{"username": "", "password": ""}` -> `{"token": "mps_...", "setupRequired": true}`. This token can only finish the setup, and lasts an hour. Expired invitations answer 403. 2. `POST /api/auth/complete-setup` with `Authorization: Bearer `, body `{"newPassword", "username"?}` -> a full session `{"token": "mps_...", "expiresAt", "account"}`. Password: 12+ characters, different from the temporary one (anything shaped like a temporary password is refused) and not containing the temporary username. Optional `username`: 3-32 of letters, digits, `_ - .`, not starting `guest-`, not taken. After this, sign in normally with `POST /api/auth/login`; the session token lasts 7 days. 3. `POST /api/account/keys` with `Authorization: Bearer `, body `{"label"}` -> `{"key": "mpakt_...", "id", "prefix"}`. The key is shown only this once. People can do all of this, revoke keys and change passwords in a browser at `/account`. **Verified accounts.** Invited accounts start unverified; the owner verifies those they trust. An unverified account can use the AI API, the chat (without tools) and Compute boxes without internet. Only verified accounts can: download the volunteer programs, give a box internet access, run container (`Dockerfile`/`image`) boxes, connect their own domains to boxes, and use the chat's tools and MCP servers. Endpoints that need it answer 403 with `"verifiedOnly": true`. `GET /api/account` shows `verified`. An account with no password yet answers `POST /api/auth/login` with 403 `{"passwordNotSet": true, "setWith": "setupCode" | "apiKey", "username"}`: the owner sets it with `POST /api/auth/setup`, an imported key's account with `POST /api/auth/claim`. The original key endpoints still work, behind accounts: - `POST /api/keys` with `Authorization: Bearer ` -> `{"ok", "key", "id", "prefix"}` - `POST /api/keys/revoke` `{"key"}` — revoke a key by presenting it; no sign-in needed. If it was its account's last key, everything the account holds is released. Keys issued before accounts existed still exist. Each belongs to an account of its own, which works once the owner approves it. Its holder can give that account a password: `POST /api/auth/claim` `{"key", "password", "username"?}` (once; omit `username` to keep the account's current name; returns a session token if the account is already approved). Account endpoints (all need `Authorization: Bearer `): - `GET /api/account` — the signed-in account - `POST /api/account/logout` - `POST /api/account/password` `{"currentPassword", "newPassword"}` — signs out every other session - `GET /api/account/keys` — ids, labels, prefixes, last use; never the keys - `POST /api/account/keys` `{"label"}` — at most 20 keys - `DELETE /api/account/keys/:id` — takes effect immediately Owner endpoints (session of the owner account): - `POST /api/admin/invites` `{"note"?}` -> `{"username", "password", "expiresAt", "signInAt"}` — a temporary username and password, shown once - `GET /api/admin/accounts?status=pending|approved|disabled|rejected` - `POST /api/admin/accounts/:usernameOrId/approve` | `/reject` | `/disable` | `/enable` | `/verify` | `/unverify` - `DELETE /api/admin/accounts/:usernameOrId` - `GET /api/auth/setup` -> `{"needed", "owner"}`; `POST /api/auth/setup` `{"code", "password"}` sets the owner password on a fresh server, using the one-time code the server wrote to `/owner-setup-code`. Everything an account loads — models, sessions, compiled graphs, uploaded tensors — is shared by all of its keys. Disabling or deleting an account stops its keys and releases all of it. Limits per account: 4 models, 16 sessions, 32 compiled graphs, 128 MB of v2 uploads. Rate limits: 10 sign-in attempts per IP per minute, 20 v1 tasks per 10 s per account. ## The one rule for performance Every request crosses the open internet, and the latency dwarfs the arithmetic. In order of what they save: 1. Decode on the server: `POST /api/v3/generate` runs the whole generation loop beside the volunteer and returns every token from one request. 2. Compile graphs you run repeatedly: `POST /api/v3/graphs` once, then send only bindings. 3. Choose tokens on the device: end graphs in `argmax` or `sample`; never bring logits home to take their maximum. 4. Put whole layers — or whole decode steps — in one graph. Intermediates stay on the volunteer. 5. Let long outputs draft their own continuation: `speculative` on `/api/v3/generate` (below) verifies several tokens per pass without changing them. ## Graphs `POST /api/v3/graph` body: `{"graph": {...}, "encoding": "base64"?}` ``` { "inputs": [ ... ], "steps": [ {"op": "matmul", "in": ["x", "w"], "out": "h", "params": {"transB": true}}, ... ], "outputs": ["y"], "session": "s_..."?, // run inside a session "retain": {"name": "ref"}?, // keep outputs in the session instead of returning them "microbatches": n? // split inputs marked "split": true along dim 0 (not with a session) } ``` Input forms: - `{"name", "shape", "data": [numbers]}` or `{"name", "shape", "dataB64": ""}` — uploaded with this call - `{"name", "model": "m_...", "weight": ""}` — a loaded weight; nothing transfers - `{"name", "session": ""}` — a tensor held in the session - `{"name", "tensorId": "t_..."}` — a v2 uploaded tensor Refs are input names or earlier step outputs. Response: `{"ok", "elapsedMs", "stages", "outputs": {"y": {"shape", "data" | "dataB64"}}, "session"?}`. With `"encoding": "base64"` outputs are base64 f32, a quarter the size. Conventions: tensors are row-major f32. Ops that speak of rows and columns fold every leading dimension into rows, so `[batch, seq, C]` behaves as `[batch*seq, C]`. Checkpoint weights are `[out, in]`: pass `transB: true` to `matmul`/`linear` rather than transposing. Integer inputs (token ids) are f32 values holding integers. ## Operations Full detail: `GET /api/ops`. Parameters marked (feature) are optional capabilities; a step using one only goes to volunteers that implement it, and a graph no connected volunteer can run is refused naming the missing op or feature. Linear algebra and elementwise: - `matmul(a [..,K], b [K,N] | [N,K] with transB)` -> `[..,N]` - `linear(x, w, bias [N])` params `transB` — x·w + bias - `transpose(a [R,C])` -> `[C,R]` - `add | sub | mul(a, b)` — b: same shape, a row vector the width of a, or one value - `scale(a)` params `factor` - `relu`, `silu` (x·sigmoid x), `tanh`, `sigmoid` - `gelu` — tanh approximation = HF `gelu_new` / `gelu_pytorch_tanh` (GPT-2, Phi-2, Gemma) - `gelu_exact` — erf GELU = HF `gelu` (BERT, Falcon, GPT-NeoX) - `gelu_quick` — x·sigmoid(1.702x) = HF `quick_gelu` (CLIP) - `softcap(a)` params `cap` — cap·tanh(a/cap), Gemma 2 final-logit softcapping - `softmax(a)` over the last axis Normalisation: - `rmsnorm(x, gamma)` params `eps` (1e-6), `weightOffset` (feature; multiply by gamma+offset — Gemma: 1), `groupSize` (feature; normalise each group of channels — per-head q/k norm with groupSize = head_dim, gamma has groupSize values) - `layernorm(x, gamma, beta)` params `eps` (feature when given; default 1e-5, BERT 1e-12). No bias: pass `zeros`. Transformer blocks: - `rope(x [T, heads*headDim])` params `headDim`, `offset` (absolute position of row 0), `base` (rope_theta, default 10000); features: `rotaryDim` (rotate the first rotaryDim channels, pass the rest), `interleaved` (pair 2i,2i+1 — GPT-J/Cohere/GLM; default is half-split, Llama/Qwen/Mistral/NeoX), `scaling` (config.json `rope_scaling`/`rope_parameters` as-is: `linear`, `llama3`, `yarn`, `longrope`, `dynamic`), `invFreq` (explicit rotaryDim/2 inverse frequencies), `attentionFactor` (multiplies cos/sin; yarn/longrope derive it) - `attention(q [T, heads*headDim], k [S, kvHeads*headDim], v [S, kvHeads*vHeadDim], bias?)` -> `[T, heads*vHeadDim]`. Params `heads`, `headDim`, `kvHeads`, `vHeadDim`, `causal` (default true), `posOffset` (position of q row 0; with a cache, the current position). Features: `scale` (replaces 1/sqrt(headDim)), `softcap`, `window` (causal: key s visible when pos-window < s <= pos; non-causal: |pos-s| <= window), `alibi: true` (BLOOM slopes) | `alibiMax` (MPT) | `alibiSlopes` (explicit), and a 4th input `bias` `[T,S]` or `[heads,T,S]` added to scores (use -1e9 to mask; data must be finite). - `swiglu(gate, up)` — silu(gate)·up. `geglu(gate, up)` — gelu(gate)·up. - `moe(x [T,C], logits [T,E], bias [E]?, experts...)` -> `[T,C]`. Params `topK`, `scoring` ("softmax" | "sigmoid"), `normalize` (renormalise chosen weights), `scale` (routed_scaling_factor), `nGroup`, `topKGroup`, `groupTopK` (how many of a group's best scores make its score; default 1), `selectionBias` (a bias input that chooses experts but does not weight them), `layout`: "separate" (gate_e [I,C], up_e [I,C], down_e [C,I] per expert, as on the Hub), "fused" (gate_up [E,2I,C], down [E,C,I]), "stacked" (gate [E,I,C], up [E,I,C], down [E,C,I]); `activation` "silu" | "gelu". Expert weights are used as stored. - `embedding(ids [T], table [V,C])` -> `[T,C]` - `conv1d(x [T,C], weight [C,1,K], state [K-1,C])` — depthwise causal, pure - `short_conv(x, in_proj [3C,C], conv [C,1,K], out_proj [C,C], state [K-1,C])` — LFM2 gated block; state advanced in place Decoding and shapes: - `argmax(a)`, `reduce_max(a)` — over the last axis, one per row - `sample(logits)` params `temperature` (0 = argmax), `topK`, `topP`, `seed`, `step` - `reduce_sum`, `reduce_mean` — whole tensor -> `[1]` - `concat(a, b)`, `slice(a)` params `axis` (0 or last; -1 = last), `start`, `length`. slice accepts a model weight (split fused projections). - `reshape(a)` params `shape`; `zeros()` params `shape` - `cache_write(cache [maxRows,W], values [rows,W])` params `offset` — writes in place and returns the cache - `causal_mask(a [.., n, n])` ## Architectures Map config.json onto parameters instead of approximating: | config.json / model | parameter | |---|---| | `rope_theta` | rope `base` | | `rope_scaling` / `rope_parameters` (llama3, yarn, longrope, linear, dynamic) | rope `scaling`, the object as-is. Llama 3.1/3.2/3.3 need it at every length. longrope: include `max_position_embeddings` unless `attention_factor` is given | | `partial_rotary_factor`, GPT-J `rotary_dim` | rope `rotaryDim` | | GPT-J, CodeGen, Cohere, GLM, DeepSeek MLA rope | rope `interleaved: true` | | `sliding_window` (Mistral v0.1, Gemma 2/3, Cohere 2 — on sliding layers) | attention `window` | | `attn_logit_softcapping` (Gemma 2) | attention `softcap` | | `final_logit_softcapping` (Gemma 2) | `softcap` op on the logits | | `query_pre_attn_scalar` (Gemma) | attention `scale` = value^-0.5 | | `attention_multiplier` (Granite) | attention `scale` | | BLOOM / MPT ALiBi | attention `alibi: true` / `alibiMax: 8` | | T5 relative position bias | attention bias input `[heads, T, S]` | | `q_norm`/`k_norm` per head (Qwen3, Gemma 3) | rmsnorm `groupSize: head_dim` | | Gemma norm weights | rmsnorm `weightOffset: 1`; embeddings `scale` by sqrt(hidden_size) | | `hidden_act: gelu` / `gelu_new` / `gelu_pytorch_tanh` / `quick_gelu` | `gelu_exact` / `gelu` / `gelu` / `gelu_quick` | | Gemma MLP | `geglu` | | `layer_norm_eps` | layernorm `eps` | | `head_dim` != hidden/heads, MLA value width | attention `headDim`, `vHeadDim` | | fused `qkv_proj`, `gate_up_proj`, `c_attn` | `slice` the weight on axis 0, or `linear` then `slice` the activation on axis -1 | | Mixtral | `moe` scoring softmax, normalize true | | Qwen2/Qwen3-MoE | `moe` softmax, normalize = `norm_topk_prob`; shared expert: ordinary MLP gated by `sigmoid` | | OLMoE | `moe` softmax | | DeepSeek-V2/V3 | `moe` sigmoid (V3), `selectionBias` = `e_score_correction_bias`, `nGroup`/`topKGroup`, normalize, `scale` = `routed_scaling_factor` | Not supported: pre-quantised checkpoints (GPTQ, AWQ, bitsandbytes, int8, fp8) are refused at load (to shrink a model, use `quantize` on load, below); backward passes; middle-axis slice/concat. ## Models - `GET /api/v3/models/inspect?source=hf:Org/Model` — tensor index only: names, shapes, dtypes, bytes - `POST /api/v3/models` `{"source", "include": ["model.layers.0.*"], "exclude", "names"}` -> `{"modelId", "tensors": [{name, tensorId, shape, dtype}], "bytesTransferred", "storedOnCoordinator": 0}` - `GET /api/v3/models`, `DELETE /api/v3/models/:id` `quantize` on `POST /api/v3/models`: `"none"` (default), `"q8"` (8.5 bits per weight), `"q4"` (5 bits; embeddings, head, v_proj and down_proj stay q8) or `"auto"` (the least aggressive that fits the pool now). Volunteers convert matrices as they arrive; norms, biases and small or odd-width tensors stay as published; ops read the quantized form directly. The response carries `quantize` and `residentBytes`. A volunteer that cannot quantize keeps the published dtype. It costs accuracy: SmolLM2-135M-Instruct vs bf16 over 49 positions, q8 matched the top token 48/49 (weights 270 -> 143 MB), q4 43/49 (110 MB). Sources: `hf:Org/Model`, `hf:Org/Model@revision/file.safetensors`, or an https URL to a safetensors file whose host honours range requests. Sharded repos (`model.safetensors.index.json`) are followed. The coordinator streams weights from the host straight to volunteers and stores none of them. f32, f16 and bf16 are accepted; bf16/f16 stay packed on volunteers. f64, int64 and bool tensors (buffers such as position_ids, masks) are converted to f32 as they stream, so a whole checkpoint loads. Capacity is checked first: `GET /api/v3/status` reports `device.freeBytes` and `device.maxTensorBytes` (a tensor must fit on one volunteer). ## Sessions and the KV cache - `POST /api/v3/sessions` `{"modelId"?, "label"?}` -> `{"session": {"sessionId"}}` - `GET /api/v3/sessions/:id`, `DELETE /api/v3/sessions/:id` - `POST /api/v3/sessions/:id/checkpoint`, `/restore` — copy the session's tensors to the coordinator so a lost volunteer costs a re-push instead of a re-prefill A session is pinned to one volunteer. Allocate caches with `zeros` and `retain` them; each step writes new rows with `cache_write` at `offset: pos` and attends with `posOffset: pos`. `cache_write` mutates in place, so caches need no `retain` after allocation. ## Compiled graphs and server-side decoding - `POST /api/v3/graphs` `{"graph", "label"?}` -> `{"graphId", "bindings", "bodyBytes", "perCallBytes"}`. Holes: an input `{"name", "shape", "bind": "ids"}` (fixed shape, contents per call) and a param `{"$bind": "pos"}` (optionally `"add"`, `"mul"`). - `POST /api/v3/graph` `{"graphId", "bind": {"ids": [4097], "pos": 41}, "session"?, "outputs"?}` - `GET /api/v3/graphs`, `DELETE /api/v3/graphs/:id` - `POST /api/v3/generate` `{"graphId", "sessionId", "ids": [last prompt token], "startPos": prompt length, "maxNewTokens", "stopIds", "temperature", "topK", "topP", "seed", "stream"}` — the graph must bind `ids` and `pos` and output one token id. Streams `event: token` / `event: done` over SSE, or one JSON body with `"stream": false`. At most 4096 new tokens. With a `sessionId`, when the session's worker holds every input and supports it, tokens after the first are produced on the worker with no per-token round trip; `workerLoop` in the result reports it, results are identical, and `"workerLoop": false` forces the ordinary path. Applies when the graph binds only `ids`, `pos`, `step` and sampling parameters, reads only model weights and session tensors, and retains nothing. Speculative decoding: add `"speculative": {"graphIds": ["gt_..."], "context"?: [ids so far], "always"?: bool}` — compiled verify graphs (the decode step over k+1 positions: `ids` bound with shape `[k+1]`, each row sampled with `{"$bind": "step", "add": row}`, output the k+1 ids; up to 16, each binding `ids` and `pos`). Drafts are prompt lookup (the continuation of the latest earlier occurrence of the last 1-4 tokens); the length is chosen per pass from past hits. Tokens are those plain decoding gives (same seed). `done` carries `speculation: {verifies, drafted, accepted, produced, plain}`. Batching: a graph may list several sessions pinned to one worker, `"sessions": ["s_a","s_b"]` beside `session`, and an input `{"name", "session": "", "of": i}` reads the i-th one's tensor; decode steps of several conversations then share one pass. Prefill is one ordinary graph call. Graphs time out after 5 minutes. - `GET|POST /api/v3/random` `{"min"?, "max"?, "count"?}` -> `{"min", "max", "count", "values", "value", "sources"}` — uniform integers in [min, max] (default [84, 2147483646]) from a pool that mixes the OS CSPRNG, the drand beacon, and the text and tensors passing through the grid. `count` up to 10000. `/api/v3/generate` without a `seed` draws its seed this way. ## v2 and v1 v2 (`/api/vgpu/*`, also `/api/v2/*`): `POST /graph` (graphs without models or sessions), `GET /status`, `POST|GET /tensors`, `DELETE /tensors/:id` (128 MB of uploads per account). v1 (`/api`, also `/api/v1`): `POST /api?wait=1` `{"data": {"A", "M", "K", "B", "N"}}` multiplies two matrices on three volunteers and returns the majority answer with `verified: true`. Results are limited to 699,050 elements (`M*N`); a task volunteers keep failing ends with `status: "failed"` and an `error`. `GET /api/task/:id`, `GET /api/status`. ## mPAKT Compute (v0, beta) A second service on the same grid, versioned separately: the AI API above is v3, Compute is **version 0 (beta)** and may change. `GET /api/versions` lists it under `services.compute`. A **box** is a public GitHub repository run on volunteers' devices when a request arrives, and not otherwise; it is reachable at its own hostname, `https://-.`. People manage boxes at `/compute` (a dashboard with a tutorial); everything it does is in the API below. **Read this first.** - **Nothing is stored.** Code is pulled from GitHub into memory when needed. A box has no disk that survives a request: use a database over HTTP (which needs internet access, below). - **Volunteers can see everything they run.** Your code, every request and every response pass through someone else's computer. Never put secrets in a box that you would not show a stranger. Boxes run in a sandbox, so a box cannot harm the volunteer, but the volunteer is not blind to the box. - **No network unless you turn it on, and then only through Tor.** Slow, and some sites refuse Tor. It is for verified accounts. The volunteer's own address is never used. - Public repositories on GitHub only. Beta: expect rough edges and downtime. Auth: a session token (`Authorization: Bearer mps_...`) or an API key (`x-api-key`). 240 calls per minute per account; 20 deploys per 10 minutes. Errors are `{"error": "..."}`. - `GET /api/compute/v0` — no auth. Runtimes and their minimum memory, CPU classes, limits, pricing, and the live pool (`volunteers`, `minMb`, `maxMb`, `runtimes` per CPU class). - `POST /api/compute/v0/boxes` `{"name", "repo", "ref"?, "minMb"?, "prefMb"?, "cpus"?, "internet"?}` -> 201 `{"ok", "box"}`. `name`: 2-40 of letters, digits, space, `_ - .`. `repo`: `owner/name` or a github.com URL (`/tree/` accepted); `ref`: branch or tag (default branch if omitted). `minMb`: what a volunteer must have free to be chosen; `prefMb`: what the box gets (and is billed for) when available; 16-8192, runtime floors below. `cpus`: any of `web` (a browser tab: cheap, plentiful), `x86_64`, `arm64`, `other` (installed volunteers); default all. `internet`: false by default. At most 20 boxes per account. - `GET /api/compute/v0/boxes`, `GET .../boxes/:id` — `box`: `id`, `name`, `repo`, `ref`, `url`, `subdomain`, `runtime` (null until deployed), `entry`, `commit`, `private` (deployed through the account's GitHub connection), `deployedAt`, `minMb`, `prefMb`, `cpus`, `internet`, `enabled`, `domains`, `warmVolunteers`, `stats` (`requests`, `errors`, `ms`, `credits`). - Redeploying on push: boxes follow their branch and redeploy by themselves when it moves (`autoDeploy`, default true; set false with PATCH to stop; `autoDeployedAt`, `autoError` on the box). The owner's GitHub app webhook (`POST /api/github/webhook`, signed) makes it immediate; otherwise each followed repository is looked at every 20 s. - Private repositories: `GET /api/compute/v0/github` -> `{"connected", "via": "app"|"token"|null, "login", "app": {"installUrl"}|null, "tokens"}`. `POST .../github/token` `{"token"}` connects a fine-grained personal access token (Contents: Read-only), checked with GitHub and stored encrypted. `POST .../github/connect` -> `{"url"}`: a GitHub sign-in through the owner's GitHub app (browser only: it sets a cookie the callback `/compute/github/callback` checks). `DELETE .../github` disconnects and stops the account's private boxes. A repository GitHub will not show anonymously deploys through the connection; its code reaches volunteers from the server, never as a GitHub link or token. One GitHub user per account. - `PATCH .../boxes/:id` — any of the creation fields, and `enabled` (false switches it off; requests answer 503). Changing `repo` clears the deployment. - `POST .../boxes/:id/deploy` -> `{"ok", "box"}` — resolves the branch to a commit, reads what the repository is (below) and goes live. Nothing runs until a request arrives. Redeploy to pick up new commits; the box keeps serving the old commit until then. - `DELETE .../boxes/:id`. - `GET .../boxes/:id/log` -> `{"requests": [{"at", "method", "path", "status", "ms"}]}` — the last 50 requests, newest first. Never headers, bodies or query strings; kept in memory only. - `GET .../wallet` -> `{"credits", "spent", "earned", "tier", "usdPerThousandCredits": 1, "payUrl", "payments", "purchases"}` (`earned`: credits volunteering paid; purchases carry `bonus`); `POST .../wallet/requests` `{"credits", "note"?}` asks the owner for credits (three open requests at most). - `POST .../checkout` `{"credits"}` -> `{"url", "credits", "bonus", "amount", "currency"}` — buy credits by card (`bonus`: extra credits from your contributor tier, same price), when the grid has card payments (`payments.enabled` in the wallet): open `url`, pay, credits arrive within moments. Credits come in multiples of 10. - `GET .../tutorial`, `POST .../tutorial/tour` — eleven quests that pay XP and credits as you do the things (change your password, create and deploy a box, open its address, change its memory and CPUs, turn internet on or off, connect a domain, run a volunteer with your code, embed the script on a site). - Volunteer rewards: `GET .../volunteer` -> your volunteer code (`vc_` + 12 characters), tier, progress, today and last-30-day accepted work, devices, rules and snippets; `POST .../volunteer/rotate` issues or replaces the code (the old one stops at once); `DELETE .../volunteer` turns crediting off. Work a volunteer does under a code (`data-account` on the embed tag, `account =` / `--account` in `volunteer.py`, or a `volunteer.py` downloaded while signed in) and the grid accepts earns 25% of its billed value in credits, up to a daily cap, never for the code's own account's boxes, models or tasks. Tiers by accepted work over 30 days: Bronze (100 credits) 5% off boxes and 5% bonus on card purchases, Silver (1,000) 10%/10%, Gold (4,000) 15%/20%. The owner lists codes at `GET .../admin/volunteer-codes` and resets or revokes one with `POST .../admin/volunteer-codes/:account/reset|revoke`. - Domains (verified accounts): `POST .../boxes/:id/domains` `{"domain"}` -> 201 `{"domain": {"host", "status": "pending", "records": [...]}}`. Add the records: a TXT at `_mpakt.` with `mpakt-verify=` (proves the name is yours) and a CNAME from `` to the box's assigned address. Then `POST .../domains/:host/verify` (checks the TXT; DNS can take minutes; 409 until found) and `POST .../domains/:host/approve` (status `approved`: the box now answers on it). `GET .../domains`, `DELETE .../domains/:host`. Five per box; the grid's own domain cannot be claimed. Your DNS provider or CDN must also route the name to the grid. ### What a repository is Decided at deploy from the files, or from an optional `mpakt.json` at the repository root (or at `root`): | Files | Runtime | Memory floor | |---|---|---| | `index.html` | `static` — served from memory, GET and HEAD only, `404.html` or (`"spa": true`) `index.html` for unknown paths, free of volunteers | none | | A Vite project: `index.html` beside `vite.config.*`, or beside a `package.json` that uses `vite` | `static`, built by a volunteer (below) | none | | `worker.js`, `index.js`, `main.js`, `index.mjs`, `worker.ts`, `index.ts`, `main.ts`, `src/index.ts`, `src/worker.ts`, `src/index.js`...; the `main` of `wrangler.toml` / `wrangler.json(c)`; an `api/` folder of functions | `javascript` | 32 MB | | `main.py`, `app.py` or `handler.py` | `python` | 128 MB | | `Dockerfile` (or `"image"` in `mpakt.json`) | `docker` — verified accounts only | 64 MB | Server type: detected from mpakt.json, then the files, then the repository's languages (GitHub's language bar); or set it on the box: `type` (`auto`, `static`, `vite`, `javascript`, `python`, `docker`), `entry`, `root`, `port`, `build`, `output` on POST/PATCH `/boxes` (win over mpakt.json; applied at the next deploy; `detectedBy` and `languages` on the box say what decided). `mpakt.json` fields, all optional: `runtime` (`static`, `javascript` (or `typescript`), `python`, `docker`), `entry` (the file to load, or `api/`), `root` (a subdirectory to treat as the project), `port` (container boxes; default 8080), `image` (a ready-made image on Docker Hub or a well-known registry, instead of a Dockerfile), `spa` (static: single-page fallback), `build` and `output` (a static site's build command and its output folder); `runtime` may be `vite`. **Sites with a build step (Vite).** A Vite project (`npm create vite`: React, Vue, Svelte, vanilla) deploys like on Vercel: a volunteer installs its npm packages (devDependencies too, registry only, no install scripts) and runs `npm run build` (or `vite build`) with no network; the output folder (`build.outDir`, else `dist/`) is sent to the grid, checked against the volunteer's list of file hashes, and served from memory as a static site: `index.html` for unknown page paths (`"spa": false` to turn off), 404 for missing assets, `Cache-Control: immutable` for content-hashed `assets/` files. The deploy answers at once; the box's `buildState` is `building`, `waiting` (no volunteer that can build is connected; it starts when one does), `ready` or `failed` (with `error`), `buildLog` has the build's last lines, and `site` is what is being served (the last good build stays up while a new one builds or after one fails). Before the first build finishes the site answers 503 with the reason and `Retry-After`. A repository that commits its built site is served from it unless the type is set to `vite`. Any static site can have a build: mpakt.json `"runtime": "static", "build": "npm run site", "output": "out"`. Billing: the build is memory x time like a request (1 GB on installed volunteers), then static prices. Who builds: browsers first — Vite itself in the browser's Worker (no VM), with rolldown/Rollup/esbuild/lightningcss as WebAssembly; a React app ~20-30 s cold, mostly package downloads — then installed volunteers. Vite 6/7 build in any browser; Vite 8 needs WebAssembly threads, i.e. a cross-origin-isolated page: the owner's own dashboard (`/compute` open = the owner's browser builds that account's sites and nothing else; `GET /api/compute/v0/builder` is its token) and, untested, Chrome 137+ anywhere (Document-Isolation-Policy); Firefox/Safari volunteers on other sites build Vite 6/7 only. With no capable builder and the dashboard closed, `buildState` is `waiting` and says it starts when one connects or the dashboard is opened. In browsers type checks (`tsc`, `vue-tsc`, `svelte-check`) are skipped; a `node` step needing what a browser lacks (brotli, child processes) is a warning and the Vite output is kept; Vite <=7 plugins needing such things fall back to the browser's Linux VM. npm workspaces: with type `vite` and no folder the Vite app is found; a workspace member is built from the workspace top (`npm run build -w `, packages installed once from package-lock.json). `buildState.how` is `worker`, `vm` or `native`; `owner: true` when the owner's dashboard built it. Untick `web` in the box's CPUs to build only on installed volunteers. Limits: output 24 MB / 2,000 files, 15 min (30 in a browser's VM). Not supported: SSR frameworks (Next.js, Nuxt...; static exports only), network during the build, packages needing install scripts, Yarn/pnpm `workspace:` dependencies, build-time env vars. **JavaScript box.** An ES module (a CommonJS `module.exports` also works), in JavaScript or TypeScript, with npm packages from `package.json` (below): ```js export default { async fetch(request, env) { // a standard Request; return a Response, a string, or an object (sent as JSON) const url = new URL(request.url) if (url.pathname === '/files') return Response.json(env.files.list()) // env.files: the repository's own files return new Response('hello ' + url.pathname) } } ``` `export default function (request, env)`, `export function fetch` and `export function handler` are accepted too. `fetch(url)` inside a box works only with internet on. A JavaScript entry that is a Node.js server (it calls `.listen()` or requires `http`/`express`/..., and has no `export default`) is run as `node ` and must listen on `$PORT` (8080); it is only offered to browser volunteers, which run it in a Linux virtual machine, with `npm ci`/`npm install` handled from `package-lock.json` (install scripts are not run). **TypeScript, npm packages, Cloudflare Workers and Vercel projects** run on every kind of volunteer (a browser alone is enough): - TypeScript and JSX (`.ts .tsx .mts .cts .jsx`) are compiled on the volunteer (Sucrase), one file at a time, no type checking: types and `import type` vanish, enums and namespaces work, imports may be extensionless, a folder (`index.ts`) or `./x.js` for `./x.ts`; tsconfig `paths`/`baseUrl` and `jsx`/`jsxImportSource` are honoured; JSON imports work. Not supported: decorators, `const enum` across files. - npm: `dependencies` (not `devDependencies`) are installed from the npm registry (the exact `package-lock.json` tree when present), integrity-checked, no install scripts, at most 600 packages / 96 MB; resolved by `exports` conditions `workerd`, `worker`, `browser`, `import`, `default`. A Hono app works as it is. In a browser, Node built-ins are limited to `events`, `process`, `util`, `crypto`, `async_hooks`, `path`, `url`, `timers`. - Workers: `export default { fetch(request, env, ctx) }`; the entry is `main` from `wrangler.toml` / `wrangler.json(c)`, whose `[vars]` are in `env`. No KV/D1/R2/Durable Object bindings; `ctx.waitUntil` does not outlive the answer. - Vercel: `api/hello.ts` -> `/api/hello`, `api/index.ts` -> `/api`, `api/users/[id].ts`, `[...slug]`, `[[...slug]]`; `_`-prefixed files are helpers. A function is `export default function (request)` returning a Response, named `GET`/`POST`/... exports (405 otherwise), or `(req, res)` with `req.query` (route parameters included), `req.body`, `req.cookies`, `res.status/setHeader/json/send/redirect/end`. All other paths are static files served by the grid itself: `public/` if present, else the top folder minus code and configuration. `vercel.json` and build-step frameworks (Next.js...) are not supported. - A Node.js server in TypeScript (express...) runs in the browsers' Linux VM, compiled as it loads. ```ts // src/index.ts, with "hono" in package.json dependencies import { Hono } from 'hono' const app = new Hono() app.get('/api/users/:id', (c) => c.json({ id: c.req.param('id') })) export default app ``` **Python box.** Define either: ```python def handler(request): # or async def; request is a dict: # method, url, path, query, headers (lower-case names), body (bytes), text return "hello" # str, bytes, dict (JSON), (body, status[, headers]) or {"status", "headers", "body"} # or any WSGI app, named app or application (Flask, Bottle, ...) ``` `requirements.txt` is installed for the box. In browsers that means pure-Python and Pyodide-built packages; on installed volunteers pip runs only if the box has internet. In an `async def handler` on a browser volunteer, `import mpakt; r = await mpakt.fetch(url)` does a request through the grid (internet on); elsewhere use the standard library. **Container box.** A `Dockerfile` (or an `image`) for a server that listens on `port` (default 8080). On installed volunteers (Linux with Docker) the Dockerfile is built with no network, so it must be self-contained (`FROM` an image on Docker Hub, ghcr.io, quay.io, gcr.io, mcr.microsoft.com, public.ecr.aws or registry.k8s.io; no `ADD` from a URL, no `# syntax=`, no `RUN --mount/--network`); Browser volunteers can also run containers, in an emulated 32-bit x86 Linux (64-bit images too, see below). An `image` (or the one image a Dockerfile is `FROM`) with a `linux/386` build on a supported registry runs from its own layers (the grid reads the manifest at deploy and passes the layers on; checked by digest in the browser; up to 160 MB compressed); with no `port` in `mpakt.json`, the one exposed port is used. Otherwise `FROM` picks one of three prebuilt bases by the image's name — `node`, `python` or `alpine`/`busybox`/`debian`/`ubuntu` (the tag is ignored) — and `RUN`, `COPY`/`ADD` (from the repository), `ENV`, `ARG`, `WORKDIR`, `USER`, `EXPOSE`, `CMD`, `ENTRYPOINT`, multi-stage builds and `.dockerignore` are understood. It is slow, especially the first request. An image with no `linux/386` build runs from its `linux/amd64` build (also `FROM image@sha256:...`, a digest pin, runs that exact image): each 64-bit program in it runs as the 32-bit Alpine build of the same program when there is one — busybox, bash, dash, GNU coreutils, grep, sed, findutils, tar, gzip; Node.js 22/24/26 (same major); Python 3.12/3.14 (same minor; the image's site-packages stay on the path); Redis 7/8 (same major) — unless that process would load 64-bit native code (Node addons, Python C extensions, Redis modules). Everything else (Go and Rust programs, other tools, interpreters with 64-bit native code) runs under Blink, an x86-64 emulator, started per process by the kernel, so 32-bit and emulated programs mix in one box. 32-bit stand-ins run at the VM's usual speed (node:22-alpine, python:3.12-slim, redis:7: first request 1-2.5 min, then 0.1-1 s); emulated programs are far slower (a Go server: ~2 min to start, 4–7 s a request; an emulated Node.js or CPython: tens of minutes or more). Installed volunteers run amd64 images natively and are preferred for them. The deploy response's `vm` and `vmNote` fields say whether browsers can run it and, if not, why; for a 64-bit image `vmProcs` lists the programs expected to run 32-bit or emulated, and `vmReport` what the last browser that started it decided, with timings. Not in a browser: images with neither a 386 nor an amd64 build, Python packages with C extensions Pyodide lacks, packages that compile native code at install, boxes over about 350 MB in Firefox; those need an installed volunteer. ### Limits Request body 1 MB; response 6 MB; a box's code up to 8 MB (24 MB for a static site); 30 s per request (the first request on a volunteer may take up to 2 minutes, a browser VM 5); 20 requests per second per box (50 grid-wide); a request waits up to 5 s for a free volunteer before 503; internet: 16 `fetch` calls per request, 4 MB per answer, 64 MB per hour per box, no mail ports. Every response carries `X-Mpakt-Box`. The grid itself answers 402 (the box's owner is out of credits), 413, 429, 502 (the box or its volunteer failed), 503 (switched off, not deployed, grid busy, or no volunteer with enough free memory and the right CPU is connected right now) and 504 (timed out). ### Spreading Each request goes where it is expected to finish soonest, counting queued work, cold starts and the volunteer's recent busyness; a browser is held to about half its time and two boxes at once, an installed volunteer to about 90%. Boxes under steady load are warmed on more volunteers (2+ from about 12 requests a minute, up to 8) and let go after 3 idle minutes. Volunteers fetch box code from GitHub themselves by commit (browsers file by file from raw.githubusercontent.com for boxes of up to 40 files; installed ones the commit archive), verified against SHA-256 hashes the grid recorded, with the grid as fallback. `GET /api/health` has `compute.spread`. ### Pricing Credits: 1,000 credits = US$1. New accounts get 1,000. Charged per request: 0.1 credit per thousand requests, plus run time x memory (0.004 credit per GB-second on browser volunteers, 0.008 on installed ones, at the memory the box was given), plus 20.5 credits per GB of internet traffic. Static sites cost only the per-request part. `GET /api/compute/v0` carries the exact numbers. ## Volunteering Any site can contribute its visitors' compute with one tag; it needs no key and no account: ``` ``` To volunteer a computer directly, sign in with an approved account and download `volunteer.py` from `/downloads` (it is reserved for verified users) and run `python3 volunteer.py` (a copy downloaded while signed in has your volunteer code in it, so its SHA-256 differs from the listed one only by that line; `account =` in `volunteer.ini` or `--account` sets it otherwise). It needs Python 3.9+ and no key or account, uses every CPU, GPU, TPU and NPU it has a library for, and reads its limits from `volunteer.ini`, where each percentage is a share of the capacity free at that moment. On Linux with Docker (rootless Docker is recommended; never join the `docker` group for this) it also runs Compute boxes in locked-down containers with no network of their own; `work` in `volunteer.ini` includes `boxes` by default. To credit your account, add `data-account="vc_..."` (your volunteer code) to the tag. Browsers contact GitHub, jsDelivr, npm and PyPI from the visitor's connection to run boxes; see `/agreement.html`. Visitors see a notice with Opt out and Continue; opting out stops all work and makes no further connection. A participating page may also run other people's Compute boxes for the grid, in a sandboxed frame that cannot see the page, its cookies or its storage. `window.mpaktGrid.stop()`, `.start()`, `.status`, `.onStatus(fn)` let a site offer its own control. The site's Content-Security-Policy must allow the host in `script-src`, `connect-src` and `frame-src`, and `worker-src blob:`. What runs on the device is described at `/agreement.html`. ## Chat `/chat` is a chatbot for signed-in accounts on the grid's models, with no API key: pick a loaded model or load one that fits (loaded models are shared). It uses the session token from `/account`; its endpoints are under `/api/chat/` and are not part of the keyed API. What matters if you call them yourself: - `GET /api/chat/models` lists models; `POST /api/chat/models` `{"repo": "Org/Model"}` loads any Hugging Face model by id and answers 202. `POST /api/chat/models/check` `{"repo"}` (12 a minute) checks one without loading: it returns `{"ok", "problems", "warnings", "facts", "placement", "resources"}` — config, tokenizer, chat template, weights and operations against what the connected volunteers can do. A model too big for one volunteer is split into stages across several (pipeline placement). - The page can tokenize for itself: `GET /api/chat/models/:org/:name/prompt` (template, defaults, stop ids, tokenizer hash; `offload: false` when it must send text) and `.../tokenizer/:hash` (the tokenizer.json), then a message body `{"prompt": [ids]}` instead of `text`. Replies arrive as `ids` events; `done` has `tokenized: "client"`, and `batched` (steps decoded in a batch with other conversations). Speculative decoding is on; `"speculative": false` in a message turns it off. - Prompts are rendered with the model's own chat template. Sampling uses temperature 0.2 by default and a repetition penalty of 1.1 over the last 64 tokens, computed on the device (the `sample.penalty` feature of the `sample` op; see `GET /api/ops`). - **End-to-end encryption**, on by default in the page: `GET /api/chat/e2e` -> `{"algorithm", "publicKey", "fingerprint"}` (ECDH P-256, HKDF-SHA-256, AES-256-GCM). Send `X-Chat-E2E: .` (both base64) and conversation bodies as `{"e2e": {"iv", "ct"}}`; answers and stream events come back sealed the same way. It hides chats from anything between you and the grid (such as the CDN in front of it). It does **not** hide them from the grid itself or from the volunteers whose devices run the model. With page tokenization the grid sees token ids instead of text, which are still decodable. - **Tools** (web search, page fetch, calculator, clock) and **MCP servers**: open a conversation with `{"tools": true, "mcp": ["deepwiki", ...]}`; `GET /api/chat/mcp` lists the servers. Tool traffic leaves the grid through Tor. For verified accounts only; for others `tools` is ignored. ## Clients - Python, standard library only: `clients/mpakt_grid.py` — `Account(host).login/create_key`, `VirtualGPU(host, key)`, `Graph` with a method per op and `decoder_layer(...)`. - MCP: `GRID_HOST=https://HOST GRID_KEY=mpakt_... node mcp/server.js` — tools for status, operations, model inspection and loading, graphs, sessions, compiled graphs and generation, plus a `grid://guide` resource.