# Budgets and approvals

Per-agent budgets and approval thresholds, enforced on our side: what happens when a purchase needs a person.

An agent spends your money, so every agent has limits it can't change: a monthly budget and an approval threshold. Both are checked on our side before anything is held, on every request. Going over either doesn't fail the purchase: it asks an owner, and the agent carries on once they say yes.

## The limits

Per-agent limits

| Limit | Default | What it does |
|---|---|---|
| `monthly_budget_credits` | 50,000 credits ($50) per UTC calendar month | A live purchase that would take the agent's spend this month over its budget needs approval (`over_budget`). At 80% the owners get a `budget.80pct` event and email. |
| `approval_threshold_credits` | 20,000 credits ($20) | A single request whose price × items is above it needs approval (`over_threshold`). `null` means never ask. The dashboard offers $5, $20, $50 or never. |
| `ip_allowlist` | empty (any IP) | IP addresses or CIDR ranges the key may be used from. Anything else gets `403 ip_not_allowed`. |

Set them on the Agents page, or with `PATCH https://api.arettic.com/v1/orgs/{orgId}/agents/{agentId}` (a member, or a write org key). An agent key can read its own limits (`GET /v1/agent`, `GET /v1/balance`) but never change them.

**curl**

```bash
curl -X PATCH https://api.arettic.com/v1/orgs/$ORG_ID/agents/$AGENT_ID \
  -H "Authorization: Bearer $ARETTIC_ORG_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "monthly_budget_credits": 200000, "approval_threshold_credits": 50000 }'
```

## What is checked, in order

1. **Balance.** The org can't pay for price × items: declined, `402 insufficient_credits`. No approval can fix that; top up.
2. **Trial limit.** An org that has never topped up can spend at most 50 trial credits an hour: declined, `429 trial_limit`.
3. **Approval threshold,** then **monthly budget**: over either, the request becomes an approval (below).

The hold step checks the budget again under a lock, so two requests sent at the same moment can't both slip under it.

## When a purchase needs approval

The request is not run and nothing is held. The answer is HTTP 202:

**Response**

```json
{
  "status": "approval_required",
  "approval_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
  "reason": "over_threshold",
  "amount": { "credits": "24000", "usd": "24.000" },
  "expires_at": "2026-10-01T11:10:00.000Z",
  "message": "This is above the agent's approval threshold. An owner has been asked to approve it; retry with approval_id once approved."
}
```

1. Every owner gets an email with a one-click link to approve or decline, no sign-in needed. They can also decide on the Approvals page, or with `POST /v1/orgs/{orgId}/approvals/{id}/decision` and `{ "decision": "approve" }` or `"reject"` (optional `note`). Webhook endpoints get `approval.requested` and then `approval.decided`.
2. The agent checks `GET /v1/approvals/{id}`: `status` is `pending`, `approved`, `rejected`, `expired` or `used`, with `reason`, `agent`, `tool`, `items`, `amount`, `expires_at`, `decided_at` and `decision_note`.
3. Once `approved`, the agent sends **the same request again** with `approval_id`. It runs without the threshold and budget checks, for exactly the tool, inputs and amount that were approved.

- Approvals expire after 24 hours; using one after that gets `410 approval_expired`. Send the request again for a new one.
- A declined request gets `403 approval_rejected`.
- Changing the tool or the inputs, or a price that went up since, gets `409 approval_mismatch`. So does using an approval twice, or another agent's.
- Sending the same request again while its approval is pending returns the same approval: owners aren't emailed twice.

**TypeScript**

```ts
import { Arettic } from "@arettic/sdk";

const arettic = new Arettic();
const request = { tool_id: "peopledatalabs-enrich-company", inputs };
let result = await arettic.execute(request);
if (result.status === "approval_required") {
  const approval = await arettic.waitForApproval(result.approval_id); // polls every 5 s, up to 24 h
  if (approval.status !== "approved") throw new Error(`Not approved: ${approval.status}`);
  result = await arettic.execute({ ...request, approval_id: result.approval_id });
}
```

**Python**

```python
import time

from arettic import Arettic

client = Arettic()
request = {"tool_id": "peopledatalabs-enrich-company", "inputs": inputs}
result = client.execute(request)
if result["status"] == "approval_required":
    approval_id = result["approval_id"]
    while (approval := client.approval(approval_id))["status"] == "pending":
        time.sleep(5)
    if approval["status"] != "approved":
        raise RuntimeError(f"Not approved: {approval['status']}")
    result = client.execute(request, approval_id=approval_id)
```

Over MCP, `execute` returns the same `approval_required` answer and `get_approval` checks it; see [MCP](/docs/mcp).

## Other guards

- **Velocity alerts.** If an agent spends more than 3× its usual hourly rate in an hour (and at least 1,000 credits, $1), the owners get an email, at most once a day per agent.
- **Revoke at once.** Revoking an agent's key on the Agents page (or `POST /v1/orgs/{orgId}/agents/{agentId}/revoke-key`) stops it on the next request. Rotating issues a new key and revokes the old one in the same step.
- **Plan limits.** Each plan has a number of agents and seats; going past it gets `402 plan_limit`, with a link to upgrade in `message`.

Agents and seats per plan

| Plan | Agents | Seats |
|---|---|---|
| Pay as you go | 2 | 1 |
| Pro | 10 | 5 |
| Max | no limit | no limit |

Updated 2026-09-30. This page as HTML: https://arettic.com/docs/budgets-and-approvals · Markdown: https://arettic.com/docs/budgets-and-approvals.md · JSON: https://arettic.com/docs/budgets-and-approvals.json
