sable network
Buy on Index
Documentation · all sections

Sandbox compute

POST /v1/sandboxes runs code in an isolated sandbox, bills it in vCPU-seconds and GB-seconds of memory, and returns a signed receipt. Same API key as inference, same credit balance, same spend caps.

Agents need somewhere to run code far more often than they need a GPU. What Sable adds on top of the execution itself is the control plane: a hard spend ceiling, a key scope, a retry that cannot execute or bill twice, and a per-run receipt you can hand to someone else as evidence.

Run some code

curl https://api.buildsable.com/v1/sandboxes \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-d '{
  "language": "python",
  "code": "print(sum(range(10)))"
}'

The response:

{
  "id": "sbx_7f3c…",
  "object": "sandbox.run",
  "status": "succeeded",
  "exit_code": 0,
  "stdout": "45\n",
  "stderr": "",
  "truncated": false,
  "duration_ms": 412,
  "vcpu": 1,
  "vcpu_seconds": 1,
  "cost_micro_usd": 17,
  "receipt": { "receipt": "eyJ2IjoyLC…", "signature": "0x…", "signer": "0x…" },
  "replayed": false
}

Each output stream is capped at 256 KiB, cut on a UTF-8 character boundary; when either stream is cut, truncated is true.

Request fields

FieldDefaultNotes
codeDefaultrequiredNotesThe program to run. Fed to the runtime over stdin.
languageDefaultpythonNotespython, node, or bash.
imageDefaultper languageNotesExplicit runtime image. Overrides the language default.
timeout_secsDefault30NotesClamped to the deployment maximum: 4 hours on production. For long jobs use stream: true so output (and liveness) arrives as it happens.
vcpuDefault1NotesClamped to the deployment maximum.
mem_mbDefault512NotesClamped to the deployment maximum. Memory is billed (see below), so ask for what the run needs, not the ceiling.
envDefaultnoneNotesEnvironment variables. Request-scoped, never stored.
networkDefaultfalseNotesNetwork egress. Off by default: generated code doesn't get the internet unless you ask. On a deployment whose sandbox backend does not permit egress, network: true is refused rather than silently ignored.

Streaming output

Pass "stream": true and the response becomes an SSE stream: sable.stdout and sable.stderr events carry output the moment your program prints it, and a terminal sable.result event carries the exact same response body the buffered call returns, signed receipt included. Billing is identical either way, and a client that disconnects mid-stream is still billed for the run (the slot was held and the run completes on the node).

curl -N https://api.buildsable.com/v1/sandboxes \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-d '{
  "stream": true,
  "language": "python",
  "code": "import time\nfor i in range(3):\n    print(i, flush=True)\n    time.sleep(1)"
}'

# event: sable.stdout   data: {"text":"0\n"}     ← arrives at t≈0s
# event: sable.stdout   data: {"text":"1\n"}     ← t≈1s
# event: sable.stdout   data: {"text":"2\n"}     ← t≈2s
# event: sable.result   data: { …full response with receipt… }

Output caps apply identically (256 KiB per stream); chunks past the cap are dropped from the stream and truncated is true on the result. On a node that predates streaming, the gateway falls back to a buffered run and the terminal event still carries the full output.

Both SDKs consume this for you: sable.runCodeStream(req) (TypeScript, an async iterator of stdout/stderr/result events) and sable.run_code_stream(...) (Python, a generator yielding ("stdout", text) / ("stderr", text) then ("result", run)).

Retrying safely

A sandbox call executes code and bills for it, so a blind retry after a dropped connection is a second execution and a second charge. Send an optional Idempotency-Key header and the retry is safe: at most one execution per key, per account. The key is claimed before the code runs, so two concurrent retries race to a single winner rather than both executing.

curl https://api.buildsable.com/v1/sandboxes \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-H 'Idempotency-Key: 6f1c2a8e-4b77-4a1d-9d0e-2b7d5c9a1f30' \
-d '{
  "language": "python",
  "code": "print(sum(range(10)))"
}'

The key is any printable-ASCII string up to 200 characters: a UUID or a hash of the job is the usual choice. A non-ASCII or over-length key is a 400 rather than a silent no-op, because a key that quietly does nothing is worse than no key at all. Keys are scoped to your account, and a request without the header behaves exactly as it did before.

SituationWhat you get
First request with a keyWhat you getThe run executes normally. "replayed": false.
Retry after that run finishedWhat you getThe original signed receipt, replayed byte-identical, with "replayed": true. The code is not executed again and nothing is billed again.
Retry while the run is still in flightWhat you get409 with type: conflict. The only alternatives would be to stall or to run your code twice. Retry once it finishes.
Retry after the run failed (502 backend or capacity unavailable)What you getThe key was released, so this genuinely re-runs. Nothing was billed for the failed attempt.
Key over 200 chars, or containing non-ASCIIWhat you get400, before anything runs.

A replay returns empty output

Sandbox stdout and stderr are never written to disk or a log line (see Privacy below, and the privacy contract), so there is nothing to replay them from. A replayed response carries the original receipt and the run metadata, and both output streams come back as "":

{
  "id": "sbx_7f3c…",
  "object": "sandbox.run",
  "status": "succeeded",
  "exit_code": 0,
  "stdout": "",
  "stderr": "",
  "truncated": false,
  "duration_ms": 412,
  "vcpu": 1,
  "vcpu_seconds": 1,
  "cost_micro_usd": 17,
  "receipt": { "receipt": "eyJ2IjoyLC…", "signature": "0x…", "signer": "0x…" },
  "replayed": true
}

