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.
- Declare which tools spend money
- Give an agent a mandate with per-transaction and total caps
- Limit a mandate to named merchants and a time window
- See what an agent spent and what is still held
- Release a payment that never went through
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
PUT /api/mandates-tools
{"tools": {"shop/pay": {"amount_arg": "amount", "currency_arg": "currency",
"merchant_arg": "merchant", "mode": "log"}}}amount_arg,currency_argandmerchant_argname the call's arguments. Dotted paths such aspayment.amountwork.currencycan replacecurrency_argwhen the tool always pays in one currency.mode: log(the default) lets calls through. Any call no mandate covers is audited asmandate.would_deny, so you can see what enforcement would stop.mode: enforcerefuses those calls before anything is sent.
2. Give the agent a mandate
POST /api/mandates/buyer
{"currency": "GBP", "per_txn": "50", "total": "500",
"merchants": ["acme", "globex"], "not_after": "2026-12-31T23:59:59Z"}- An empty
merchantslist allows any merchant. not_beforedefaults to now.not_afteris 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:
POST /api/mandates/ledger/{entry}/releaseIf 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
GET /api/mandates?agent=buyer # caps, spent, held, remaining, verified
GET /api/mandates/buyer/{ref}/ledger
DELETE /api/mandates/buyer/{ref} # revokeAudit
Every decision writes a row:
mandate.createandmandate.revoke;mandate.reserved,mandate.settled,mandate.releasedandmandate.uncertain;mandate.deniedandmandate.would_deny;mandate.tampered;mandate.tools.save.
Each row names the agent, the tool, the amount, the currency, the merchant and the mandate.