> ## 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.

# Auto-Approve

> Free per-template cap hook vs Pro strategy-scoped budget ledger

# Auto-Approve — Free vs Pro

QTG's auto-approve subsystem lets pre-defined movement templates auto-approve
small, well-bounded transfers without operator intervention. There are two
implementations:

| Tier     | Hook                        | When it runs                                                  | Cap source                                            | Concurrency safety                                               |
| -------- | --------------------------- | ------------------------------------------------------------- | ----------------------------------------------------- | ---------------------------------------------------------------- |
| **Free** | `_SimpleCapAutoApproveHook` | `settings.auto_approve_enabled=True` + no Pro hook registered | `MovementPlanTemplate.daily_cap_*` columns            | TOCTOU — concurrent submits can overshoot (single-operator gate) |
| **Pro**  | `AutoApproveHookImpl`       | Pro bootstrap registers it; takes precedence over Free        | `AutoApprovePolicy` rows + `BudgetLedgerEntry` ledger | `SELECT FOR UPDATE` — exact serializable                         |

Both implement the same `AutoApproveHook` protocol
(`src/qtg/domain/protocols.py`); the registry (`pro_registry.py`) selects
which one is live.

## Free hook: `_SimpleCapAutoApproveHook`

Source: `src/qtg/application/services/free_auto_approve.py`

### Evaluation order

1. `settings.auto_approve_enabled` (env `MG_AUTO_APPROVE_ENABLED`)
   → `auto_approve_disabled_settings`
2. Template lookup by `template_key` → `template_not_found`
3. Template `auto_approve_enabled=True` → `template_auto_approve_disabled`
4. Template `daily_cap_asset == intent.asset` → `asset_mismatch`
5. Per-request: `intent.amount <= daily_cap_amount`
   → `amount_exceeded_per_request`
6. 24h rolling SUM (other approved requests for same template, last 24h)
   `+ intent.amount <= daily_cap_amount` → `daily_cap_exceeded`
7. Approved → `free_simple_cap_passed`

### Enabling a template for auto-approve

```bash theme={null}
# admin HMAC signed
curl -X PATCH "$QTG_BASE/v3/plan-templates/$TEMPLATE_KEY" \
  -H "X-QTG-Key-Id: $ADMIN_KEY_ID" \
  -H "$(qtg-hmac-sign PATCH /v3/plan-templates/$TEMPLATE_KEY '{"auto_approve_enabled":true,"daily_cap_amount":"100","daily_cap_asset":"USDC"}')" \
  -d '{"auto_approve_enabled": true, "daily_cap_amount": "100", "daily_cap_asset": "USDC"}'
```

The PATCH route returns the updated row and writes a
`plan_template.update_admin_fields` audit row (Plan 1 boot\_check enforces
descriptor presence).

DB-level CHECK constraint blocks `auto_approve_enabled=true` with NULL caps
(`cap_consistency_violation` → 422).

### TOCTOU caveat

Free hook has no `SELECT FOR UPDATE`. Two concurrent submits both pass the
24h SUM check before either commits, so cumulative cap can overshoot.

* **Operating assumption**: single QTG operator, low-concurrency desktop
  use. Empirically the race never fires; QTG is the only writer.
* **Test invariant**: `tests/qtg/test_free_simple_cap_toctou_documented.py`
  uses a monkeypatched barrier to reproduce the race deterministically and
  asserts both submits pass when cumulative would exceed cap. The test
  documents the failure mode — it does not "fix" it.
* **Escape hatch**: enable the Pro hook
  (`register_auto_approve_hook(AutoApproveHookImpl())`) for exact
  serializable cap enforcement via `BudgetLedgerEntry`.

### Pro overrides Free

When Pro is installed its hook is registered at bootstrap and takes precedence; the Free
hook is never invoked. The Pro path additionally requires `strategy_id` and `client_id` on
the movement, because policy lookup keys on both.

## Settings reference

| env                       | default | meaning                                                                          |
| ------------------------- | ------- | -------------------------------------------------------------------------------- |
| `MG_AUTO_APPROVE_ENABLED` | `False` | Master gate for the hook. False → Free hook skipped, `auto_approve_result=NULL`. |
| `MG_AUTO_APPROVE_DRY_RUN` | `False` | Pro-only: log "would approve" instead of approving.                              |