Keep the output from the first response. Idempotency protects your credit and your side effects; it cannot hand you back an answer the gateway deliberately never kept. Always branch on replayed before treating stdout as the result of the run.

What it costs

Billing has two dimensions, both wall-clock, both rounded up, minimum one second (a partial second still occupies a slot):

  • vCPU-seconds: seconds the run held its slot × vCPUs requested.
  • GB-seconds of memory: seconds × GB requested (default rate 2 micro-USD per GB-second ≈ $0.0072 per GB-hour, before margin). Memory is what actually limits how many sandboxes a host can run at once, so it is priced rather than free.

cost_micro_usd on the response is the total charged amount across both dimensions, and it also appears in the receipt and in GET /v1/usage.

Two things you are not billed for:

  • Capacity we failed to deliver. If the backend is unavailable the call returns 502 with error_class: backend_unavailable and costs nothing. The attempt is still recorded so you can see it happened.
  • Requests we refuse. Scope denials, spend-cap rejections, and credit exhaustion all short-circuit before any capacity is reserved.

A run that times out is billed: it held the slot for the full window.

While a run is in flight it reserves its worst case against your prepaid balance (every second of its timeout_secs, on every vCPU and every MB of memory it asked for), and the reservation is released the moment the run settles, when you are charged the actual usage instead. A generous timeout_secs with a large mem_mb therefore reserves a lot of credit even if the run finishes in a second. Spendable credit is your balance minus what in-flight requests have reserved, so a 402 can mean credit is reserved by requests already in flight rather than that your balance is empty; the error message says which. That state is temporary: retry once those runs settle. See Cost & metering.

Concurrency

An account can hold a bounded number of sandbox runs in flight at once (default 8). A request over the cap returns 429 with a Retry-After header: nothing is executed and nothing is billed. Queue on your side and retry, or finish runs faster with a tighter timeout_secs.

Spend controls

Everything that governs an inference key governs sandboxes too (see API key controls):

  • Spend cap: a key over its monthly cap gets 402 before anything runs.
  • Resource allowlist: allowed_models doubles as a resource allowlist. A key with an allowlist must include sandbox (or the specific image) or the run is refused with 403. A key scoped to one model can't quietly pivot into running arbitrary code.
  • Expiry and per-key rate limits apply unchanged.

Privacy

Your code, its stdout, and its stderr cross the gateway in-frame, are returned to you, and are never written to disk or a log line. The sandboxes table has no column that could hold them.

The one content-derived value that persists is content_fp: a sha256 prefix of the submitted payload, the same fingerprint scheme the inference receipts use. It proves which payload ran without revealing what it was.

Receipts

Sandbox receipts are v: 2 and describe compute rather than tokens:

{
  "v": 2,
  "kind": "sandbox",
  "resource": "python:3.12-slim",
  "unit": "vcpu_seconds",
  "quantity": 1,
  "cost_micro_usd": 17,
  "status": "succeeded",
  "exit_code": 0,
  "content_fingerprint": "3954a524ccabab70",
  "logging": "metadata-only"
}

They are signed by the same key as inference receipts and verify through the same POST /v1/receipts/verify. A distinct version rather than a reinterpreted inference receipt, because vCPU-seconds are not tokens and a receipt shouldn't lie about what it measured.

Isolation, honestly

Production sandboxes run on Sable-operated infrastructure. A deployment runs one of two backends:

  • http-runner: a gVisor/microVM runner behind a small wire contract (published at node agent). This is the production path: every job runs in a gVisor sandbox with capabilities dropped, a read-only rootfs, memory and pids ceilings, and no network unless the request asks for it. The gateway schedules each run onto an enrolled fleet node (today a single Sable-operated house node), claiming a per-job concurrency lease in a database transaction before it dials, and falls back to the configured single backend when no enrolled node has a free slot.
  • local-docker: the local Docker daemon. Correct for development and single-tenant self-hosting, but it is not a security boundary against hostile code: a container escape lands on the gateway host. The gateway logs a warning at boot when this backend is selected.

If a run is refused with 501, sandbox compute isn't enabled on that deployment.

Shipped
Sable Launchpad — launch a coin with a character, on Robinhood ChainProof of Backing — continuous reserve verification, published wholeSupport Program register opened to SABL holdersEight models added, including the Claude 5 and Gemini 3.8 familiesKnow Your Agent — a machine-checkable credential at a passport handleAutopilot — receipted evidence for a cheaper configurationThe Verifiable Arena — every score backed by a real receipted runTime Machine — record a request, replay it, diff the resultSable Notary — sign and anchor work Sable did not runIntelligence Engine — compile a configuration into an immutable buildSealed Calls — commit a prediction now, reveal it laterAgent Post — cross-account mail between agents, with postageSable Legacy — a dead man's switch for sealed contentThe Judge — a panel of models, one signed verdictSable Memory, Agent Runtime and the non-custodial payment railPersistent sandbox sessions with exec, files and snapshotsMCP Gateway — every tool call allowlisted, metered and receiptedOAuth 2.1 + dynamic client registration for one-click MCP connectSable Vault — a private registry with public settlement