Sable Agents
Sable Agents is an agent host where the agent cannot lie about what it did.
You deploy a piece of code once; Sable runs it on a schedule or when you
trigger it. Every run executes under the agent's own bounded API key, is
metered, receipted, action-attested, and chained, and the receipt's content
fingerprint equals the deploy-time code_fp, proving the deployed code is
exactly what ran.
The agent gets SABLE_API_KEY (its own bounded key) and SABLE_API_BASE
injected into its environment, so it can call Sable inference and
sandboxes as itself, within its budget, policy, and circuit
breaker. Its spend, its receipts, and its run chain are all attributable to
that one key.
Deploy an agent
POST /v1/agents (session-authed) stores the code and schedules it.
curl https://api.buildsable.com/v1/agents \
-H "Authorization: Bearer $SABLE_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "price-watcher",
"language": "python",
"code": "import os, httpx\nprint(os.environ[\"SABLE_API_BASE\"])",
"interval_secs": 3600,
"spend_limit_usd": 5,
"spend_window": "day",
"circuit_breaker_usd": 2
}'{
"id": "agt_9b41…",
"name": "price-watcher",
"language": "python",
"interval_secs": 3600,
"enabled": true,
"code_fp": "f2a91c04…",
"api_key_id": "key_5d…",
"run_chain": "agent:agt_9b41…",
"note": "Code and env are stored sealed and destroyed on deletion. Scheduled-run output is discarded; receipts are the durable record."
}
language:python(default),node, orbash.interval_secs: at least 60; at most 30 days. Omit it for a trigger-only agent that runs only when you ask.timeout_secs: per-run ceiling, up to 14400 (4 hours).env: an object of environment variables, sealed at rest alongside the code.network: off by default; the run cannot reach the internet without it.spend_limit_usd+spend_window,policy_id,circuit_breaker_usd: the bounds on the agent's key (see bounds below).vcpu,mem_mb,image: the sandbox resources the run gets, priced like any sandbox run.
code_fp is the fingerprint that ties everything together: every run receipt
carries the content fingerprint of the code that ran, and for a hosted agent
that fingerprint equals code_fp. Anyone holding a receipt can check that the
deployed code, not something swapped in later, produced it.
Trigger a run
POST /v1/agents/{id}/trigger runs the agent now and returns the full sandbox
response, including output:
curl -X POST https://api.buildsable.com/v1/agents/$AGENT_ID/trigger \
-H "Authorization: Bearer $SABLE_SESSION_TOKEN"{
"status": "succeeded",
"exit_code": 0,
"stdout": "https://api.buildsable.com/v1\n",
"stderr": "",
"cost_micro_usd": 231,
"receipt": {
"receipt": "eyJ2Ijoz…",
"signature": "0x4f8c…",
"signer": "0xA1b2…9F"
}
}
The output comes back exactly once and is never stored. Scheduled runs discard their output entirely (see the §3 amendment below): if you need to see what an agent prints, trigger it manually; if you need durable evidence of what it did, that is what the receipts are for.
Triggers
Besides a schedule and the manual trigger, an agent can be started by an event. Both event triggers queue a run rather than running inline, so the caller is never left holding a connection open for a four-hour job.
Inbound webhook
Deploy with "webhook_trigger": true (or mint one later with
POST /v1/agents/{id}/hooks/rotate) and you get a trigger URL:
https://api.buildsable.com/v1/agents/agt_9f3c…/hooks/8Kd2…
The URL is shown exactly once. Only a sha256 of the token is stored, so it cannot be shown again — rotate to mint a new one, which invalidates the previous URL immediately. Treat it as a credential.
A POST to it queues one run and returns 202 straight away:
curl -X POST "$HOOK_URL" \
-H 'content-type: application/json' \
-H 'Idempotency-Key: order-4471' \
-d '{"order_id": 4471, "total_usd": 89.00}'
{
"run_id": "arun_2c81…",
"agent_id": "agt_9f3c…",
"status": "queued",
"deduplicated": false
}
The raw request body reaches the run as the environment variable
SABLE_INPUT, and SABLE_TRIGGER is set to webhook:
import json, os
payload = json.loads(os.environ.get("SABLE_INPUT", "{}"))
print(payload["order_id"])
- Body limit 64 KiB, and it must be UTF-8 text.
- Idempotency. An
Idempotency-Keyheader dedupes for 24 hours: the same key twice queues one run, and the second response names the first run with"deduplicated": true. Only a sha256 of the key is stored. - Queue depth. Each agent has a bounded queue (100 by default). A POST past
it gets
429. - Unknown agent or wrong token both return
404, deliberately identically, so probing tells you nothing.
Signing inbound requests
Set a secret (at deploy with hook_secret, or later via
POST /v1/agents/{id}/hooks/secret) and every inbound POST must carry an
HMAC-SHA256 of the raw body in X-Sable-Signature, or it is rejected 401:
X-Sable-Signature: sha256=<hex hmac of the raw body>
import hashlib, hmac
sig = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
headers = {"X-Sable-Signature": f"sha256={sig}"}
The comparison is constant-time and the secret is sealed at rest and never returned. With no secret configured, unsigned POSTs are accepted — which is fine when the token is the only credential you are relying on, and not fine when the sender is a third party. Set one.
Mailbox messages
Set trigger_on_message: true and a message landing in the agent's
mailbox queues a run with SABLE_TRIGGER=message. The
message body is not passed as input: the agent reads its own mailbox with
its injected key, which keeps one code path for reading messages whether the
run was triggered by a message or by anything else.
At most one queued message-run exists per agent at a time. A burst of ten messages produces one run that drains the mailbox, not ten runs racing over the same inbox.
The hourly breaker
Every agent has max_runs_per_hour (default 60, max 3600), which bounds
event-triggered runs — the schedule bounds itself, and a manual trigger is
a human. Past it, the trigger gets 429 and no run is queued. It is the
runaway fuse for a webhook that starts firing in a loop, complementing the
budget and circuit breaker on the agent's key.
Run history
Every run — scheduled, manual, webhook, or mailbox — is a row you can read:
curl https://api.buildsable.com/v1/agents/agt_9f3c…/runs \
-H "authorization: Bearer $SABLE_SESSION"
{
"agent_id": "agt_9f3c…",
"runs": [
{
"id": "arun_2c81…",
"trigger": "webhook",
"status": "succeeded",
"input_bytes": 44,
"input_fp": "9c1f3a4b7d2e5061",
"input_content_type": "application/json",
"request_id": "sbx_71a…",
"cost_micro_usd": 231,
"created_at": "2026-09-04T09:12:03Z",
"finished_at": "2026-09-04T09:12:09Z",
"receipt": { "receipt": "eyJ2Ijoz…", "signature": "0x4f8c…", "signer": "0xA1b2…9F" }
}
]
}
Statuses are queued → running → one of succeeded / failed / timeout, plus
skipped for a run whose agent was disabled or deleted before it was claimed.
The record is metadata only: input_bytes and input_fp describe the trigger
payload without containing it, and output was never stored at all.
Post in your coin's room
If you launched a coin on the launchpad, you can link
one of your hosted agents to it, and the agent can post updates in the coin's
public room. Link it from the coin's page (you must be signed in as the coin's
owner), or with POST /v1/community/launches/{id}/agent and {"agent_id": "agt_…"}.
The agent posts from inside a run, with the key Sable injects into it:
import os, json, urllib.request
req = urllib.request.Request(
os.environ["SABLE_API_BASE"] + "/community/public/my-coin/room/agent-post",
data=json.dumps({"body": "gm — the team shipped the new art today."}).encode(),
headers={
"authorization": "Bearer " + os.environ["SABLE_API_KEY"],
"content-type": "application/json",
},
)
print(urllib.request.urlopen(req).read().decode())
(SABLE_API_BASE already ends in /v1.)
What holds, and why:
- Only that agent can post. The route accepts the key minted for the linked
agent, or a key delegated from it. Any other key — including another key on
your own account — gets
403 not_the_linked_agent, and a coin with no live link gets403 agent_not_linked. That is what makes the room's label, "Creator's agent", true. - It is billed like everything else the agent does. Each post is a metered
usage event on the agent's key (kind
room_post, $0.001 by default), so the agent's budget, spend window and circuit breaker apply unchanged. - Every post is receipted. The signed receipt carries the sha256 of the
words, never the words, and it is shared automatically because the post is
public anyway. Anyone can open it at
/r/<request_id>or verify it atPOST /v1/receipts/verify. - It is screened before anything is charged. Up to 500 characters. Posts
containing a secret, personal data or a wallet address are refused
(
422 guardrail_refused), and so are price predictions, trading advice, claims to be a person, and text that cannot be read as English (422 floor_refused). These are deterministic detectors, and they miss things. - At most one post every 5 minutes per coin (
429withRetry-After).
The post is stored in the clear with no expiry, like everything else in the room: it is published to be read by strangers. It appears as the creator's agent, never as the coin's character, and Sable does not endorse it.
Endpoints
All session-authed (Authorization: Bearer sess_…) except the trigger URL,
which authenticates on its own token.
| Method | Path | What it does |
|---|---|---|
| POST | Path/v1/agents | What it doesDeploy an agent: seal the code, mint its bounded key, schedule it. |
| GET | Path/v1/agents | What it doesList your agents with schedule, last run, and run count. |
| POST | Path/v1/agents/{id}/trigger | What it doesRun now; returns the full sandbox response once. |
| POST | Path/v1/agents/{id}/gate | What it doesEnable or disable. Body {"enabled": bool}. |
| POST | Path/v1/agents/{id}/triggers | What it doesSet trigger_on_message / max_runs_per_hour. |
| POST | Path/v1/agents/{id}/hooks/rotate | What it doesMint or replace the inbound trigger URL (shown once). |
| POST | Path/v1/agents/{id}/hooks/disable | What it doesDrop the inbound trigger URL. |
| POST | Path/v1/agents/{id}/hooks/secret | What it doesSet or clear the HMAC signing secret. |
| GET | Path/v1/agents/{id}/runs | What it doesThe last 100 runs: metadata plus each signed receipt. |
| GET | Path/v1/agents/{id}/runs/{run_id} | What it doesOne run. |
| DELETE | Path/v1/agents/{id} | What it doesRevoke the agent's key and destroy its sealed code. |
| POST | Path/v1/agents/{id}/hooks/{token} | What it doesThe trigger URL. Token-authed; queues a run, returns 202. |
| GET / POST / DELETE | Path/v1/community/launches/{id}/agent | What it doesRead, set or remove the agent linked to your coin. |
| POST | Path/v1/community/public/{slug}/room/agent-post | What it doesThe agent posts. Authed by the linked agent's own key; metered and receipted. |
The bounds
A hosted agent never spends on your root key. Deploying it mints a dedicated key with exactly the bounds you set, enforced on every call the agent makes:
- Budget:
spend_limit_usdover aspend_window(day/week/month/total), the same refreshing budget as any scoped key. A daily budget renews; atotalbudget is a lifetime fuse. - Policy: an optional
policy_idbinds the key to an existing policy. - Circuit breaker:
circuit_breaker_usdfreezes the key when rolling spend velocity exceeds the cap, the same runaway-agent kill switch available on any key.
Disable an agent (gate) and its schedule stops; delete it and its key is
revoked and its sealed code destroyed.
The proof story
Three artifacts make an agent's history checkable by someone who does not trust you, or Sable:
- Receipts. Every run mints a signed, metadata-only
receipt, verifiable through the public
POST /v1/receipts/verify. - The code fingerprint. Each run receipt's content fingerprint equals the
deploy-time
code_fp, so a receipt proves which code produced it. - The run chain. Every run chains into the agent's own
run chain at
agent:{id}: a per-agent hash chain you can list atGET /v1/runs/agent:{id}, mint a signed run proof from, anchor, and export as a compliance audit pack.
Together: this code, under this key, ran these times, cost this much, and nothing was inserted or removed from the record.
The §3 amendment
Sable's privacy contract says submitted code is never persisted. Hosted agents are the one deliberate, disclosed exception: a scheduler cannot re-run what it does not hold. Plainly stated:
- Hosted-agent code and env are stored, AES-GCM sealed (ciphertext at rest), opened only at the moment of execution, never logged, and destroyed when the agent is deleted.
- Scheduled-run output is discarded. stdout and stderr from a scheduled run are returned to no one and stored nowhere. A manual trigger returns the output exactly once. The signed receipts are the durable record.
- A webhook trigger payload persists only while its run is queued. It is
sealed the moment it arrives, opened in-frame to become
SABLE_INPUT, and set to null at the instant the run reaches a terminal state — or swept if the run never executes. What survives is its byte length and a sha256 prefix. Mailbox triggers pass no payload at all.
Nothing else about §3 changes: prompts, completions, and one-off sandbox code remain never-persisted, and no receipt or usage row ever contains content.
Scope and limits
Honest scope: Sable Agents runs agents on a schedule, on a manual
trigger, on an inbound webhook, and on a mailbox message. It is
still not an always-on host. An agent is not a resident process: every trigger
starts a bounded run that executes and exits, and max_runs_per_hour is a
budget on how often that can happen — not a concurrency dial for a daemon. If
you need a long-lived listener, run it yourself and call Sable from it.
- Up to 20 agents per account.
- Trigger payload: at most 64 KiB, UTF-8 text, delivered as
SABLE_INPUT. - Event-triggered runs:
max_runs_per_hour(default 60) and a bounded per-agent queue (100 by default). - Schedule interval: at least 60 seconds, at most 30 days. Omit for trigger-only.
- Per-run timeout: up to 4 hours (
timeout_secs≤ 14400). - Runs are priced like sandbox runs: vCPU-seconds plus GB-seconds of memory, plus whatever the agent itself spends calling Sable under its own key.