# Spend Policies

Spend policies define the rules under which an agent is permitted to make payments. They are enforced at the RailProtocol gateway before any transaction is submitted to the blockchain. A call that would breach a policy is rejected before funds leave the wallet.

***

## Setting policies

### CLI

```bash
rail policies set my-agent \
  --daily-budget 100 \
  --per-call-limit 1.00 \
  --allowed-domains "api.dune.com,api.browserbase.com,fal.ai" \
  --rate-limit "500 per hour"
```

### SDK

```typescript
await rail.policies.set({
  dailyBudget: { usdc: 100 },
  perCallLimit: { usdc: 1.00 },
  allowedDomains: ["api.dune.com", "api.browserbase.com", "fal.ai"],
  blockedDomains: [],
  rateLimit: { calls: 500, window: "1h" },
  protocolPreference: "auto",
});
```

### API

```bash
PUT /v1/policies/{agentId}
Authorization: Bearer $RAIL_API_KEY

{
  "dailyBudget": { "usdc": 100 },
  "perCallLimit": { "usdc": 1.00 },
  "allowedDomains": ["api.dune.com", "api.browserbase.com"],
  "rateLimit": { "calls": 500, "window": "1h" }
}
```

***

## Policy fields

### `dailyBudget`

**Type:** `{ usdc: number }` — **Optional**

Maximum USDC the agent can spend in a rolling 24-hour window. The window resets 24 hours after the first payment of the current period.

```typescript
dailyBudget: { usdc: 100 }
```

When 80% of the daily budget is consumed, RailProtocol emits a `budget.warning` webhook event. At 100%, all subsequent payments are rejected with `DailyBudgetExceededError` until the window resets.

Set `dailyBudget` to `null` to remove the limit.

***

### `perCallLimit`

**Type:** `{ usdc: number }` — **Optional**

Maximum USDC the agent can spend on a single `rail.pay()` call. Payments that would exceed this limit are rejected before the x402 or MPP handshake begins.

```typescript
perCallLimit: { usdc: 1.00 }
```

This is a hard cap per individual transaction, independent of the daily budget.

***

### `allowedDomains`

**Type:** `string[]` — **Optional**

Whitelist of domains the agent is permitted to pay. Calls to domains not on this list are rejected with `DomainNotAllowedError` before any payment is attempted.

```typescript
allowedDomains: ["api.dune.com", "api.browserbase.com", "fal.ai"]
```

Subdomain matching: `"api.dune.com"` allows only that exact subdomain. `"dune.com"` allows `dune.com` and all subdomains.

Omit this field to allow payments to any domain. Use this with caution — unrestricted domains mean any endpoint the agent resolves to can be paid.

***

### `blockedDomains`

**Type:** `string[]` — **Optional**

Explicit blocklist that takes precedence over `allowedDomains`. A domain on the blocklist is always rejected, even if it is also on the allowlist.

```typescript
blockedDomains: ["api.legacy-service.com"]
```

***

### `rateLimit`

**Type:** `{ calls: number, window: string }` — **Optional**

Maximum number of payment calls within a time window. This limits how frequently the agent can make payments, regardless of the amount.

```typescript
rateLimit: { calls: 500, window: "1h" }
```

Valid window values: `"1m"`, `"5m"`, `"15m"`, `"1h"`, `"24h"`.

***

### `protocolPreference`

**Type:** `"auto" | "x402" | "mpp"` — **Default:** `"auto"`

Controls which protocol the payment router prefers when a service supports both x402 and MPP.

```typescript
protocolPreference: "auto"  // x402 preferred, falls back to MPP
protocolPreference: "x402"  // always use x402 if available
protocolPreference: "mpp"   // always use MPP if available
```

***

## Viewing active policies

```bash
rail policies get my-agent
```

```
Agent:            my-agent
Daily budget:     100.00 USDC (used today: 12.40 USDC)
Per-call limit:   1.00 USDC
Allowed domains:  api.dune.com, api.browserbase.com, fal.ai
Blocked domains:  (none)
Rate limit:       500 / hour (used this hour: 47)
Protocol pref:    auto
```

***

## Policy violation events

When a policy rejects a payment, the event is:

* Logged in the dashboard under the **Policy Events** tab
* Available in `rail transactions list` with status `rejected`
* Sent as a webhook if you have configured a `policy.violation` webhook

Webhook payload:

```json
{
  "event": "policy.violation",
  "agentId": "my-agent",
  "reason": "per_call_limit_exceeded",
  "detail": "Requested 2.50 USDC, limit is 1.00 USDC",
  "endpoint": "https://api.expensive-service.com/query",
  "timestamp": "2026-05-09T14:23:01Z"
}
```

***

## Budget alerts

Set a webhook to be notified when the daily budget reaches a threshold:

```bash
rail alerts create \
  --agent my-agent \
  --type budget_pct \
  --threshold 80 \
  --notify webhook:https://hooks.yourapp.com/rail
```

This fires when the agent has consumed 80% of its daily budget.

***

## Example: locked-down research agent

A tightly scoped agent that can only query specific data APIs and cannot spend more than $5 per day:

```typescript
await rail.policies.set({
  dailyBudget: { usdc: 5 },
  perCallLimit: { usdc: 0.25 },
  allowedDomains: [
    "api.dune.com",
    "api.coingecko.com",
    "api.thegraph.com",
  ],
  rateLimit: { calls: 100, window: "1h" },
  protocolPreference: "x402",
});
```

***

## Further reading

* [Agent wallets](/features/agent-wallets.md)
* [Observability](/features/observability.md)
* [CLI reference: `rail policies`](/cli/reference.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.railprotocol.org/features/spend-policies.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
