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 three 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.
3 · Claimed identity and execution binding
Pay once through x402 to anchor an agent_id to the settlement
wallet and receive an agent_secret exactly once. Claimed agents can
protect identity mutations, enable strict guard authentication, recover access
with an EIP-191 wallet signature, and bind an allowed decision to one short-lived,
one-shot execution.
Quickstart
Sixty seconds, copy-paste. Everything is plain HTTP and JSON. Unclaimed agents
can use the free routes without authentication; claimed identity mutations and
strict guard checks require X-Agent-Secret.
# 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 \
-H "Content-Type: application/json" \
-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 \
-H "Content-Type: application/json" \
-d '{"subject":"my-agent","scopes":["read","write","pay"]}' | jq -r .token)
curl -s -X POST $BASE/v1/tokens/delegate \
-H "Content-Type: application/json" \
-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 \
-H "Content-Type: application/json" \
-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. |
| Hosted state | PERSISTENCE=on: revocations, daily spend, Telegram bindings, pending approvals, claimed identities, verdict rows and audit events persist in MySQL. |
| Restart guarantee | Pending verdicts hydrate with their remaining expiry. Runtime consumption is single-instance authoritative; this is not multi-replica coordination. |
| Deployment modes | off keeps legacy in-memory behavior; shadow dual-writes while memory remains authoritative; on hydrates durable state at boot. |
| 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) |
wait | boolean | no | for a review, hold the request until a human decides or the timeout expires |
bind | boolean | no | return a short-lived, one-shot verdict_id when the decision is allowed |
X-Agent-Secret. A claimed agent with strict mode enabled also requires
that header on free and paid guard checks. Unclaimed agents retain the legacy open flow.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.
Human approval response
{
"verdict": "review",
"approval_id": "a1b2c3d4e5f60708",
"approval_status": "pending",
"poll": "/v1/approvals/a1b2c3d4e5f60708"
}
Poll the returned path, or send "wait":true. Approval is final
for that request; a human-approved request is charged when it resolves, and later
automated decisions see the updated daily ledger.
Execution binding
A policy decision should authorize one concrete execution, not become a reusable
permission slip. Add "bind":true to a guard request. An immediate
allow, or a later human approval, includes:
{
"verdict": "allow",
"verdict_id": "<one-shot id>",
"expires_in_seconds": 300
}
curl -s -X POST "$BASE/v1/verdicts/$VERDICT_ID/consume" \
-H "Content-Type: application/json" \
-d '{}'
| Case | Behavior |
|---|---|
| First valid consume | HTTP 200; the execution authorization is spent. |
| Second consume | HTTP 409 already_consumed with the first consumption time. |
| Unknown or expired id | HTTP 404. |
| Expires before use | The same-day spend charge is refunded automatically. |
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. |
Pay-to-Claim agent identity
Unclaimed ids keep the legacy open behavior. To protect an identity, claim it through either x402 rail. There is no free claim path.
{"agent_id": "my-agent"}
After the normal 402 handshake and successful settlement, the first claim returns HTTP 201. The settlement wallet becomes the durable owner.
{
"agent_id": "my-agent",
"agent_secret": "<shown exactly once>",
"claimed_at": "2026-07-19T00:00:00.000Z",
"claimed_by": "0x...payer wallet"
}
X-Agent-Secret header. Repeating a claim returns
409 already_claimed and is not settled.Rotate the secret
curl -s -X POST "$BASE/v1/agents/rotate" \
-H "Content-Type: application/json" \
-H "X-Agent-Secret: $AGENT_SECRET" \
-d '{"agent_id":"my-agent"}'
Rotation atomically invalidates the previous secret and returns its replacement once.
Enable or disable strict guard authentication
curl -s -X POST "$BASE/v1/agents/strict" \
-H "Content-Type: application/json" \
-H "X-Agent-Secret: $AGENT_SECRET" \
-d '{"agent_id":"my-agent","strict":true}'
Strict agents must send X-Agent-Secret on guard checks across free
and paid rails. Claim, rotation and strict-mode changes fail closed with
503 feature_disabled unless durable persistence is connected and hydrated.
Wallet-signature secret recovery
1 · Issue a challenge
{"agent_id": "my-agent"}
The response contains a five-minute nonce, canonical
message and expires_at.
2 · Sign and submit
Sign the exact message with the claiming wallet using EIP-191
personal_sign, then send:
{
"agent_id": "my-agent",
"nonce": "...",
"signature": "0x..."
}
Success returns a new one-time agent_secret and immediately
invalidates the old one. Nonces are domain-bound, stored only as hashes, consumed
once in the same transaction, and swept after expiry.
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.
v0.9 operational reads
These views expose only the minimum data required by each role. They require a
connected, hydrated PERSISTENCE=on deployment and return
503 feature_disabled instead of falling back to memory.
Public aggregate metrics
A fixed aggregate-only schema: claimed and strict agent counts, approval and verdict counts, active spend agents, and ledger-change totals. It never includes agent ids, wallets, destinations, chats, individual amounts, event payloads or revenue.
Agent-owner spend
curl -s "$BASE/v1/agents/my-agent/spend" \
-H "X-Agent-Secret: $AGENT_SECRET"
Returns the current daily total and the latest 50 v0.9-forward ledger changes. History is observational and PII-minimized: delta, balance-after, reason, reference id and time only. It is never used to authorize spend.
Operator approval feed
curl -s "$BASE/v1/approvals?status=pending&limit=25" \
-H "Authorization: Bearer $OPERATOR_BEARER_KEY"
status is optional: pending, approved,
denied or expired. limit is 1–100 and defaults
to 25. The operator key grants this read-only god-view; it grants no mutation and
does not bypass agent-secret checks.
Error codes
Failures use stable error codes. Token-specific and v0.9
identity/execution errors include:
| 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. |
invalid_field | A field has the wrong type or value. |
forbidden | The claimed agent secret is missing or invalid. |
unauthorized | The operator bearer credential is missing or invalid. |
not_found | The agent, verdict or other requested resource does not exist or has expired. |
already_consumed | A one-shot verdict id has already authorized an execution. |
feature_disabled | The route requires connected, hydrated durable storage or deployment configuration that is unavailable. |
Paid endpoints (x402)
The free routes remain open for unclaimed agents. Paid mirrors provide guard
verdicts and token verification, plus the paid-only identity claim. They are gated
by the x402 payment standard:
call without payment to receive an HTTP 402 challenge, sign it, then
retry with the payment header.
| Endpoint prefix | Rail | Price | Listed on |
|---|---|---|---|
/paid/v1/* | USDC on Base (eip155:8453), Coinbase CDP facilitator | $0.01 / call | x402 Bazaar / Agentic.Market |
/paid-okx/v1/* | USDT0 on X Layer (eip155:196), OKX OnchainOS facilitator | 0.01 USDT / call | OKX.AI |
Both rails expose guard/check, tokens/verify and
agents/claim (POST to use, GET to see the
402 challenge). The 402
response declares the correct network, asset and amount for its rail — clients
should always read the challenge rather than hardcoding payment details.
# see a challenge without paying anything
curl -i https://api.clawinabox.xyz/paid/v1/guard/check
# → HTTP 402 + PAYMENT-REQUIRED header (base64 x402 envelope)
# paid-only identity claim; use /paid-okx for the X Layer rail
curl -X POST https://api.clawinabox.xyz/paid/v1/agents/claim \
-H "Content-Type: application/json" \
-d '{"agent_id":"my-agent"}'
# → 402, then retry with PAYMENT-SIGNATURE
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
Open the official Claw-in-a-Box bot @ClawInABoxBot, verify the exact username to avoid scammers, and press Start. This can't be skipped — Telegram won't let a bot message someone who has never opened the chat.
Request a one-time bind code
curl -X POST https://api.clawinabox.xyz/v1/operators/register \
-H "Content-Type: application/json" \
-H "X-Agent-Secret: $AGENT_SECRET" \
-d '{"agent_id":"YOUR_AGENT_ID"}'
The secret header is required when the id is claimed. Omit it only for an unclaimed id using the legacy open flow.
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" \
-H "X-Agent-Secret: $AGENT_SECRET" \
-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.X-Agent-Secret.Self-hosting
Node.js >= 18. Run npm install. MySQL is required for claimed
identity, recovery, durable operational reads and the full
PERSISTENCE=on behavior.
cd service
npm install
GUARD_SECRET=$(openssl rand -hex 32) \
PERSISTENCE=on \
DB_HOST=127.0.0.1 DB_USER=claw DB_PASSWORD=<password> DB_NAME=claw \
PORT=8787 node server.js
node test-all.js
| Persistence mode | Behavior |
|---|---|
off | Legacy in-memory behavior; durable identity features are disabled. |
shadow | Memory remains authoritative while supported mutations are dual-written asynchronously. |
on | Requires a connected, hydrated database; durable state is loaded at boot and identity features are enabled. |
503 feature_disabled when durable storage is unavailable.Full source is on GitHub under Apache-2.0. The agent-facing API doc is also served live at /skill.md.