@sandboxrouter/sdk 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Runs as plain JavaScript on Node 20+. (In TypeScript 5.2+, or Node 24+, `await u
23
23
 
24
24
  Set the keys of the providers you use. `create()` starts the sandbox on the cheapest one that fits: list price for the size you asked for, corrected where we measured what a provider really bills. Providers within 10% of each other on price go fastest first, by start times measured where you run the router. Providers that failed their last real-account check go last. Ties are alphabetical, and no provider pays for placement. If that provider is down, busy or out of credit, the next one takes it, and every attempt is on `sbx.attempts`. The sandbox stays on that provider until you `kill()` it.
25
25
 
26
- Site and docs: [sandboxrouter.vercel.app](https://sandboxrouter.vercel.app) · npm: [`@sandboxrouter/sdk`](https://www.npmjs.com/package/@sandboxrouter/sdk)
26
+ Site and docs: [sandboxrouter.ai](https://sandboxrouter.ai) · npm: [`@sandboxrouter/sdk`](https://www.npmjs.com/package/@sandboxrouter/sdk)
27
27
 
28
28
  ## Why
29
29
 
@@ -38,46 +38,46 @@ AI agents need a disposable computer to run code a model wrote: install packages
38
38
 
39
39
  ## Prices
40
40
 
41
- List price while running, 2 vCPU / 4 GB, cheapest first; waitlisted sign-ups follow. The router ranks by these, corrected where we measured what a provider really bills, and tries providers whose last real-account check failed last. "Up to" = CPU billed only while busy, shown with every core busy. "Not run yet" = built from the provider's docs and tested against mocks only. Click a provider for its billing details; more in [Reading the price table](https://sandboxrouter.vercel.app/docs#price-table).
41
+ List price while running, 2 vCPU / 4 GB, cheapest first; waitlisted sign-ups follow. The router ranks by these, corrected where we measured what a provider really bills, and tries providers whose last real-account check failed last. "Up to" = CPU billed only while busy, shown with every core busy. "Not run yet" = built from the provider's docs and tested against mocks only. Click a provider for its billing details; more in [Reading the price table](https://sandboxrouter.ai/docs#price-table).
42
42
 
43
43
  | Provider | $/hr | Plan needed | Real-account check | Source |
44
44
  |---|---|---|---|---|
45
- | [Agent37](https://sandboxrouter.vercel.app/providers/agent37) | $0.007 | none | passed (2026-11-11) | [pricing](https://www.agent37.com/docs/agents-api/billing) |
46
- | [Boat](https://sandboxrouter.vercel.app/providers/boat) | $0.018 | $20/mo (credited) | not run yet | [pricing](https://docs.boat.dev/pricing.md) |
47
- | [Maritime](https://sandboxrouter.vercel.app/providers/maritime) | $0.022 | $20/mo (credited) | passed (2026-11-13) | [pricing](https://maritime.sh/pricing) |
48
- | [Sailboxes (Sail Research)](https://sandboxrouter.vercel.app/providers/sail) | up to $0.062 | none | passed (2026-11-13) | [pricing](https://docs.sailresearch.com/sailboxes-pricing.md) |
49
- | [Northflank](https://sandboxrouter.vercel.app/providers/northflank) | $0.067 | none | not run yet | [pricing](https://northflank.com/pricing) |
50
- | [Morph Cloud](https://sandboxrouter.vercel.app/providers/morph) | $0.100 | none | not run yet | [pricing](https://cloud.morph.so/web/pricing) |
51
- | [exe.dev](https://sandboxrouter.vercel.app/providers/exedev) | $0.105 | $15/mo | not run yet | [pricing](https://exe.dev/docs/standalone-vms.md) |
52
- | [Novita AI Agent Sandbox](https://sandboxrouter.vercel.app/providers/novita) | $0.117 | none | passed (2026-11-11) | [pricing](https://docs.novita.ai/guides/sandbox-pricing) |
53
- | [Freestyle](https://sandboxrouter.vercel.app/providers/freestyle) | $0.134 | none | passed (2026-11-11) | [pricing](https://www.freestyle.sh/docs/vms/pricing-and-limits) |
54
- | [Beam](https://sandboxrouter.vercel.app/providers/beam) | $0.140 | $20/mo (credited) | not run yet | [pricing](https://www.beam.cloud/pricing) |
55
- | [CodeSandbox SDK (Together)](https://sandboxrouter.vercel.app/providers/codesandbox) | $0.149 | none | not run yet | [pricing](https://docs.together.ai/docs/together-code-sandbox-legacy) |
56
- | [Fly.io Sprites](https://sandboxrouter.vercel.app/providers/sprites) | up to $0.164 | none | passed (2026-11-11) | [pricing](https://fly.io/sprites) |
57
- | [Blaxel](https://sandboxrouter.vercel.app/providers/blaxel) | $0.166 | none | passed (2026-11-13) | [pricing](https://blaxel.ai/pricing) |
58
- | [HOPX](https://sandboxrouter.vercel.app/providers/hopx) | $0.166 | none | not run yet | [pricing](https://hopx.ai/pricing) |
59
- | [Superserve](https://sandboxrouter.vercel.app/providers/superserve) | $0.166 | maybe $20/mo (credited) | passed (2026-11-11) | [pricing](https://superserve.ai/pricing) |
60
- | [Tensorlake](https://sandboxrouter.vercel.app/providers/tensorlake) | up to $0.200 | none | not run yet | [pricing](https://www.tensorlake.ai/pricing) |
61
- | [Upstash Box](https://sandboxrouter.vercel.app/providers/upstash) | up to $0.200 | none | not run yet | [pricing](https://upstash.com/pricing/box) |
62
- | [Cloudflare Sandbox](https://sandboxrouter.vercel.app/providers/cloudflare) | up to $0.216 | $5/mo | passed (2026-11-11) | [pricing](https://developers.cloudflare.com/containers/platform/pricing/) |
63
- | [Amazon Bedrock AgentCore Code Interpreter](https://sandboxrouter.vercel.app/providers/agentcore) | up to $0.217 | none | passed (2026-11-13) | [pricing](https://aws.amazon.com/bedrock/agentcore/pricing/) |
64
- | [Modal](https://sandboxrouter.vercel.app/providers/modal) | up to $0.238 | none | passed (2026-11-11) | [pricing](https://modal.com/pricing) |
65
- | [AWS Lambda MicroVMs](https://sandboxrouter.vercel.app/providers/lambda) | $0.252 | none | passed (2026-11-13) | [pricing](https://aws.amazon.com/lambda/pricing/) |
66
- | [Deno Sandbox](https://sandboxrouter.vercel.app/providers/deno) | up to $0.300 | $20/mo | not run yet | [pricing](https://deno.com/sandbox) |
67
- | [Runloop](https://sandboxrouter.vercel.app/providers/runloop) | $0.317 | none | not run yet | [pricing](https://www.runloop.ai/pricing) |
68
- | [Vercel Sandbox](https://sandboxrouter.vercel.app/providers/vercel) | up to $0.341 | none | passed (2026-11-11) | [pricing](https://vercel.com/docs/vercel-sandbox/pricing) |
69
- | [InstaBox (sign-up waitlisted)](https://sandboxrouter.vercel.app/providers/instabox) | $0.017 | none | not run yet | [pricing](https://instabox.sh/docs/pricing.md) |
70
- | [Koyeb Sandboxes (not taking new accounts)](https://sandboxrouter.vercel.app/providers/koyeb) | $0.058 | $29/mo | not run yet | [pricing](https://www.koyeb.com/docs/reference/instances) |
71
- | [Dedalus Machines (sign-up waitlisted)](https://sandboxrouter.vercel.app/providers/dedalus) | $0.149 | none | not run yet | [pricing](https://docs.dedaluslabs.ai/dcs/dm/pricing) |
72
- | [LangSmith Sandboxes (sign-up waitlisted)](https://sandboxrouter.vercel.app/providers/langsmith) | $0.189 | none | not run yet | [pricing](https://www.langchain.com/pricing) |
73
- | [OpenScale Sandboxes](https://sandboxrouter.vercel.app/providers/openscale) | skipped (default image is 2 vCPU / 1 GB; a bigger size needs image.openscale) | none | passed (2026-11-13) | [pricing](https://openscale.so/sandboxes) |
74
- | [Azure Container Apps dynamic sessions](https://sandboxrouter.vercel.app/providers/azure) | skipped (max 1 vCPU) | none | not run yet | [pricing](https://learn.microsoft.com/en-us/azure/container-apps/billing#dynamic-sessions) |
75
- | [Daytona](https://sandboxrouter.vercel.app/providers/daytona) | skipped (default image is 1 vCPU / 1 GB; a bigger size needs image.daytona) | none | passed (2026-11-13) | [pricing](https://www.daytona.io/pricing) |
76
- | [Alibaba Cloud Agent Sandbox](https://sandboxrouter.vercel.app/providers/alibaba) | skipped (max 2 GB memory) | none | not run yet | [pricing](https://www.alibabacloud.com/help/en/agent-sandbox/product-overview/billing-overview) |
77
- | [E2B](https://sandboxrouter.vercel.app/providers/e2b) | skipped (default image is 2 vCPU / 0.5 GB; a bigger size needs image.e2b) | none | passed (2026-11-13) | [pricing](https://e2b.dev/pricing) |
78
- | [Docker (local)](https://sandboxrouter.vercel.app/providers/docker) | free | none | passed (2026-11-13) | [pricing](https://docs.docker.com/engine/) |
79
-
80
- Checked 2026-11-04 to 2026-11-06.
45
+ | [Agent37](https://sandboxrouter.ai/providers/agent37) | $0.007 | none | passed (2026-11-13) | [pricing](https://www.agent37.com/docs/agents-api/billing) |
46
+ | [Boat](https://sandboxrouter.ai/providers/boat) | $0.018 | $20/mo (credited) | not run yet | [pricing](https://docs.boat.dev/pricing.md) |
47
+ | [Maritime](https://sandboxrouter.ai/providers/maritime) | $0.022 | $20/mo (credited) | passed (2026-11-13) | [pricing](https://maritime.sh/pricing) |
48
+ | [Sailboxes (Sail Research)](https://sandboxrouter.ai/providers/sail) | up to $0.062 | none | passed (2026-11-13) | [pricing](https://docs.sailresearch.com/sailboxes-pricing.md) |
49
+ | [Northflank](https://sandboxrouter.ai/providers/northflank) | $0.067 | none | not run yet | [pricing](https://northflank.com/pricing) |
50
+ | [Morph Cloud](https://sandboxrouter.ai/providers/morph) | $0.100 | none | not run yet | [pricing](https://cloud.morph.so/web/pricing) |
51
+ | [exe.dev](https://sandboxrouter.ai/providers/exedev) | $0.105 | $15/mo | not run yet | [pricing](https://exe.dev/docs/standalone-vms.md) |
52
+ | [Novita AI Agent Sandbox](https://sandboxrouter.ai/providers/novita) | $0.117 | none | passed (2026-11-13) | [pricing](https://docs.novita.ai/guides/sandbox-pricing) |
53
+ | [Freestyle](https://sandboxrouter.ai/providers/freestyle) | $0.134 | none | passed (2026-11-13) | [pricing](https://www.freestyle.sh/docs/vms/pricing-and-limits) |
54
+ | [Beam](https://sandboxrouter.ai/providers/beam) | $0.140 | $20/mo (credited) | not run yet | [pricing](https://www.beam.cloud/pricing) |
55
+ | [CodeSandbox SDK (Together)](https://sandboxrouter.ai/providers/codesandbox) | $0.149 | none | not run yet | [pricing](https://docs.together.ai/docs/together-code-sandbox-legacy) |
56
+ | [Fly.io Sprites](https://sandboxrouter.ai/providers/sprites) | up to $0.164 | none | passed (2026-11-13) | [pricing](https://fly.io/sprites) |
57
+ | [Blaxel](https://sandboxrouter.ai/providers/blaxel) | $0.166 | none | passed (2026-11-13) | [pricing](https://blaxel.ai/pricing) |
58
+ | [HOPX](https://sandboxrouter.ai/providers/hopx) | $0.166 | none | not run yet | [pricing](https://hopx.ai/pricing) |
59
+ | [Superserve](https://sandboxrouter.ai/providers/superserve) | $0.166 | maybe $20/mo (credited) | passed (2026-11-13) | [pricing](https://superserve.ai/pricing) |
60
+ | [Tensorlake](https://sandboxrouter.ai/providers/tensorlake) | up to $0.200 | none | not run yet | [pricing](https://www.tensorlake.ai/pricing) |
61
+ | [Upstash Box](https://sandboxrouter.ai/providers/upstash) | up to $0.200 | none | not run yet | [pricing](https://upstash.com/pricing/box) |
62
+ | [Cloudflare Sandbox](https://sandboxrouter.ai/providers/cloudflare) | up to $0.216 | $5/mo | passed (2026-11-13) | [pricing](https://developers.cloudflare.com/containers/platform/pricing/) |
63
+ | [Amazon Bedrock AgentCore Code Interpreter](https://sandboxrouter.ai/providers/agentcore) | up to $0.217 | none | passed (2026-11-13) | [pricing](https://aws.amazon.com/bedrock/agentcore/pricing/) |
64
+ | [Modal](https://sandboxrouter.ai/providers/modal) | up to $0.238 | none | passed (2026-11-13) | [pricing](https://modal.com/pricing) |
65
+ | [AWS Lambda MicroVMs](https://sandboxrouter.ai/providers/lambda) | $0.252 | none | passed (2026-11-13) | [pricing](https://aws.amazon.com/lambda/pricing/) |
66
+ | [Deno Sandbox](https://sandboxrouter.ai/providers/deno) | up to $0.300 | $20/mo | not run yet | [pricing](https://deno.com/sandbox) |
67
+ | [Runloop](https://sandboxrouter.ai/providers/runloop) | $0.317 | none | not run yet | [pricing](https://www.runloop.ai/pricing) |
68
+ | [Vercel Sandbox](https://sandboxrouter.ai/providers/vercel) | up to $0.341 | none | passed (2026-11-13) | [pricing](https://vercel.com/docs/vercel-sandbox/pricing) |
69
+ | [InstaBox (sign-up waitlisted)](https://sandboxrouter.ai/providers/instabox) | $0.017 | none | not run yet | [pricing](https://instabox.sh/docs/pricing.md) |
70
+ | [Koyeb Sandboxes (not taking new accounts)](https://sandboxrouter.ai/providers/koyeb) | $0.058 | $29/mo | not run yet | [pricing](https://www.koyeb.com/docs/reference/instances) |
71
+ | [Dedalus Machines (sign-up waitlisted)](https://sandboxrouter.ai/providers/dedalus) | $0.149 | none | not run yet | [pricing](https://docs.dedaluslabs.ai/dcs/dm/pricing) |
72
+ | [LangSmith Sandboxes (sign-up waitlisted)](https://sandboxrouter.ai/providers/langsmith) | $0.189 | none | not run yet | [pricing](https://www.langchain.com/pricing) |
73
+ | [OpenScale Sandboxes](https://sandboxrouter.ai/providers/openscale) | skipped (default image is 2 vCPU / 1 GB; a bigger size needs image.openscale) | none | passed (2026-11-13) | [pricing](https://openscale.so/sandboxes) |
74
+ | [Azure Container Apps dynamic sessions](https://sandboxrouter.ai/providers/azure) | skipped (max 1 vCPU) | none | not run yet | [pricing](https://learn.microsoft.com/en-us/azure/container-apps/billing#dynamic-sessions) |
75
+ | [Daytona](https://sandboxrouter.ai/providers/daytona) | skipped (default image is 1 vCPU / 1 GB; a bigger size needs image.daytona) | none | passed (2026-11-13) | [pricing](https://www.daytona.io/pricing) |
76
+ | [Alibaba Cloud Agent Sandbox](https://sandboxrouter.ai/providers/alibaba) | skipped (max 2 GB memory) | none | not run yet | [pricing](https://www.alibabacloud.com/help/en/agent-sandbox/product-overview/billing-overview) |
77
+ | [E2B](https://sandboxrouter.ai/providers/e2b) | skipped (default image is 2 vCPU / 0.5 GB; a bigger size needs image.e2b) | none | passed (2026-11-13) | [pricing](https://e2b.dev/pricing) |
78
+ | [Docker (local)](https://sandboxrouter.ai/providers/docker) | free | none | passed (2026-11-13) | [pricing](https://docs.docker.com/engine/) |
79
+
80
+ Checked 2026-11-04 to 2026-11-13.
81
81
 
82
82
  ## Install
83
83
 
@@ -89,7 +89,7 @@ Then the SDK of each provider you use (many need none) and its keys: both are li
89
89
 
90
90
  ## More
91
91
 
92
- - [Docs](https://sandboxrouter.vercel.app/docs): routing, fallback, streaming, files, pause, the HTTP server and Python.
92
+ - [Docs](https://sandboxrouter.ai/docs): routing, fallback, streaming, files, pause, the HTTP server and Python.
93
93
  - [Reference](docs/reference.md): the full library, CLI and HTTP API, desktops and phones, the real-account compatibility table, and how each adapter maps the spec.
94
94
  - [CONTRIBUTING.md](CONTRIBUTING.md): run a provider for real and send the result, fix a price, or add a provider.
95
95
  - [CHANGELOG.md](CHANGELOG.md) · [SECURITY.md](SECURITY.md) · [MIT](LICENSE)
package/dist/billing.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export const BILLED_VS_LIST = {
2
- sail: { ratio: 1.458, date: "2026-11-06" },
2
+ agent37: { ratio: 0.998, date: "2026-11-14" },
3
+ sail: { ratio: 0.495, date: "2026-11-06" },
3
4
  };
package/dist/catalog.d.ts CHANGED
@@ -2,10 +2,16 @@ import type { Capability, ProviderId, SandboxSpec } from "./types.js";
2
2
  export interface ProviderInfo {
3
3
  id: ProviderId;
4
4
  name: string;
5
- /** List price while running, USD. */
5
+ /** List price while running, USD, and the rules that turn running time into a bill. */
6
6
  pricing: {
7
7
  vcpuHour: number;
8
8
  gbHour: number;
9
+ /** Each sandbox is billed for at least this many seconds of running time. Default: no minimum. */
10
+ minSeconds?: number;
11
+ /** Running time is rounded up to a multiple of this many seconds. Default 1 (per second). */
12
+ incrementSeconds?: number;
13
+ /** A fixed charge per sandbox created, USD, on top of running time. */
14
+ perSandboxUsd?: number;
9
15
  note?: string;
10
16
  source: string;
11
17
  checked: string;
@@ -258,6 +264,17 @@ export declare function cloudflareInstance(vcpu: number, memoryGb: number): {
258
264
  readonly vcpu: 4;
259
265
  readonly memoryGb: 12;
260
266
  };
267
+ /** How a provider turns running time into a bill: minimum, rounding and fixed per-sandbox charge. */
268
+ export declare function billingRules(id: ProviderId): {
269
+ minSeconds: number;
270
+ incrementSeconds: number;
271
+ perSandboxUsd: number;
272
+ };
273
+ /**
274
+ * What a provider bills for one sandbox that ran `seconds` at `hourlyUsd`: running time rounded up to
275
+ * the provider's increment, at least its minimum, plus any fixed per-sandbox charge.
276
+ */
277
+ export declare function billedUsd(id: ProviderId, hourlyUsd: number, seconds: number): number;
261
278
  export declare function info(id: ProviderId): ProviderInfo;
262
279
  export declare const DEFAULT_VCPU = 2;
263
280
  export declare const DEFAULT_MEMORY_GB = 4;
package/dist/catalog.js CHANGED
@@ -91,7 +91,7 @@ const ENTRIES = [
91
91
  gbHour: 0.0162,
92
92
  note: "per second, plus disk above 5 GiB",
93
93
  source: "https://www.daytona.io/pricing",
94
- checked: "2026-11-04",
94
+ checked: "2026-11-13",
95
95
  },
96
96
  capabilities: ["pause", "custom-image"],
97
97
  limits: { maxVcpu: 4, maxMemoryGb: 8 },
@@ -119,6 +119,7 @@ const ENTRIES = [
119
119
  id: "vercel",
120
120
  name: "Vercel Sandbox",
121
121
  pricing: {
122
+ minSeconds: 60,
122
123
  vcpuHour: 0.128,
123
124
  gbHour: 0.0212,
124
125
  note: "iad1; active CPU, provisioned RAM in minimum 1-minute increments (2 GB/vCPU); full-CPU estimate; Hobby free within quotas, Pro $20/mo usage credit; creations $0.60/million and retained snapshots $0.08/GB-month extra",
@@ -153,7 +154,7 @@ const ENTRIES = [
153
154
  gbHour: 0.021875,
154
155
  note: "bills actual CPU and memory use; this is the full-use price; storage extra",
155
156
  source: "https://fly.io/sprites",
156
- checked: "2026-11-04",
157
+ checked: "2026-11-13",
157
158
  },
158
159
  capabilities: [],
159
160
  // Not published per sprite; 8 GB is what accounts report getting by default.
@@ -221,7 +222,7 @@ ENTRIES.push({
221
222
  gbHour: 0,
222
223
  note: "fixed sizes billed per second ($0.018/h for 2 vCPU / 4 GB up to $0.20/h for 16 / 32); needs a plan from $20/mo, paid back as VM time",
223
224
  source: "https://docs.boat.dev/pricing.md",
224
- checked: "2026-11-04",
225
+ checked: "2026-11-13",
225
226
  },
226
227
  // "pause" here is Boat's stop: the disk is kept, running processes end.
227
228
  capabilities: ["pause", "custom-image"],
@@ -251,7 +252,7 @@ ENTRIES.push({
251
252
  gbHour: 0.00833,
252
253
  note: "per second on Northflank Cloud, ephemeral disk included; sandboxes run as deployment services (microVMs)",
253
254
  source: "https://northflank.com/pricing",
254
- checked: "2026-11-04",
255
+ checked: "2026-11-13",
255
256
  },
256
257
  capabilities: ["custom-image"],
257
258
  limits: { maxVcpu: 8, maxMemoryGb: 16 },
@@ -295,7 +296,7 @@ ENTRIES.push({
295
296
  gbHour: 0.00945,
296
297
  note: "per second; CPU billed on actual use (I/O wait free), memory on peak; fixed 2 vCPU / 8 GB session, this is the 100%-CPU price",
297
298
  source: "https://aws.amazon.com/bedrock/agentcore/pricing/",
298
- checked: "2026-11-04",
299
+ checked: "2026-11-13",
299
300
  },
300
301
  capabilities: [],
301
302
  limits: { maxVcpu: 2, maxMemoryGb: 8 },
@@ -308,6 +309,8 @@ ENTRIES.push({
308
309
  id: "azure",
309
310
  name: "Azure Container Apps dynamic sessions",
310
311
  pricing: {
312
+ minSeconds: 3600,
313
+ incrementSeconds: 3600,
311
314
  vcpuHour: 0.03,
312
315
  gbHour: 0,
313
316
  note: "flat $0.03 per session-hour (built-in Shell pools), billed per allocated session in 1-hour increments, East US; needs a session pool you create once",
@@ -343,11 +346,13 @@ ENTRIES.push({
343
346
  id: "agent37",
344
347
  name: "Agent37",
345
348
  pricing: {
349
+ minSeconds: 60,
350
+ incrementSeconds: 60,
346
351
  vcpuHour: 0.8 / 730,
347
352
  gbHour: 0.7 / 730,
348
353
  note: "whole-minute billing: monthly shape total / 730 / 60; running estimate includes default disk (2 vCPU / 4 GB / 4 GB disk = $4.76/mo); sleeping/stopped disk $0.09/GB-mo; gVisor; $5 minimum top-up, wallet must hold one running day",
349
354
  source: "https://www.agent37.com/docs/agents-api/billing",
350
- checked: "2026-11-05",
355
+ checked: "2026-11-13",
351
356
  },
352
357
  capabilities: ["pause", "custom-image"],
353
358
  limits: { maxVcpu: 4, maxMemoryGb: 32 },
@@ -387,6 +392,8 @@ ENTRIES.push({
387
392
  id: "codesandbox",
388
393
  name: "CodeSandbox SDK (Together)",
389
394
  pricing: {
395
+ minSeconds: 60,
396
+ incrementSeconds: 60,
390
397
  vcpuHour: 0.0743,
391
398
  gbHour: 0,
392
399
  note: "fixed tiers: Pico 2 vCPU / 1 GB $0.0743/h, Nano 2 / 4 $0.1486/h, up to 64 / 128 $4.7552/h; credits $0.01486, billed per started minute; Build free, 10 concurrent; Scale $170/mo incl. 1,100 credits",
@@ -415,14 +422,15 @@ ENTRIES.push({
415
422
  id: "sail",
416
423
  name: "Sailboxes (Sail Research)",
417
424
  pricing: {
425
+ perSandboxUsd: 0.01,
418
426
  vcpuHour: 0.015,
419
427
  gbHour: 0.008,
420
- note: "per second on what the box uses (CPU time, RAM it holds, disk $0.0007/GiB-h), billed per minute; RAM counts the VM's file cache, up to the 16 GiB minimum limit, so file-heavy work can hold more than you asked for; paused/sleeping free; creation $0.01 at 2-4 vCPU on the free plan ($5/mo credit with a card), waived on Pro $250/mo",
428
+ note: "on what the box uses (CPU time, RAM it holds, disk $0.0007/GiB-h), sampled per second and not rounded up; RAM counts the VM's file cache, up to its memory limit (16 GiB at least), so file-heavy work can hold more than you asked for; paused/sleeping free; $0.01 per new Sailbox on the free plan, waived on Pro and Enterprise",
421
429
  source: "https://docs.sailresearch.com/sailboxes-pricing.md",
422
- checked: "2026-11-06",
430
+ checked: "2026-11-13",
423
431
  },
424
432
  capabilities: ["pause"],
425
- limits: { maxVcpu: 64, maxMemoryGb: 512 },
433
+ limits: { maxVcpu: 64, maxMemoryGb: 256 },
426
434
  credentials: [["SAIL_API_KEY"]],
427
435
  docs: "https://docs.sailresearch.com/sailboxes.md",
428
436
  }, {
@@ -661,6 +669,20 @@ export const BOAT_SIZES = [
661
669
  export function cloudflareInstance(vcpu, memoryGb) {
662
670
  return CLOUDFLARE_INSTANCES.find((t) => t.vcpu >= vcpu && t.memoryGb >= memoryGb) ?? CLOUDFLARE_INSTANCES[CLOUDFLARE_INSTANCES.length - 1];
663
671
  }
672
+ /** How a provider turns running time into a bill: minimum, rounding and fixed per-sandbox charge. */
673
+ export function billingRules(id) {
674
+ const p = CATALOG.find((c) => c.id === id)?.pricing;
675
+ return { minSeconds: p?.minSeconds ?? 0, incrementSeconds: p?.incrementSeconds ?? 1, perSandboxUsd: p?.perSandboxUsd ?? 0 };
676
+ }
677
+ /**
678
+ * What a provider bills for one sandbox that ran `seconds` at `hourlyUsd`: running time rounded up to
679
+ * the provider's increment, at least its minimum, plus any fixed per-sandbox charge.
680
+ */
681
+ export function billedUsd(id, hourlyUsd, seconds) {
682
+ const r = billingRules(id);
683
+ const billed = Math.max(r.minSeconds, Math.ceil(Math.max(0, seconds) / r.incrementSeconds) * r.incrementSeconds);
684
+ return (hourlyUsd * billed) / 3600 + r.perSandboxUsd;
685
+ }
664
686
  export function info(id) {
665
687
  const p = CATALOG.find((c) => c.id === id);
666
688
  if (!p)
package/dist/check.d.ts CHANGED
@@ -17,7 +17,8 @@ export interface CheckReport {
17
17
  ok: boolean;
18
18
  /**
19
19
  * Set when the run says nothing about the provider: the account running it couldn't pay (402: no
20
- * balance, plan not started). Such a run isn't saved, so the provider keeps its last real result.
20
+ * balance, plan not started), or the provider's SDK isn't installed where it ran. Such a run isn't
21
+ * saved, so the provider keeps its last real result.
21
22
  */
22
23
  inconclusive?: string;
23
24
  }
package/dist/check.js CHANGED
@@ -42,21 +42,28 @@ export async function checkProvider(router, provider, opts = {}) {
42
42
  }
43
43
  };
44
44
  let sbx;
45
- let unpaid;
45
+ let notRun;
46
46
  await step("create", async () => {
47
47
  try {
48
48
  sbx = await router.create({ timeoutMinutes: 10, ...fixedSize(provider), ...opts.spec }, { provider, fallback: false });
49
49
  }
50
50
  catch (err) {
51
- if (err.attempts?.at(-1)?.status !== 402)
51
+ const msg = err.message;
52
+ if (err.attempts?.at(-1)?.status === 402) {
53
+ notRun = "the account running the check can't pay (402)";
54
+ return { status: "skip", detail: `the account can't pay (402), so this run says nothing about ${provider}: ${msg}` };
55
+ }
56
+ // The provider's SDK isn't installed where the check runs: that says nothing about the provider.
57
+ const pkg = /needs the "([^"]+)" package/.exec(msg)?.[1];
58
+ if (!pkg)
52
59
  throw err;
