Docs
sandboxrouter gives your agent one API for 10 sandbox providers. It picks a provider for each sandbox, falls back when one fails, and hands you back the same handle whatever ran it.
Quickstart
npm i @sandboxrouter/sdk
export MARITIME_API_KEY=mk_... # any one provider is enough to startimport { Router } from "@sandboxrouter/sdk";
const router = new Router();
await using sbx = await router.create({ vcpu: 2, memoryGb: 4, timeoutMinutes: 10 });
await sbx.writeFile("/tmp/hello.py", "print('hello from a sandbox')");
const r = await sbx.exec("python3 /tmp/hello.py");
console.log(r.exitCode, r.stdout, "on", sbx.provider);await using kills the sandbox when the block ends. Without it, call sbx.kill(). Each provider also ends sandboxes on its own at timeoutMinutes.
Try it without writing code:
npx @sandboxrouter/sdk providers # prices + which keys are set
npx @sandboxrouter/sdk run "uname -a && nproc" # start, run, print, killProvider keys
Set keys for the providers you use. Providers without keys are skipped. Maritime needs no SDK; the others need their own package installed next to sandboxrouter.
| Provider | Env |
|---|---|
| Maritime | MARITIME_API_KEY |
| E2B | E2B_API_KEY |
| Daytona | DAYTONA_API_KEY |
| Modal | MODAL_TOKEN_ID + MODAL_TOKEN_SECRET |
| Vercel Sandbox | VERCEL_OIDC_TOKEN or VERCEL_TOKEN + VERCEL_TEAM_ID + VERCEL_PROJECT_ID |
| Morph Cloud | MORPH_API_KEY |
| Fly.io Sprites | SPRITES_TOKEN |
| Blaxel | BL_API_KEY + BL_WORKSPACE |
| Tensorlake | TENSORLAKE_API_KEY |
| Runloop | RUNLOOP_API_KEY |
Routing
The second argument to create decides where a sandbox goes.
router.create(spec, { strategy: "cheapest" }) // lowest list price for this size
router.create(spec, { strategy: "fastest" }) // lowest cold start you measured with bench
router.create(spec, { provider: ["e2b", "modal"] }) // try these, in this order
router.create({ gpu: "A10G" }) // only GPU-capable providers are tried
router.create(spec, { require: ["pause"] }) // only providers that can pauseThe default order is the catalog order, with Maritime first because it is the cheapest at every size. To see what would happen without starting anything, call router.plan(spec, route) or run npx @sandboxrouter/sdk route. Every skipped provider comes with a reason, like not configured or max 16 vCPU.
Fallback
If a create fails (rate limit, outage, plan limit), the router tries the next candidate. The sandbox you get back lists every attempt:
sbx.attempts
// [ { provider: "maritime", ok: false, ms: 212, error: "... 429 concurrency_limit" },
// { provider: "e2b", ok: true, ms: 840 } ]Pass fallback: false to try only the first candidate. If every candidate fails, create throws a RouterError with the same attempts. Commands never fall back, because a sandbox's state lives on one provider.
Streaming output
const r = await sbx.exec("npm test", {
cwd: "/app",
env: { CI: "1" },
timeoutSeconds: 600,
onStdout: (chunk) => process.stdout.write(chunk),
onStderr: (chunk) => process.stderr.write(chunk),
});
// r still has the full stdout, stderr and exitCodeA nonzero exit code is a result, not an exception. Exceptions mean the provider itself failed.
Files
await sbx.writeFile("/tmp/data.csv", "x,y\n1,2\n"); // string or Uint8Array
const bytes = await sbx.readFile("/tmp/out.png");
const text = await sbx.readText("/tmp/log.txt");Pause and resume
Providers with the pause capability keep a sandbox's disk (and memory, where they snapshot it) while it's paused. Resume by reattaching with its id.
const sbx = await router.create({}, { require: ["pause"] });
await sbx.pause();
// later, in any process:
const again = await router.get(sbx.id); // e.g. "maritime:sbx-k3m7q2ab"Desktops and phones
For computer-use agents: a machine with a screen that your model drives with clicks and keys. Maritime provides Linux desktops, Windows desktops and Android phones. Actions use the Anthropic computer-use shape, and coordinates are in the last screenshot's frame.
const desk = await router.computer({ os: "linux", externalUserId: "user_42" });
const shot = await desk.act({ action: "screenshot" }); // { imageB64, width, height, frameId }
await desk.act({ action: "left_click", coordinate: [412, 230] });
await desk.actBatch([{ action: "type", text: "hello" }, { action: "key", text: "Return" }]);
// A login or CAPTCHA? Give the screen to a person:
const { url } = await desk.viewer("control", { reason: "Please sign in" });The same externalUserId always returns the same computer, with its files and logins. From the terminal: npx @sandboxrouter/sdk desktop --control prints a link to the live screen.
HTTP server
Any language can use the router over HTTP. Provider keys stay on the server; clients hold one token.
SANDBOXROUTER_TOKEN=secret npx @sandboxrouter/sdk serve --port 8787
# allow a website (like this one's Playground) to call it from the browser:
SANDBOXROUTER_TOKEN=secret npx @sandboxrouter/sdk serve --cors https://your-site.example| Call | Body |
|---|---|
| GET /v1/providers | |
| POST /v1/route | same as create; returns the candidate order |
| POST /v1/sandboxes | { vcpu, memoryGb, gpu, env, timeoutMinutes, image, provider, strategy, fallback, require } |
| POST /v1/sandboxes/:id/exec | { cmd, cwd, env, timeoutSeconds, stream } |
| PUT /v1/sandboxes/:id/files?path= | raw bytes |
| GET /v1/sandboxes/:id/files?path= | |
| POST /v1/sandboxes/:id/pause | |
| DELETE /v1/sandboxes/:id | |
| POST /v1/computers | { os, externalUserId, screen } |
| POST /v1/computers/:id/actions | one action or { actions: [...] } |
| GET /v1/computers/:id/screenshot | image bytes |
| POST /v1/computers/:id/viewer | { mode: "watch" | "control" } |
With stream: true, exec answers text/event-stream: stdout and stderr events carry JSON string chunks, then one result event.
Python
A client with no dependencies lives in the repo's python/ folder. It talks to the HTTP server.
from sandboxrouter import Client
sr = Client("http://127.0.0.1:8787", token="secret")
with sr.create(vcpu=2, memory_gb=4, strategy="cheapest") as sbx:
sbx.exec("pip install rich", on_stdout=lambda c: print(c, end=""))
print(sbx.provider, sbx.estimated_hourly_usd)Bench and leaderboard
bench starts real sandboxes on every provider you have keys for, times them, kills them, and prints a table. --share also writes a report you can add to the repo's results/ folder in a pull request; the Rankings page is built from those reports.
npx @sandboxrouter/sdk bench --runs 5 --share --label "SF, home wifi"The label says where you ran it, because the network between you and each provider is part of the number. Use a city or cloud region, not an address.
Adding a provider
Implement the Provider interface in src/types.ts (create, connect, exec, files, kill, and pause if it's real), and add a catalog entry with a link to the provider's pricing page. Pull requests welcome on GitHub.