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 compute you can prove.

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:

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.

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

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:

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:

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.