Use Cases

Building Governed Financial Agents

Financial agents need approval paths, limits, tenant boundaries, and evidence before money or records move.

Yaz CalebMarch 7, 202616 min
  • Financial AI agents need runtime authorization before refunds, ACH, wires, invoice approvals, trades, or treasury changes execute.
  • OAuth scopes, API keys, and SSO identify access, but they do not decide whether this payment, refund, or trade should run now.
  • Effective controls combine thresholds, maker-checker approvals, policy versioning, and audit records tied to each attempted action.

A financial agent can pass functional testing, hold valid API credentials, and still execute the wrong action if recommendation and execution share the same authority. The gap is not unique to trading: payments, refunds, invoice approvals, treasury operations, and portfolio rebalancing all need per-action limits, approval thresholds, and a record of who allowed the tool call to run.

The Regulatory Landscape

Three regulatory frameworks converge on AI agents in financial services, and each demands specific controls:

  • SEC examination priorities that mention AI and emerging technology. The SEC's Division of Examinations added "AI and emerging technology" as a standalone priority for the first time. Examiners are looking at how firms use AI in trading, portfolio management, and client communications. They specifically flag: whether AI outputs are subject to human review before execution, whether firms have tested AI tools for bias and accuracy, and whether adequate disclosures are made to clients about AI use. An AI agent that autonomously executes trades without documented guardrails is a finding waiting to happen.
  • SR 11-7 (Federal Reserve Model Risk Management). The Fed's guidance on model risk management was written for statistical models but maps directly to AI agents. It requires: effective challenge of model outputs (an independent party must validate results), ongoing monitoring with quantitative thresholds, and a model inventory with documented limitations. An AI trading agent is a model. Its tool calls are model outputs. SR 11-7 requires that those outputs be challengeable, monitorable, and documented.
  • SOX Sections 302 and 404. For publicly traded companies, Sarbanes-Oxley requires CEO/CFO certification that internal controls over financial reporting are effective (Section 302) and independent auditor attestation of those controls (Section 404). If an AI agent can initiate, approve, or modify financial transactions, it is part of your internal control environment. An agent that can both initiate and approve a transaction violates segregation of duties. An agent without audit logs makes Section 404 attestation hard to defend.

Five Financial Agent Risks

Financial agents create risk at five specific points. Each requires a distinct control:

  1. Unauthorized transactions. The agent executes a trade or transfer that no human authorized. This is the aggregate-exposure scenario. The control: per-transaction authorization with amount thresholds and mandatory human approval above the threshold.
  2. Limit breaches. The agent stays within per-transaction limits but violates aggregate limits: position concentration, daily volume caps, counterparty exposure. The control: budget-scoped authorization that tracks cumulative exposure, not just individual transactions.
  3. Regulatory reporting failures. The agent executes reportable transactions without generating the required reports (SARs, CTRs, large trader reports). The control: post-action hooks that trigger reporting workflows when transaction characteristics match reporting thresholds.
  4. Segregation of duties violations. The agent both recommends and executes, or both initiates and approves. SOX and most internal control frameworks require separation between these functions. The control: role-scoped policies where an agent with "analyst" context can recommend but not execute, and an agent with "trader" context can execute but only pre-approved recommendations.
  5. Decision record gaps. The agent takes actions that are not logged, or recorded without sufficient context for reconstruction. The control: structured decision records for every protect() call, with full tool call arguments, policy evaluation details, and execution results.

Implementation: Financial Agent with protect()

The core pattern wraps every financial tool call in protect(). The agent code is clean. The authorization logic lives entirely in the policy, not in application code:

financial_agent.py
import os
import anthropic
from veto import Veto, Decision
from decimal import Decimal

client = anthropic.Anthropic()
veto = Veto(api_key=os.environ["VETO_API_KEY"], project="trading-agent")

TOOLS = [
    {
        "name": "get_market_data",
        "description": "Fetch current price and volume for a ticker",
        "input_schema": {
            "type": "object",
            "properties": {
                "ticker": {"type": "string"},
                "exchange": {"type": "string", "enum": ["NYSE", "NASDAQ", "LSE"]},
            },
            "required": ["ticker"],
        },
    },
    {
        "name": "execute_trade",
        "description": "Submit a trade order to the broker",
        "input_schema": {
            "type": "object",
            "properties": {
                "ticker": {"type": "string"},
                "side": {"type": "string", "enum": ["buy", "sell"]},
                "quantity": {"type": "integer"},
                "order_type": {"type": "string", "enum": ["market", "limit"]},
                "limit_price": {"type": "number"},
            },
            "required": ["ticker", "side", "quantity", "order_type"],
        },
    },
    {
        "name": "transfer_funds",
        "description": "Transfer funds between accounts",
        "input_schema": {
            "type": "object",
            "properties": {
                "from_account": {"type": "string"},
                "to_account": {"type": "string"},
                "amount": {"type": "number"},
                "currency": {"type": "string"},
            },
            "required": ["from_account", "to_account", "amount", "currency"],
        },
    },
    {
        "name": "approve_recommendation",
        "description": "Record approval of a trading recommendation",
        "input_schema": {
            "type": "object",
            "properties": {
                "recommendation_id": {"type": "string"},
                "approved_by": {"type": "string"},
            },
            "required": ["recommendation_id", "approved_by"],
        },
    },
]


