← Back to Plugins
Tools

Belay

fathomforge By fathomforge 👁 55 views ▲ 0 votes

Spend caps, rate limits and a graceful pause ladder for OpenClaw agents. In-process, zero runtime dependencies, no telemetry.

GitHub

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

Loading comments...