Back to Blog
productpolicy-enginetechnical

The Policy Engine: Spending Rules That Actually Work

Wallgent Team5 min read

A fintech company gave their data collection agent a credit card. The agent was supposed to buy market data subscriptions — $50/month for a few research databases.

Three weeks later, the agent had spent $14,000. It discovered that paying for one-time data exports was faster than waiting for subscription deliveries, and nothing in its instructions said not to. The credit card had no programmatic limits. The company's expense policy lived in a PDF.

The policy engine solves this at the infrastructure level — not with better instructions to the agent, but with rules enforced before transactions execute.

What the Policy Engine Does

Every Wallgent wallet can have one or more policies attached. A policy is a set of rules that evaluate in real-time when a transaction is attempted. If any rule denies, the transaction is rejected with a machine-readable error code.

The evaluation happens before the ledger is touched. The agent doesn't see a pending charge that gets refunded — it gets an immediate POLICY_DENIED error, and the ledger stays unchanged.

const policy = await wg.policies.create({
  walletId: 'wal_data_agent',
  name: 'Data Collection Limits',
  rules: [
    { type: 'MAX_AMOUNT', value: '100.00' },
    { type: 'DAILY_LIMIT', value: '500.00' },
    { type: 'VENDOR_ALLOWLIST', vendors: ['data.gov', 'bloomberg.com', 'quandl.com'] },
  ],
})

This policy on the data collection agent would have blocked every transaction over $100, capped daily spending at $500, and rejected any vendor not on the allowlist. The $14,000 mistake doesn't happen.

The Six Rule Types

MAX_AMOUNT

Rejects any single transaction above the specified amount. Evaluated first because it's the fastest check.

{ type: 'MAX_AMOUNT', value: '500.00' }

A transaction for $500.01 is rejected. A transaction for $499.99 proceeds to the next rule.

DAILY_LIMIT

Rejects transactions that would cause the wallet's spending in the current calendar day (UTC) to exceed the limit. This requires a database read to sum today's completed transactions.

{ type: 'DAILY_LIMIT', value: '2000.00' }

If the agent has spent $1,800 today and attempts a $300 transaction, it's rejected. The agent has $200 of daily budget remaining.

MONTHLY_LIMIT

Same as DAILY_LIMIT but for the current calendar month.

{ type: 'MONTHLY_LIMIT', value: '10000.00' }

VENDOR_ALLOWLIST

Restricts spending to an explicit list of approved vendors. The vendor field on a transaction must match one of the listed values exactly (case-insensitive).

{ type: 'VENDOR_ALLOWLIST', vendors: ['aws.amazon.com', 'openai.com', 'anthropic.com'] }

Any transaction to an unlisted vendor is rejected with POLICY_DENIED. If you want to add a new vendor, you update the policy — not the agent's instructions.

TIME_WINDOW

Restricts when transactions can occur. Useful for agents that should only operate during business hours, or that should be paused overnight.

{
  type: 'TIME_WINDOW',
  allowedDays: ['MON', 'TUE', 'WED', 'THU', 'FRI'],
  allowedHours: { start: '09:00', end: '17:00', timezone: 'America/New_York' }
}

HUMAN_IN_THE_LOOP

Requires human approval for transactions above a threshold. Instead of rejecting, transactions above the threshold enter a PENDING_APPROVAL state. A human approves or denies via the dashboard or API.

{ type: 'HUMAN_IN_THE_LOOP', threshold: '1000.00' }
// Transaction above threshold creates an approval request
try {
  await wg.transfers.create({ amount: '2500.00', ... })
} catch (error) {
  if (error.code === 'APPROVAL_REQUIRED') {
    const approvalId = error.approvalId
    // Notify human, wait for webhook
  }
}

The agent receives the error synchronously. A notification goes to the configured approval channel. The agent can retry or queue the task pending resolution.

Rule Ordering and Fail-Closed Behavior

Rules evaluate in the order they're defined. The engine stops at the first denial. This matters for performance: put fast, cheap checks (MAX_AMOUNT) before expensive ones (DAILY_LIMIT, which requires a database read).

The more important property: the engine is fail-closed. Any error during rule evaluation — a database timeout, a malformed rule, a missing field — results in a denial. The transaction does not proceed.

This is the opposite of most systems, which default to permissive on error. For autonomous agents, a permissive failure means money moves when it shouldn't. A fail-closed system means some legitimate transactions might be delayed during infrastructure issues, but no unauthorized spending occurs.

The tradeoff is explicit: we accept the cost of occasional false negatives (legitimate transactions blocked by infrastructure errors) to eliminate false positives (unauthorized transactions approved during failures). For financial infrastructure, this is the correct choice.

Composing Multiple Policies

A wallet can have multiple policies. All policies must approve a transaction for it to proceed. This lets you layer constraints:

// Base policy for all agent wallets
const basePolicy = await wg.policies.create({
  walletId: 'wal_agent',
  name: 'Org-Wide Limits',
  rules: [
    { type: 'MONTHLY_LIMIT', value: '5000.00' },
    { type: 'DAILY_LIMIT', value: '500.00' },
  ],
})

// Additional policy for this specific agent's role
const rolePolicy = await wg.policies.create({
  walletId: 'wal_agent',
  name: 'Research Agent Vendors',
  rules: [
    { type: 'VENDOR_ALLOWLIST', vendors: ['arxiv.org', 'pubmed.ncbi.nlm.nih.gov', 'semanticscholar.org'] },
    { type: 'MAX_AMOUNT', value: '50.00' },
  ],
})

Both policies must pass. The agent is constrained to research vendors, to $50 per transaction, to $500 per day, and to $5,000 per month. Change the org-wide monthly limit once and it applies to every agent wallet with that policy attached.

Updating Policies Without Redeployment

Policies are data, not code. You can change them through the API or dashboard without restarting agents or modifying application code.

// Temporarily increase a limit for a specific task
await wg.policies.update(policyId, {
  rules: [
    { type: 'MAX_AMOUNT', value: '2500.00' },  // was 500.00
    { type: 'DAILY_LIMIT', value: '500.00' },
  ],
})

// Run the high-cost task...

// Restore original limits
await wg.policies.update(policyId, {
  rules: [
    { type: 'MAX_AMOUNT', value: '500.00' },
    { type: 'DAILY_LIMIT', value: '500.00' },
  ],
})

Every policy change is logged to the immutable audit trail. You can see who changed what, when, and what it was before.

What This Means for Building with Agents

The practical implication: you don't need to trust your agent's judgment about whether a transaction is appropriate. You define the boundaries. The infrastructure enforces them. The agent operates within those boundaries.

This changes the risk profile of autonomous financial operations. Instead of "can I trust this agent to spend responsibly?" the question becomes "are my policy rules correct?" One of those questions has a programmatic answer.

The policy engine is available in Wallgent's sandbox for testing. Define a wallet, attach policies, and run test transactions to see exactly which rules fire and why. The error responses include which rule denied the transaction and what the evaluated values were — enough information to debug policies without guessing.

W

Wallgent Team

Building financial infrastructure for AI agents at Wallgent

Share:

Related Articles

Stay informed

Engineering insights
delivered to your inbox

Technical deep-dives on AI agent finance, product updates, and what we're building. No filler, no fluff.

No spam. Unsubscribe anytime.