sable network
Buy on Index
Documentation · all sections

The Terminal

Every other compute surface on Sable asks for a credential first, which means the first thing a stranger meets is a signup form. The Terminal removes that step for exactly one purpose: so someone can watch the product prove itself before deciding whether to trust it.

POST /v1/terminal/run takes a few lines of code, runs them in a real sealed sandbox — the same sandbox::run_inner path a paying customer's POST /v1/sandboxes call takes — meters the result, and hands back a real signed receipt that verifies at the public POST /v1/receipts/verify. No API key, no wallet, no account. The only thing that differs from a paid run is who pays: the operator's own house account, not the visitor, who has none.

It is real on purpose. This repo deleted a synthetic demo console once already, because a fabricated demo is worse than no demo on a product whose entire pitch is private intelligence.

Availability

The Terminal is off on the production gateway. It is the most abusable surface Sable has — it spends the operator's money, on demand, for anyone on the internet — so the route is not registered at all unless a deployment switches it on. On api.buildsable.com today it answers 404 with an empty body.

The check is one request, and it costs nothing: GET the route. A gateway that serves the Terminal answers 405 (the route exists, the method is wrong); one that does not answers 404. The public Terminal page does exactly this on load, which is why it says “not switched on here” rather than rendering an editor that cannot work.

Run something

curl -sS https://api.buildsable.com/v1/terminal/run \
  -H 'content-type: application/json' \
  -d '{"code":"print(sum(range(10_001)))"}'
{
  "id": "sbx_…",
  "object": "terminal.run",
  "status": "succeeded",
  "exit_code": 0,
  "stdout": "50005000\n",
  "stderr": "",
  "truncated": false,
  "duration_ms": 842,
  "vcpu_seconds": 1,
  "cost_micro_usd": 14,
  "receipt": { "receipt": "eyJ2IjoyLC…", "signature": "0x…", "signer": "0x…", "payload": { } },
  "limits": { "language": "python", "vcpu": 1, "mem_mb": 256, "timeout_secs": 10, "network": false }
}

The request body has two fields and no others:

FieldTypeMeaning
codeTypestringMeaningThe source to run. Required, non-empty, at most 4,000 bytes.
streamTypebooleanMeaningRelay stdout/stderr live over SSE instead of buffering.

Anything else is a 400 naming the field. That is deliberate: network, image, vcpu and timeout_secs are all knobs the paid sandbox has and this one does not, and silently ignoring them is how a caller comes to believe a knob works.

Streaming

{"stream": true} returns text/event-stream carrying exactly the event names /v1/sandboxes emits, so one client parser covers both:

event: sable.stdout
data: {"text":"50005000\n"}

event: sable.result
data: { …the same body shown above… }

data: [DONE]

A failed run emits event: sable.error with a content-free message instead of sable.result. The run is metered, billed and receipted in full even if the visitor closes the tab — the container ran either way.

The ceilings

They are constants in the gateway, not configuration. An env-tunable ceiling on a keyless execution endpoint is one typo away from an open compute faucet.

CeilingValue
vCPUValue1
MemoryValue256 MiB
Wall clockValue10 seconds
SourceValue4,000 bytes
Output, per streamValue16,000 bytes
Network egressValuenone, pinned off rather than defaulted off

Every response restates them in a limits block, so a client never has to hard-code numbers that might drift — and the runtime an operator chose (python, node or bash) is disclosed there too.

Two more bounds sit outside the response: a per-address rate limit on this endpoint's own token bucket, far tighter than the shared public one, and a global daily budget in micro-USD reserved before the container starts and settled after, so concurrent runs cannot collectively commit more than the day allows.

What a refusal means

StatusCondition
404, empty bodyConditionThe Terminal is not switched on for this deployment. Not an error — an absence.
400ConditionEmpty code, source over 4,000 bytes, an unknown field, or a body over 32 KiB.
403ConditionThe payload is on the operator's denylist, or the house account is suspended.
429 + Retry-AfterConditionOne of three different things — read the message.

The three 429s are worth separating, because they are not the same problem and the message says which:

  • Rate limited. Too many runs from one address in a minute. Wait.
  • Daily budget spent. The endpoint's ceiling for this UTC day is committed. Retry-After counts down to 00:00 UTC, and that reset is real.
  • House account out of credit. The account that pays for these runs has no prepaid balance. This one does not clear at midnight — it is an operator condition and needs an operator. The message says so rather than borrowing the budget sentence, because a reset that never comes is worse than no estimate at all.

Privacy

Identical to /v1/sandboxes, and it must stay identical.

Your code, and the stdout and stderr it produces, are request-scoped: they cross the gateway in-frame, are returned to you exactly once, and are never written to the database or to a log line. The Terminal's own tables carry money, counts and timestamps — there is no column that could hold them. Even the client IP the rate limiter keys on lives only in an in-memory bucket that dies with the process, so there is no record of who ran what.

The one content-derived value that outlives the request is content_fingerprint, a sha256 prefix of what you submitted, which the receipt carries for every sandbox run. That is what makes the receipt publishable while the code is not.

What the receipt proves

The same thing every Sable compute receipt proves, no more:

  • That a real metered run happened — a container, real vCPU-seconds, a real debit against a real prepaid balance, signed by the gateway's key over the metadata and your input's fingerprint.
  • Not that the host could not read your code. The Terminal is the ordinary sandbox tier. Host-blind execution is the confidential tier, and it is a different claim with different hardware behind it.
  • Not that the output is correct. A receipt describes what ran and what it cost. It has no opinion about the answer.

Verify one yourself at the verifier, which recovers the signer in your own browser rather than asking Sable whether Sable's signature is good.

For operators

Two things, and the second is the one that gets missed.

Switch it on. SABLE_TERMINAL_ENABLED=true, plus a configured sandbox backend. Without both, the route is never registered — which is strictly better than a route that exists and refuses, because an endpoint that rejects still tells a prober it is there.

Fund the house account. SABLE_TERMINAL_ACCOUNT_ID names the account every run is billed to. The route creates it on first use with a zero balance and no way to top itself up, and the daily budget is a ceiling, not an allowance: it caps what may be spent, it does not provide it. So with billing enforced and an unfunded house account, every run is refused by the ordinary prepaid-credit check while the budget table is never touched — the endpoint reads as permanently broken for reasons the budget numbers do not explain. Credit that account the way any account is credited, and keep it topped up.

The remaining knobs — SABLE_TERMINAL_DAILY_BUDGET_MICRO_USD, SABLE_TERMINAL_RATE_LIMIT_PER_MIN, SABLE_TERMINAL_LANGUAGE, SABLE_TERMINAL_IMAGE — are documented in .env.example, with boot-checked ranges: a value outside its range refuses boot rather than silently degrading a surface that spends real money for anonymous callers.

Two kill switches exist once it is running. Suspending the house account with POST /v1/admin/accounts/suspend takes the Terminal dark without a deploy, and setting the daily budget to zero refuses everything while leaving the route mounted.

Need more than ten seconds?

Then you want the paid path, and it is the same execution engine without the training wheels: sandbox compute runs for up to four hours on your own budget, with your own image, optional network egress, persistent sessions, and the same signed receipt on every run.

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