Reference

Documentation

Everything you need to submit compute jobs, embed the worker widget on your site, and integrate with the coordinator API. Write kernels in plain Python with py2wgsl, submit from a terminal over SSH, or embed the worker on your site. Guides for submitters and site owners, plus a WebSocket protocol reference.

Getting started

Embedding the widget

Add the Warppool worker to any site in three steps. Visitors see a consent prompt before any compute runs. See live demos to preview how it looks on a real page.

1

Get your embed snippet

The coordinator exposes your site-specific snippet at /api/embed-config. Fetch it with curl or visit the URL in a browser.

terminal
curl https://warppool.tech/api/embed-config
2

Paste the script tag into your HTML

Drop this into your page's <body>, ideally near the closing tag. The script loads async and never blocks rendering.

your-site.html
<!-- in your <body>, ideally near the end -->
<script
  src="https://warppool.tech/w.js"
  api-key="2835ef562fcdfd84eeb061d9c7e9d1ad"
  data-budget="3"
  data-priority="idle"
  data-jobs="research-grants,acme"
  async
></script>

<!-- defaults to a consent strip in the corner;
     swap for inline placement: -->
<div id="warppool-consent"></div>
3

Customize

Tune the worker via data attributes on the script tag.

api-key
Your site's API key from the Warppool dashboard. Credits accrue here.
data-budget
GPU percentage cap (1–25). Default: 3. The worker measures real utilization and backs off.
data-priority
idle (default) runs only when the visitor isn't interacting. active runs continuously. visible-only pauses in background tabs.
data-jobs
Comma-separated allowlist of job classes. research-grants is the default pool. Add your site's slug to spend earned credits on your own jobs.
Consent is required. The widget always shows a consent prompt before compute begins. Visitors choose "Allow for this site," "Just this visit," or "No thanks." The prompt never reappears after a decision. No compute runs without explicit opt-in.
Authentication

API keys

Embed widgets require an API key to connect workers to the coordinator. Keys are tied to a domain and validated on every WebSocket connection.

Generating a key

Keys are generated via the admin API. You need the ADMIN_TOKEN to create one.

terminal
# Generate a key for your domain
$ curl -X POST https://warppool.tech/api/admin/keys \
    -H "Authorization: Bearer $ADMIN_TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"site": "example.com"}'

> { "key": "a1b2c3d4e5f6...",
    "site": "example.com",
    "created_at": "2026-06-15T10:00:00" }

# List all keys (values redacted)
$ curl https://warppool.tech/api/admin/keys \
    -H "Authorization: Bearer $ADMIN_TOKEN"

Using the key

Add the key as an api-key attribute on the embed script tag. The loader script (w.js) reads it and passes it to the iframe.

your-site.html
<script
  src="https://warppool.tech/w.js"
  api-key="a1b2c3d4e5f6..."
  async
></script>

Origin validation

On each WebSocket connection, the coordinator checks that the request origin matches the domain registered to the key. Subdomains are supported: www.example.com matches a key registered to example.com. Requests from non-matching origins are rejected.

Workers without keys. Direct workers (e.g., worker.html) connect without a key for internal use and testing. Only the embed widget path requires an API key.
For job submitters

Running jobs on Warppool

Submit a WGSL compute kernel and a chunk manifest. The coordinator distributes chunks across available browser workers and returns the aggregated result. No SDK — everything is HTTPS and JSON.

What you provide

  1. The WGSL kernel.

    A compute shader that reads chunk parameters from a uniform buffer and writes output to a storage buffer. If you've written a CUDA kernel, the mental model is identical; the syntax is closer to Rust.

  2. The chunk manifest.

    A JSON document describing how the job splits into chunks — one parameter set per chunk. The coordinator hands one entry to each worker.

  3. The reducer.

    How to combine per-chunk results into a single answer. Built-ins: sum, concat, max, min, argmax, topk. Or supply a second WGSL kernel for custom reductions.

  4. The redundancy policy.

    Default k=2 with a third opinion on disagreement. Higher k for safety-critical runs; k=1 for cheap exploratory sweeps.

  5. An optional deadline.

    Without a deadline the coordinator picks the cheapest workers. Add one to recruit faster nodes and finish by a wall-clock target.

End-to-end example: Monte Carlo π

monte_carlo_pi.wgsl
@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var<storage, read_write> hits: atomic<u32>;

struct Params {
  seed_start:         u32,
  num_samples:        u32,
  samples_per_thread: u32,
}

// PCG hash — fast, good statistical properties
fn pcg(x: u32) -> u32 {
  var s = x * 747796405u + 2891336453u;
  var w = ((s >> ((s >> 28u) + 4u)) ^ s) * 277803737u;
  return (w >> 22u) ^ w;
}

