Overview
You are an agent. Increasingly, agents hire other agents, call paid APIs, and move money. The common shortcut — hand a sub-agent your full credentials and hope — fails in three familiar ways: scope creep (the helper ends up with more power than the task needs), zombie access (nobody remembers what was handed out, so nothing gets cleanly withdrawn), and confused deputies (a token leaks and whoever holds it is believed).
Claw-in-a-Box makes the safe pattern the easy pattern, across two capabilities:
1 · Delegatable capability tokens
Mint a root capability, then delegate attenuated children offline (macaroon-style HMAC chaining). A child can never hold a scope its parent lacks, never outlive its parent, and is bound to a single audience. Revoke any token and every descendant dies with it — by construction, not bookkeeping.
2 · Spend-policy verdicts
Before acting, ask POST /v1/guard/check and get back
allow review
deny with the exact rules that fired. Four policy
primitives: per-transaction and daily spend limits, destination allowlists,
human-approval thresholds, and time windows.
Quickstart
Sixty seconds, copy-paste. Everything is a plain JSON POST — no SDK, no auth for the demo deployment.
# 0) set the base URL and confirm the service is up
BASE=https://api.clawinabox.xyz
curl -s $BASE/healthz
# 1) check a spend BEFORE doing it
curl -s -X POST $BASE/v1/guard/check \
-d '{"agent_id":"my-agent","action":"transfer","amount":150,"destination":"0xabc"}'
# -> {"verdict":"review","triggered_rules":["require_approval"],...}
# 2) mint a root capability, then delegate a NARROWER child
ROOT=$(curl -s -X POST $BASE/v1/tokens \
-d '{"subject":"my-agent","scopes":["read","write","pay"]}' | jq -r .token)
curl -s -X POST $BASE/v1/tokens/delegate \
-d "{\"parent_token\":\"$ROOT\",\"audience\":\"worker-1\",\"scopes\":[\"read\"],\"ttl_seconds\":600}"
# 3) revoke the root — every child dies with it, transitively
curl -s -X POST $BASE/v1/tokens/revoke -d "{\"token\":\"$ROOT\"}"
30 → allow, 150
→ review, 999 →
deny. Or split one big spend into several small
ones and watch the daily total catch it.How it works
The design thesis is that bounded authorization — a grant that can only shrink as it moves, and dies with its ancestors — is one abstraction that should be enforced wherever an agent acts.
Attenuation is enforced, not requested
Tokens are HMAC-chained: each child's signature is keyed by its parent's. That makes three properties hold on every verify, on the server, with no way for a client to forge or broaden them without the secret:
| Property | What it means |
|---|---|
| Subset scopes | A child's scopes must be a subset of its parent's. Requesting more returns 403 scope_escalation. |
| Clamped lifetime | A child's expiry is clamped to its parent's — a child never outlives its parent. |
| Bound audience | Each token is minted for a single audience and should be presented only by that party. |
Cascading revocation
Revoke any token and every token delegated under it fails verification
from that moment, transitively, with revoked_ancestor. Revoke the root
to kill the entire tree. This is cryptographic, not a lookup table you have to keep
in sync.
One idea, three enforcement surfaces
The same four policy primitives compile to different surfaces; what changes is who enforces them.
| Surface | Instance | Guarantee grade |
|---|---|---|
| HTTP service | this token + guard API | gateway-enforced |
| Agent-protocol sim | NANDA Town auth: delegatable plugin | gateway-enforced, adversarially validated |
| On-chain smart accounts | session-key constraint compiler (roadmap) | protocol-enforced |
Guarantees & limits
Read this before pointing anything important at the hosted demo.
Concretely:
| What | Status |
|---|---|
| Token forgery / broadening | Prevented by construction — impossible without the server secret. |
| Attenuation & cascading revocation | Enforced on every verify, server-side. |
| Demo state (revocations, daily totals) | In-memory — resets on restart; daily totals reset at UTC midnight. |
| Production funds | Do not use the hosted demo for real money at this stage. |
API reference — Spend guard
Send the action you intend to take; get back a verdict and the rules that fired.
Request fields
| Field | Type | Req | Notes |
|---|---|---|---|
agent_id | string | no | identity for daily accumulation (default anonymous) |
action | string | no | free-form label, e.g. transfer, api_call |
amount | number | yes for spends | in whatever unit your policy uses |
destination | string | no | checked against allowlist rules if present |
policy | string / object | no | preset name or inline policy object (default standard) |
Response
{
"verdict": "allow | review | deny",
"triggered_rules": ["spend_limit.per_tx"],
"reasons": ["amount 999 exceeds per-tx limit 200"],
"policy_used": "standard",
"spent_today_after": 150,
"evaluated_at": "2026-07-09T12:00:00.000Z"
}
Interpreting the verdict
| Verdict | What your agent should do |
|---|---|
| allow | Proceed. The amount is recorded against the agent's daily total. |
| review | Do not proceed autonomously. Surface the action to your human operator and only continue on explicit confirmation. Treat it as a hard stop, not a soft warning. |
| deny | Do not proceed and do not retry with the same parameters. reasons tells you which limit to respect. |
deny always wins over review.
Policies
Presets
| Preset | Per-tx | Daily | Review above |
|---|---|---|---|
conservative | 50 | 200 | 20 |
standard (default) | 200 | 1000 | 100 |
permissive | 1000 | 5000 | 500 |
Inline policy schema — four primitives
{
"name": "my-policy",
"rules": [
{"type": "spend_limit", "per_tx": 200, "daily": 1000},
{"type": "allowlist", "field": "destination", "values": ["0xgood"]},
{"type": "require_approval", "when_amount_over": 100},
{"type": "time_window", "allow_utc_hours": [[9, 18]]}
]
}
| Rule | Semantics |
|---|---|
spend_limit | Denies when a single amount exceeds per_tx, or when the agent's accumulated daily total would exceed daily. |
allowlist | Denies any destination not listed. Omit or set "mode":"off" to disable. |
require_approval | Downgrades the verdict to review above the threshold. |
time_window | Denies outside the given UTC hour ranges. |
Issue a root capability
{"subject": "my-agent", "scopes": ["read", "write", "pay"], "ttl_seconds": 3600}
Returns {"token": "<base64url>"}. ttl_seconds defaults to 3600.
Delegate an attenuated child
{"parent_token": "<token>", "audience": "worker-1", "scopes": ["read"], "ttl_seconds": 600}
Returns {"token": "<child token>"}. The service enforces:
| Rule | Behavior |
|---|---|
| Subset scopes | Child scopes must be a subset of the parent's, else 403 scope_escalation. |
| Clamped expiry | Child expiry is clamped to the parent's — a child never outlives its parent. |
| Dead parents can't delegate | A revoked or expired parent cannot delegate. |
Any token holder can delegate. Chains can be arbitrarily deep; every hop attenuates.
Verify a presented token
{"token": "<token>", "presenter": "worker-1"}
presenter is optional but recommended: when present, the service also
checks the token is being presented by its bound audience. Success:
{
"valid": true,
"context": {
"subject": "worker-1", "scopes": ["read"],
"expires_at": 1760000000,
"chain_tids": ["a1b2...", "c3d4..."], "depth": 2
}
}
Failures return HTTP 400/403 with
{"valid": false, "verdict": "deny", "error": "<code>", "detail": "<human readable>"}.
Revoke with cascade
{"token": "<token>"}
Returns {"revoked_tid": "...", "cascades": true}. Every token delegated
under the revoked one fails verification from this moment, transitively, with
revoked_ancestor. Revoke the root to kill the entire tree.
Error codes
Verify and delegate failures use a stable error code:
| Code | Meaning |
|---|---|
invalid_token | Malformed or unparseable token. |
invalid_signature | HMAC does not match — forged or tampered. |
scope_escalation | A child requested a scope its parent lacks. |
expired_ancestor | The token or one of its ancestors has expired. |
revoked_ancestor | The token or an ancestor was revoked. |
audience_mismatch | presenter is not the token's bound audience. |
missing_field | A required request field was absent. |
Route review approvals to your own Telegram
By default, when your agent trips a review rule, the approval request goes to the service operator. You can instead bind your own Telegram so every review for your agent lands on your phone with Approve / Deny buttons.
Open a chat with the bot
In Telegram, search for the Claw-in-a-Box bot and press Start. This can't be skipped — Telegram won't let a bot message someone who has never messaged it first.
Request a one-time bind code
curl -X POST https://api.clawinabox.xyz/v1/operators/register \
-H "Content-Type: application/json" \
-d '{"agent_id":"YOUR_AGENT_ID"}'
Response includes a bind_code valid for 15 minutes:
{
"agent_id": "YOUR_AGENT_ID",
"bind_code": "A1B2C3D4",
"expires_in_seconds": 900,
"instructions": "Open Telegram, message the bot, and send: /bind A1B2C3D4"
}
Send the code to the bot
In the bot chat, send /bind A1B2C3D4. The bot replies:
✅ Bound. Review requests for agent YOUR_AGENT_ID will now come to this chat.
Verify, then test
Confirm the routing switched to you:
curl -s https://api.clawinabox.xyz/v1/operators/YOUR_AGENT_ID
# -> {"agent_id":"YOUR_AGENT_ID","routing":"caller"}
"routing":"caller" means reviews now go to your Telegram;
"routing":"operator" means it's still falling back to the operator.
Fire a test request that trips review:
curl -X POST https://api.clawinabox.xyz/v1/guard/check \
-H "Content-Type: application/json" \
-d '{"agent_id":"YOUR_AGENT_ID","amount":150}'
An approval message with Approve / Deny buttons appears on your phone. Tapping a button takes effect immediately.
register + /bind
again and the newest binding overwrites the previous one. Each agent_id routes
to a single chat at a time; agents that aren't bound continue to route to the service operator.acme-payments-7f3a9c, not test), one id per
agent, and don't reuse ids across environments — whoever knows the id shares its daily
budget and can request a bind code for it.Self-hosting
Zero dependencies, Node.js >= 18. No npm install. State (revocations,
daily totals) is in-memory by design at this stage.
cd service
GUARD_SECRET=$(openssl rand -hex 32) PORT=8787 node server.js
node test.js # 12-case smoke suite, exits 0 on success
Full source is on GitHub under Apache-2.0. The agent-facing API doc is also served live at /skill.md.