Approving agent spending
When an agent can pay for things, have a rule or a named person agree to each purchase, keep a running limit per agent, and keep the record of who said yes.
Agents can now pay. Card networks issue stand-in card numbers to agents, payment providers publish toolkits for them, and consumer assistants ask their user to confirm each purchase with a fingerprint. That confirmation answers the right question for someone spending their own money. At work the money belongs to the organisation, so the question is different: who is allowed to agree to this purchase, and what were they shown when they did?
Deliverd answers it with three pieces you may already use, put in the order a payment needs them:
| Answers | Where | |
|---|---|---|
| The gate | May this purchase happen, and who agrees to it? | Each purchase, before it is made |
| A budget | Is this agent still inside what it may spend this run, day or month? | The running total, after the gate and before paying |
| The record | Who agreed, under which rule, what they saw, and what was paid | Afterwards, for finance and audit |
1. Register the purchase
Name the purchase as an action, with a schema for its input. editableFields lets an approver lower the amount before agreeing; the agent then pays the corrected figure, because it pays with the input that comes back.
curl -X POST "$DELIVERD_URL/api/v1/actions" \
-H "Authorization: Bearer $DELIVERD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"actionId": "payments.purchase",
"name": "Buy something for the organisation",
"displayTitle": "Pay {{amount}} {{currency}} to {{merchant}}",
"editableFields": ["amount"],
"inputSchema": {
"type": "object",
"required": ["merchant", "amount", "currency", "description"],
"properties": {
"merchant": { "type": "string" },
"amount": { "type": "number", "minimum": 0 },
"currency": { "type": "string", "enum": ["EUR", "GBP", "USD"] },
"description": { "type": "string" }
}
}
}'2. Write the rules
Rules match on the validated amount and merchant, not on a risk label the agent chose. Above 1,000, two finance leads must agree. Small buys from suppliers you already use go through without anyone. Everything else matches no rule, and no matching rule means a person decides. Gate policy rules are applied on Business Starter, Business and Enterprise; without them every purchase goes to a person.
deliverd.policy.json
{
"name": "Agent purchases",
"rules": [
{
"name": "Large purchases need two people",
"priority": 10,
"effect": "require_human",
"match": {
"actions": ["payments.purchase"],
"input": { "amount": { "gt": 1000 } }
},
"params": { "requiredApprovals": 2, "approvers": ["finance-leads"] },
"explanation": "Over 1,000 two finance leads sign it off."
},
{
"name": "Small buys from our usual suppliers go through",
"priority": 20,
"effect": "allow",
"match": {
"actions": ["payments.purchase"],
"input": {
"amount": { "lte": 50 },
"merchant": { "in": ["Office Supplies Ltd", "Cloud Hosting Inc"] }
}
}
}
]
}Push it as a draft, try it with deliverd policy simulate, and replay it against the last 30 days of decisions before putting it in force. See Agent policy and Policy as code.
3. Set a budget
A rule looks at one purchase. A budget looks at the total, so forty small purchases that each pass a rule still stop at the limit. An administrator sets it under Agents → Platform overview, for one agent or for every agent, in EUR, GBP, USD (or tokens), per run, day, month, total.
| Call | Scope | Does |
|---|---|---|
GET /api/v1/budgets | gates:read | The budgets that apply to this agent: switched on, and for every agent or for this one. |
POST /api/v1/budgets/{id}/spend | gates:write and gates:execute | Report a spend before making it. Body: amount in the budget's unit, runId for a budget per run, and a note shown to the person if more is asked for. |
allowed- Inside the limit. The spend is recorded; go ahead.
pending- Past the limit. Nothing is recorded, and a person is asked whether to extend the budget and by how much;
approvalUrlis where they answer. Report the same amount again later. refused- Stop. A refusal stands for the rest of that run or period.
4. Put it together
One purchase
// 1. Ask about this purchase. A rule answers, or a person does.
const decision = await deliverd.gate({
action: "payments.purchase",
externalId: `purchase:${order.id}`, // the same on a retry
input: { merchant, amount, currency, description },
});
if (!decision.allowed) return decision.reason;
// 2. Report it against the agent's budget, at the amount that was agreed.
const res = await fetch(`${DELIVERD_URL}/api/v1/budgets/${budgetId}/spend`, {
method: "POST",
headers: {
Authorization: `Bearer ${DELIVERD_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount: decision.input.amount,
runId,
note: `${decision.input.merchant}: ${decision.input.description}`,
}),
});
const spend = await res.json();
if (spend.status === "pending") return `Waiting for a person: ${spend.approvalUrl}`;
if (spend.status === "refused") return spend.reason;
// 3. Pay, with the input that came back, then say what happened.
const receipt = await pay(decision.input, { decision: decision.decisionToken });
await deliverd.gates.recordExecution(decision.id, { result: { receipt: receipt.id } });- Ask the gate first, so the budget is charged the amount a person agreed to, not the one the agent first proposed.
- Pay with
decision.input, not with the object you sent. If an approver lowered the amount, the lower amount is the one they agreed to. - Pass
decisionTokento the service that makes the payment and have it verify the token before it pays, so a purchase nobody agreed to is refused there too. - Record the execution, so the record shows what was paid against what was allowed.
From ChatGPT and other assistants
An assistant connected over MCP calls check_gate with the same action and input, and record_execution after paying; see ChatGPT and dots and MCP server. Tell it in its instructions that purchases for the organisation go through payments.purchase first.
Try it on your own agent.
Free to start, no card. Sign up with Google, Microsoft or GitHub, and the first page after hands you an API key.