@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
  let tid = gid.x;
  var state = pcg(pcg(params.seed_start + tid));
  var local_hits: u32 = 0u;
  for (var i: u32 = 0u; i < params.samples_per_thread; i++) {
    state = pcg(state);
    let x = f32(state) / 4294967296.0;
    state = pcg(state);
    let y = f32(state) / 4294967296.0;
    if (x*x + y*y < 1.0) { local_hits += 1u; }
  }
  atomicAdd(&hits, local_hits);
}
submit.sh
# 1. Upload the kernel
$ curl https://warppool.tech/api/kernels \
    -F "src=@monte_carlo_pi.wgsl"
> { "kernel_id": "wgsl-pi-7f3" }

# 2. Build a chunk manifest — 100 chunks of
# 100M samples each, distinct seed ranges
$ python -c "
import json
print(json.dumps({
  'chunks': [
    {'seed_start': i * (1<<24),
     'num_samples': 100_000_000,
     'samples_per_thread': 256}
    for i in range(100)
  ],
}))" > manifest.json

# 3. Submit the job
$ curl https://warppool.tech/api/jobs \
    -H "Authorization: Bearer $WARPPOOL_TOKEN" \
    -F "kernel_id=wgsl-pi-7f3" \
    -F "manifest=@manifest.json" \
    -F "reducer=sum" \
    -F "redundancy=2"
> { "job_id": "job-9a2c",
    "queued_at": "2026-05-27T14:02:11Z",
    "chunks": 100, "est_seconds": 38 }

# 4. Stream results back
$ curl https://warppool.tech/api/jobs/job-9a2c/stream
event: chunk_done
data: {"chunk":0,"hits":78539701}
event: chunk_done
data: {"chunk":1,"hits":78540234}
...
event: job_done
data: {"pi":3.14159265}

Pricing

Researchers · free

Free for researchers

Anyone with a verifiable institutional affiliation — university, public lab, K-12 school — runs free with no monthly cap. Class projects, replication studies, and grad work all qualify.

Pay as you go

Per-millisecond billing

Commercial workloads are metered per millisecond of completed, verified compute. Failed and retried chunks cost nothing. Indicative rate: ~$0.04 / GPU-hour.

No SDK, no lock-in. The submit protocol is HTTPS and JSON; the worker protocol is JSON over WebSocket. Anything that speaks REST can submit a job. Sites that embed the worker tag earn credits they can spend on their own jobs.
Write kernels in Python

Python kernels with py2wgsl

Don't want to hand-write WGSL? py2wgsl compiles a restricted subset of Python into complete WGSL compute modules. Your kernel is an ordinary Python function — it is parsed with the ast module and translated, never executed. It's a standalone package on PyPI with zero runtime dependencies, usable anywhere WGSL runs.

Install & usage

Install from PyPI. The base package is pure stdlib; the optional [gpu] extra pulls in wgpu so you can execute compiled kernels straight from Python.

terminal
$ pip install py2wgsl        # zero runtime dependencies
$ pip install "py2wgsl[gpu]"   # + wgpu, to run kernels from Python

Annotate a function's parameters with the provided types, write the body in plain Python, and call compile_kernel. You get back the full WGSL module plus the binding layout metadata you need to set up bind groups.

kernel.py
from py2wgsl import (
    Array, f32, compile_kernel,
    global_id, array_length,
)

# Each scalar param becomes a uniform field;
# each Array becomes a storage buffer.
def scale_add(
    scale: f32, offset: f32,
    values: Array[f32], result: Array[f32],
):
    i = global_id()
    if i < array_length(result):
        result[i] = values[i] * scale + offset

kernel = compile_kernel(scale_add, workgroup_size=64)
python
# The complete WGSL module — feed straight
# to device.createShaderModule()
>>> print(kernel.wgsl)
@group(0) @binding(0) var<uniform> params: Params;
@group(0) @binding(1) var<storage, read> values: ...
@group(0) @binding(2) var<storage, read_write> result: ...
@compute @workgroup_size(64)
fn main(...) { ... }

# Binding layout metadata for your bind group
>>> kernel.buffers
[UniformField('scale', ...), BufferBinding('result', read_write=True), ...]

There is also compile_kernel_source(src) to compile from a source string, and python -m py2wgsl.demo prints two worked examples — including a Monte Carlo π kernel like the one this network runs.

Parameter → binding mapping

Parameters map to bindings by their type annotation, in declaration order. Buffers the kernel writes to get read_write access; the rest are read. The entry point is always fn main with @builtin(global_invocation_id).

f32, i32, u32
A field in the Params uniform struct at @binding(0). (float/int also accepted.)
Array[f32 | i32 | u32]
A storage buffer at the next binding, in declaration order.