async def run_financial_agent(user_message: str, user_context: dict):
    """Financial agent with Veto authorization on each governed tool call."""
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model=os.environ["ANTHROPIC_MODEL"],
            max_tokens=4096,
            tools=TOOLS,
            messages=messages,
        )

        if response.stop_reason != "tool_use":
            return response

        tool_blocks = [b for b in response.content if b.type == "tool_use"]
        tool_results = []

        for block in tool_blocks:
            decision = veto.protect(
                tool=block.name,
                arguments=block.input,
                context={
                    "user_id": user_context["user_id"],
                    "role": user_context["role"],
                    "account_id": user_context["account_id"],
                    "desk": user_context.get("desk", "general"),
                },
            )

            if decision.action == Decision.ALLOW:
                result = await execute_tool(block.name, block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": str(result),
                })

            elif decision.action == Decision.DENY:
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": f"BLOCKED: {decision.reason}",
                    "is_error": True,
                })

            elif decision.action == Decision.APPROVAL_REQUIRED:
                approval = veto.wait_for_approval(
                    decision_id=decision.id,
                    timeout=decision.approval_timeout,
                )
                if approval.granted:
                    result = await execute_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result),
                    })
                else:
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": f"DENIED by {approval.reviewer}: {approval.reason}",
                        "is_error": True,
                    })

        messages.append({"role": "assistant", "content": response.content})
        messages.append({"role": "user", "content": tool_results})

Policy: Transaction Limits, Approvals, and Budget Caps

The policy defines the authorization rules declaratively. The authorization check stays at middleware regardless of how complex the rules get. Note the budget blocks: these are Veto's economic authorization feature, which tracks cumulative spend across multiple scopes (per-trade, daily, and weekly) without requiring any state management in your application code:

financial_agent_policy.yaml
name: trading-agent-production
description: Authorization policy for trading desk agent

rules:
  - tool: get_market_data
    action: allow
    constraints:
      rate_limit: 500/hour

  - tool: execute_trade
    conditions:
      # Small trades: auto-approve
      - match:
          arguments.quantity: "<= 100"
          arguments.order_type: "limit"
        action: allow
        budget:
          scope: daily
          limit: 50000
          currency: USD
          track_by: context.account_id

      # Medium trades: require senior trader approval
      - match:
          arguments.quantity: "<= 1000"
        action: require_approval
        approval:
          channel: workspace
          timeout: 300s
          reviewers:
            - role: senior_trader
          context_shown:
            - tool_name
            - arguments
            - session_history
            - portfolio_exposure

      # Large trades: require desk head + compliance
      - match:
          arguments.quantity: "> 1000"
        action: require_approval
        approval:
          tiers:
            - level: 1
              reviewers:
                - role: desk_head
              timeout: 600s
            - level: 2
              reviewers:
                - role: compliance_officer
              timeout: 1800s
          final_escalation: deny

  - tool: transfer_funds
    conditions:
      - match:
          arguments.amount: "<= 10000"
        action: allow
        budget:
          scope: daily
          limit: 100000
          currency: USD
          track_by: context.account_id

      - match:
          arguments.amount: "<= 100000"
        action: require_approval
        approval:
          channel: workspace
          timeout: 600s
          reviewers:
            - role: treasury_ops

      - match:
          arguments.amount: "> 100000"
        action: deny
        reason: "Transfers > $100K require manual processing"

    budget:
      scope: weekly
      limit: 500000
      currency: USD
      track_by: context.account_id

  - tool: approve_recommendation
    conditions:
      # Segregation of duties: analyst role cannot approve
      - match:
          context.role: "analyst"
        action: deny
        reason: "Segregation of duties: analysts cannot approve recommendations"
      - match:
          context.role: "(senior_trader|desk_head)"
        action: allow

default_action: deny
logging:
  level: full
  retention: 7years
  reason: "SOX Section 802: record retention"

