> ## Documentation Index
> Fetch the complete documentation index at: https://jephalabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Outflow Velocity Cap

> A per-token rolling-window ceiling that brakes runaway outflow — the aggregate safety net under every per-movement guard

# Outflow Velocity Cap

Every other fund-outflow guard in QTG answers **"is this one movement correct?"** — the address allowlist (is the destination registered), the signed-recipient invariant (does the signed transaction pay the approved address), the memo gate, the SSRF pin. None of them answer **"is the total volume leaving this instance normal?"**

A movement can be individually legitimate on every axis and still be one drop in an abnormal flood:

* **Runaway strategy code** — a controller or strategy bug emits well-formed withdrawals at high frequency to *allowlisted* destinations. Every request passes every per-movement guard. Nothing stops the bleed until the funds are gone. (This is QTG's stated #1 product threat: *protect the team from its own runaway strategy code*.)
* **Stolen API / admin credential** — an attacker drains funds one valid request at a time.
* **Operator fat-finger** — a single movement with one extra zero.

The outflow velocity cap is the **first-line aggregate brake**: a per-token, rolling-window ceiling on total outflow, enforced in-process at the approval/reservation chokepoint. When a token's window fills, every further over-cap approval for that token is auto-rejected for the rest of the window — no operator action, no global flag.

<Note>
  This is a **Free** core-safety feature. It rides the existing `BalanceReservation` ledger — no new table, no oracle, no external dependency. Enabling it never breaks an existing flow: a token with no configured ceiling is uncapped.
</Note>

## It is a speed bump, not a wall

QTG is operator-owned infrastructure, not a custodian. When an altcoin is hacked or depegs, you **must** be able to emergency-withdraw everything *right now*, exceeding the cap. The velocity cap is therefore a speed bump with an **operator override** (see [Operator override](#operator-override)), not a hard wall. The override gating is the load-bearing design decision — it requires a capability the runaway-code / stolen-strategy-key adversary does **not** have.

## What it protects (and what it does not)

The cap defends adversaries that sit **on the API surface**. It does **not** defend a direct DB-write attacker — the running counter lives in the DB, so anyone who can write the DB can route around it. That is the already-accepted ["DB-write = game over"](/concepts/security-model) trust boundary; the cap's job is to bound the realistic API-surface adversary for a self-hosted operator.

| Adversary                     | Has                             | Cap defends?                             |
| ----------------------------- | ------------------------------- | ---------------------------------------- |
| Runaway strategy code         | strategy / agent API credential | **Yes**                                  |
| Stolen API / admin credential | API credential                  | **Yes** — capped to one window's ceiling |
| Operator fat-finger           | operator session                | **Yes**                                  |
| DB-write attacker             | direct DB write                 | **No** — out of scope                    |

## Coverage: CEX lanes today

<Warning>
  The cap structurally covers **CEX-balance outflow lanes** — the live Upbit ↔ Bithumb (+ Binance) withdrawal path, which is exactly the runaway-strategy / stolen-credential surface it targets. **On-chain bridge lanes (CCTP, CCIP, USDT0, Stargate) are out of scope by construction** and are **not** counted or capped today.
</Warning>

The reason is structural, not an oversight: the cap derives its counter from the `BalanceReservation` ledger. A movement only contributes to the counter if its approval produces a balance reservation. CEX withdrawals snapshot a venue balance and **hold** against it → they produce a counted reservation. On-chain bridge outflows are signer-bound and have **no CEX balance snapshot**, so the reservation step *blocks* rather than holds → they never produce a counted reservation.

On-chain bridge outflow accounting is a separate, deferred concern. A coverage-probe gate test (`tests/qtg/test_outflow_cap_coverage_probe.py`) pins this boundary so that any future change giving bridge lanes a real reservation context must consciously update the cap — a new outflow lane cannot silently escape it.

Two lanes are deliberately excluded even though they touch the ledger:

* **Agent Wallet Top-Up** has its own envelope caps and separate authority ("top-up only"); counting it again would double-cap. Excluded.
* **Capital transfers** move funds *between venues you already own* (internal rebalancing), not out the door to an external recipient. They are not the external blast radius the cap targets, so they are filtered out of both the counter and enforcement, keeping the two symmetric.

## Configuration

Two environment variables, both in the Free `MG_*` settings. Ceilings live in **env, not the DB** — the API-surface adversary cannot change them; changing a ceiling needs host access plus a restart, which is acceptable for a safety ceiling (it is not a hot-tuned knob).

```bash theme={null}
# Per-token ceiling. Decimal string. Asset key matches the reservation asset (case-insensitive).
MG_OUTFLOW_CAPS_JSON={"USDC": "50000", "USDT": "50000", "BTC": "2"}

# Rolling window length in hours. Default 24. Must be >= 1.
MG_OUTFLOW_CAP_WINDOW_HOURS=24
```

| Setting                       | Meaning                                   | Notes                                                                                                                                                       |
| ----------------------------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `MG_OUTFLOW_CAPS_JSON`        | Per-token outflow ceiling, decimal string | **Unlisted token = uncapped** (opt-in per token). Add tokens deliberately.                                                                                  |
| `MG_OUTFLOW_CAP_WINDOW_HOURS` | Length of the rolling window              | Default `24`, validated `>= 1`. A non-positive window would put the cutoff at/after "now" and fail open — so the server **refuses to boot** on a bad value. |

<Warning>
  Configuration is **fail-closed**: malformed JSON, a non-decimal ceiling, or a window `< 1` causes the server to refuse to boot — consistent with the other `boot_validate_*` invariants. A typo never silently disables the brake.
</Warning>

### How usage is counted

The window usage for a token is the sum of each qualifying movement's reserved amount, **counted once**, over the last `window_hours`:

* A movement counts its **reserved** amount once, even after it completes (the funds *did* leave).
* A movement that was **released** (cancelled / failed / rolled back) counts **zero** — it never left.
* `capital_transfer` reservations are excluded.

Because the counter is derived from the reservation ledger rather than a separate tally, it cannot drift from movement truth — it is self-healing.

## Breach behaviour — token self-throttle

When approving a movement would push its token over the ceiling:

1. **The approval is rejected at the reservation chokepoint.** A typed `OutflowCapExceeded` error is raised (HTTP `409 Conflict`); no reservation commits and the movement stays in `PENDING_APPROVAL`. **No new terminal state** — the movement is not failed, just held.
2. **The token self-throttles.** Once the window is full, *every* subsequent over-cap approval for that token is auto-rejected for the rest of the window, with no operator action and no global flag. **Other tokens, each under their own ceiling, are unaffected** — a flooded `USDC` window does not block a normal `BTC` withdrawal.
3. **A loud `outflow_cap_breached` audit event is emitted** (token, window usage, ceiling, requested amount, movement id, actor) for forensics and future notification delivery.

The operator who sees the breach can, if they want a harder stop, cancel the affected movements (`POST /v3/movements/{id}/actions/cancel`). The cap deliberately does **not** build a global kill-switch — that would halt unrelated legitimate outflows.

<Note>
  Concurrency is safe. The check takes a transaction-scoped, **per-asset advisory lock** before reading window usage, held until the approving transaction commits. Two parallel over-cap approvals for the same token cannot both read stale usage and both slip through — they serialize, and the second sees the first's committed reservation. This is proven by a red-green concurrency test (`tests/qtg/test_outflow_cap_race.py`). Uncapped tokens skip the lock entirely, so the common path never contends.
</Note>

## Operator override

The emergency path — a withdrawal that *must* exceed the cap — is a single flag on the **authenticated approve action**, not on movement creation:

```http theme={null}
POST /v3/movements/{id}/approve
{
  "approver_id": "ops-oncall",
  "compiled_plan_hash": "sha256:...",
  "cap_override": true
}
```

When `cap_override` is set **and** the approving role is `operator` or `admin`, the velocity check is skipped for that approval, and a loud `outflow_cap_overridden` warning is always logged — so even a malicious operator-role override is noisy.

The placement is the security crux:

* The override rides the **approve endpoint**, whose route is already restricted to admin/operator HMAC roles — the same authenticated surface that carries `approver` today. It is **not** on `POST /v3/movements` create, where an agent fills the `intent` / `input_params` dicts.
* An **agent / strategy role can never override.** A `cap_override` echoed into a movement's `intent` or `input_params` is **ignored** — the check reads the flag only from the authenticated approval action. A stolen *strategy* key is not operator-role, so it cannot set an honored override.
* **Auto-approve and Agent Wallet Top-Up callers never send `cap_override`**, so their over-cap movements hard-reject unconditionally.

This is why the cap survives exactly the adversary it targets: the runaway strategy code or stolen strategy key that creates the flood has no way to wave it through.

<Warning>
  Do not hand an `operator`- or `admin`-role API key to automated strategy code. The override boundary is only as strong as your role assignment — an agent-role key for bots, operator/admin only for humans (and `qtg-mcp` agent bindings). See [RBAC: Three Roles](/concepts/security-model#rbac-three-roles).
</Warning>

## Relationship to the Pro budget ledger

There is a mechanically similar **Pro** feature — the budget ledger — that also does windowed amount caps. They are kept separate on purpose:

|                   | Pro budget ledger                                              | Free outflow velocity cap                                                                |
| ----------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
| Question answered | "How much can **this strategy** auto-approve without a human?" | "What is the **absolute outflow ceiling** for the whole instance, regardless of policy?" |
| Bound to          | an auto-approve policy + `strategy_id`                         | nothing — global per-token                                                               |
| Purpose           | per-strategy auto-approve accounting                           | safety brake / circuit breaker                                                           |
| License           | Pro                                                            | Free                                                                                     |

The velocity cap is always called a **cap / safety ceiling**, never a "budget" — to keep the two from being confused. It shares no code with the Pro ledger.

## Operator checklist

Before relying on the cap, confirm you can answer:

* Which tokens have a ceiling, and is each ceiling above your normal daily volume but below a runaway flood?
* Is the window length right for your strategy cadence?
* Are all automated callers on **agent**-role keys (so they cannot override)?
* Do you have an alert wired to the `outflow_cap_breached` event? (The structured event ships today; the delivery channel — Telegram/Slack — is a separate, later feature that subscribes to it.)
* Does your on-call know that `cap_override: true` on the approve action is the emergency-withdraw path, and that it is audited?

## Related pages

* [Security Model](/concepts/security-model) — where the cap sits in defense-in-depth
* [Approval/Risk Policy Guide](/guide/approval-risk-policy) — per-movement and per-strategy policy layers
* [Approval/Risk Policy Plane](/concepts/approval-risk-policy-plane)
* [Movement Lifecycle](/concepts/movement-lifecycle) — the `PENDING_APPROVAL` gate the cap holds at