Supported subset

  • Assignments (first assignment declares a typed var), annotated and augmented assignments
  • if/elif/else, while, for i in range(...) with constant step, break, continue, bare return
  • Arithmetic, comparison, boolean, and bitwise operators; a if c else b (compiles to select)
  • Math builtins bare or as math.*: sqrt, sin, cos, floor, min, max, clamp, pow, abs, … plus math.pi / math.e / math.tau
  • Explicit casts f32(x), i32(x), u32(x)
  • GPU builtins: global_id(), local_id(), workgroup_id(), array_length(buf), barrier(), storage_barrier()
WGSL wins over Python. A few semantics are deliberately not Python's — the compiler errors on the ambiguous cases it can catch. Integers are 32-bit and wrap (not bignums); / on two integers is a compile error (use // or cast with f32()); there are no implicit int/float conversions; conditions must be bool (no truthiness); and there are no atomics, vectors, textures, or user-defined function calls yet. Don't put secrets in kernel source — the workers that run it can read it.
Terminal access

Submitting jobs over SSH

Prefer a terminal to REST? Register a public key from your dashboard and submit jobs over SSH with the warp CLI, the way you'd queue work on an HPC cluster. You keep your private key; the server only ever stores the public half.

  1. Register your public key.

    Open the SSH Access tab on your dashboard, give the key a name, and paste the contents of your public key file (e.g. ~/.ssh/id_ed25519.pub). Accepted types: ssh-ed25519, ssh-rsa, and ecdsa-sha2-nistp256/384/521. Keys are identified by their SHA256: fingerprint and can be revoked any time. Only public keys are stored — private key material never touches the server.

  2. Find your gateway login.

    Each account gets a stable gateway username of the form wp-<8 chars>, shown in the SSH Access tab alongside the host. Connect with your registered key:

    terminal
    $ ssh wp-3f9a2c1b@ssh.warppool.tech
  3. Submit with the warp CLI.

    Run warp submit over the connection — inline as a one-shot command, or interactively once you're on the gateway.

    terminal
    # One-shot: submit a Monte Carlo pi job
    $ ssh wp-3f9a2c1b@ssh.warppool.tech \
          warp submit --samples 100000000 --name my-job
    
    # Or bring your own compiled kernel + manifest
    $ warp submit --kernel kernel.wgsl \
          --manifest manifest.json --reducer sum
    
    > job-9a2c queued · 100 chunks · est 38s
  4. Fetch your results.

    Job products land under jobs/<job-id>/ on the gateway. Pull them down with scp once the run completes:

    terminal
    $ scp wp-3f9a2c1b@ssh.warppool.tech:jobs/job-9a2c/output.json .
Every run is metered to the millisecond. Whether you submit over SSH, REST, or the dashboard, completed compute is recorded in a usage ledger and billed per GPU-millisecond — failed and retried chunks cost nothing. Researchers with a verified institutional affiliation run free.
REST API

API reference

All endpoints are served by the coordinator at your deployment's base URL. Responses are JSON.

POST/api/jobs

Create a new compute job. For Monte Carlo pi estimation, send total_samples. For Q-learning grid world, send job_type: "q_learning" with grid parameters.

request
curl -X POST https://warppool.tech/api/jobs \
  -H "Content-Type: application/json" \
  -d '{"total_samples": 1000000}'
response
{
  "job_id": "a1b2c3d4",
  "status": "running"
}

GET/api/jobs

List all jobs with status, chunk progress, and pi estimate (if applicable).

response
[
  {
    "job_id": "a1b2c3d4",
    "total_samples": 1000000,
    "status": "completed",
    "pi_estimate": 3.14182,
    "chunks_total": 4,
    "chunks_completed": 4,
    "created_at": "2026-05-30T12:00:00",
    "completed_at": "2026-05-30T12:00:14"
  }
]

GET/api/jobs/{job_id}

Get full details for a single job, including per-chunk status and worker assignments.

response
{
  "job_id": "a1b2c3d4",
  "job_type": "pi",
  "total_samples": 1000000,
  "status": "completed",
  "pi_estimate": 3.14182,
  "chunks": [
    {
      "chunk_id": "c001",
      "worker_id": "w1a2b3c4",
      "num_samples": 250000,
      "status": "completed"
    }
  ],
  "created_at": "2026-05-30T12:00:00",
  "completed_at": "2026-05-30T12:00:14"
}

GET/api/workers

List all connected workers with GPU info and chunk completion counts.

response
[
  {
    "worker_id": "w1a2b3c4",
    "status": "idle",
    "gpu_vendor": "apple",
    "gpu_device": "Apple M2",
    "gpu_architecture": "apple-7",
    "chunks_completed": 14,
    "connected_at": "2026-05-30T11:58:00"
  }
]

