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.
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:
| Field | Type | Meaning |
|---|---|---|
code | Typestring | MeaningThe source to run. Required, non-empty, at most 4,000 bytes. |
stream | Typeboolean | MeaningRelay 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.
| Ceiling | Value |
|---|---|
| vCPU | Value1 |
| Memory | Value256 MiB |
| Wall clock | Value10 seconds |
| Source | Value4,000 bytes |
| Output, per stream | Value16,000 bytes |
| Network egress | Valuenone, 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
| Status | Condition |
|---|---|
404, empty body | ConditionThe Terminal is not switched on for this deployment. Not an error — an absence. |
400 | ConditionEmpty code, source over 4,000 bytes, an unknown field, or a body over 32 KiB. |
403 | ConditionThe payload is on the operator's denylist, or the house account is suspended. |
429 + Retry-After | ConditionOne 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-Aftercounts 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.