Docs

A computer is one HTTP call away. boringd boots a Firecracker microVM — jailed, resource-capped, network-isolated — and hands you a serial console, a VNC display, or an AI that drives it. Snapshot-restore means a shell is ready in ~3 ms. Machines self-destruct when their TTL expires — or pass "persistent": true to keep one running until you delete it.

Base URL

Run your own boringd (see the repo) and point everything at your deployment. It listens on this by default:

http://localhost:8080

Quickstart

Boot a machine and read it back:

# boot a shell (python3 + node), 60s TTL
curl -s -X POST http://localhost:8080/v1/machines \
  -H 'content-type: application/json' \
  -d '{"template":"python","ttl_seconds":60}'

# → {"id":"m-1a2b3c4d","mode":"snapshot","boot_ms":3,
#    "template":"python","expires_at":"..."}

REST

POST/v1/machinesBoot a machine. Body: {template, ttl_seconds, net}
GET/v1/machinesList running machines
GET/v1/machines/{id}Fetch one machine
DELETE/v1/machines/{id}Destroy a machine now
POST/v1/machines/{id}/branchFork a running machine into a live clone
GET/v1/machines/{id}/screenshotPNG screenshot of a desktop
POST/v1/machines/{id}/uploadUpload a file to /root (X-Filename header)
GET/v1/machines/{id}/download?path=…Download a file from the machine
GET/healthzLiveness + running count

Templates: python (headless shell, snapshot, ~3 ms) and desktop (GUI over VNC). ttl_seconds is clamped to 15–900. Pass "net": true to give the machine internet (cold-boots instead of snapshot; pip/npm/apk install work). Guests are NAT'd and egress-firewalled. File transfer and previews (below) need a connected machine — a desktop, or a shell with net. Pass "persistent": true for a machine with no TTL (runs until you delete it) — honored only when the server sets BORING_ALLOW_PERSISTENT=1, else it falls back to the TTL.

WebSockets

Interactive channels upgrade to WebSocket (binary frames):

/v1/machines/{id}/ttySerial console — bytes ⇄ the guest /dev/ttyS0
/v1/machines/{id}/vncRFB/VNC framebuffer for desktop machines
/v1/machines/{id}/agent?goal=…Computer-use agent — drives the screen; streams narration JSON
/v1/machines/{id}/shell-agent?goal=…Terminal agent — writes + runs code; streams narration JSON

Previews

Run a server inside a connected machine and open its port through boringd — works locally (over a tunnel) and on public deployments, no wildcard DNS:

http://localhost:8080/v1/machines/<machine-id>/web/<port>/

Inference

An OpenAI-compatible gateway — Claude runs on Anthropic, everything else routes through OpenRouter (set your own BORING_OPENROUTER_KEY).

POST/v1/chat/completionsChat completions (streaming + non-streaming)
GET/v1/modelsList available models
curl -s http://localhost:8080/v1/chat/completions \
  -H 'content-type: application/json' \
  -d '{"model":"claude-sonnet-4-6","messages":[{"role":"user","content":"hi"}]}'

Storage

Persistent volumes (S3-backed) that outlive a machine. Create one, save a machine's /root into it, then restore it into a fresh machine by passing its id as volume on launch. Volumes are addressed by an unguessable id and garbage-collected on a TTL.

POST/v1/volumesCreate a volume. Body: {ttl_seconds}
GET/v1/volumes/{id}Metadata + usage
DELETE/v1/volumes/{id}Delete a volume
GET/v1/volumes/{id}/filesList files
PUT/v1/volumes/{id}/file?path=…Upload a file
GET/v1/volumes/{id}/file?path=…Download a file
POST/v1/machines/{id}/save?volume=…Save a machine's /root into a volume

Attach on launch: POST /v1/machines with {"volume":"vol-…"} restores the volume into /root first.

TypeScript client

An Effect-native client lives in the repo at packages/sdk (not on npm) — Schema-validated responses, typed errors, a streaming serial console, retries built in:

import { Effect, Stream } from 'effect';
import { make } from '@boring/sdk';

const boring = make({ baseUrl: 'http://localhost:8080' });

Effect.runPromise(
  Effect.gen(function* () {
    const vm = yield* boring.createMachine({ template: 'python', ttlSeconds: 60 });
    console.log(vm.id, vm.mode, `${vm.boot_ms}ms`);

    // the serial console is a Stream; the socket closes with the Scope
    yield* Effect.scoped(
      Effect.gen(function* () {
        const tty = yield* boring.connectTty(vm.id);
        yield* tty.send('print("hello from a microVM")\n');
        yield* tty.output.pipe(Stream.runForEach((b) => Effect.sync(() => process.stdout.write(b))));
      })
    );

    yield* boring.destroyMachine(vm.id);
  })
);

Also: listMachines, getMachine(id), branchMachine(id). Errors are tagged (RequestError, ResponseError).

MCP server

Let any AI — Claude Desktop, Cursor, … — spin up and drive a computer over the Model Context Protocol. It lives in the repo at packages/mcp (not on npm yet); run it from source and point your MCP client at it:

{
  "mcpServers": {
    "boring-computers": { "command": "node", "args": ["/path/to/packages/mcp/index.mjs"] }
  }
}

Tools: launch_computer, run_task (plain-English task → an agent writes + runs the code, returns a live URL), screenshot, preview_url, fork_computer, stop_computer.