How SubstraOS works
A decentralized AI compute layer on Solana. People contribute GPU from a browser tab or native client; capacity is pooled and sold as AI inference; contributors earn. DePIN meets DeAI.
Overview
SubstraOS is a three-tier stack. Demand (apps) sits on top, a routing and gateway layer in the middle, and the supply (pooled GPUs) at the base. Products create demand; the substrate provides supply; verified work and earnings tie the two together.
Contribute compute
LIVETurn idle GPU into earnings. The browser path is live today:
Use the network
Playground LIVE — chat with pooled models straight from the browser (wallet-gated). Requests hit the gateway, the router selects a worker, and tokens stream back; the fallback node always answers so you never see a dead network.
Developer API (OpenAI-compatible endpoint + keys + usage) is live. LIVE See the API reference sections below.
Model hub and Agents are built to the design and gated behind "coming soon" while they harden. BUILDING
API quickstart
LIVEThe gateway is OpenAI-compatible and responds with SSE (streaming) for every request -- there is no non-streaming JSON branch. When you use the raw openai SDK, always pass stream=True; a non-streaming call would expect a JSON body and fail to parse the event stream. If you want a single aggregated completion object, use the official SubstraOS SDKs (@substraos/sdk for JS/TS, substraos for Python), which read the stream and assemble one chat.completion for you. The gateway is centralized in v1; depending on the model you pick, inference runs on the pooled community network (people's GPUs) or a cloud-backed Pro upstream.
curl -N https://api.substraos.tech/v1/chat/completions \
-H "Authorization: Bearer $SUBSTRA_KEY" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"model":"qwen2.5-7b-instruct","messages":[{"role":"user","content":"Hello"}],"stream":true}'Response shape. On HTTP 200 the wire is Content-Type: text/event-stream, with frames written as data: <json>\n\n in this order:
1. routing-meta (one frame)
data: {"route":"browser|network|fallback|pro","worker_id":"w_..."|null,"request_id":"req_...","model":"..."}
2. chunk (one or more)
data: {"id":"req_...","object":"chat.completion.chunk","model":"...","choices":[{"index":0,"delta":{"content":"..."},"finish_reason":null}]}
3. terminator
data: [DONE]Note that id is the request_id (not an OpenAI chatcmpl- id), and the SubstraOS SDKs transparently skip the routing-meta frame for you.
Official SDKs are publishing soon: @substraos/sdk (JS/TS) and substraos (Python). They are not on npm / PyPI yet -- install from the monorepo source for now. The streaming wire format is OpenAI-compatible, so the openai client works today with stream=True.
import { SubstraOS } from "@substraos/sdk";
const client = new SubstraOS({ apiKey: "sk_sub_live_..." }); // baseURL defaults to https://api.substraos.tech
const stream = await client.chat.completions.create({
model: "qwen2.5-7b-instruct",
messages: [{ role: "user", content: "Hello" }],
stream: true,
});
for await (const chunk of stream) process.stdout.write(chunk.choices?.[0]?.delta?.content ?? "");from substraos import SubstraOS
client = SubstraOS(api_key="sk_sub_live_...") # base_url defaults to https://api.substraos.tech
for chunk in client.chat.completions.create(
model="qwen2.5-7b-instruct",
messages=[{"role": "user", "content": "Hello"}],
stream=True,
):
print((chunk["choices"][0].get("delta") or {}).get("content", ""), end="")API keys & auth
LIVEAuthentication is your Solana wallet, not a password. Sign in with your wallet (SIWS) to get a dashboard session, then mint an API key.
- Sign-in flow (to obtain a session):
POST /v1/auth/nonce{ walletAddress }returns{ nonce, message, expiresAt }; signmessagewith your wallet;POST /v1/auth/login{ walletAddress, nonce, signature, message }returns{ accessToken, expiresAt, userId, walletAddress }. (The dashboard does this for you.) - Create / rotate your key:
POST /v1/auth/apikey(session-JWT-gated) returns{ key, keyPrefix, id, createdAt }. You get one key per account -- creating a new one rotates and revokes the old one. The full secretsk_sub_live_...is shown once; store it now, it is never shown again. - List key metadata:
GET /v1/auth/apikeyreturns{ keys: [...] }(prefix + metadata only, never the secret). - Revoke:
DELETE /v1/auth/apikey/:idreturns{ id, revoked: true }. Revocation is instant.
Send your key as Authorization: Bearer sk_sub_live_... on every request. Only POST /v1/chat/completions accepts the API key -- the key-management, usage, and other /v1/* routes require the dashboard session (JWT), not the key. Keys draw the same prepaid credit balance as the playground.
Models & tiers
LIVECommunity models run on pooled people's GPUs (browser/WebGPU contributors), with a rented fallback node that always answers -- expect small models and intermittent capacity. Pro models are cloud-backed (resold from an upstream provider) and billed cost-plus through the same credits. Fetch the live catalog any time at GET https://api.substraos.tech/v1/models (public, no auth; upstream provider ids are stripped from the response).
model id tier runtime context price (in + out combined) qwen2.5-7b-instruct community browser / WebGPU 32,768 $10.00 / 1M tokens qwen2.5-3b-instruct community browser / WebGPU 4,096 $10.00 / 1M tokens
model id tier runtime context input $/1M output $/1M llama-3.3-70b-pro pro cloud-backed 131,072 $0.20 $0.64 llama-3.1-8b-pro pro cloud-backed 131,072 $0.04 $0.06 qwen-2.5-coder-32b-pro pro cloud-backed 32,768 $1.32 $2.00
503 pro_paused).Image generation
LIVEPOST /v1/images/generations generates images on the Pro tier — cloud-backed upstream, not community GPU. It accepts the same auth as chat: a dev API key (sk_sub_…) or a session JWT. Plain JSON round-trip (no SSE): the response carries a base64 PNG in data[0].b64_json. Billing is prepaid credits (~3 cr per 1024×1024 image), debited from the key owner's balance.
curl https://api.substraos.tech/v1/images/generations \
-H "Authorization: Bearer $SUBSTRA_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"flux-2-klein","prompt":"a red fox, studio light","size":"1024x1024"}'import { SubstraOS } from "@substraos/sdk";
const client = new SubstraOS({ apiKey: "sk_sub_live_..." });
const img = await client.images.generate({
model: "flux-2-klein",
prompt: "a red fox, studio light",
size: "1024x1024",
});
const b64Png = img.data[0].b64_json; // base64 PNGfrom substraos import SubstraOS client = SubstraOS(api_key="sk_sub_live_...") img = client.images.generate(model="flux-2-klein", prompt="a red fox, studio light", size="1024x1024") b64_png = img["data"][0]["b64_json"] # base64 PNG
Limits. 10 images / min per account, plus the shared per-key 60 requests / min. Errors use the standard envelope: 402 insufficient_credits (top up first), 422 invalid_body, 404 model_not_found, 400 not_an_image_model, 429 rate_limited, 503 pro_image_unavailable (Pro tier temporarily off), 503 pro_image_paused (daily image budget reached — retry later), 503 pro_image_failed (upstream error).
Limits & billing
LIVEBilling is prepaid credits. 1 credit = $0.01 USD (deposits convert at credits = USDC x 100). A request first places a short hold for its estimated max cost, then settles to actual usage on completion. If your available balance can't cover the request estimate, the call returns 402 insufficient_credits before any work is scheduled. Top up credits in the dashboard.
Rate limit: each API key is capped at 60 requests per minute (fixed 60-second window). Exceeding it returns 429 rate_limited with a retry_after field (seconds) in the JSON body and a Retry-After response header. Session/playground requests (no API key) are not RPM-limited. The limiter fails closed -- if the backend rate-limit store is unavailable, requests return 429 with retry_after: 60.
Errors
LIVEErrors use an OpenAI-style envelope. request_id is present on gateway/Pro responses; retry_after is present on 429: { "error": { "message": "...", "type": "...", "code": 400, "request_id": "req_...", "retry_after": 30 } }
400 invalid_request malformed body or unknown/invalid field 401 invalid_auth missing, invalid, or revoked key / session 402 insufficient_credits not enough credits to cover the request -- top up 404 model_not_found unknown or disabled model id 429 rate_limited per-key RPM exceeded (see retry_after) 500 internal_error unexpected server error -- retry 503 no_capacity no worker available right now -- retry shortly 503 pro_unavailable Pro tier disabled or not configured 503 pro_paused Pro tier paused (daily budget reached)
The canonical type strings are exactly: invalid_request, invalid_auth, insufficient_credits, model_not_found, rate_limited, no_capacity, internal_error (plus the Pro-only pro_unavailable, pro_paused, pro_upstream_error).
200, so a worker failure mid-stream is delivered inside the stream, not as the envelope above. It arrives as a final data frame carrying only message + type (no code, no request_id), followed by [DONE]:data: {"error":{"message":"no_capacity","type":"no_capacity"}}
data: [DONE]type: "no_capacity"; a Pro upstream failure surfaces as type: "pro_upstream_error". Treat any in-stream error frame as a failed request and retry.Compute tiers
Work verification
BUILDINGThe central hard problem: there is no cheap cryptographic proof that a remote machine actually ran a specific LLM forward pass — inference is non-deterministic across hardware, so naive output-hashing fails for honest nodes, and TEEs/ZK are out of scope.
Instead SubstraOS uses a pragmatic layered rubric that makes cheating unprofitable and detectable rather than impossible:
- Canary jobs — prompts with known-good answers, scored against ground truth.
- Redundancy sampling — the same job to multiple nodes, compared by semantic agreement (embedding similarity, not string equality).
- Timing & plausibility — empty or implausibly-timed completions are rejected outright.
- Reputation — an EWMA score with asymmetric decay; new nodes start on probation and gate into higher-value work.
- Rate limits & a trusted fallback oracle — bound abuse and provide a tiebreaker.
Semantic checks currently run in shadow mode while thresholds are calibrated on real honest-node output — they record and move reputation but don’t yet gate earnings; the robust timing/empty signals do. Collusion is the acknowledged residual weakness. Earnings always accrue on verified work, never uptime.
Earnings & payouts
LIVETwo ledgers, one rule (verified work only). Points are your leaderboard score — a flat amount per verified job. Not money, not convertible, not a token. USDC is your real earnings — 75% of buyer spend on jobs your node served.
Provider USDC payouts are live. Paid verified work accrues a real USDC balance; you withdraw it to the wallet you signed in with, paid out in USDC on Solana mainnet from a $0.01 minimum. The off-chain ledger is the source of truth, and a withdrawal can never exceed what was verifiably earned.
Architecture
Off-chain first. Postgres and Redis are the source of truth — nodes, jobs, reputation, the credit/earnings ledger, and receipts. The on-chain footprint is deliberately minimal: wallet sign-in signature verification and USDC payouts. No custom programs.
Solana via Alchemy. All RPC routes through the backend; the provider key is server-side only and never ships to the browser.
Roadmap
Each phase ships independently. Token integration stays additive and late.
Token
LIVEThe $SUBSTRAOS token is live on pump.fun — an external, founder-launched asset. The contract address:
Nothing in the product depends on the token. It is shown here as an informational contract address only — contribution today is leaderboard points plus live USDC payouts, and no flow requires the token. Any product integration (provider rewards, fee discounts, converting accumulated contribution points) is additive and still on the roadmap — launching the token does not wire any of it in. This is not financial advice.