Economic Authorization: Multi-Scope Budgets

Per-transaction limits are necessary but not sufficient. Aggregate exposure happens when each individual trade is within limits, but the total position is not. Veto's economic authorization tracks budgets across multiple scopes simultaneously:

budget_scopes.yaml
# Budget tracking across multiple time windows and dimensions
budgets:
  per_trade:
    execute_trade:
      max_notional: 50000
      currency: USD

  daily:
    execute_trade:
      max_notional: 500000
      max_trades: 200
      currency: USD
      reset: "16:00 America/New_York"
    transfer_funds:
      max_amount: 100000
      max_transfers: 20
      currency: USD

  weekly:
    execute_trade:
      max_notional: 2000000
      currency: USD
    transfer_funds:
      max_amount: 500000
      currency: USD

  per_ticker:
    execute_trade:
      max_position_pct: 25
      basis: portfolio_value
      track_by: arguments.ticker

  per_counterparty:
    transfer_funds:
      max_exposure: 250000
      currency: USD
      track_by: arguments.to_account

When the daily budget for execute_trade hits $500,000, the next trade is denied regardless of its individual size. When a single ticker exceeds 25% of portfolio value, further buys of that ticker are blocked. The agent does not need to track any of this. Veto maintains the running totals and evaluates them on every protect() call.

DIY Limit Checking vs Declarative Policy

Teams that build financial agents without a policy engine end up with limit checking scattered across application code. Every tool handler has its own validation logic, its own threshold constants, and its own logging format. Changing a limit means deploying new code. Adding a new approval tier means refactoring the execution loop. The comparison:

comparison.txt
DIY Limit Checking  Declarative Policy (Veto)
─────────────────────────────────────  ─────────────────────────────────────
Limits hardcoded in application code  Limits defined in YAML policy
Change requires code deploy  Change requires policy update (no deploy)
Each tool has its own validation; one protect() call per tool
Budget tracking is manual state mgmt  Budget tracking is evaluated at decision time
Decision record format varies per tool  Structured decision record on governed decisions
Segregation of duties is ad-hoc  Segregation enforced by role in context
No aggregate limit tracking  Multi-scope budgets (daily/weekly/ticker)
Testing mocks business logic  Policy evaluation is unit-testable
Compliance evidence is scattered  Each governed decision is a compliance record
Adding approval tiers = refactor  Adding approval tiers = YAML change

The declarative approach is auditable because the decision is reviewable. When your SOX auditor asks "show me the control that gates transactions over $50,000," you point to the YAML policy and the decision record. With DIY limit checking, you point to a code review and hope the reviewer caught the edge case.

SR 11-7 Mapping: Agent Controls as Model Risk Management

SR 11-7 centers model risk management on effective challenge, ongoing monitoring, and documentation. Here is how Veto maps agent-control evidence to that operating model:

  • Effective challenge: The require_approval action creates an effective-challenge workflow. An independent human reviews the agent's proposed action and can approve, deny, or modify it. The approval log records who challenged, what they decided, and why.
  • Ongoing monitoring: Budget tracking provides quantitative monitoring. When the agent approaches a limit, Veto can alert before the limit is hit. Decision records provide qualitative monitoring: denial rates, approval rates, and patterns that indicate drift in agent behavior.
  • Documentation: The YAML policy is the model's documentation. It describes exactly what the agent is authorized to do, under what conditions, with what limits. Policy version history tracks changes over time. Decision records provide the evidence that the documented controls are enforced.

First governed call

Adding financial controls to an existing agent is a single integration point: wrap your tool execution with protect(), define your limits and approval thresholds in a YAML policy, and Veto handles budget tracking, approval routing, and decision records. Your agent code stays identical. The controls are entirely external and auditable.

Sign up to add financial guardrails to your agent, or read the financial agents implementation guide and SOC 2 evidence documentation for detailed walkthroughs.

FAQ

What controls do fintech AI agents need before moving money?

They need policy checks for amount, destination, account, vendor, user role, environment, risk score, and business context. High-value refunds, ACH pulls, wires, trades, and vendor changes should require human approval or maker-checker review before execution.

Why are OAuth scopes not enough for financial AI agents?

OAuth scopes say an application can call a payment or banking API. They do not evaluate whether a specific transfer amount, beneficiary, refund, invoice, or trade is allowed under your policy at that moment.

How does Veto help with financial agent control evidence?

Veto records the attempted tool call, arguments, matched policy, decision, approver, timestamp, and policy version. That creates evidence for incident review, SOX-style change control, payment operations, and regulated AI governance.

Sign up