Tools
Belay
Spend caps, rate limits and a graceful pause ladder for OpenClaw agents. In-process, zero runtime dependencies, no telemetry.
Install
openclaw plugins install npm:@fathomforge/belay
Configuration Example
{
"plugins": {
"entries": {
"belay": {
"enabled": true,
"config": {
"mode": "observe",
"timeZone": "America/Los_Angeles",
"limits": { "spendPerDayUsd": 5, "modelCallsPerMinute": 30 },
"recorder": { "file": "/home/node/.openclaw/belay-trail.jsonl" },
"stateFile": "/home/node/.openclaw/belay-state.json"
}
}
}
}
}
README
# Belay
**A seatbelt for OpenClaw agents.** Spend caps, model-call and tool-call rate limits, and a
graceful pause ladder — running inside your gateway, with no server, no account, no telemetry, and
no network calls except the alerts you configure yourself.
*Not affiliated with the OpenClaw Foundation.*
```
warn → block a tool call → end the run → pause the account
```
Your agent doesn't get killed. It gets caught.
---
## Why this exists
OpenClaw has no spend cap, no model-call rate limit, no cross-session budget, and no graceful
pause. The default agent runtime timeout is **48 hours**. Built-in loop detection is off by
default, tool-only, and single-run. Both upstream requests for spending controls were closed as
"not planned" — including one from an operator who lost **$169 in a single session** when a
failover re-sent a 517K-token context down a chain of pricier models.
Meanwhile the failure modes are mundane and recurring: an agent invents a task and loops on it, a
hallucinated URL 404s and gets retried 700 times, an hourly heartbeat quietly reprocesses a 174K
context all month.
Belay is the layer that notices and steps in — gradually.
## What it does
| Guard | What it catches |
|---|---|
| **Request-size limits** | Bytes sent to models per run, per minute, per day — **measured exactly**, on any provider |
| **Spend caps** | Per run, per hour, per calendar day, per agent (needs a provider that reports token usage) |
| **Model-call rate** | The storm: dozens of calls a minute from one stuck turn |
| **Tool-call rate** | Runaway tool use |
| **Identical-call limit** | The same call repeated across a run — the retry-loop signature |
| **Tool-error rate** | Error storms, which usually precede cost storms |
| **The pause ladder** | Escalates only as far as it needs to, and steps back down when things go quiet |
| **Flight recorder** | A local JSONL line per decision, readable with `belay incidents` |
## Install
```bash
openclaw plugins install npm:@fathomforge/belay
```
Installing from a local path or an unreviewed source additionally needs
`--force --accept-capabilities`; OpenClaw asks for explicit consent before loading it.
Then add to `openclaw.json`:
```json
{
"plugins": {
"entries": {
"belay": {
"enabled": true,
"config": {
"mode": "observe",
"timeZone": "America/Los_Angeles",
"limits": { "spendPerDayUsd": 5, "modelCallsPerMinute": 30 },
"recorder": { "file": "/home/node/.openclaw/belay-trail.jsonl" },
"stateFile": "/home/node/.openclaw/belay-state.json"
}
}
}
}
}
```
**Grant conversation-hook access** — Belay cannot meter anything without it:
```bash
openclaw config patch --stdin <<'JSON'
{ plugins: { entries: { belay: { hooks: { allowConversationAccess: true } } } } }
JSON
```
Non-bundled plugins are blocked from typed conversation hooks unless you opt in. Without this the
plugin loads, reports itself active, and silently does nothing — you'll see
`typed hook "llm_output" blocked because ...` in the gateway log.
Restart the gateway, then **verify — don't assume**:
```bash
openclaw config get plugins.entries.belay
openclaw logs --limit 50 | grep belay # expect a "belay active: ..." line
```
> [!WARNING]
> **Setting `"enabled": true` for a plugin that isn't actually installed will block your gateway
> from becoming ready.** Install first, then enable. If your gateway won't start after editing
> config, this is the first thing to check.
### Start in observe mode
`"mode": "observe"` meters, records and alerts, but **never blocks, ends or pauses anything**.
Run it that way for a week. Check `belay incidents` to see what it *would* have done. When the
reports look right, switch to `"mode": "enforce"`.
This matters most if your gateway serves real users. A guardrail you don't trust yet should not be
able to interrupt anyone.
**Per-agent modes** let you enforce where it's safe and observe where it isn't:
```json
{
"mode": "enforce",
"agents": {
"my-private-bot": { "spendPerDayUsd": 2 },
"my-group-bot": { "mode": "observe" }
}
}
```
A global `"mode": "observe"` always wins over a per-agent `enforce`, so a gateway-wide safety
setting can't be defeated by a stale override.
## See what it's doing
```bash
belay status
belay incidents --hours 24
belay incidents --agent my-group-bot --json
```
```
Belay status
Spend recorded today (per agent):
main $0.8421 (2026-09-02)
group-bot $2.1 (2026-09-02) <- PAUSED
Decisions in the last 24h: 3
logged: 1
blocked: 1
paused: 1
```
Both commands read local files directly, so they work even when the gateway is down — which is
when you most want them.
## Configuration
Every limit is optional. **An unset limit is not enforced** — it is never treated as zero.
```json
{
"mode": "enforce",
"timeZone": "America/Los_Angeles",
"limits": {
"spendPerRunUsd": 0.5,
"spendPerHourUsd": 2,
"spendPerDayUsd": 5,
"modelCallsPerMinute": 30,
"toolCallsPerMinute": 60,
"identicalToolCalls": 20,
"toolErrorsPerMinute": 30
},
"agents": {
"my-group-bot": { "spendPerDayUsd": 1 }
},
"ladder": {
"cooldownMs": 60000,
"decayMs": 900000,
"renotifyMs": 21600000,
"maxRung": "endRun"
},
"alerts": {
"minRung": "warn",
"telegram": { "botTokenEnv": "BELAY_TELEGRAM_TOKEN", "chatId": "<your chat id>" }
},
"pause": { "enabled": false },
"recorder": { "file": "/home/node/.openclaw/belay-trail.jsonl" },
"stateFile": "/home/node/.openclaw/belay-state.json"
}
```
**Defaults are deliberately timid.** No spend cap is enabled out of the box — Belay can't know what
*you* consider expensive, and a tool that blocks work on install gets uninstalled. Rate limits ship
on but set well above any sane workload. Account pausing is off unless you turn it on.
### Pausing an account
The ladder's top rung stops a channel account outright. **On OpenClaw 2026.8.2 this does not work,
and Belay ships with the ladder topping out at `endRun` because of it.**
`channels.stop` is reached through the gateway's method dispatch, which is reserved for plugin HTTP
routes — a hook handler is not an authenticated request scope, so the call is refused:
```
Gateway method dispatch is reserved for plugin HTTP routes that declare
contracts.gatewayMethodDispatch: ["authenticated-request"]
```
Declaring that contract is not sufficient; the call site is the problem. Verified on a live gateway.
This costs less than it sounds. **Ending the run already blocks the offending run and every
subsequent one while the breach persists** — the agent cannot spend. Pausing additionally stops the
channel accepting new inbound messages, which is a smaller increment than it appears.
If you set `ladder.maxRung: "pause"` and `pause.enabled: true` anyway, Belay will attempt it, and
**will tell you plainly when it fails**, including the exact command to stop the account by hand:
```
PAUSE FAILED (...) -- the account is still running.
Stop it by hand: openclaw gateway call channels.stop --params '{"channel":"telegram","accountId":"default"}'
```
An alert that claimed a pause which never happened would be worse than no alert at all.
**Use `botTokenEnv`, not `botToken`.** An inline token ends up in config backups, in
`openclaw config get` output, and in any screenshot you post while asking for help. Belay accepts
an inline token but will warn you about it every startup.
## What Belay can and cannot see
This is a guardrail plugin running inside your gateway, so the honest answer matters.
**It cannot access:**
- Your prompts or your agents' replies
- Tool parameters — they are SHA-256 hashed before counting, and the hash is what's stored
- Your API keys or credentials
- Any network destination except the alert endpoints you configure
**It can see:** token counts, model and provider names, tool *names*, timings, and your Belay
config. The flight recorder stores decisions — scope, rung, rule, the number that broke it — and
has no field for content. It rebuilds every record field by field, so a bug elsewhere can't smuggle
text onto disk.
**If Belay itself has a bug, it gets out of the way.** Every hook handler is wrapped so an
exception is logged and treated as "pass". A defect degrades to *no guardrail*, never to a downed
gateway or a blocked message. Your gateway's availability never depends on this code being
bug-free — only your cost protection does.
## Costs and pricing
Belay computes cost from token counts using a small bundled price table, because OpenClaw only
records dollar figures when you've configured `models.providers.*.models[].cost` — and most people
haven't. A meter that trusted those numbers would report $0.00 and enforce nothing.
Three situations produce an honest "I don't know" rather than a wrong number, and Belay warns about
each of them:
- **Your provider reports no token usage at all.** Verified for `google/gemini-3.8-flash` on
OpenClaw 2026.8.2 and reported upstream as
[#141581](https://github.com/openclaw/openclaw/issues/141581). Check yours with
`openclaw status --usage`.
- **The model isn't in the price table** and you haven't set a `prices` override.
- **The provider reported only a token total**, with no input/output split. Those are priced up to
5× apart, so no honest dollar figure exists.
### Provider support
What a provider reports decides which controls work. Measured on OpenClaw 2026.8.2:
| provider | token usage | request size | use |
|---|---|---|---|
| **OpenAI** (`gpt-5.4-nano`) | **✓ reported** | ✗ absent | **Exact dollar caps.** Set prices and go |
| **Google** (`gemini-3.8-flash`) | ✗ absent | **✓ reported** | **Exact request-size limits** |
| Anthropic | untested | untested | OpenClaw 2026.8.2 could not complete a request to any Anthropic model ([#141582](https://github.com/openclaw/openclaw/issues/141582)) |
**Neither
... (truncated)
tools
Comments
Sign in to leave a comment