# Spending policies and budgets

> Rules the gateway applies to every purchase before any money moves — deny, require approval, or allow-list — plus a daily budget for the whole workspace.

A policy is a short text document attached to your workspace. The gateway evaluates it **on the server, before the payment is taken**, for every purchase by every wallet in the workspace. An agent can't skip it, and a model can't talk its way past it.

## The language

```yaml
deny:
  category == "Adult"
  provider.reputation < 0.3
approval:
  price > 5
allow:
  provider.verified == true
  currency == "USD"
```

There are three kinds of section, and each line is `field comparison value`:

| Section | A purchase is caught when… | Result |
|---|---|---|
| `deny:` | **any** line matches | `POLICY_DENIED`: refused |
| `approval:` | **any** line matches | `APPROVAL_REQUIRED`: held for a person |
| `allow:` | **all** lines in one allow policy match, for at least one allow policy | if allow policies exist and none matches: `POLICY_DENIED` |

### Fields

| Field | Type | Example |
|---|---|---|
| `price` | number, **in dollars** | `price > 5` |
| `category` | string | `category == "OCR"` |
| `currency` | string | `currency == "USD"` |
| `provider.verified` | boolean | `provider.verified == true` |
| `provider.reputation` | number, 0 to 1 | `provider.reputation < 0.3` |
| `provider.id` | string | `provider.id == "prv_3652ec9e…"` |
| `capabilityId` | string | `capabilityId != "cap_…"` |
| `agentId` | string | `agentId == "experiments"` |
| `walletId` | string | `walletId == "wal_…"` |

Comparisons: `==` `!=` `<` `<=` `>` `>=`. Put strings in quotes. `#` starts a comment. A line that refers to a field the purchase doesn't have never matches.

## Evaluation order

1. **Deny rules.** The first matching line refuses the purchase.
2. **The workspace daily budget.** If this purchase would take today's spending over the budget, it's refused.
3. **Approval rules.** The first matching line holds the purchase, unless a person has already approved this exact spend. See [Approvals](https://docs.zanora.dev/buyers/approvals.md).
4. **Allow rules.** If any exist, at least one must match completely.
5. Otherwise the purchase is **allowed**.

## Adding a policy

In the console: **Buyer → Policies → Add a rule**, choose the section, and type one rule per line.

With the API:

```bash
curl -s -X POST https://api.zanora.dev/v1/policies -H "x-api-key: $WKEY" \
  -H 'content-type: application/json' \
  -d '{"document":"approval:\n  price > 5\ndeny:\n  provider.verified == false"}'
```

The response lists the parsed policies, each with its own `id`. A document with several sections becomes several policies.

| Call | Does |
|---|---|
| `GET /v1/policies` | list your workspace's policies, including disabled ones |
| `POST /v1/policies/:id/disable` | stop applying one, from the next purchase |
| `POST /v1/policies/:id/enable` | start applying it again |

Policies can't be edited. To change one, add the corrected version and then disable the old one. That keeps a record of which rules applied to which purchases.

## Daily budget

The workspace's `dailyBudgetMinor`, set at signup, caps total spending across all its wallets per UTC day. When a purchase would exceed it, the purchase is refused with `POLICY_DENIED` and `matchedRule: "workspace.dailyBudget"`.

## Recipes

```yaml title="Human in the loop above $5"
approval:
  price > 5
```

```yaml title="Only verified, well-rated sellers"
allow:
  provider.verified == true
  provider.reputation >= 0.6
```

```yaml title="Sandbox an experimental agent"
deny:
  agentId == "experiments"
  price > 0.25
```

```yaml title="Pin one seller"
allow:
  provider.id == "prv_3652ec9e…"
```

> **Warning — The pause after repeated failures isn't a policy:**
>
> Separately from your rules, if one seller fails three calls in a row for a wallet (a 5xx, a crash, an MCP `isError` or a timeout, not a 4xx for a bad request), that wallet is paused from buying from that seller (`WALLET_PROVIDER_PAUSED`). This protects your balance from a broken seller. It lifts on its own, or you can [lift it now](https://docs.zanora.dev/api/wallets.md#post-v1-wallets-id-resume-provider).