53
- unpaid = err.message;
54
- return { status: "skip", detail: `the account can't pay (402), so this run says nothing about ${provider}: ${unpaid}` };
60
+ notRun = `the machine running the check doesn't have the "${pkg}" package`;
61
+ return { status: "skip", detail: `${notRun}, so this run says nothing about ${provider}: npm i ${pkg}` };
55
62
  }
56
63
  return `${sbx.id}, ~$${sbx.estimatedHourlyUsd}/hr`;
57
64
  });
58
- if (unpaid)
59
- return { provider, results, ok: false, inconclusive: "the account running the check can't pay (402)" };
65
+ if (notRun)
66
+ return { provider, results, ok: false, inconclusive: notRun };
60
67
  if (!sbx)
61
68
  return { provider, results, ok: false };
62
69
  const box = sbx;
package/dist/cli.js CHANGED
@@ -164,7 +164,8 @@ async function main() {
164
164
  const list = c.listHourlyUsd !== c.estimatedHourlyUsd ? ` (list $${c.listHourlyUsd.toFixed(4)})` : "";
165
165
  const size = `${c.size.vcpu} vCPU / ${c.size.memoryGb} GB`;
166
166
  const speed = c.medianCreateMs !== undefined ? `${c.medianCreateMs} ms` : "";
167
- const cols = [`$${c.estimatedHourlyUsd.toFixed(4)}/hr${list}`, size, speed, tested].filter(Boolean).join(" ");
167
+ const fee = c.perSandboxUsd ? ` + $${c.perSandboxUsd}/sandbox` : "";
168
+ const cols = [`$${c.estimatedHourlyUsd.toFixed(4)}/hr${list}${fee}`, size, speed, tested].filter(Boolean).join(" ");
168
169
  console.log(`${mark} ${c.provider.padEnd(11)} ${cols}${c.skip ? ` (skipped: ${c.skip})` : ""}`);
169
170
  }
