Your AI Agent Has a Credit Card and No Spending Limit
I built a policy engine for agent payments. Here's what the problem actually looks like when you try to solve it.
The ACK demo ships with a policy engine. It checks two things: is the payment under $5, and is the recipient on an allowlist. About 94 lines. It works for a demo.
But a $5 per-transaction cap does nothing if the agent makes twenty $5 payments. The demo's own source code comments on this: a per-transaction cap is trivially split-gameable. So I started building the version you'd actually ship.
ack-policy is an open-source policy engine that sits between an AI agent receiving a payment request and executing it. It answers one question at runtime: is this payment within the scope the user authorized?
Allow, deny, and the third answer
Most authorization systems return two values. Yes or no. Allow or deny.
Agent payments need a third. Say your agent is buying office supplies and encounters a vendor that isn't on the allowlist. Denying outright means the agent can never pay a new vendor without you manually updating a config file. Approving silently defeats the purpose of having a policy.
ack-policy returns three values: approved, denied, and approval_required. The third value means a soft constraint wasn't met, so the agent pauses and asks the human to break the tie. The unknown-vendor case routes to approval_required. A hard budget violation routes to denied.
The cost of this design: every integration needs to handle three branches instead of two. You can't just wrap the policy check in an if statement. You need a switch.
const result = await evaluate(policy, request, store)
switch (result.decision) {
case "approved": return executePayment(request)
case "denied": return rejectPayment(result.reason)
case "approval_required":
return requestHumanApproval(request, result.reason)
}The split attack
A per-transaction cap of $5 with a rolling budget of $50 over 24 hours means the agent can make ten $5 payments and then stops. The 11th gets denied.
Without the rolling budget, there is no stop. The agent just keeps making $5 payments until the underlying wallet is empty. Every per-transaction limit without a cumulative budget has this problem. It's like putting a speed limit on a highway but no limit on how far you can drive.
Rolling window budgets track cumulative spend across a configurable time window:
const policy = definePolicy({
maxAmount: { USDC: 10_000_000n },
budget: {
windowMs: 24 * 60 * 60 * 1000,
maxAmount: { USDC: 50_000_000n },
},
})Those numbers are in smallest subunits, which matters. USD has 2 decimal places. USDC has 6. ETH has 18. 10_000_000n is 10 USDC, not 10 million. Every limit is a bigint in the currency's smallest unit. No floating point anywhere near money.
The atomic check-and-reserve is the part that makes this work. When evaluate() runs, it checks the budget and reserves the amount in a single operation. Ten concurrent $5 payments against a $20 budget approve exactly four and deny six, because each reservation is atomic. Without atomicity, all ten could read the budget as having $20 available and all ten approve.
What happens when the payment fails
Budget enforcement creates a lifecycle problem. The policy reserves $5 against the budget, the agent submits the payment, and the payment fails. Without a way to release that reservation, failed payments permanently consume budget. After enough failures, the agent has a full budget and can't spend any of it.
ack-policy uses a three-phase lifecycle: reserve, commit, release.
evaluate() reserves the amount and returns a requestId. On payment success, you call store.commit(requestId) to lock it in. On payment failure, you call store.release(requestId) to free the budget.
const result = await evaluate(policy, request, store)
// result.requestId is set on approved/approval_required
try {
await executePayment(request)
await store.commit(result.requestId)
} catch {
await store.release(result.requestId)
}The cost: every integration now has three store calls instead of one check. And the store needs to be durable. The in-memory store works for testing, but a production system needs Redis or Postgres backing the reservation state. If the process crashes between reserve and commit, you need a TTL-based expiry to reclaim orphaned reservations.
Three layers of agent payment safety
This policy engine is layer 2 in a three-layer stack I've been writing about across several LedgerDrift articles:
Layer 1: Authorization. What the agent may do. ACK grants define the scope at the time the user sets up the agent. "You may spend up to $200 on office supplies from these vendors." This is the contract.
Layer 2: Enforcement. What the agent shouldn't do. ack-policy checks constraints at runtime, before the payment executes. Rolling budgets, recipient validation, per-transaction caps. This is the guardrail.
Layer 3: Evidence. What the agent did do. Dispute evidence bundles prove after the fact whether a payment was within scope. When the chargeback system has no reason code for agent errors, cryptographic evidence is what fills the gap.
Authorization without enforcement is a suggestion. Enforcement without evidence is unverifiable. You need all three.
What this doesn't solve
Grant integration is blocked on ACK v2. Today, you configure the policy manually. The next version should read constraints directly from a signed grant artifact, so the user's authorization and the runtime enforcement are guaranteed to match. That gap is the biggest open problem.
True concurrency is limited by the runtime. JavaScript is single-threaded, so the in-memory store handles concurrent evaluate() calls sequentially. Real atomicity across multiple processes needs a database-backed store with row-level locking.
Intent capture is the hardest problem of all. "Buy me office supplies" becoming "booked a hotel" is a policy violation that the engine can't express as a field comparison. The policy checks amounts, recipients, and budgets. It can't check whether the agent understood the instruction correctly. That's a different layer of the stack entirely.
Sources
- ack-policy - Open-source policy engine for agent payments with rolling budgets and three-valued decisions
- ACK Demo Payment Policy - The ~94-line demo policy that ack-policy extends
- ACK Issue #138: Rolling Budgets - Original feature request for cumulative spend tracking
- Agent Commerce Has No Chargeback Code - Part 1 of the series: the dispute infrastructure gap
- Agent Commerce Needs Evidence, Not a Reason Code - Part 2 of the series: post-payment dispute evidence
Frequently Asked Questions
Built by Trio, a fintech-native engineering partner helping teams build the next generation of financial technology and infrastructure.
Subscribe to Ledger Drift for high-signal insights into how modern fintech is built, from systems to code to teams.