# Review that scales with the stakes

Make approving a risky agent write take more than one click, flag approvals made too fast to have been read, and see which reviewers skip the checks.

A person who approves forty routine comments in a row will approve the forty-first, and the bulk close of 150 tickets after it, with the same click. **Review friction** asks for more where more is at stake, so an approval of a risky write is evidence that someone read it.

## What an approval asks for

Every held write has a risk score from 0 to 100 and the factors behind it (see [Approval auto-triage](/docs/govern/approval-triage)). The score sets the stakes:

| Stakes | Risk | What approving asks for |
| --- | --- | --- |
| Low | below 40 | Nothing extra |
| Medium | 40 to 59 | At least 4 seconds with the request open |
| High | 60 or more | At least 10 seconds open, a tick against each risk factor, and for a call on a list of 10 or more items, the number of items typed in |

The item count is never shown next to the question: the approver has to read it from what will be sent. In the console's approvals inbox, **Approve and run** stays off until every box is ticked and the count is filled in. Rejecting never asks for anything.

`GET /api/approvals` and `GET /api/approvals/{id}` return `review_required` with `stakes`, `risk`, `checks` (the factors to confirm), `min_review_s` and `asks_count`. A client sends the review with the decision:

```json
POST /api/approvals/{id}/approve
{"digest": "…", "review": {"checked": ["write", "bulk", "bulk_large"], "count": 150, "dwell_ms": 24000}}
```

## Warn or enforce

`TICKETIQ_APPROVAL_REVIEW_FRICTION` decides what happens to an approval that skipped the check or came too fast:

| Mode | Effect |
| --- | --- |
| `warn` (default) | The approval goes ahead. The decision records the review and the row `agent.approval.review.flagged` names what was missing |
| `enforce` | An approval through the API without the review is refused with `428` and told what to check. The request stays pending |
| `off` | No check and nothing recorded |

In `warn` nothing that approves today needs a new step: Teams cards, the mobile app and scripts work as before. `enforce` needs every client that approves through the API to send the review: the console does; update the mobile app and any script first. A channel that cannot ask for the check, such as a Teams card, is recorded and audited in every mode, never refused.

A batch approval sends no review, so in `enforce` a high-risk request in a batch is refused even with `allow_high_risk`: approve it on its own.

## On the record

The review (`stakes`, `checked`, `too_fast`, `dwell_s`, and `via`: `console` or `channel`) is stored on the decision and carried on its [trust ledger](/docs/govern/trust-ledger) receipt. Reviewer metrics (`GET /api/approvals/metrics`, `agents.read` and `audit.read`) count, per reviewer, `unchecked_high_risk` and `too_fast` approvals next to the rubber-stamp flag.
