sandboxrouter

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

terminal
npm i @sandboxrouter/sdk
export MARITIME_API_KEY=mk_...   # any one provider is enough to start
agent.ts
import { 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:

terminal
npx @sandboxrouter/sdk providers                 # prices + which keys are set
npx @sandboxrouter/sdk run "uname -a && nproc"   # start, run, print, kill

Provider 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.

ProviderEnv
MaritimeMARITIME_API_KEY
E2BE2B_API_KEY
DaytonaDAYTONA_API_KEY
ModalMODAL_TOKEN_ID + MODAL_TOKEN_SECRET
Vercel SandboxVERCEL_OIDC_TOKEN or VERCEL_TOKEN + VERCEL_TEAM_ID + VERCEL_PROJECT_ID
Morph CloudMORPH_API_KEY
Fly.io SpritesSPRITES_TOKEN
BlaxelBL_API_KEY + BL_WORKSPACE
TensorlakeTENSORLAKE_API_KEY
RunloopRUNLOOP_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 pause

The 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 exitCode

A 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.

terminal
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
CallBody
GET /v1/providers
POST /v1/routesame 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/actionsone action or { actions: [...] }
GET /v1/computers/:id/screenshotimage 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.

terminal
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.