What's the Best Way to Give AI Agents Controlled Spending Authority?
A Marketing AI agent's second ad purchase of the day, $150, landed as pending_human in front of the store owner about one second after its first $220 ad purchase cleared automatically. Same agent, same API key, same rule
A Marketing AI agent's second ad purchase of the day, $150, landed as pending_human in front of the store owner about one second after its first $220 ad purchase cleared automatically. Same agent, same API key, same rule, the only difference was that the two spends together crossed a $300-a-day line.
Disclosure: I work on Pink Agentic AI Payments at PinkWallet.
Short answer: give each AI agent the same kind of layered spending authority you'd give an employee with a corporate card: a monthly budget, a per-payment cap, a daily window that catches cumulative drift, an approval band for anything bigger, and a hard block on categories that should never be self-service. Pink Agentic AI Payments checks every one of those limits on the server, before a payment executes, so the limits hold even if the agent's own context has no memory of what it already spent today. Here's that model, run live against a sandbox.
Borrow the model you already trust: employee spend authority
Companies don't give employees unlimited corporate cards, and they don't rely on employees self-reporting their own spending either. They give a card with a limit, a manager who sees the statement, and a policy for what's never reimbursable. The same five levers work for an AI agent, and they map directly onto this rule engine:
- Per-agent budget: a hard monthly ceiling, like a department budget.
-
Per-payment cap: no single transaction above X, regardless of what's left in the month. In Pink this is an amount rule with window
tx. - Daily window: catches an agent that stays under its per-payment cap but fires off many small payments in one day.
- Approval band: amounts above a threshold route to a named human instead of executing.
- Blocked categories: gift cards, crypto, personal purchases, never allowed, no approval path, no exceptions.
Step 1: a workspace with that authority already assigned per agent
curl -s -X POST "https://agentic-sandbox.pinkwallet.com/v1/sandbox/workspaces?utm_source=devto&utm_medium=article&utm_campaign=ai-agent-spending-authority" \
-H 'content-type: application/json' \
-d '{"company":"Bramble & Finch Roasters","email":"[email protected]","template":"coffee"}'
Trimmed real response:
{
"workspace_id": "9vmbv7tb8n",
"name": "Bramble & Finch Roasters",
"template": "coffee",
"agents": [
{ "id": "a_purch", "name": "Purchasing AI", "monthly_budget": 4000, "per_payment_cap": 500 },
{ "id": "a_inv", "name": "Inventory AI", "monthly_budget": 2500, "per_payment_cap": 300 },
{ "id": "a_mkt", "name": "Marketing AI", "monthly_budget": 3000, "per_payment_cap": 300 },
{ "id": "a_pay", "name": "Payroll & Bills AI", "monthly_budget": 32000, "per_payment_cap": 15000 }
],
"rules": 11
}
Four agents, four different budgets and caps, sized for what each one actually does. Payroll's agent can move far more per payment than marketing's, because its job is to pay a fixed vendor on schedule, not to improvise.
Step 2: set the budget and cap for one agent explicitly
curl -s -X PATCH "https://agentic-sandbox.pinkwallet.com/v1/admin/agents/a_mkt" \
-H "authorization: Bearer $ADMIN" -H 'content-type: application/json' \
-d '{"monthly":3500,"perPay":300}'
{
"id": "a_mkt", "name": "Marketing AI", "dept": "Marketing", "owner": "owner",
"monthly": 3500, "perPay": 300, "spentMonth": 2060, "spentToday": 220, "status": "active"
}
monthly and perPay are the only two fields this PATCH call takes. monthly is checked before every payment. perPay is shown to the agent as single_payment_cap, but in the current sandbox the engine doesn't enforce that field on its own, so a hard per-payment limit is an amount rule with window tx. There's no hourly budget in this engine, only transaction, daily, and monthly windows, and the daily one is where most real drift gets caught.
Step 3: the daily window is what catches cumulative drift, not just one big payment
The rule table for this agent has a matched pair:
| Rule | Payee | Amount | Window | Action |
|---|---|---|---|---|
| Ads up to $300 a day | cat:ads | $0β$300 | day |
allow |
| Ads above $300 a day: ask the owner | cat:ads | $300+ | day |
ask β Maya Chen (Owner) |
Neither rule looks at a single payment in isolation. The day window is the agent's total spend for the day across all its payments, including this one, compared against the line. The payee scope (cat:ads) decides whether the rule applies at all; it doesn't narrow the running total to ads only.
First ad spend, well under the line:
curl -s -X POST "https://agentic-sandbox.pinkwallet.com/v1/payments" -H "authorization: Bearer $MKT" \
-H 'content-type: application/json' \
-d '{"payee_id":"p_meta","amount":220,"purpose":"Instagram ad boost, morning promo"}'
{ "decision": "allowed", "rule": "Ads up to $300 a day", "credential": { "max_amount": 220, "locked_to": "Meta Ads" } }
Second ad spend, same day, same agent, same rule pair:
curl -s -X POST "https://agentic-sandbox.pinkwallet.com/v1/payments" -H "authorization: Bearer $MKT" \
-H 'content-type: application/json' \
-d '{"payee_id":"p_meta","amount":150,"purpose":"Instagram ad boost, afternoon top-up"}'
{
"decision": "pending_human",
"rule": "Ads above $300 a day: ask the owner",
"hold_id": "pay_50bbb7ea049b",
"who": "Maya Chen (Owner)",
"approvers_needed": 1,
"expires_at": "2026-10-20T12:59:25.026Z"
}
$220 plus $150 is $370, over the $300 line, so the second request held instead of executing. The agent never had to track its own running total. One nuance worth knowing: after this hold, the agent's spentToday field still read $220, not $370, because a pending_human payment hasn't actually moved money yet. It only counts toward the window once it's approved.
Step 4: a human approves, with the admin key, not the agent's own key
Approving a hold is a real admin-key call: POST /v1/admin/requests/{hold_id}/approve with {"by": "<approver name>"}. This step reproduces the $220 allowed / $150 pending_human pair in a fresh coffee-template workspace (company "Thistle & Oak Roasters," workspace_id t91ic75cpd) to show the approval closing the loop end to end.
The owner approves the held $150 with the admin key, the same key behind the approvals console:
curl -s -X POST "https://agentic-sandbox.pinkwallet.com/v1/admin/requests/pay_7b5fcf234bea/approve" \
-H "authorization: Bearer $ADMIN" -H 'content-type: application/json' \
-d '{"by":"Maya Chen"}'
{
"payment_id": "pay_7b5fcf234bea",
"decision": "approved",
"amount": 150,
"rule": "Ads above $300 a day: ask the owner",
"credential": { "type": "virtual_card", "max_amount": 150, "locked_to": "Meta Ads", "single_use": true },
"approved_by": ["Maya Chen"]
}
The agent's own key can't take that same action on its own behalf. Calling the identical endpoint with the Marketing AI agent's key instead of the admin key gets rejected outright:
curl -s -X POST "https://agentic-sandbox.pinkwallet.com/v1/admin/requests/pay_7b5fcf234bea/approve" \
-H "authorization: Bearer $MKT" -H 'content-type: application/json' \
-d '{"by":"Maya Chen"}'
{ "error": "unknown route for an agent key" }
After approval, the agent's own key can still read the resolved state and the now-issued credential with GET /v1/payments/{id}:
{
"payment_id": "pay_7b5fcf234bea",
"decision": "approved",
"amount": 150,
"credential": { "type": "virtual_card", "max_amount": 150, "locked_to": "Meta Ads", "single_use": true },
"approved_by": ["Maya Chen"]
}
And the agent's spentToday only moves once the hold clears: it read $220 right after the hold was created, and $370 ($220 + the now-approved $150) once Maya Chen's approval went through. In practice, this is a human opening the approvals console at a URL shaped like /console?key=<admin_key>, seeing the hold, and clicking approve. The API call above is what that click does under the hood.
Step 5: blocked categories and new payees close the rest of the loop
Spending authority isn't just a number. Two more real calls against the same agent:
Blocked, no approval path at all:
{ "decision": "blocked", "rule": "Never: gift cards, cash-like, crypto", "why": "matched Β· block" }
A brand-new payee, routed to the owner regardless of amount:
{ "decision": "pending_human", "rule": "New payee: always ask the owner", "who": "Maya Chen (Owner)", "approvers_needed": 1 }
A first-time vendor gets a human look once, even at $100, which is the same instinct behind requiring a second signature the first time a new supplier gets paid.
What each lever actually buys
| Lever | What it stops |
|---|---|
| Monthly budget | The agent running up a bill no one approved for the month |
| Per-payment cap | One oversized payment in a single call |
| Daily window | Many small payments that individually pass the cap but add up |
| Approval band | Anything large enough that a human should see it before it executes |
| Blocked categories | Categories with no legitimate agent use case, with no approval override |
Honest limits
This is test money against a public sandbox. Production with real money is early access, not something generally available yet. The evaluation order checked monthly budget and a company-wide daily ceiling as hard stops ahead of the rule table itself, according to the docs; this sandbox run exercised the per-agent rule table, not the company-wide ceiling specifically.
FAQ
Does raising a monthly budget override the daily window? No. The docs describe both as separate checks, monthly budget as a hard stop checked ahead of the rule table, the daily window as a rule within that table; raising one doesn't change the other.
What happens if the owner never approves the hold? It carries an expires_at. After that, the payment is no longer actionable and the agent has to ask again if the work still needs doing.
Can different categories have different spending authority on the same agent? Yes. The ads pair above is scoped by payee: "cat:ads", so the same agent can have a tighter window for ads than for, say, approved software subscriptions, inside one rule table.
Try it
Sandbox is free, no account required: agentic-sandbox.pinkwallet.com. Full docs: pinkwallet.com/agentic/developers. Production with real money is early access only, not generally available.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.