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

# Send limits, reputation & budgets

> Two safety valves that stop one runaway agent from sinking everyone's delivery — an automatic brake and a hard cap you set in advance.

When many agents send on your behalf, one bad actor — a loop that blasts a stale
list, a prompt that emails the wrong people — can drag down delivery for
everyone. Dairo gives you two ways to contain it: a budget is the hard cap you
set in advance, and reputation is the automatic brake that reacts to bounces and
complaints. Use either or both.

Both are part of the optional agent layer. They use separate scopes: reading
reputation uses `agents:read` and clearing a tripped agent uses `agents:write`;
reading budgets uses `budgets:read` and setting or deleting one uses
`budgets:write`.

## Reputation

Reputation watches each agent's bounces and complaints and gives you a simple
`shouldSend` signal to check before you dispatch. If an agent goes off the rails,
it's contained automatically.

### Check before you send

The fastest use: read one agent's state and gate on `shouldSend` (or
`verdict === "allow"`) before a batch.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl https://api.dairo.app/v1/agents/agt_7h2k9q4m3xeyrt6bv8wn0pcd/reputation \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const rep = await dairo.agents.getReputation("agt_7h2k9q4m3xeyrt6bv8wn0pcd");
  if (!rep.shouldSend) console.warn("paused:", rep.state, rep.lastReason);
  ```

  ```python title="Python" theme={null}
  rep = dairo.agents.reputation("agt_7h2k9q4m3xeyrt6bv8wn0pcd")
  if not rep.should_send:
      print("paused:", rep.state, rep.last_reason)
  ```
</CodeGroup>

Each agent reports a `state`, a `verdict`, its live bounce and complaint rates
over the window, and a `shouldSend` boolean:

| `state`       | `verdict`  | Meaning                                   |
| ------------- | ---------- | ----------------------------------------- |
| `healthy`     | `allow`    | Sending normally.                         |
| `throttled`   | `throttle` | Rate-limited after warning-level signals. |
| `quarantined` | `block`    | Tripped — sends are paused.               |
| `review`      | `review`   | Held for a human decision.                |

`overrideLocked` flags an agent you've pinned so it won't auto-trip again.

### See your whole fleet

`GET /v1/agents/reputation` returns every agent's state, newest-tripped first,
with the window and thresholds alongside the data.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl https://api.dairo.app/v1/agents/reputation \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const report = await dairo.agents.listReputation();
  for (const a of report.agents) {
    console.log(a.agentId, a.state, a.verdict, a.shouldSend);
  }
  ```

  ```python title="Python" theme={null}
  report = dairo.agents.list_reputation()
  for a in report.agents:
      print(a.agent_id, a.state, a.verdict, a.should_send)
  ```

  ```text title="MCP" theme={null}
  Tool: list_agents  (scope agents:read)
  Args: { "action": "listReputation" }
  ```
</CodeGroup>

```json theme={null}
{
  "agents": [
    {
      "agentId": "agt_7h2k9q4m3xeyrt6bv8wn0pcd",
      "state": "healthy",
      "verdict": "allow",
      "shouldSend": true,
      "window": { "sent": 1200, "bounces": 5, "complaints": 0 },
      "rates": { "bounceRate": 0.004, "complaintRate": 0.0001 },
      "lastReason": null,
      "lastTrippedAt": null,
      "overrideLocked": false
    }
  ],
  "window": { "hours": 24 },
  "thresholds": { "complaintWarn": 0.001, "complaintTrip": 0.002, "bounceWarn": 0.03, "bounceTrip": 0.04 }
}
```

### Clear a tripped agent

`DELETE /v1/agents/{id}/reputation` is an operator override: it lifts a
quarantine or resolves a `review` back to `healthy`, and returns the reset state.
It uses `agents:write` and is recorded in your audit log.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl -X DELETE https://api.dairo.app/v1/agents/agt_7h2k9q4m3xeyrt6bv8wn0pcd/reputation \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const cleared = await dairo.agents.clearReputation("agt_7h2k9q4m3xeyrt6bv8wn0pcd");
  console.log(cleared.state, cleared.shouldSend);
  ```

  ```python title="Python" theme={null}
  cleared = dairo.agents.clear_reputation("agt_7h2k9q4m3xeyrt6bv8wn0pcd")
  print(cleared.state, cleared.should_send)
  ```
</CodeGroup>

## Send budgets

A budget is a hard ceiling on sending that Dairo enforces for you. Set one on your
whole account, a single API key, or an individual agent, and Dairo stops the next
send the moment it would cross the line — so one bad loop can never quietly torch
your sender reputation. If a human triggers every send, you may never need it. If
code or an AI agent sends on its own, a budget is the seatbelt.

### Pick what a budget covers

A budget attaches to one of three things. Set the ceiling where it makes sense —
on the whole account, on the key a service uses, or on a specific agent.

| Covers      | Use it for                                                     | What you pass                     |
| ----------- | -------------------------------------------------------------- | --------------------------------- |
| **Account** | A single ceiling across everything you send.                   | `scope: "account"`                |
| **Key**     | One service or integration, isolated from the rest.            | `scope: "key"` + the key's id     |
| **Agent**   | One agent, so a misbehaving bot can't spend the whole account. | `scope: "agent"` + the agent's id |

### Set a budget

One call creates or updates a budget. Pass the scope, the thing it covers, and at
least one limit. You get back the budget with its live usage so far.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl -X PUT https://api.dairo.app/v1/budgets \
    -H "Authorization: Bearer $DAIRO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
          "scope": "agent",
          "scopeId": "agt_7h2k9q4m3xeyrt6bv8wn0pcd",
          "limits": { "maxSendsPerDay": 500, "maxNewRecipientsPerHour": 100, "hardStopOnComplaint": true }
        }'
  ```

  ```ts title="TypeScript" theme={null}
  const { budget } = await dairo.budgets.set({
    scope: "agent",
    scopeId: "agt_7h2k9q4m3xeyrt6bv8wn0pcd",
    limits: {
      maxSendsPerDay: 500,
      maxNewRecipientsPerHour: 100,
      hardStopOnComplaint: true,
    },
  });

  console.log(budget.usage.sendsLast24h, "of", budget.limits.maxSendsPerDay);
  ```

  ```python title="Python" theme={null}
  budget = dairo.budgets.set(
      scope="agent",
      scope_id="agt_7h2k9q4m3xeyrt6bv8wn0pcd",
      limits={"maxSendsPerDay": 500, "maxNewRecipientsPerHour": 100, "hardStopOnComplaint": True},
  )

  print(budget.usage.sends_last_24h, "of", budget.limits["maxSendsPerDay"])
  ```

  ```text title="MCP" theme={null}
  Tool: manage_budgets  (scope budgets:write, confirm required)
  Args: { "action": "set", "scope": "agent", "scopeId": "agt_…", "limits": { "maxSendsPerDay": 500 }, "confirm": true }
  > "Cap this agent at 500 sends a day and stop it cold on any spam complaint."
  ```