GET/api/embed-config

Returns the embed script tag snippet configured for your coordinator's URL.

response
{
  "embed_html": "<script src=\"https://warppool.tech/w.js\" api-key=\"YOUR_API_KEY\" async></script>"
}
WebSocket

WebSocket protocol

Workers connect to the coordinator at /ws. All messages are JSON with a type field. Here's the full message flow.

Direction Type Payload
Server → Worker welcome {worker_id} — assigned unique 8-char hex ID
Worker → Server gpu_capabilities {data: {vendor, device, architecture, ...}}
Server → Worker compute_chunk {job_id, chunk_id, seed_start, num_samples, samples_per_thread}
Worker → Server chunk_result {job_id, chunk_id, hits}
Server → Worker ping Keepalive, sent every 30 seconds
Worker → Server pong Keepalive reply
server → worker
{
  "type": "compute_chunk",
  "job_id": "a1b2c3d4",
  "chunk_id": "c001",
  "seed_start": 0,
  "num_samples": 250000,
  "samples_per_thread": 1000
}
worker → server
{
  "type": "chunk_result",
  "job_id": "a1b2c3d4",
  "chunk_id": "c001",
  "hits": 196382
}
Security model

Trust & verification

Submitters can't trust volunteer machines; volunteers can't trust submitter kernels. The protocol is built so neither side has to.

Redundant dispatch

Every chunk goes to 2–3 independent workers. Results are compared within numeric tolerance. Disagreement triggers re-runs on disjoint nodes; persistent disagreement quarantines the kernel for human review.

Canary chunks

A fraction of dispatched chunks have known answers seeded by the coordinator. Workers don't know which. Returning the wrong answer to a canary downranks the worker silently.

Browser sandbox

Workers run inside the browser's own GPU sandbox. No filesystem access, no native APIs, no persistent state across reloads. Bounded memory per chunk. Visitor closes the tab — the worker is gone.

Per-job audit trail

Every dispatch and result for your job is appended to a log keyed by worker ID and chunk hash. You can replay it, verify the reduction, and check that no worker's output was silently dropped.

Important caveat. Volunteer compute is not zero-trust compute. We protect submitters from lying workers via redundancy, and volunteers from malicious kernels via the browser's GPU sandbox — but kernels you submit are visible to the workers that run them. Don't put secrets in your shader source.
FAQ

Common questions

Does Warppool work in all browsers?

Warppool runs on any browser with modern GPU-compute support — Chrome 113+, Edge 113+, and Firefox (Nightly today). Safari support is on the way. Warppool automatically detects whether a visitor's browser and device can contribute and silently skips those that can't — no errors, no broken pages.

Do I need HTTPS?

Yes. Warppool taps your browser's GPU, which browsers only expose over a secure connection, so production sites need HTTPS. During local development, localhost is exempt from this requirement.

Can I self-host the coordinator?

Yes. The coordinator is a Python/FastAPI app. Run it with uvicorn server.main:app --host 0.0.0.0 --port 8000 or deploy the provided Docker image. Workers connect to whatever URL you point them at.

How are chunk seeds managed?

The coordinator assigns non-overlapping seed ranges to each chunk. The WGSL shader uses a PCG hash PRNG seeded from each range, so results are deterministic and reproducible given the same seed.

What happens if a worker disconnects mid-chunk?

The coordinator detects the dropped WebSocket and marks the chunk as unfinished. It gets reassigned to the next available worker. No results are lost.

Is there a rate limit on the API?

The open-source coordinator has no built-in rate limiting. In a production deployment, add rate limiting at the reverse proxy layer (nginx, Cloud Run, etc.) based on your capacity.

Publisher FAQ

Will visitors actually opt in to share GPU?

Sites running the tag today see consent rates of 45–65% — higher than any cookie banner. The ask is small and the reward is legible ("we fund research grants" rather than "we sell your data"). Anonymized aggregate consent stats are published by category so publishers can benchmark.

Does the embed widget hurt page performance?

The script weighs 11 KB gzipped, loads async, and doesn't run a kernel until consent is granted and the page has been idle for at least 4 seconds. By default the worker uses 3% GPU on a 16ms budget — well below the threshold where users notice.

What about visitors on phones or slow devices?

The worker only attaches if the device's calibration benchmark clears a minimum threshold (roughly: modern phone or anything with discrete graphics). Slow devices never see the consent strip. Battery-powered devices that aren't on AC get a lower default budget.

Is it legal under GDPR / CCPA / DSA?

The tag collects no personal data and processes no identifiable signals — so the regulations that apply to ad networks don't apply here. We still surface a one-click stop and a transparency page because that's the right thing to do.