@sandboxrouter/sdk 0.8.1 → 0.8.2

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
@@ -43,7 +43,7 @@ List price while running, 2 vCPU / 4 GB, cheapest first; waitlisted sign-ups fol
43
43
  | Provider | $/hr | Plan needed | Real-account check | Source |
44
44
  |---|---|---|---|---|
45
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) |
46
+ | [Boat](https://sandboxrouter.ai/providers/boat) | $0.018 | $20/mo (credited) | passed (2026-11-14) | [pricing](https://docs.boat.dev/pricing.md) |
47
47
  | [Maritime](https://sandboxrouter.ai/providers/maritime) | $0.022 | $20/mo (credited) | passed (2026-11-13) | [pricing](https://maritime.sh/pricing) |
48
48
  | [Sailboxes (Sail Research)](https://sandboxrouter.ai/providers/sail) | up to $0.062 | none | passed (2026-11-14) | [pricing](https://docs.sailresearch.com/sailboxes-pricing.md) |
49
49
  | [Northflank](https://sandboxrouter.ai/providers/northflank) | $0.067 | none | not run yet | [pricing](https://northflank.com/pricing) |
package/dist/cli.js CHANGED
@@ -9,7 +9,7 @@ import { gc, listOpen } from "./gc.js";
9
9
  import { contributor, publishableCheck } from "./results.js";
10
10
  import { loadReports, makeReport, renderLeaderboard } from "./leaderboard.js";
11
11
  import { DEFAULT_MEMORY_GB, DEFAULT_VCPU, accessLabel, benchmarksPublishable, hourlyCost, listingOrder, planLabel, unfit } from "./catalog.js";
12
- import { Router } from "./router.js";
12
+ import { Router, rankUsd, runHours } from "./router.js";
13
13
  import { createApp } from "./server.js";
14
14
  import { RouterError } from "./types.js";
15
15
  const HELP = `sandboxrouter: one API for every sandbox provider
@@ -40,7 +40,7 @@ Usage:
40
40
  sandboxrouter screenshot <provider:id> [--out screen.png]
41
41
  sandboxrouter serve [--port 8787] [--host 127.0.0.1] [--cors https://site.example,http://localhost:3000]
42
42
 
43
- Spec flags: --vcpu N --memory GB --gpu T4 --timeout MIN
43
+ Spec flags: --vcpu N --memory GB --gpu T4 --timeout MIN --expect SEC (how long you'll use it; ranks by the real bill)
44
44
  Route flags: --provider e2b[,modal,...] --strategy balanced|verified|cheapest|fastest (default balanced: cheapest real cost, speed breaks near-ties)
45
45
  --no-fallback --fallback-any (also on setup errors like a bad key)
46
46
  --create-timeout SEC (per provider, default 120) --require gpu,custom-image
@@ -54,6 +54,7 @@ const { values, positionals } = parseArgs({
54
54
  memory: { type: "string" },
55
55
  gpu: { type: "string" },
56
56
  timeout: { type: "string" },
57
+ expect: { type: "string" },
57
58
  provider: { type: "string" },
58
59
  strategy: { type: "string" },
59
60
  "no-fallback": { type: "boolean" },
@@ -96,6 +97,7 @@ const spec = {
96
97
  memoryGb: num(values.memory),
97
98
  gpu: values.gpu,
98
99
  timeoutMinutes: num(values.timeout),
100
+ expectedSeconds: num(values.expect),
99
101
  };
100
102
  const providers = list(values.provider);
101
103
  const by = (() => {
@@ -165,7 +167,10 @@ async function main() {
165
167
  const size = `${c.size.vcpu} vCPU / ${c.size.memoryGb} GB`;
166
168
  const speed = c.medianCreateMs !== undefined ? `${c.medianCreateMs} ms` : "";
167
169
  const fee = c.perSandboxUsd ? ` + $${c.perSandboxUsd}/sandbox` : "";
168
- const cols = [`$${c.estimatedHourlyUsd.toFixed(4)}/hr${list}${fee}`, size, speed, tested].filter(Boolean).join(" ");
170
+ // With --expect, what that run would be billed (minimum charge and fee included): what it's ranked by.
171
+ const hours = runHours(spec);
172
+ const run = spec.expectedSeconds ? ` ≈ $${(rankUsd(c, hours) * hours).toFixed(6)} for ${Math.round(hours * 3600)} s` : "";
173
+ const cols = [`$${c.estimatedHourlyUsd.toFixed(4)}/hr${list}${fee}${run}`, size, speed, tested].filter(Boolean).join(" ");
169
174
  console.log(`${mark} ${c.provider.padEnd(11)} ${cols}${c.skip ? ` (skipped: ${c.skip})` : ""}`);
170
175
  }
171
176
  return;
package/dist/router.d.ts CHANGED
@@ -31,7 +31,7 @@ export interface Candidate {
31
31
  failing: boolean;
32
32
  /**
33
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.
34
+ * spreads it over the run (`expectedSeconds`, else `timeoutMinutes`, default 30), so it weighs more on short ones.
35
35
  */
36
36
  perSandboxUsd?: number;
37
37
  /** Why this provider won't be tried, if it won't. */
@@ -184,10 +184,18 @@ export declare function parseId(id: string): {
184
184
  * The default order. Providers whose last real-account check failed go last. The rest go cheapest
185
185
  * first by effective price (list price x what the provider really billed, where measured), except that
186
186
  * providers within PRICE_TIE of the cheapest remaining one count as a tie and go fastest first (by
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).
187
+ * median create time; unknown speed after known), then cheapest, then alphabetical. Price is what a run
188
+ * of `lifetimeHours` would be billed (`rankUsd`), as USD per hour.
189
189
  */
190
190
  export declare function balanced(list: Candidate[], lifetimeHours?: number): Candidate[];
191
+ /** Hours ranking assumes a sandbox runs: `expectedSeconds` (no longer than the lifetime), else `timeoutMinutes` (default 30). */
192
+ export declare function runHours(spec: SandboxSpec): number;
193
+ /**
194
+ * USD per hour a candidate is ranked by: what a run of `hours` would be billed, divided by `hours`. The
195
+ * run is rounded up to the provider's minimum and billing increment (`billingRules`), and its
196
+ * per-sandbox charge is added, so short runs favor providers that bill by the second without a fee.
197
+ */
198
+ export declare function rankUsd(c: Candidate, hours: number): number;
191
199
  /** Seconds to wait for one provider's create before trying the next (RouteOptions.createTimeoutSeconds). */
192
200
  export declare const DEFAULT_CREATE_TIMEOUT_S = 120;
193
201
  /** Seconds a whole create may take over every provider it tries (RouteOptions.deadlineSeconds). */
package/dist/router.js CHANGED
@@ -181,7 +181,7 @@ export class Router {
181
181
  // depends on who the provider is, only on its price, what it really billed, how fast it started for
182
182
  // this router and whether its last real-account check failed. An explicit list keeps the caller's order.
183
183
  if (!explicit) {
184
- const hours = (spec.timeoutMinutes ?? 30) / 60;
184
+ const hours = runHours(spec);
185
185
  const byCost = (a, b) => rankUsd(a, hours) - rankUsd(b, hours) || a.provider.localeCompare(b.provider);
186
186
  const byList = (a, b) => a.listHourlyUsd - b.listHourlyUsd || a.provider.localeCompare(b.provider);
187
187
  if (strategy === "fastest") {
@@ -439,8 +439,8 @@ function round(n) {
439
439
  * The default order. Providers whose last real-account check failed go last. The rest go cheapest
440
440
  * first by effective price (list price x what the provider really billed, where measured), except that
441
441
  * providers within PRICE_TIE of the cheapest remaining one count as a tie and go fastest first (by
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).
442
+ * median create time; unknown speed after known), then cheapest, then alphabetical. Price is what a run
443
+ * of `lifetimeHours` would be billed (`rankUsd`), as USD per hour.
444
444
  */
445
445
  export function balanced(list, lifetimeHours = 0.5) {
446
446
  const cost = (c) => rankUsd(c, lifetimeHours);
@@ -460,9 +460,21 @@ export function balanced(list, lifetimeHours = 0.5) {
460
460
  };
461
461
  return [...order(list.filter((c) => !c.failing)), ...order(list.filter((c) => c.failing))];
462
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);
463
+ /** Hours ranking assumes a sandbox runs: `expectedSeconds` (no longer than the lifetime), else `timeoutMinutes` (default 30). */
464
+ export function runHours(spec) {
465
+ const lifetime = (spec.timeoutMinutes ?? 30) * 60;
466
+ return Math.min(spec.expectedSeconds ?? lifetime, lifetime) / 3600;
467
+ }
468
+ /**
469
+ * USD per hour a candidate is ranked by: what a run of `hours` would be billed, divided by `hours`. The
470
+ * run is rounded up to the provider's minimum and billing increment (`billingRules`), and its
471
+ * per-sandbox charge is added, so short runs favor providers that bill by the second without a fee.
472
+ */
473
+ export function rankUsd(c, hours) {
474
+ const { minSeconds, incrementSeconds } = billingRules(c.provider);
475
+ const seconds = hours * 3600;
476
+ const billed = Math.max(minSeconds, Math.ceil(seconds / incrementSeconds - 1e-9) * incrementSeconds);
477
+ return (c.estimatedHourlyUsd * billed) / Math.max(seconds, 1) + (c.perSandboxUsd ?? 0) / Math.max(hours, 1 / 3600);
466
478
  }
467
479
  /** Seconds to wait for one provider's create before trying the next (RouteOptions.createTimeoutSeconds). */
468
480
  export const DEFAULT_CREATE_TIMEOUT_S = 120;
package/dist/types.d.ts CHANGED
@@ -18,6 +18,14 @@ export interface SandboxSpec {
18
18
  env?: Record<string, string>;
19
19
  /** Hard lifetime in minutes (default 30). */
20
20
  timeoutMinutes?: number;
21
+ /**
22
+ * How long you expect to use the sandbox, in seconds. Only a ranking hint: the sandbox still lives
23
+ * until you kill it or `timeoutMinutes` runs out. Without it, ranking assumes the whole lifetime.
24
+ * With it, providers are ranked by what that run would really be billed, minimum charge, billing
25
+ * increment and per-sandbox fee included, so a provider that bills per second can beat one that is
26
+ * cheaper per hour but charges a full minute.
27
+ */
28
+ expectedSeconds?: number;
21
29
  /**
22
30
  * Provider-specific image/template, keyed by provider, e.g.
23
31
  * { maritime: "python", e2b: "code-interpreter-v1", modal: "python:3.13-slim" }.
@@ -1,6 +1,6 @@
1
1
  import { type RouteOptions, type SandboxSpec, type Strategy } from "./types.js";
2
2
  /** Every key a SandboxSpec can have. Anything else in a request is a mistake (e.g. "memory" for "memoryGb"). */
3
- export declare const SPEC_KEYS: readonly ["vcpu", "memoryGb", "gpu", "env", "timeoutMinutes", "image", "metadata"];
3
+ export declare const SPEC_KEYS: readonly ["vcpu", "memoryGb", "gpu", "env", "timeoutMinutes", "expectedSeconds", "image", "metadata"];
4
4
  /** Every key RouteOptions can have. */
5
5
  export declare const ROUTE_KEYS: readonly ["provider", "strategy", "fallback", "require", "createTimeoutSeconds", "deadlineSeconds"];
6
6
  export declare const STRATEGIES: readonly Strategy[];
package/dist/validate.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CATALOG } from "./catalog.js";
2
2
  import { RouterError } from "./types.js";
3
3
  /** Every key a SandboxSpec can have. Anything else in a request is a mistake (e.g. "memory" for "memoryGb"). */
4
- export const SPEC_KEYS = ["vcpu", "memoryGb", "gpu", "env", "timeoutMinutes", "image", "metadata"];
4
+ export const SPEC_KEYS = ["vcpu", "memoryGb", "gpu", "env", "timeoutMinutes", "expectedSeconds", "image", "metadata"];
5
5
  /** Every key RouteOptions can have. */
6
6
  export const ROUTE_KEYS = ["provider", "strategy", "fallback", "require", "createTimeoutSeconds", "deadlineSeconds"];
7
7
  export const STRATEGIES = ["default", "balanced", "verified", "cheapest", "fastest"];
@@ -16,7 +16,7 @@ const stringMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v
16
16
  * Called by `plan()`, so every create, route and CLI command goes through it.
17
17
  */
18
18
  export function validate(spec = {}, route = {}) {
19
- for (const k of ["vcpu", "memoryGb", "timeoutMinutes"]) {
19
+ for (const k of ["vcpu", "memoryGb", "timeoutMinutes", "expectedSeconds"]) {
20
20
  if (spec[k] !== undefined && !positive(spec[k]))
21
21
  throw bad(`${k} must be a positive number, got ${show(spec[k])}`);
22
22
  }
package/dist/verified.js CHANGED
@@ -3,6 +3,7 @@ export const VERIFIED = {
3
3
  agentcore: "2026-11-13",
4
4
  lambda: "2026-11-13",
5
5
  blaxel: "2026-11-13",
6
+ boat: "2026-11-14",
6
7
  cloudflare: "2026-11-13",
7
8
  daytona: "2026-11-13",
8
9
  e2b: "2026-11-13",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sandboxrouter/sdk",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
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",