</CodeGroup>

```json theme={null}
{
  "budget": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "scope": "agent",
    "scopeId": "agt_7h2k9q4m3xeyrt6bv8wn0pcd",
    "enabled": true,
    "limits": { "maxSendsPerDay": 500, "maxNewRecipientsPerHour": 100, "hardStopOnComplaint": true },
    "usage": { "sendsLast24h": 18, "newRecipientsLastHour": 3, "accountHasComplaint": false },
    "createdAt": "2026-06-12T10:00:00Z",
    "updatedAt": "2026-06-12T10:00:00Z"
  }
}
```

#### The limits you can set

Set any combination of these. A budget needs at least one.

| Limit                     | What it caps                                                                                                      |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `maxSendsPerDay`          | Total sends in a rolling 24-hour window.                                                                          |
| `maxNewRecipientsPerHour` | First-time recipients per hour — the classic shape of a runaway loop blasting a fresh list.                       |
| `hardStopOnComplaint`     | When `true`, the moment a recipient reports spam, every further send under this budget stops until you intervene. |

<Note>
  `maxSpendPerDayCents` is reserved — accepted in your config but not currently
  enforced. Cap on sends and recipients, the limits Dairo acts on.
</Note>

### What happens at the ceiling

When a send would cross a budget, Dairo refuses it with a `429` carrying the
`budget_exceeded` error code — whether the ceiling it hit was a send cap, a
new-recipient cap, or the complaint hard-stop. The code lets an agent branch
cleanly: back off, queue for later, or alert a human, instead of retrying into a
wall.

```json theme={null}
{
  "error": {
    "code": "budget_exceeded",
    "message": "Daily send budget reached for this agent."
  }
}
```

<Tip>
  An agent that reacts to its own ceiling should catch `budget_exceeded` and pause
  the loop rather than retry. See [Errors](/concepts/errors) for the full list of
  codes and how to handle them.
</Tip>

### Check usage against a cap

Every budget carries live usage, so you can see how close a scope is to its
ceiling before it gets there. List all budgets, or fetch one.

<CodeGroup>
  ```bash title="cURL" theme={null}
  # Every budget
  curl https://api.dairo.app/v1/budgets \
    -H "Authorization: Bearer $DAIRO_API_KEY"

  # Only the account-wide one
  curl https://api.dairo.app/v1/budgets/account \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  const { budgets } = await dairo.budgets.list();

  const { budget: account } = await dairo.budgets.get("account");
  console.log(account.usage.sendsLast24h, account.usage.newRecipientsLastHour);
  ```

  ```python title="Python" theme={null}
  budgets = dairo.budgets.list()

  account = dairo.budgets.get("account")
  print(account.usage.sends_last_24h)
  ```
</CodeGroup>

Fetch one budget by passing `account`, or a key or agent id as the `scopeId`.

### Remove a budget

Deleting a budget removes the cap entirely — sending under that scope is no longer
ceilinged.

<CodeGroup>
  ```bash title="cURL" theme={null}
  curl -X DELETE https://api.dairo.app/v1/budgets/agt_7h2k9q4m3xeyrt6bv8wn0pcd \
    -H "Authorization: Bearer $DAIRO_API_KEY"
  ```

  ```ts title="TypeScript" theme={null}
  await dairo.budgets.delete("agt_7h2k9q4m3xeyrt6bv8wn0pcd");
  ```

  ```python title="Python" theme={null}
  dairo.budgets.delete("agt_7h2k9q4m3xeyrt6bv8wn0pcd")
  ```
</CodeGroup>

<Note>
  Setting or deleting a budget is a privileged action. A key that can only send mail
  can't raise its own ceiling — so a leaked sending key stays boxed in by the limits
  you set.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Land in the inbox" icon="shield-check" href="/webhooks/deliverability">
    The bounces and complaints that drive send health.
  </Card>

  <Card title="Agent identity & provenance" icon="id-badge" href="/agents/agent-passport">
    The agent identities reputation keys on and budgets attach to.
  </Card>

  <Card title="Why Dairo is agent-native" icon="diagram-project" href="/agent-first/agent-patterns">
    Patterns for safe, autonomous email agents.
  </Card>
</CardGroup>
