Subscribe to get high-signal insights on how modern fintech is built.

engineering

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.

By Alex Kugell ·

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.

The Split Attack: With and Without Atomic Reservation
With atomic reserve
$20 budget, ten $5 requests
#1$5reserve$15
#2$5reserve$10
#3$5reserve$5
#4$5reserve$0
#5$5denied$0
#6$5denied$0
#7$5denied$0
#8$5denied$0
#9$5denied$0
#10$5denied$0
Total spent: $20
Without atomic reserve
All 10 read budget as $20
#1$5approve$20
#2$5approve$20
#3$5approve$20
#4$5approve$20
#5$5approve$20
#6$5approve$20
#7$5approve$20
#8$5approve$20
#9$5approve$20
#10$5approve$20
Total spent: $50

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:

Three Layers of Agent Payment Safety
1
Authorization
What the agent may do
ACK grants, scoped permissions
Setup time
2
Enforcement
What the agent shouldn't do
ack-policy, rolling budgets, recipient checks
Runtime (pre-payment)
3
Evidence
What the agent did do
Dispute bundles, cryptographic proof
Post-payment

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

Frequently Asked Questions

Why do AI agent payments need a policy engine?
A per-transaction cap alone is trivially split-gameable. An agent can make twenty $5 payments against a $5 limit. A policy engine adds rolling budgets, cumulative spend tracking, and atomic check-and-reserve to prevent this.
What is the split attack in agent payments?
A split attack exploits per-transaction limits by breaking a large payment into many small ones. Without a rolling window budget that tracks cumulative spend over time, an agent can drain a wallet making payments that each individually pass the limit.
What does approval_required mean in ack-policy?
It is a third return value beyond approved and denied. When a soft constraint is not met, like an unknown vendor, the agent pauses and asks a human to decide rather than silently approving or blocking outright.

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.

Keep reading

engineeringThe Token Arbitrage EconomyStolen API keys, mass-registered accounts, and USDT resale markets. AI compute fraud looks nothing like payment fraud, a...
fintechWhen the Billing System Decides Who Gets the GPUAt AI-company scale, the billing system sits in the inference hot path. Every API request passes through it before the G...
analysisThe 20-Point Gap Between Stablecoin Adoption and Stablecoin ProtectionVisa asked Americans if they'd use stablecoins with bank-level protections. Adoption jumped 20 points. The GENIUS Act ju...
View more ›