# Agent spend mandates

Give an agent a signed authority to spend, with caps per transaction and in total, a merchant list and a time window. It is checked before every call to a tool that pays.

A spend mandate is a standing authority for one agent. For example: "The buyer agent may spend up to £50 a transaction and £500 in all, with Acme and Globex, until 31 December."

Liyagent checks the mandate before any call to a tool you have declared as paying, and records every decision on a ledger. Liyagent calls no payment rail itself. The paying tool is whatever MCP server you connect, and the mandate decides whether the agent may ask it.

Nothing changes until you declare a paying tool. Agents and tools that are not declared work exactly as before.

## 1. Declare the tools that pay

```http
PUT /api/mandates-tools
{"tools": {"shop/pay": {"amount_arg": "amount", "currency_arg": "currency",
                        "merchant_arg": "merchant", "mode": "log"}}}
```

- `amount_arg`, `currency_arg` and `merchant_arg` name the call's arguments. Dotted paths such as `payment.amount` work.
- `currency` can replace `currency_arg` when the tool always pays in one currency.
- `mode: log` (the default) lets calls through. Any call no mandate covers is audited as `mandate.would_deny`, so you can see what enforcement would stop.
- `mode: enforce` refuses those calls before anything is sent.

## 2. Give the agent a mandate

```http
POST /api/mandates/buyer
{"currency": "GBP", "per_txn": "50", "total": "500",
 "merchants": ["acme", "globex"], "not_after": "2026-12-31T23:59:59Z"}
```

- An empty `merchants` list allows any merchant.
- `not_before` defaults to now.
- `not_after` is required, because a mandate always ends.

The terms are signed with the platform keyring (a detached JWS over their RFC 8785 form). The signature is checked again on every use. A cap changed in the database without being re-signed is refused and audited as `mandate.tampered`. It is never honoured.

## What happens on a paying call

Before anything is sent, Liyagent looks for one of the agent's active mandates that:

- is in the call's currency;
- is in its time window;
- allows the merchant;
- covers the amount per transaction;
- has room in its total.

The mandate row is locked while this is decided, so concurrent payments cannot overspend it together. The amount is reserved on the ledger and then resolved:

| The call | Ledger status |
| --- | --- |
| Answered without an error | `settled` |
| Not sent, or answered with an error | `released` |
| Cut off mid-call, or left running as a task | `uncertain` |

An `uncertain` entry still counts against the total, because the payment may have gone through. Once you know it didn't, release it:

```http
POST /api/mandates/ledger/{entry}/release
```

If no mandate covers the call, it is refused with the reason, for example "mdt_… allows at most 50.0000 GBP a transaction". The refusal is audited as `mandate.denied`.

## See and revoke

```http
GET    /api/mandates?agent=buyer           # caps, spent, held, remaining, verified
GET    /api/mandates/buyer/{ref}/ledger
DELETE /api/mandates/buyer/{ref}           # revoke
```

## Audit

Every decision writes a row:

- `mandate.create` and `mandate.revoke`;
- `mandate.reserved`, `mandate.settled`, `mandate.released` and `mandate.uncertain`;
- `mandate.denied` and `mandate.would_deny`;
- `mandate.tampered`;
- `mandate.tools.save`.

Each row names the agent, the tool, the amount, the currency, the merchant and the mandate.
