Docs
sandboxrouter gives your agent one API for 34 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 E2B_API_KEY=... # any one provider's key is enough to startimport { Router } from "@sandboxrouter/sdk";
const router = new Router();
const sbx = await router.create({ timeoutMinutes: 10 });
try {
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);
} finally {
await sbx.kill();
}Always kill the sandbox when you're done; each provider also ends it on its own at timeoutMinutes. In TypeScript 5.2+ (or Node 24+) you can write await using sbx = ... instead, which kills it when the block ends.
Ask for a size with { vcpu: 2, memoryGb: 4 }. With no size you get 2 vCPU / 4 GB, except on providers whose default image has a fixed size (Daytona 1 vCPU / 1 GB, E2B 2 vCPU / 0.5 GB, OpenScale Sandboxes 2 vCPU / 1 GB), where you get that. Ask for more and those providers are skipped unless you pass a custom image built at the size you need.
No account yet? Use Docker on your own machine: start Docker, then pass { provider: "docker" }. It's free, and fine for trying things out, but a container shares your kernel, so use a cloud provider for untrusted code.
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. Some adapters call the provider's HTTP API directly; the rest need the provider's own package installed next to sandboxrouter (each provider's page says which).
| Provider | Env |
|---|---|
| Agent37 | AGENT37_API_KEY |
| Alibaba Cloud Agent Sandbox | ALIBABA_SANDBOX_API_KEY |
| Amazon Bedrock AgentCore Code Interpreter | AGENTCORE_CODE_INTERPRETER_ID + AWS_REGION + AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY or AGENTCORE_CODE_INTERPRETER_ID + AWS_REGION + AWS_PROFILE |
| AWS Lambda MicroVMs | AWS_REGION + LAMBDA_MICROVM_IMAGE + AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY or AWS_REGION + LAMBDA_MICROVM_IMAGE + AWS_PROFILE |
| Azure Container Apps dynamic sessions | AZURE_SESSIONS_POOL_ENDPOINT + AZURE_SESSIONS_TOKEN or AZURE_SESSIONS_POOL_ENDPOINT + AZURE_TENANT_ID + AZURE_CLIENT_ID + AZURE_CLIENT_SECRET |
| Beam | BEAM_TOKEN + BEAM_WORKSPACE_ID |
| Blaxel | BL_API_KEY + BL_WORKSPACE |
| Boat | BOAT_API_KEY |
| Cloudflare Sandbox | CLOUDFLARE_SANDBOX_URL + CLOUDFLARE_SANDBOX_TOKEN |
| CodeSandbox SDK (Together) | CSB_API_KEY |
| Daytona | DAYTONA_API_KEY |
| Dedalus Machines | DEDALUS_API_KEY |
| Deno Sandbox | DENO_DEPLOY_TOKEN |
| Docker (local) | Docker running locally |
| E2B | E2B_API_KEY |
| exe.dev | EXE_TOKEN |
| Fly.io Sprites | SPRITES_TOKEN |
| Freestyle | FREESTYLE_API_KEY |
| HOPX | HOPX_API_KEY |
| InstaBox | INSTABOX_API_KEY |
| Koyeb Sandboxes | KOYEB_API_TOKEN |
| LangSmith Sandboxes | LANGSMITH_API_KEY |
| Maritime | MARITIME_API_KEY |
| Modal | MODAL_TOKEN_ID + MODAL_TOKEN_SECRET |
| Morph Cloud | MORPH_API_KEY |
| Northflank | NORTHFLANK_API_TOKEN + NORTHFLANK_PROJECT_ID |
| Novita AI Agent Sandbox | NOVITA_API_KEY |
| OpenScale Sandboxes | OPENSCALE_API_KEY |
| Runloop | RUNLOOP_API_KEY |
| Sailboxes (Sail Research) | SAIL_API_KEY |
| Superserve | SUPERSERVE_API_KEY |
| Tensorlake | TENSORLAKE_API_KEY |
| Upstash Box | UPSTASH_BOX_API_KEY |
| Vercel Sandbox | VERCEL_OIDC_TOKEN or VERCEL_TOKEN + VERCEL_TEAM_ID + VERCEL_PROJECT_ID |
Routing
The second argument to create decides where a sandbox goes.
router.create(spec) // cheapest real cost; speed breaks near-ties
router.create(spec, { strategy: "cheapest" }) // lowest list price alone
router.create(spec, { strategy: "fastest" }) // lowest start time, measured where you run it
router.create(spec, { strategy: "verified" }) // providers that passed the check first
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 pauseWith no strategy, the router goes cheapest first among providers you have keys for, by list price for the size you asked for. Where we measured what a provider really bills (a sandbox run for a fixed time, then the provider's own bill), its price is corrected by that ratio (BILLED_VS_LIST). Providers within 10% of each other on price go fastest first, by start times this router measured itself, or from your sandboxrouter bench run until it has some. Speed depends on where you run it, so the published benchmarks are never used for routing. Providers whose last 14-step real-account check failed go last. Ties break alphabetically, so no provider gets a better slot by who it is. Prices come from each provider's public pricing page and live in src/catalog.ts. A service that runs sandboxes on its own accounts can create its router with new Router({ requireVerified: true }) to use only providers that passed the check. Docker is never picked automatically; name it with { provider: "docker" }. 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.
Reading the price table
- Order: providers anyone can sign up for come first, cheapest first, ties alphabetical; waitlisted or closed sign-ups follow, then providers that can't take the size. The router goes cheapest first among providers you have keys for, with speed breaking near-ties (see Routing).
- List price / hr: each provider's public list price while running, read off its pricing page. An estimate, not a bill: the benchmarks page compares it with real bills where a provider has a billing API.
- ≤: CPU is billed only while busy, so the price shown is with every core busy and idle work costs less.
- “at 2 vCPU · 0.5 GB only”: the provider's default image runs at that size whatever you ask for; a bigger size needs your own image.
- Plan needed: the monthly plan required before the API creates sandboxes (“credited” = paid back as usage). For a small project it can cost more than the usage.
- Cold start: medians from the benchmark reports submitted so far; with few reports, a rough guide.
- Real account: whether the 14-step check has passed on a real account. “Not yet” means the adapter is built from the provider's docs and tested against a mock of its API only.
- Docker runs on your own machine and is only used when you ask for it by name.
Fallback
If a provider looks unavailable (a timeout, a network error, 429 or 5xx, or a create that takes longer than createTimeoutSeconds, default 120), the router tries the next candidate. The sandbox you get back lists every attempt:
sbx.attempts
// [ { provider: "daytona", ok: false, ms: 212, error: "... 429 too many requests" },
// { provider: "e2b", ok: true, ms: 840 } ]An account that can't pay (402: no balance, or a plan needed) also falls back, and stays on attempts. A setup error (400, 401, 403, 404 or 422: a bad key, a bad image or request) stops instead, so a mistake isn't hidden by quietly sending the work to another, maybe pricier, provider. Pass fallback: "any" to fall back on every error, or fallback: false to try only the first candidate. If every candidate fails, create throws a RouterError with the same attempts. A sandbox that turns up after its create timed out is killed, or left in the ledger for sandboxrouter gc if that fails. 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. "e2b:iz5sb5a4bt3pmjx7c5ms2"Desktops and phones
For computer-use agents: a machine with a screen that your model drives with clicks and keys. Seven providers offer Linux and Windows desktops, Android phones and iPhone simulators; the Desktops table has prices and sizes. router.computer({ os }) starts one on the cheapest provider for that OS that you have keys for, ties alphabetical. Actions use the Anthropic computer-use shape everywhere, and coordinates are in the last screenshot's frame. Not every provider has a live view or sleep; computer.supports tells you, and calling one that's missing throws a clear error.
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 (needs the token):
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, createTimeoutSeconds, 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.
Hosted key
The hosted service runs the same HTTP API on sandboxrouter's own provider accounts, so one key replaces all the provider keys. Sign in at the dashboard, add credit by card and create a key. Each sandbox is billed by the second at its provider's list price plus 5.5%, and the dashboard shows what each one cost.
import { Client } from "@sandboxrouter/sdk";
const sr = new Client({ baseUrl: "https://api.sandboxrouter.ai", apiKey: process.env.SANDBOXROUTER_API_KEY });
const sbx = await sr.create({ vcpu: 2, memoryGb: 4 });
console.log(sbx.provider, (await sbx.exec("echo hi")).stdout);
await sbx.kill();An agent can open an account itself: POST /v1/signup returns a key with no credit and a link its person uses to pay and claim the account. The steps are in /auth.md. On the hosted API, pause and desktops are off for now, and accounts run up to 5 sandboxes at once for up to 4 hours each. See the terms and refund policy.
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(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 benchmarks page and the cold-start column in the provider table are 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.