Delegated sub-keys
POST /v1/keys/delegate mints a narrower child of the calling API key. It's
authenticated by the parent key itself, not a wallet session. The point is
that an agent can hand a sub-agent a bounded credential mid-run, without a human
in the loop.
curl https://api.buildsable.com/v1/keys/delegate \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-d '{
"name": "research-subagent",
"spend_limit_usd": 0.50,
"allowed_models": ["sable-llama-3.1-8b", "sandbox"],
"expires_in_minutes": 15,
"rate_limit_per_min": 10
}'The plaintext key is returned exactly once.
The guarantee
A child is ≤ its parent on every axis, always. There is no request shape that widens a scope; the gateway clamps rather than validates, so an over-broad request produces a correctly-narrow key instead of an error.
| Field | Rule |
|---|---|
spend_limit_usd | The tighter of the two |
allowed_models | Intersection with the parent's |
expires_in_minutes | The earlier expiry of the two |
rate_limit_per_min | The tighter of the two |
| privacy tier | Inherited; not settable by the child |
Two behaviours are worth being explicit about, because the intuitive reading is the opposite:
- Omitting a field inherits the parent's limit; it does not mean
"unlimited". A child that leaves
spend_limit_usdunset under a $5 parent gets $5, not infinity. - A disjoint allowlist is refused, not granted. Asking for only models the
parent can't use returns
400rather than issuing an unrestricted key.
The tier is inherited rather than accepted from the request because tier selects routing, and on the confidential tier, which hardware attests the call. A child picking its own would let it change where its traffic executes.
Spend rolls up
A parent's monthly cap counts its own usage plus every descendant's. Without that, delegation would be a trivial way around a budget: mint a child, spend through it, never touch your own counter.
So a $5 parent that delegates a $0.50 child still stops at $5 total across the whole tree.
And it isn't just the immediate parent: every ancestor's cap is enforced on each request. A child can never help exceed any budget above it, including by sibling fan-out, where ten $0.50 children under a $2 parent would otherwise add up to $5 of spend against a $2 budget. The tree stops at the tightest ancestor, whichever level it sits at.
Live children are bounded
A parent can hold a limited number of live (unexpired, unrevoked) children at once, default 32. Delegating past the cap is refused; let short-lived children expire, or revoke ones you no longer need. This keeps a runaway loop from minting keys faster than anyone could audit them.
Revocation cascades
Revoking a key revokes every key beneath it. A revoked parent whose children kept working would mean revocation didn't revoke anything an attacker bothered to delegate first.
Revoking a child leaves the parent untouched.
Depth is bounded
A chain is limited to 3 keys including the root: agent → sub-agent → tool.
A fourth level returns 400. This keeps a compromised key from minting an
unbounded chain that's tedious to audit and slow to revoke.
Suggested pattern
Give each task its own short-lived key:
- The orchestrator holds one long-lived key with the real budget.
- Per task, delegate a child scoped to just the models that task needs, capped at what the task should cost, expiring in minutes.
- Don't bother revoking; let it expire.
Worst case for a leaked or runaway sub-key is then bounded by construction rather than by noticing.