170
171
  return;
@@ -248,7 +249,7 @@ async function main() {
248
249
  else {
249
250
  const unpaid = reports.filter((r) => r.inconclusive).length;
250
251
  const ran = reports.length - unpaid;
251
- console.log(`\n${reports.filter((r) => r.ok).length}/${ran} passed${unpaid ? `; ${unpaid} not run (the account can't pay)` : ""}`);
252
+ console.log(`\n${reports.filter((r) => r.ok).length}/${ran} passed${unpaid ? `; ${unpaid} not run (the account can't pay, or the provider's package isn't installed)` : ""}`);
252
253
  }
253
254
  if (reports.some((r) => !r.ok && !r.inconclusive))
254
255
  process.exitCode = 1;
package/dist/cost.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { readdirSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { benchmarksPublishable, fittingSpec, hourlyCost } from "./catalog.js";
3
+ import { benchmarksPublishable, billingRules, fittingSpec, hourlyCost } from "./catalog.js";
4
4
  import { Router } from "./router.js";
5
5
  /**
6
6
  * The size the sandbox really has, as "<vcpus> <memoryMb>". nproc and MemTotal can show the whole host
@@ -123,6 +123,21 @@ export const BILL_READERS = {
123
123
  source: `sail /sailboxes/spend cost_usd_nanos (estimate at list rates; ${fees}; ${Math.round(row.vcpu_seconds ?? 0)} vCPU-s, ${Math.round(row.memory_gib_seconds ?? 0)} GiB-s)`,
124
124
  };
125
125
  },
126
+ async agent37(e) {
127
+ const h = { Authorization: `Bearer ${process.env.AGENT37_API_KEY}` };
128
+ const api = process.env.AGENT37_API_URL ?? "https://api.agent37.com";
129
+ const q = new URLSearchParams({ from: e.startedAt.slice(0, 10), to: e.endedAt.slice(0, 10) });
130
+ const j = await getJson(`${api}/v1/usage?${q}`, h);
131
+ const id = e.sandboxId.replace(/^agent37:/, "");
132
+ const row = (j.instances ?? []).find((i) => i.id === id);
133
+ if (!row)
134
+ return { source: "agent37 /v1/usage: not posted yet" };
135
+ return {
136
+ usd: round((row.total_micros ?? 0) / 1e6),
137
+ seconds: (row.awake_minutes ?? 0) * 60,
138
+ source: `agent37 /v1/usage total_micros (billed in whole minutes: ${row.awake_minutes ?? 0} awake)`,
139
+ };
140
+ },
126
141
  async e2b() {
127
142
  return { source: "no billing API: compare the console usage tab (console.e2b.dev, Usage)" };
128
143
  },
@@ -195,12 +210,17 @@ export function billedVsList(dir = "results/cost") {
195
210
  const predicted = e.predictedUsd;
196
211
  if (!usd || !predicted || usd <= 0 || predicted <= 0 || !benchmarksPublishable(e.provider))
197
212
  continue;
213
+ // A fixed per-sandbox charge (Sail's $0.01) isn't an hourly rate: take it out, or a 10-minute run
214
+ // turns it into "bills 46% over list" for every hour after.
215
+ const running = usd - billingRules(e.provider).perSandboxUsd;
216
+ if (running <= 0)
217
+ continue;
198
218
  if (/integer cents/i.test(e.bill.source) && predicted < 1)
199
219
  continue;
200
220
  const date = run.date.slice(0, 10);
201
221
  const prev = latest.get(e.provider);
202
222
  if (!prev || date >= prev.date)
203
- latest.set(e.provider, { ratio: Math.round((usd / predicted) * 1000) / 1000, date });
223
+ latest.set(e.provider, { ratio: Math.round((running / predicted) * 1000) / 1000, date });
204
224
  }
205
225
  }
206
226
  return [...latest].map(([provider, v]) => ({ provider, ...v })).sort((a, b) => a.provider.localeCompare(b.provider));
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export { Router, RoutedSandbox, defaultProviders, defaultComputerProviders, pars
6
6
  export { VERIFIED, FAILING } from "./verified.js";
7
7
  export { BILLED_VS_LIST } from "./billing.js";
8
8
  export { validate, splitRequest, SPEC_KEYS, ROUTE_KEYS, STRATEGIES } from "./validate.js";
9
- export { CATALOG, hourlyCost, unfit, info, type ProviderInfo } from "./catalog.js";
9
+ export { CATALOG, hourlyCost, unfit, info, billingRules, billedUsd, type ProviderInfo } from "./catalog.js";
10
10
  export { createApp, HttpError, ERROR_STATUS, ORPHAN_GRACE_MS, type ServerHooks } from "./server.js";
11
11
  export { Client, ClientError, RemoteSandbox, type CreateResponse } from "./client.js";
12
12
  export { RoutedComputer, createComputer, splitComputerRequest, DEFAULT_COMPUTER_TIMEOUT_S, DEFAULT_COMPUTER_DEADLINE_S, type ComputerRoute, planComputer, computerCatalog, computerHourly, COMPUTER_ENTRIES, type ComputerCatalogEntry, type ComputerCandidate, type ComputerProviderId, type ActionResult, type ComputerAction, type ComputerActionName, type ComputerInfo, type ComputerOs, type ComputerProvider, type ComputerSpec, type ProviderComputer, } from "./computers.js";
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ export { Router, RoutedSandbox, defaultProviders, defaultComputerProviders, pars
6
6
  export { VERIFIED, FAILING } from "./verified.js";
7
7
  export { BILLED_VS_LIST } from "./billing.js";
8
8
  export { validate, splitRequest, SPEC_KEYS, ROUTE_KEYS, STRATEGIES } from "./validate.js";
9
- export { CATALOG, hourlyCost, unfit, info } from "./catalog.js";
9
+ export { CATALOG, hourlyCost, unfit, info, billingRules, billedUsd } from "./catalog.js";
10
10
  export { createApp, HttpError, ERROR_STATUS, ORPHAN_GRACE_MS } from "./server.js";
11
11
  export { Client, ClientError, RemoteSandbox } from "./client.js";
12
12
  export { RoutedComputer, createComputer, splitComputerRequest, DEFAULT_COMPUTER_TIMEOUT_S, DEFAULT_COMPUTER_DEADLINE_S, planComputer, computerCatalog, computerHourly, COMPUTER_ENTRIES, } from "./computers.js";
package/dist/router.d.ts CHANGED
@@ -29,6 +29,11 @@ export interface Candidate {
29
29
  verified: boolean;
30
30
  /** Failed a step of its last real-account check (as of this release, or `RouterOptions.failing`). */
31
31
  failing: boolean;
32
+ /**
33
+ * A fixed charge per sandbox, USD, on top of `estimatedHourlyUsd` (Sail's free plan: $0.01). Ranking
34
+ * spreads it over the sandbox's lifetime (`timeoutMinutes`, default 30), so it weighs more on short ones.
35
+ */
36
+ perSandboxUsd?: number;
32
37
  /** Why this provider won't be tried, if it won't. */
33
38
  skip?: string;
34
39
  }
@@ -179,9 +184,10 @@ export declare function parseId(id: string): {
179
184
  * The default order. Providers whose last real-account check failed go last. The rest go cheapest
180
185
  * first by effective price (list price x what the provider really billed, where measured), except that
181
186
  * providers within PRICE_TIE of the cheapest remaining one count as a tie and go fastest first (by
182
- * median create time; unknown speed after known), then cheapest, then alphabetical.
187
+ * median create time; unknown speed after known), then cheapest, then alphabetical. A fixed
188
+ * per-sandbox charge counts as spread over `lifetimeHours` (the sandbox's timeoutMinutes).
183
189
  */
184
- export declare function balanced(list: Candidate[]): Candidate[];
190
+ export declare function balanced(list: Candidate[], lifetimeHours?: number): Candidate[];
185
191
  /** Seconds to wait for one provider's create before trying the next (RouteOptions.createTimeoutSeconds). */
186
192
  export declare const DEFAULT_CREATE_TIMEOUT_S = 120;
187
193
  /** Seconds a whole create may take over every provider it tries (RouteOptions.deadlineSeconds). */
package/dist/router.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CATALOG, DEFAULT_MEMORY_GB, DEFAULT_VCPU, accessLabel, fixedSize, hourlyCost, unfit } from "./catalog.js";
1
+ import { CATALOG, DEFAULT_MEMORY_GB, DEFAULT_VCPU, accessLabel, billingRules, fixedSize, hourlyCost, unfit } from "./catalog.js";
2
2
  import { RoutedComputer, createComputer, planComputer, } from "./computers.js";
3
3
  import { loadBench, median } from "./bench-store.js";
4
4
  import { Ledger, defaultLedgerFile } from "./ledger.js";
@@ -163,6 +163,9 @@ export class Router {
163
163
  verified: this.verified.has(id),
164
164
  failing: this.failing.has(id),
165
165
  };
166
+ const fee = billingRules(id).perSandboxUsd;
167
+ if (fee)
168
+ c.perSandboxUsd = fee;
166
169
  if (!p)
167
170
  c.skip = "no adapter registered";
168
171
  else {
@@ -178,7 +181,8 @@ export class Router {
178
181
  // depends on who the provider is, only on its price, what it really billed, how fast it started for
179
182
  // this router and whether its last real-account check failed. An explicit list keeps the caller's order.
180
183
  if (!explicit) {
181
- const byCost = (a, b) => a.estimatedHourlyUsd - b.estimatedHourlyUsd || a.provider.localeCompare(b.provider);
184
+ const hours = (spec.timeoutMinutes ?? 30) / 60;
185
+ const byCost = (a, b) => rankUsd(a, hours) - rankUsd(b, hours) || a.provider.localeCompare(b.provider);
182
186
  const byList = (a, b) => a.listHourlyUsd - b.listHourlyUsd || a.provider.localeCompare(b.provider);
183
187
  if (strategy === "fastest") {
184
188
  list.sort((a, b) => (a.medianCreateMs ?? Infinity) - (b.medianCreateMs ?? Infinity) || byCost(a, b));
@@ -190,7 +194,7 @@ export class Router {
190
194
  list.sort((a, b) => Number(b.verified) - Number(a.verified) || byCost(a, b));
191
195
  }
192
196
  else {
193
- return balanced(list);
197
+ return balanced(list, hours);
194
198
  }
195
199
  }
196
200
  return list;
@@ -435,18 +439,20 @@ function round(n) {
435
439
  * The default order. Providers whose last real-account check failed go last. The rest go cheapest
436
440
  * first by effective price (list price x what the provider really billed, where measured), except that
437
441
  * providers within PRICE_TIE of the cheapest remaining one count as a tie and go fastest first (by
438
- * median create time; unknown speed after known), then cheapest, then alphabetical.
442
+ * median create time; unknown speed after known), then cheapest, then alphabetical. A fixed
443
+ * per-sandbox charge counts as spread over `lifetimeHours` (the sandbox's timeoutMinutes).
439
444
  */
440
- export function balanced(list) {
441
- const byCost = (a, b) => a.estimatedHourlyUsd - b.estimatedHourlyUsd || a.provider.localeCompare(b.provider);
445
+ export function balanced(list, lifetimeHours = 0.5) {
446
+ const cost = (c) => rankUsd(c, lifetimeHours);
447
+ const byCost = (a, b) => cost(a) - cost(b) || a.provider.localeCompare(b.provider);
442
448
  const bySpeed = (a, b) => (a.medianCreateMs ?? Infinity) - (b.medianCreateMs ?? Infinity) || byCost(a, b);
443
449
  const order = (group) => {
444
450
  const rest = [...group].sort(byCost);
445
451
  const out = [];
446
452
  while (rest.length) {
447
- const limit = rest[0].estimatedHourlyUsd * (1 + PRICE_TIE);
453
+ const limit = cost(rest[0]) * (1 + PRICE_TIE);
448
454
  // Always take at least the cheapest, so the loop ends whatever the prices are.
449
- const tie = rest.filter((c, i) => i === 0 || c.estimatedHourlyUsd <= limit);
455
+ const tie = rest.filter((c, i) => i === 0 || cost(c) <= limit);
450
456
  out.push(...tie.sort(bySpeed));
451
457
  rest.splice(0, tie.length);
452
458
  }
@@ -454,6 +460,10 @@ export function balanced(list) {
454
460
  };
455
461
  return [...order(list.filter((c) => !c.failing)), ...order(list.filter((c) => c.failing))];
456
462
  }
463
+ /** USD per hour a candidate is ranked by: its hourly estimate plus any per-sandbox charge spread over the lifetime. */
464
+ function rankUsd(c, lifetimeHours) {
465
+ return c.estimatedHourlyUsd + (c.perSandboxUsd ?? 0) / Math.max(lifetimeHours, 1 / 60);
466
+ }
457
467
  /** Seconds to wait for one provider's create before trying the next (RouteOptions.createTimeoutSeconds). */
458
468
  export const DEFAULT_CREATE_TIMEOUT_S = 120;
459
469
  /** Seconds a whole create may take over every provider it tries (RouteOptions.deadlineSeconds). */
package/dist/verified.js CHANGED
@@ -1,20 +1,20 @@
1
1
  export const VERIFIED = {
2
- agent37: "2026-11-11",
2
+ agent37: "2026-11-13",
3
3
  agentcore: "2026-11-13",
4
4
  lambda: "2026-11-13",
5
5
  blaxel: "2026-11-13",
6
- cloudflare: "2026-11-11",
6
+ cloudflare: "2026-11-13",
7
7
  daytona: "2026-11-13",
8
8
  e2b: "2026-11-13",
9
- sprites: "2026-11-11",
10
- freestyle: "2026-11-11",
9
+ sprites: "2026-11-13",
10
+ freestyle: "2026-11-13",
11
11
  maritime: "2026-11-13",
12
- modal: "2026-11-11",
13
- novita: "2026-11-11",
12
+ modal: "2026-11-13",
13
+ novita: "2026-11-13",
14
14
  openscale: "2026-11-13",
15
15
  sail: "2026-11-13",
16
- superserve: "2026-11-11",
17
- vercel: "2026-11-11",
16
+ superserve: "2026-11-13",
17
+ vercel: "2026-11-13",
18
18
  };
19
19
  /** Providers whose last saved real-account check failed a step. The default strategy tries them last. */
20
20
  export const FAILING = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandboxrouter/sdk",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "One API for every sandbox provider: route AI agents' code by price, speed or capability, with automatic fallback. Free and open source.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -201,5 +201,5 @@
201
201
  "type": "git",
202
202
  "url": "git+https://github.com/gigialc/sandboxrouter.git"
203
203
  },
204
- "homepage": "https://sandboxrouter.vercel.app"
204
+ "homepage": "https://sandboxrouter.ai"
205
205
  }