AI agent spending limits showing token budgets, tool-call budgets, cost caps, usage monitoring, and an emergency kill switch for runaway agent workflows.

How to Put a Hard Spending Limit on AI Agents: Token Budgets, Tool Budgets, and Kill Switches

AI agents are fundamentally different from ordinary chatbot requests.

A normal API request might look like:

User
  ↓
One model call
  ↓
Response

An agent can look more like:

User
  ↓
Model
  ↓
Web search
  ↓
Model
  ↓
Database tool
  ↓
Model
  ↓
Specialist agent
  ↓
Model
  ↓
Retry
  ↓
Tool
  ↓
Model
  ↓
Final answer

One user action can therefore trigger:

multiple model requests

multiple tool calls

sub-agent calls

retries

long contexts

reasoning tokens

paid search requests

sandbox/container usage

That creates a new engineering problem:

How do we guarantee that one agent run cannot spend more than we are willing to pay?

Simply checking the bill after the agent finishes is not enough.

Neither is:

if cost > 1.00:
    stop()

because by the time that code runs, the expensive API call may already have happened.

A stronger architecture looks like this:

                    User Request
                         │
                         ▼
                 ┌───────────────┐
                 │ Budget Policy │
                 └───────┬───────┘
                         │
            ┌────────────┼────────────┐
            ▼            ▼            ▼
       Dollar Budget  Model Calls  Tool Calls
            │            │            │
            └────────────┼────────────┘
                         ▼
                  Budget Ledger
                         │
                         ▼
               Reserve Before Work
                         │
                 ┌───────┴───────┐
                 │ Enough budget?│
                 └───────┬───────┘
                     Yes │ No
                         │
             ┌───────────┴──────────┐
             ▼                      ▼
         Execute                 Reject
             │
             ▼
      Settle Actual Cost
             │
             ▼
        Continue Agent

In this tutorial, we will build that pattern using:

Python

Redis

OpenAI Responses API example

integer micro-dollar accounting

model-call limits

tool-call limits

wall-clock deadlines

per-tool budgets

budget reservations

emergency kill switches

The architecture itself is provider-independent.

The same principles apply to:

OpenAI

Anthropic

Google

AWS Bedrock

Azure AI

self-hosted model gateways

paid search APIs

browser services

vector databases

code execution platforms

The most important rule is:

Never start expensive work unless the maximum possible cost of that work fits inside the remaining budget.


Why Post-Call Cost Checking Is Not a Hard Limit

Consider this implementation:

response = call_model()

spent += response_cost(response)

if spent > budget:
    stop_agent()

Suppose:

Budget:
$0.10

Spent so far:
$0.09

Next model call:
$0.18

The sequence becomes:

$0.09 spent

↓

model call executes

↓

another $0.18 consumed

↓

total = $0.27

↓

application notices budget was $0.10

↓

agent stops

The agent stopped.

But the budget did not work.

You spent:

$0.27

against a supposed:

$0.10

limit.

That is monitoring, not enforcement.


The Correct Pattern: Reserve Before Execute

Before starting work, calculate a conservative maximum cost.

Example:

Current spending:
$0.06

Already reserved:
$0.01

Maximum budget:
$0.10

New model call could cost:
$0.025

Check:

spent
+
reserved
+
new maximum cost
<=
budget

So:

0.06
+
0.01
+
0.025
=
0.095

The call can proceed.

If instead the maximum possible call cost were:

$0.04

then:

0.06 + 0.01 + 0.04
=
0.11

The call is rejected before it starts.

That is much closer to a true hard budget.


What Does “Hard Limit” Really Mean?

There are several different limits people often mix together.

LimitWhat It Controls
Token budgetMaximum model tokens
Dollar budgetMaximum estimated monetary cost
Model-call budgetNumber of LLM requests
Tool-call budgetTotal external actions
Per-tool budgetCalls to one particular tool
Runtime budgetHow long the agent may run
Delegation budgetNumber of sub-agent calls
Retry budgetNumber of repeated attempts
Provider account budgetAggregate billing across a project/account

A good production agent normally uses several at once.

For example:

Maximum cost:
$0.25

Maximum model calls:
8

Maximum tool calls:
15

Maximum web searches:
3

Maximum emails:
1

Maximum sub-agent delegations:
4

Maximum runtime:
90 seconds

Any one limit can terminate the run.


Why a Dollar Limit Alone Is Not Enough

Suppose your agent uses a very inexpensive model.

A broken loop could make:

2,000 model calls

while still staying under a relatively generous dollar budget.

That could cause:

rate-limit problems

latency

resource exhaustion

unexpected downstream traffic

large traces

poor user experience

You therefore want both:

money limits

and

behavior limits

Why a Call Limit Alone Is Not Enough

The reverse is also true.

Suppose:

max_model_calls = 10

One model call could contain:

very large input

large reasoning budget

large output allowance

and cost far more than expected.

Therefore:

call count

and:

monetary budget

solve different problems.


The Budget Policy We Will Build

Each run will have a policy like:

MAX_SPEND_USD = 0.25

MAX_MODEL_CALLS = 8

MAX_TOOL_CALLS = 15

MAX_RUNTIME_SECONDS = 120

We will also support per-tool limits:

TOOL_LIMITS = {
    "web_search": 3,
    "database_lookup": 8,
    "send_email": 1,
}

And maximum billable cost for each externally charged tool:

TOOL_MAX_COST_USD = {
    "web_search": 0.02,
    "database_lookup": 0.00,
    "send_email": 0.005,
}

These numbers are examples.

Use the actual pricing of the services your application calls.


Do Not Hard-Code Provider Prices in Application Logic

Model and tool pricing changes.

Avoid:

if model == "some-model":
    cost = tokens * 0.000002

scattered across your codebase.

Instead use configuration.

For example:

MODEL_INPUT_USD_PER_MILLION=...
MODEL_OUTPUT_USD_PER_MILLION=...

Your pricing configuration can then be updated independently.

Always verify current rates from the provider’s official pricing page before deploying.


Why We Will Store Money as Integer Micro-Dollars

Floating-point money is dangerous.

Avoid:

spent = 0.1 + 0.2

for accounting logic.

Instead:

$1
=
1,000,000 micro-dollars

So:

$0.25
=
250,000 micro-dollars

Our Redis ledger stores only integers.

That allows atomic operations such as:

HINCRBY

without floating-point accounting errors.


Project Architecture

Our runtime will look like:

                       Agent Run
                           │
                           ▼
                     BudgetManager
                           │
                           ▼
                         Redis
                  ┌────────┴────────┐
                  │                 │
               Run State      Reservations
                  │                 │
                  ▼                 ▼
            spent_micro_usd   pending cost
            reserved_usd
            model_calls
            tool_calls
            killed
            deadline

Before every billable operation:

reserve

After it completes:

settle

If it fails ambiguously:

forfeit reservation

If a known local failure happens before the request is sent:

release reservation

That distinction matters.


Step 1 — Start Redis

Using Docker:

docker run -d \
  --name agent-budget-redis \
  -p 6379:6379 \
  redis:8

Verify:

docker exec \
  agent-budget-redis \
  redis-cli PING

Expected:

PONG

Redis is useful here because reservations need to remain correct when:

multiple workers

parallel tools

multiple containers

multiple agent branches

try to spend the same budget concurrently.


Step 2 — Create the Python Project

Create:

mkdir ai-agent-budget

cd ai-agent-budget

python -m venv .venv

Activate it.

macOS/Linux:

source .venv/bin/activate

Windows:

.venv\Scripts\activate

Install:

pip install \
  openai \
  redis \
  python-dotenv

Create:

ai-agent-budget/
│
├── budget.py
├── pricing.py
├── model_client.py
├── tools.py
└── demo.py

Step 3 — Convert Dollars to Micro-Dollars Safely

Create:

pricing.py
from decimal import (
    Decimal,
    ROUND_CEILING,
)


MICRO_USD_PER_USD = (
    Decimal("1000000")
)


def usd_to_micro(
    dollars: str | Decimal,
) -> int:

    value = (
        Decimal(str(dollars))
        * MICRO_USD_PER_USD
    )

    return int(
        value.to_integral_value(
            rounding=ROUND_CEILING
        )
    )


def micro_to_usd(
    micro_usd: int,
) -> Decimal:

    return (
        Decimal(micro_usd)
        / MICRO_USD_PER_USD
    )

Now:

usd_to_micro("0.25")

returns:

250000

Step 4 — Calculate Token Cost Conservatively

Provider pricing is usually expressed as:

USD per 1 million tokens

Interestingly, the numeric value can also be treated as:

micro-dollars per token

because:

$1 / 1,000,000 tokens

=

1 micro-dollar / token

Add:

def token_cost_micro(
    input_tokens: int,
    output_tokens: int,
    input_usd_per_million: str,
    output_usd_per_million: str,
) -> int:

    input_cost = (
        Decimal(input_tokens)
        * Decimal(
            input_usd_per_million
        )
    )

    output_cost = (
        Decimal(output_tokens)
        * Decimal(
            output_usd_per_million
        )
    )

    total_micro_usd = (
        input_cost
        + output_cost
    )

    return int(
        total_micro_usd
        .to_integral_value(
            rounding=ROUND_CEILING
        )
    )

Suppose:

input price:
$2 / million tokens

output price:
$10 / million tokens

input:
10,000 tokens

maximum output:
2,000 tokens

Then:

input:
10,000 × 2 microUSD
=
20,000 microUSD
=
$0.02

output:
2,000 × 10 microUSD
=
20,000 microUSD
=
$0.02

maximum request cost:
$0.04

Use the Most Expensive Applicable Rate for Reservation

Prompt caching may reduce actual cost.

Do not depend on that discount when reserving budget.

For a conservative reservation:

assume uncached input

unless your billing contract makes another rate guaranteed.

Likewise, if your processing mode can introduce:

regional surcharge

fast-processing premium

cache-write charge

other billing dimensions

include the maximum applicable amount in the reservation calculation.

A hard limit only works when:

Every billable dimension has a known upper bound.


Step 5 — Define the Budget Run

Create:

budget.py

Start with:

import re
import time
import uuid

import redis

from pricing import (
    usd_to_micro,
)


class BudgetExceeded(
    RuntimeError
):
    pass


class AgentKilled(
    RuntimeError
):
    pass


class BudgetManager:

    def __init__(
        self,
        client: redis.Redis,
    ) -> None:

        self.client = client

Step 6 — Create a Budget Ledger

Add:

def create_run(
    self,
    *,
    max_spend_usd: str,
    max_model_calls: int,
    max_tool_calls: int,
    max_runtime_seconds: int,
    ttl_seconds: int = 86400,
) -> str:

    run_id = str(
        uuid.uuid4()
    )

    key = self._run_key(
        run_id
    )

    now_ms = int(
        time.time() * 1000
    )

    deadline_ms = (
        now_ms
        + (
            max_runtime_seconds
            * 1000
        )
    )

    self.client.hset(
        key,
        mapping={
            "limit_micro_usd":
                usd_to_micro(
                    max_spend_usd
                ),

            "spent_micro_usd":
                0,

            "reserved_micro_usd":
                0,

            "model_calls":
                0,

            "tool_calls":
                0,

            "max_model_calls":
                max_model_calls,

            "max_tool_calls":
                max_tool_calls,

            "deadline_ms":
                deadline_ms,

            "killed":
                0,

            "kill_reason":
                "",

            "created_ms":
                now_ms,
        },
    )

    self.client.expire(
        key,
        ttl_seconds,
    )

    return run_id

Add the helpers:

def _run_key(
    self,
    run_id: str,
) -> str:

    return (
        f"agent:budget:run:"
        f"{run_id}"
    )


def _reservation_key(
    self,
    reservation_id: str,
) -> str:

    return (
        "agent:budget:reservation:"
        + reservation_id
    )

Step 7 — Why Reservations Need to Be Atomic

Imagine two sub-agents run simultaneously.

Current budget:

remaining:
$0.05

Agent A checks:

Can I spend $0.04?

Yes.

At the same time Agent B checks:

Can I spend $0.04?

Yes.

Both execute.

Total:

$0.08

The limit has been violated.

The bug happened because:

check

and

reserve

were two separate operations.

They need to happen atomically.


Step 8 — Use a Redis Lua Script for Atomic Reservation

Add this near the top of budget.py:

RESERVE_LUA = """
local run_key = KEYS[1]
local reservation_key = KEYS[2]
local global_kill_key = KEYS[3]

local now_ms = tonumber(ARGV[1])
local amount = tonumber(ARGV[2])
local kind = ARGV[3]
local name = ARGV[4]
local specific_limit = tonumber(ARGV[5])
local reservation_ttl_ms = tonumber(ARGV[6])

if redis.call(
    'GET',
    global_kill_key
) == '1' then
    return {0, 'GLOBAL_KILL'}
end

if redis.call(
    'EXISTS',
    run_key
) == 0 then
    return {0, 'RUN_NOT_FOUND'}
end

if redis.call(
    'HGET',
    run_key,
    'killed'
) == '1' then
    return {0, 'RUN_KILLED'}
end

local deadline = tonumber(
    redis.call(
        'HGET',
        run_key,
        'deadline_ms'
    )
)

if now_ms > deadline then
    return {0, 'DEADLINE_EXCEEDED'}
end

local spent = tonumber(
    redis.call(
        'HGET',
        run_key,
        'spent_micro_usd'
    ) or '0'
)

local reserved = tonumber(
    redis.call(
        'HGET',
        run_key,
        'reserved_micro_usd'
    ) or '0'
)

local limit = tonumber(
    redis.call(
        'HGET',
        run_key,
        'limit_micro_usd'
    )
)

if spent + reserved + amount > limit then
    return {0, 'SPEND_LIMIT'}
end

if kind == 'model' then

    local current = tonumber(
        redis.call(
            'HGET',
            run_key,
            'model_calls'
        ) or '0'
    )

    local maximum = tonumber(
        redis.call(
            'HGET',
            run_key,
            'max_model_calls'
        )
    )

    if current + 1 > maximum then
        return {0, 'MODEL_CALL_LIMIT'}
    end

    redis.call(
        'HINCRBY',
        run_key,
        'model_calls',
        1
    )

elseif kind == 'tool' then

    local total = tonumber(
        redis.call(
            'HGET',
            run_key,
            'tool_calls'
        ) or '0'
    )

    local total_max = tonumber(
        redis.call(
            'HGET',
            run_key,
            'max_tool_calls'
        )
    )

    if total + 1 > total_max then
        return {0, 'TOOL_CALL_LIMIT'}
    end

    local field =
        'tool_calls:' .. name

    local specific = tonumber(
        redis.call(
            'HGET',
            run_key,
            field
        ) or '0'
    )

    if specific + 1 > specific_limit then
        return {0, 'SPECIFIC_TOOL_LIMIT'}
    end

    redis.call(
        'HINCRBY',
        run_key,
        'tool_calls',
        1
    )

    redis.call(
        'HINCRBY',
        run_key,
        field,
        1
    )

else
    return {0, 'INVALID_KIND'}
end

redis.call(
    'HINCRBY',
    run_key,
    'reserved_micro_usd',
    amount
)

redis.call(
    'HSET',
    reservation_key,
    'run_key',
    run_key,
    'amount_micro_usd',
    amount,
    'kind',
    kind,
    'name',
    name,
    'status',
    'reserved',
    'created_ms',
    now_ms
)

redis.call(
    'PEXPIRE',
    reservation_key,
    reservation_ttl_ms
)

return {1, 'OK'}
"""

The key operation is:

spent
+
already reserved
+
new reservation

being tested inside one Redis transaction-like script.

No second worker can slip through between checking and reserving.


Step 9 — Create a Reservation Method

Add:

def reserve(
    self,
    *,
    run_id: str,
    amount_micro_usd: int,
    kind: str,
    name: str,
    specific_limit: int = 1000000,
) -> str:

    if not re.fullmatch(
        r"[A-Za-z0-9_-]{1,100}",
        name,
    ):
        raise ValueError(
            "Invalid budget operation name."
        )

    reservation_id = str(
        uuid.uuid4()
    )

    result = self.client.eval(
        RESERVE_LUA,
        3,
        self._run_key(
            run_id
        ),
        self._reservation_key(
            reservation_id
        ),
        "agent:budget:global_kill",
        int(
            time.time() * 1000
        ),
        amount_micro_usd,
        kind,
        name,
        specific_limit,
        86400000,
    )

    allowed = int(
        result[0]
    )

    reason = str(
        result[1]
    )

    if not allowed:

        if reason in {
            "GLOBAL_KILL",
            "RUN_KILLED",
        }:
            raise AgentKilled(
                reason
            )

        raise BudgetExceeded(
            reason
        )

    return reservation_id

With decode_responses=True on the Redis connection, the returned reason will already be a normal string.


Step 10 — Create the Redis Connection

For example:

import redis


redis_client = redis.Redis(
    host="localhost",
    port=6379,
    decode_responses=True,
)

Then:

budget = BudgetManager(
    redis_client
)

Step 11 — Settle a Reservation After the Work Finishes

Once a call completes, we know the actual cost.

We now want:

reserved
→ decrease

spent
→ increase

atomically.

Add:

SETTLE_LUA = """
local run_key = KEYS[1]
local reservation_key = KEYS[2]

local actual = tonumber(
    ARGV[1]
)

if redis.call(
    'EXISTS',
    reservation_key
) == 0 then
    return {0, 'RESERVATION_NOT_FOUND'}
end

local status = redis.call(
    'HGET',
    reservation_key,
    'status'
)

if status ~= 'reserved' then
    return {0, 'ALREADY_SETTLED'}
end

local reserved_amount = tonumber(
    redis.call(
        'HGET',
        reservation_key,
        'amount_micro_usd'
    )
)

redis.call(
    'HINCRBY',
    run_key,
    'reserved_micro_usd',
    -reserved_amount
)

redis.call(
    'HINCRBY',
    run_key,
    'spent_micro_usd',
    actual
)

redis.call(
    'HSET',
    reservation_key,
    'actual_micro_usd',
    actual,
    'status',
    'settled'
)

if actual > reserved_amount then

    redis.call(
        'HSET',
        run_key,
        'killed',
        1,
        'kill_reason',
        'reservation_underestimated'
    )

    return {
        0,
        'ACTUAL_EXCEEDED_RESERVATION'
    }
end

return {1, 'OK'}
"""

Why an Actual Cost Greater Than the Reservation Is Serious

If:

reserved:
$0.02

actual:
$0.04

then your cost model was wrong.

Possible causes include:

incorrect pricing configuration

unexpected tool charges

provider pricing change

missing billing dimension

output limit not enforced

processing surcharge not included

The safest response is:

kill the run

because your guarantee is no longer trustworthy.


Step 12 — Add the Settlement Method

def settle(
    self,
    run_id: str,
    reservation_id: str,
    actual_micro_usd: int,
) -> None:

    result = self.client.eval(
        SETTLE_LUA,
        2,
        self._run_key(
            run_id
        ),
        self._reservation_key(
            reservation_id
        ),
        actual_micro_usd,
    )

    if int(result[0]) != 1:

        raise RuntimeError(
            str(
                result[1]
            )
        )

Step 13 — Handle Ambiguous API Failures Conservatively

This is one of the most important details in the entire tutorial.

Suppose you call an API.

The provider completes the request and charges you.

But:

your network times out

before the response arrives.

Your application sees:

TimeoutError

You do not know whether the provider charged:

$0

or

$0.03

If you simply release the reservation:

reserved budget returns to pool

and the agent retries, you may overspend.

For a hard-cap design:

An ambiguous network failure should consume the reserved maximum until you can prove that it did not incur cost.


Step 14 — Add a Forfeit Operation

The simplest safe implementation is:

def forfeit(
    self,
    run_id: str,
    reservation_id: str,
) -> None:

    key = self._reservation_key(
        reservation_id
    )

    amount = int(
        self.client.hget(
            key,
            "amount_micro_usd",
        )
    )

    self.settle(
        run_id=
            run_id,

        reservation_id=
            reservation_id,

        actual_micro_usd=
            amount,
    )

This may overestimate the bill.

That is intentional.

For a hard budget:

false positive spending

is safer than:

unaccounted spending

You can reconcile the ledger later if your provider offers authoritative usage records.


When Is It Safe to Release a Reservation?

You can release a reservation only when you know the expensive operation never started.

Examples:

local input validation failed

tool argument schema rejected

budget middleware failed before HTTP dispatch

user canceled before request was sent

You should not automatically release it after:

HTTP timeout

connection reset after request transmission

worker crash during API call

unknown provider error

because the upstream service may already have performed the work.


Step 15 — Add Model Pricing Configuration

In .env:

OPENAI_API_KEY=...

MODEL_NAME=your-model

MODEL_INPUT_USD_PER_MILLION=...
MODEL_OUTPUT_USD_PER_MILLION=...

Use the current official pricing page to populate these values.

Do not copy old prices from tutorials.


Step 16 — Count Input Tokens Before the Model Call

One challenge remains.

To reserve model cost, we need to know the input-token count before execution.

For the OpenAI Responses API, there is a dedicated input-token counting endpoint.

Create:

model_client.py
import os

from openai import OpenAI

from pricing import (
    token_cost_micro,
)


client = OpenAI()


MODEL = os.environ[
    "MODEL_NAME"
]


INPUT_RATE = os.environ[
    "MODEL_INPUT_USD_PER_MILLION"
]


OUTPUT_RATE = os.environ[
    "MODEL_OUTPUT_USD_PER_MILLION"
]

Step 17 — Build the Exact Model Request First

A good pattern is:

def build_request(
    user_input: str,
) -> dict:

    return {
        "model":
            MODEL,

        "instructions":
            (
                "You are a technical "
                "assistant."
            ),

        "input":
            user_input,
    }

The important part is that token counting and model execution should use the same:

model

instructions

input

tool definitions

conversation state

where applicable.


Step 18 — Reserve the Worst-Case Model Cost

def call_model_with_budget(
    *,
    budget,
    run_id: str,
    user_input: str,
    max_output_tokens: int,
):

    request = build_request(
        user_input
    )


    token_count = (
        client
        .responses
        .input_tokens
        .count(
            **request
        )
    )


    input_tokens = (
        token_count
        .input_tokens
    )


    maximum_cost = (
        token_cost_micro(

            input_tokens=
                input_tokens,

            output_tokens=
                max_output_tokens,

            input_usd_per_million=
                INPUT_RATE,

            output_usd_per_million=
                OUTPUT_RATE,
        )
    )


    reservation_id = (
        budget.reserve(

            run_id=
                run_id,

            amount_micro_usd=
                maximum_cost,

            kind=
                "model",

            name=
                MODEL,
        )
    )


    try:

        response = (
            client
            .responses
            .create(

                **request,

                max_output_tokens=
                    max_output_tokens,
            )
        )

    except Exception:

        budget.forfeit(
            run_id,
            reservation_id,
        )

        raise


    actual_cost = (
        token_cost_micro(

            input_tokens=
                response
                .usage
                .input_tokens,

            output_tokens=
                response
                .usage
                .output_tokens,

            input_usd_per_million=
                INPUT_RATE,

            output_usd_per_million=
                OUTPUT_RATE,
        )
    )


    budget.settle(
        run_id,
        reservation_id,
        actual_cost,
    )


    return response

Now the model cannot start unless:

input cost
+
maximum possible output cost

fits into the budget.


Why max_output_tokens Is Critical

Without:

max_output_tokens

you do not have a bounded output cost.

A model might generate considerably more than expected.

For hard-budget enforcement:

maximum output

must be part of the reservation calculation.

The Responses API’s reported:

output_tokens

includes generated tokens beyond just visible prose, including relevant internal output categories.

That is why we budget against the API’s token limit rather than estimating visible words.


Do Not Estimate Tokens From Word Count

Avoid:

estimated_tokens = (
    len(text.split())
    * 1.3
)

This can be useful for approximate UI estimates.

It is not appropriate for strict budget enforcement.

Different:

languages

code

JSON

tool definitions

model tokenizers

can produce very different token counts.

Use the provider’s tokenizer or token-counting endpoint where a hard preflight calculation is required.


Cached Tokens Need Careful Accounting

Prompt caching may make the actual bill smaller.

For hard reservation purposes, you can conservatively charge all input tokens at the ordinary input rate.

Example:

Actual provider billing:

8,000 normal input tokens
2,000 cached tokens

Budget ledger:

10,000 input tokens
at normal rate

Your internal budget becomes conservative.

That means the agent may stop slightly earlier than necessary, but it will not overspend because you assumed a discount that failed to materialize.


Exact Billing vs Conservative Budgeting

There are two useful numbers:

provider_estimated_bill

conservative_budget_charge

They do not have to be identical.

For user-facing analytics you may want precise cost accounting.

For enforcement you often want:

worst-case accounting

The latter is intentionally pessimistic.


Step 19 — Create Tool Budgets

Models are only part of agent cost.

A tool may charge:

per request

per search

per browser minute

per container session

per image

per document page

per GB

per transaction

Define:

tools.py
from dataclasses import (
    dataclass,
)


@dataclass(
    frozen=True
)
class ToolBudget:

    max_calls:
        int

    max_cost_usd_per_call:
        str


TOOL_BUDGETS = {

    "web_search":
        ToolBudget(
            max_calls=3,
            max_cost_usd_per_call=
                "0.02",
        ),

    "database_lookup":
        ToolBudget(
            max_calls=10,
            max_cost_usd_per_call=
                "0",
        ),

    "send_email":
        ToolBudget(
            max_calls=1,
            max_cost_usd_per_call=
                "0.005",
        ),
}

Again, these prices are illustrative.

Replace them with the real maximum cost of your integrations.


Step 20 — Wrap Every Paid Tool

Create:

from pricing import (
    usd_to_micro,
)


def execute_budgeted_tool(
    *,
    budget,
    run_id: str,
    tool_name: str,
    tool_function,
    *args,
    **kwargs,
):

    policy = (
        TOOL_BUDGETS[
            tool_name
        ]
    )


    reservation_id = (
        budget.reserve(

            run_id=
                run_id,

            amount_micro_usd=
                usd_to_micro(
                    policy
                    .max_cost_usd_per_call
                ),

            kind=
                "tool",

            name=
                tool_name,

            specific_limit=
                policy.max_calls,
        )
    )


    try:

        result = tool_function(
            *args,
            **kwargs
        )

    except Exception:

        budget.forfeit(
            run_id,
            reservation_id,
        )

        raise


    actual_cost = (
        result.actual_cost_micro_usd
    )


    budget.settle(
        run_id,
        reservation_id,
        actual_cost,
    )


    return result

Now the tool has two independent protections:

maximum call count

and

maximum money

Free Tools Still Need Tool Budgets

Suppose:

database_lookup

costs no external API money.

You might set:

maximum cost:
$0

But still limit:

maximum calls:
10

Why?

Because the tool still consumes:

database resources

network traffic

latency

context tokens

downstream rate limits

“Free” does not mean unlimited.


Separate Read and Write Tool Budgets

A useful policy might be:

read_database:
10 calls

web_search:
3 calls

send_email:
1 call

delete_record:
0 calls without approval

You can combine this with the approval-gate architecture from:

Build a Secure MCP Gateway for AI Agents

Budget enforcement controls:

how much

while authorization controls:

whether the action is allowed at all

They solve different problems.


Tool Budget vs Approval Gate

Consider:

send_email

You may enforce:

permission:
email:send

human approval:
required

tool limit:
1

maximum tool cost:
$0.005

The call only executes when every condition passes:

authorized

AND

approved

AND

within budget

AND

under call limit

That is defense in depth.


Step 21 — Build a Per-Run Kill Switch

Sometimes you want to stop a run manually.

Add:

def kill_run(
    self,
    run_id: str,
    reason: str,
) -> None:

    self.client.hset(
        self._run_key(
            run_id
        ),
        mapping={
            "killed":
                1,

            "kill_reason":
                reason,
        },
    )

Now every new reservation fails.

Example:

budget.kill_run(
    run_id,
    "Operator requested stop"
)

A Kill Switch Does Not Undo an In-Flight Request

This limitation is important.

If the application has already sent:

model request

or

external tool request

a Redis flag cannot magically reverse that request.

The kill switch guarantees:

no new budgeted operations begin

after the flag is observed.

For strong containment you should also use:

request timeouts

provider cancellation where available

maximum output limits

bounded tools

Step 22 — Add a Global Emergency Kill Switch

Sometimes you need to stop:

every agent

immediately.

For example:

billing incident

runaway loop bug

compromised tool

provider pricing misconfiguration

Set:

redis-cli SET \
  agent:budget:global_kill \
  1

Every new reservation will fail because our Lua script checks:

agent:budget:global_kill

before authorizing work.


Re-Enable Agent Execution

After the incident is resolved:

redis-cli DEL \
  agent:budget:global_kill

Do not build the global kill switch into an endpoint that any normal application user can trigger.

Protect it with:

administrator authentication

RBAC

audit logging

Add Tool-Specific Kill Switches

Sometimes only one integration is dangerous.

Example:

web search:
healthy

database:
healthy

email API:
accidentally sending duplicates

You should be able to disable:

send_email

without shutting down the whole platform.

A production design can maintain:

agent:kill:tool:send_email

and check it before tool reservation.


Useful Kill-Switch Hierarchy

A mature platform might support:

Global kill
    ↓
Tenant kill
    ↓
Agent-type kill
    ↓
Run kill
    ↓
Tool kill

Examples:

global:
stop everything

tenant:
stop customer ACME

agent:
disable research agents

run:
stop run abc-123

tool:
disable send_email

Step 23 — Add a Runtime Deadline

Our run already stores:

deadline_ms

and checks it before every reservation.

If:

current time > deadline

no more work begins.

This prevents:

hour-long agent loops

even if token costs remain low.


Runtime Limits Need Network Timeouts Too

A logical deadline is not enough.

Suppose:

agent deadline:
120 seconds

but a tool’s HTTP timeout is:

600 seconds

The agent may remain stuck in the tool call long after its runtime budget expires.

Set tool and model timeouts consistently.

For example:

overall agent:
120 sec

model request:
45 sec

tool request:
20 sec

Exact values depend on your workflow.


Step 24 — Add an Agent-Step Limit

Consider a loop:

Model
 ↓
Tool
 ↓
Model
 ↓
Tool
 ↓
Model
 ↓
Tool
 ...

Model-call and tool-call budgets already help.

It can still be useful to define:

MAX_AGENT_STEPS = 12

and increment it on each loop iteration.

This makes agent behavior easy to reason about.


OpenAI Agents SDK Also Supports Turn Limits

If you use the OpenAI Agents SDK, its runner already supports:

max_turns

A turn is an AI invocation in the agent loop, including the tool calls resulting from that model turn.

Exceeding the limit raises:

MaxTurnsExceeded

That is useful as another guardrail.

However:

A turn limit is not a monetary budget.

Use it alongside budget enforcement rather than instead of one.


Agents SDK Also Tracks Aggregate Token Usage

The Agents SDK exposes usage including:

requests

input_tokens

output_tokens

total_tokens

per-request usage

across the run.

That is useful for:

analytics

monitoring

billing reports

evaluations

But usage is observed after calls have occurred.

For strict pre-call dollar enforcement, you still need a reservation architecture around billable work.


Multi-Agent Systems Need One Shared Budget

Suppose your coordinator delegates to:

Research Agent

Data Agent

Writing Agent

Bad architecture:

Coordinator:
$0.25 budget

Research:
$0.25 budget

Data:
$0.25 budget

Writing:
$0.25 budget

One user request can now spend:

$1.00

even though someone thought the workflow budget was:

$0.25

Instead give every branch the same:

run_id

and therefore the same Redis ledger.


One Shared Budget Across A2A Agents

For the multi-agent architecture from:

MCP vs A2A: Build a Multi-Agent System in Practice

the budget should flow like:

User Request
     │
     ▼
run_id = abc
     │
     ▼
Coordinator
   /       \
  /         \
 ▼           ▼
Diagnostics Release
run=abc     run=abc
   │           │
   └─────┬─────┘
         ▼
    Shared Redis
    Budget Ledger

Now concurrent sub-agents cannot independently exceed the workflow cap.


Delegation Should Have Its Own Limit

Even if every specialist shares the same dollar budget, prevent unlimited recursion.

For example:

MAX_DELEGATIONS = 4

You may also track:

maximum delegation depth

such as:

MAX_AGENT_DEPTH = 3

This prevents:

Agent A
→ Agent B
→ Agent C
→ Agent A
→ Agent B
...

Retries Must Consume Budget

This is critical.

Do not write:

for attempt in range(5):

    try:
        call_model()
        break

    except Exception:
        retry()

while counting only the final successful request.

Every retry can incur real cost.

Each retry should create:

new reservation

new model-call count

If there is not enough budget remaining:

do not retry

Budget-Aware Retry Logic

Conceptually:

for attempt in range(
    MAX_RETRIES
):

    try:

        return (
            call_model_with_budget(
                ...
            )
        )

    except BudgetExceeded:

        raise

    except TemporaryError:

        if attempt == (
            MAX_RETRIES - 1
        ):
            raise

        sleep_with_backoff()

The budget wrapper remains inside the retry loop.

Every attempt is accounted for.

For retry architecture, see:

REST API Rate Limiting and Exponential Backoff in Python


Do Not Give Retries a Separate Hidden Budget

Bad:

normal calls:
$0.25 budget

retries:
unlimited

Retries are still work.

They should draw from the same root budget.


Parallel Tool Calls Need Atomic Reservations

Suppose the model requests four tools simultaneously:

search web

query database

read GitHub

call analytics API

Each worker performs:

reserve()

against the same Redis run.

Redis serializes the atomic Lua operations.

If only three requests fit within the remaining budget:

three proceed

one receives BudgetExceeded

That is exactly what you want.


Do Not Reserve Only After Launching Parallel Work

Wrong:

launch 10 tools

↓

calculate whether they fit

Correct:

reserve tool 1
reserve tool 2
reserve tool 3
...

↓

only launch successful reservations

Budget authorization must precede work.


Hosted Tools Require Special Attention

Some model providers offer tools executed inside the model platform, such as:

web search

file search

code execution

browser execution

containers

Those calls may have separate charges.

If one model request can autonomously make an unknown number of paid hosted-tool calls, then a pure token reservation may not bound the request cost.

For a true hard cap you need one of these:

a provider-side maximum tool-call limit

a known maximum number of tool calls

a fixed maximum charge

or

disable that hosted tool for strict-budget runs

If the number of billable operations inside one provider request cannot be bounded:

You cannot honestly claim an exact per-run hard dollar limit for that configuration.

That is an important architectural limitation.


Prefer Application-Owned Tools for Strict Budgets

If hard budgeting matters, an architecture like:

Model
   ↓
requests tool
   ↓
Your application
   ↓
Budget reservation
   ↓
Tool executes

gives you more control than:

Model provider
   ↓
autonomously executes unknown paid tools

This is another reason MCP gateways can be useful.

Your application remains the policy enforcement point.


Budget MCP Tools at the Gateway

A secure MCP gateway can enforce:

authorization

approval

budget

rate limit

audit

before calling the actual tool.

Example:

Agent
  ↓
MCP Gateway
  ↓
Scope check
  ↓
Approval check
  ↓
Budget reservation
  ↓
Tool execution
  ↓
Settlement

See:

Build a Secure MCP Gateway for AI Agents


Step 25 — Inspect the Budget State

Add:

def get_status(
    self,
    run_id: str,
) -> dict:

    raw = self.client.hgetall(
        self._run_key(
            run_id
        )
    )

    if not raw:
        raise KeyError(
            "Budget run not found."
        )

    return {
        key:
            int(value)
            if key in {
                "limit_micro_usd",
                "spent_micro_usd",
                "reserved_micro_usd",
                "model_calls",
                "tool_calls",
                "max_model_calls",
                "max_tool_calls",
                "deadline_ms",
                "killed",
                "created_ms",
            }
            else value

        for key, value
        in raw.items()
    }

Show Remaining Budget to the Agent Runtime

Calculate:

def remaining_micro_usd(
    status: dict,
) -> int:

    return (
        status[
            "limit_micro_usd"
        ]

        - status[
            "spent_micro_usd"
        ]

        - status[
            "reserved_micro_usd"
        ]
    )

You can use this to choose a cheaper strategy.


Budget-Aware Model Routing

Suppose:

remaining:
$0.20

You may use your preferred model.

Later:

remaining:
$0.015

The runtime could switch to:

smaller model

shorter context

smaller output limit

rather than immediately failing.

Architecture:

Remaining Budget
      │
      ├── High
      │     ↓
      │  stronger model
      │
      ├── Medium
      │     ↓
      │  economical model
      │
      └── Low
            ↓
        final-answer mode

This is graceful degradation.


But Never Let the Model Choose Its Own Budget

Do not tell the model:

You have $0.20.
Please decide how much you are allowed to spend.

and trust its answer.

The model can participate in:

planning

but budget enforcement must remain deterministic.

Correct:

Model proposes next action

↓

BudgetManager decides whether it can execute

Add a Final-Answer Reserve

One subtle problem appears when an agent uses all its budget researching.

Then:

no budget remains

to generate the final response.

You can solve this by reserving a portion from the start.

Example:

Total run budget:
$0.25

Reserved final-answer budget:
$0.04

Research budget:
$0.21

The agent cannot consume the final-answer reserve for intermediate work.


Two-Bucket Budget Architecture

Run Budget
   │
   ├── Exploration
   │     $0.21
   │
   └── Final Response
         $0.04

You can implement this as:

two separate Redis ledgers

or one ledger with:

reserved_final_budget

subtracted from the amount available for ordinary work.


Tool Budgets Can Also Reserve Critical Actions

Suppose an agent may:

search
search
search
send final email

If searches consume the entire budget, the actual business action cannot happen.

You might therefore create:

analysis budget

execution budget

separately.

This is particularly useful for transactional agents.


Budget by User Tier

Different product plans may have different policies.

Example:

TierCost/RunModel CallsTool Calls
Free$0.0234
Pro$0.15815
Enterprise$0.501530

The policy should come from your application database.

Never let users submit:

{
  "max_spend_usd": 1000
}

and have the backend trust it.


Budget by Workflow Type

Different tasks deserve different budgets.

Example:

simple classification:
$0.002

customer-support answer:
$0.03

deep research:
$0.50

code migration:
$1.00

The application can assign budget based on an approved workflow class.


A Budget Should Be Server-Side

Do not rely on:

const maxSpend = 0.25;

inside browser JavaScript.

A user can change it.

Budget enforcement belongs on trusted infrastructure:

API server

agent runtime

MCP gateway

with Redis or another trusted state store.


User-Facing Cost Estimates Are Still Useful

Before starting expensive workflows, you can show:

This task may use up to $0.50 of AI credits.

The actual run can still terminate earlier.

This improves transparency and helps users understand why:

deep research

costs more than:

simple summarization

Rate Limits and Spending Limits Are Different

A rate limit says:

100 requests per minute

A spending limit says:

$0.25 per run

An agent can stay under the rate limit and still spend too much.

It can also stay under its spending limit while flooding a backend with cheap calls.

Use both.


Provider-Level Spend Limits Are Defense in Depth

Your application-level budget protects:

one run

one user

one workflow

Your API provider may also support:

organization budget

project budget

billing alerts

hard monthly spend limits

Enable those as a second line of defense.

For example:

Per-run application budget:
$0.25

Daily internal budget:
$200

Provider project hard limit:
$5,000/month

Each layer protects against a different class of failure.


Provider Hard Limits Are Not a Substitute for Per-Run Budgets

A provider project limit such as:

$5,000/month

does not prevent one broken agent from spending:

$500

before the project limit is reached.

Your runtime still needs its own budget controls.


Provider Billing Enforcement May Not Be Instantaneous

Another reason to keep an application-level ledger is that provider account-level spend enforcement may use tracked billing information that is not instantaneous.

A project hard limit is excellent for containing aggregate account exposure.

It should not be treated as millisecond-level authorization for each individual tool or model call.


Step 26 — Build the Demo Run

Create:

demo.py
import redis

from budget import (
    BudgetManager,
)

from model_client import (
    call_model_with_budget,
)

from pricing import (
    micro_to_usd,
)


redis_client = redis.Redis(
    host="localhost",
    port=6379,
    decode_responses=True,
)


budget = BudgetManager(
    redis_client
)


run_id = budget.create_run(

    max_spend_usd=
        "0.10",

    max_model_calls=
        4,

    max_tool_calls=
        6,

    max_runtime_seconds=
        90,
)


print(
    "Run:",
    run_id
)


response = (
    call_model_with_budget(

        budget=
            budget,

        run_id=
            run_id,

        user_input=
            (
                "Explain connection "
                "pool exhaustion "
                "in PostgreSQL."
            ),

        max_output_tokens=
            600,
    )
)


print(
    response.output_text
)


status = budget.get_status(
    run_id
)


print(
    "Spent:",
    micro_to_usd(
        status[
            "spent_micro_usd"
        ]
    )
)


print(
    "Reserved:",
    micro_to_usd(
        status[
            "reserved_micro_usd"
        ]
    )
)

Step 27 — Test Budget Rejection

Set:

MAX_SPEND:
$0.000001

Then attempt a model call.

Expected:

BudgetExceeded:
SPEND_LIMIT

The important test is:

the model API was never called

That is the behavior we care about.


Step 28 — Test Model-Call Limits

Create:

max_model_calls = 2

Then attempt:

call 1
→ allowed

call 2
→ allowed

call 3
→ rejected

Expected reason:

MODEL_CALL_LIMIT

Step 29 — Test Tool Limits

Configure:

"web_search":
    ToolBudget(
        max_calls=2,
        max_cost_usd_per_call=
            "0.02",
    )

Then:

search 1
→ allowed

search 2
→ allowed

search 3
→ SPECIFIC_TOOL_LIMIT

Step 30 — Test Parallel Reservations

This is one of the most important tests.

Suppose:

remaining budget:
$0.05

Launch two workers simultaneously.

Each requests:

$0.04

Expected:

Worker A
→ reservation succeeds

Worker B
→ SPEND_LIMIT

or vice versa.

Never:

both succeed

Step 31 — Test the Kill Switch

Start a run.

Then execute:

redis-cli SET \
  agent:budget:global_kill \
  1

Attempt another model/tool reservation.

Expected:

GLOBAL_KILL

Then:

redis-cli DEL \
  agent:budget:global_kill

Step 32 — Test Deadline Enforcement

Create:

max_runtime_seconds = 2

Wait:

3 seconds

Then request another reservation.

Expected:

DEADLINE_EXCEEDED

Step 33 — Test Reservation Underestimation

Intentionally:

reserve:
1,000 microUSD

settle:
2,000 microUSD

Expected:

ACTUAL_EXCEEDED_RESERVATION

and:

run killed

If this ever occurs in production, investigate pricing immediately.


Monitor Budget Utilization

Useful metrics include:

average cost per run

p50 / p95 / p99 cost

budget rejection rate

model calls per run

tool calls per run

percentage of budget consumed

kill-switch events

reservation underestimation

ambiguous-call forfeits

retry cost

cost by workflow

cost by model

cost by tenant

These metrics show whether your configured limits are realistic.

For observability architecture, see:

Structured Logging and Monitoring for Cloud Services


Log Reservations as Structured Events

Example:

{
  "event": "agent_budget_reserved",
  "run_id": "abc-123",
  "operation": "model_call",
  "reserved_micro_usd": 42000,
  "remaining_micro_usd": 71000
}

After completion:

{
  "event": "agent_budget_settled",
  "run_id": "abc-123",
  "reserved_micro_usd": 42000,
  "actual_micro_usd": 27400
}

Do not log:

API keys

authorization headers

sensitive prompt contents

just because you are auditing cost.


Audit Why the Agent Stopped

A terminated run should have an explicit reason.

Examples:

SPEND_LIMIT

MODEL_CALL_LIMIT

TOOL_CALL_LIMIT

SPECIFIC_TOOL_LIMIT

DEADLINE_EXCEEDED

GLOBAL_KILL

RUN_KILLED

MAX_AGENT_DEPTH

MAX_RETRIES

This is much easier to operate than:

agent failed

Return a Graceful User Message

If the budget expires, avoid exposing internal accounting errors.

Instead:

The task reached its execution budget before all planned steps were completed. Here is the result produced so far.

When possible, return:

partial result

completed evidence

unfinished steps

rather than discarding all work.


Budget-Aware Partial Completion

Suppose the agent planned:

1. search docs
2. analyze repository
3. inspect logs
4. create detailed report

Budget expires after step 3.

A good runtime can switch to:

finalize with available evidence

instead of simply raising an exception.

This is one reason a final-response reserve is valuable.


Context Compaction Can Reduce Cost

Long-running agents often spend increasing amounts on input context.

A conversation may grow:

10K tokens
→
30K
→
80K
→
150K

even if each individual answer is small.

Context compaction can reduce future input cost.

See:

Build Long-Term Memory for AI Agents with Redis, Vector Search, and Context Compaction

However:

compaction itself

may require a model call.

That call must also consume budget.

There are no “free internal agent operations.”


Budget Compaction Calls Too

Bad:

normal agent calls
→ budgeted

memory summarization
→ not budgeted

evaluation
→ not budgeted

Your real spending can now exceed the run limit through hidden infrastructure work.

Include:

compaction

memory extraction

reranking

sub-agent calls

evaluation

tool summarization

if they happen synchronously as part of the run.


Background Work Needs Its Own Budget

If you perform:

memory extraction

offline evaluation

analytics enrichment

after the user response, that may belong to:

background_budget

rather than the original interactive-run budget.

But it must still have a budget somewhere.

Do not allow asynchronous workers to become an unmetered cost path.


Think in Terms of Cost Trees

One user request may create:

Root Request
│
├── Coordinator model
│
├── Research Agent
│     ├── model
│     ├── search
│     └── model
│
├── Database Agent
│     ├── model
│     └── database
│
└── Final model

Every node should roll up to:

one root workflow budget

You should be able to answer:

How much did this user request cost?

not merely:

How much did each microservice cost?

Charge Sub-Agent Costs to the Parent

A useful metadata model is:

root_run_id

parent_run_id

agent_id

reservation_id

Then a trace can show:

root:
$0.18

├── coordinator:
$0.03

├── diagnostics:
$0.07

├── release:
$0.04

└── final answer:
$0.04

This combines well with the multi-agent architecture described in:

MCP vs A2A in Practice


Do Not Trust Model-Reported Cost

Never ask the model:

How many tokens did you use?

and use its answer for billing.

Use:

provider usage metadata

official token counters

tool billing metadata

your own deterministic ledger

The model is not your accounting system.


Do Not Trust Tool-Reported Cost Without Validation

If a third-party tool responds:

{
  "cost": 0.01
}

decide whether that value is authoritative.

For untrusted integrations, you may prefer:

preconfigured maximum price

rather than trusting arbitrary metadata returned by the tool.


Variable-Cost Tools Need Bounded Inputs

Imagine:

transcribe_audio(file)

where cost depends on audio duration.

Before execution, determine:

duration
×
maximum price per minute

and reserve it.

Likewise:

image generation
→ number / size / quality bound

browser service
→ maximum runtime bound

storage
→ maximum bytes bound

search
→ maximum requests bound

A hard budget requires bounded parameters.


Reject Unbounded Tool Arguments

Bad:

{
  "crawl_depth": 100000
}

Good:

crawl_depth <= 3

pages <= 25

runtime <= 30 sec

This is both:

cost control

and

security control

Tool Schemas Are Part of Budget Enforcement

Suppose a tool accepts:

generate_images(
    count: int
)

The schema should enforce:

1 <= count <= 4

rather than trusting the model to request a reasonable number.

Budget safety begins before pricing calculations.


Kill Switches Should Be Operationally Accessible

A kill switch hidden inside source code is not very useful during an incident.

Operators need a controlled way to trigger:

disable workflow

disable model

disable tool

disable tenant

without waiting for:

new deployment

Possible control surfaces include:

admin dashboard

feature-flag system

Redis key

configuration service

Kill-Switch Changes Need Audit Logs

Example:

{
  "event": "tool_kill_switch_enabled",
  "tool": "send_email",
  "actor": "ops@example.com",
  "reason": "duplicate-send incident",
  "timestamp": "..."
}

A production team should be able to answer:

Who disabled the agent?

When?

Why?

When was it re-enabled?

Budget Policies Need Versioning

Suppose yesterday:

research-agent-v1
budget:
$0.10

Today:

research-agent-v2
budget:
$0.25

Store:

policy_version

with each run.

Then cost analytics can explain why behavior changed.


Example Budget Policy Object

from dataclasses import (
    dataclass,
)


@dataclass(
    frozen=True
)
class AgentBudgetPolicy:

    version:
        str

    max_spend_usd:
        str

    max_model_calls:
        int

    max_tool_calls:
        int

    max_runtime_seconds:
        int

    max_agent_depth:
        int

Example:

policy = AgentBudgetPolicy(

    version=
        "research-v3",

    max_spend_usd=
        "0.25",

    max_model_calls=
        8,

    max_tool_calls=
        15,

    max_runtime_seconds=
        120,

    max_agent_depth=
        3,
)

Store the Policy Version in Redis

Add:

policy_version

to your run hash.

That allows your logs to say:

run abc
used policy research-v3

instead of inferring configuration later.


Cost Controls Are Also a Reliability Feature

Budget enforcement is often discussed as:

financial protection

but it also protects reliability.

A runaway agent can cause:

thousands of API requests

database overload

external email flood

search quota exhaustion

rate-limit bans

worker saturation

Even if the monetary cost is small.

Budget controls limit the blast radius.


Combine Spending Limits with Rate Limits

A mature architecture might look like:

Agent Action
     │
     ▼
Authentication
     │
     ▼
Authorization
     │
     ▼
Approval
     │
     ▼
Rate Limit
     │
     ▼
Budget Reservation
     │
     ▼
Tool / Model
     │
     ▼
Audit

Each control has a different responsibility.


Cost Controls Are Not Security Controls by Themselves

A budget of:

$0.10

does not make:

delete_customer

safe.

An attacker may need only:

one cheap request

to cause damage.

Use budget controls alongside:

authorization

human approval

tool restrictions

sandboxing

not instead of them.


Hard Spending Limit Checklist

Before calling your agent budget “hard,” verify:

ControlRequired
Per-run dollar limitYes
Pre-call reservationYes
Atomic reservationYes
Output token maximumYes
Accurate input token countYes
Conservative pricingYes
Model-call limitYes
Tool-call limitYes
Per-tool call limitsRecommended
Tool maximum costRequired for paid tools
Retry accountingYes
Parallel-call accountingYes
Sub-agent shared budgetYes
Runtime deadlineYes
Kill switchYes
Ambiguous failuresConservatively charged
Pricing changesMonitored
Provider account limitRecommended
Structured loggingRecommended
Final-answer reserveUseful
Tool argument boundsRequired for variable cost
Hidden background workBudgeted separately
Unbounded hosted toolsAvoid for strict cap

What You Cannot Guarantee

It is important not to oversell this architecture.

An application can reliably refuse new work when its internal budget is exhausted.

But an exact external billing guarantee depends on the provider.

Possible sources of small differences include:

billing-accounting delays

pricing changes

hidden billing dimensions

network failures after dispatch

provider-side tool execution

regional processing charges

async work already in progress

That is why production cost control should combine:

application reservations

+

bounded provider requests

+

provider-level account limits

rather than relying on only one layer.


Provider Hard Limit vs Runtime Hard Limit

Think of them this way.

Runtime Budget

Protects:

one agent run

Example:

Never authorize more than $0.25
for this user request.

Provider Budget

Protects:

overall project/account

Example:

Stop API traffic after monthly tracked
spend reaches $5,000.

You want both.


A Production Architecture

A mature deployment might look like:

                       User Request
                            │
                            ▼
                       Agent API
                            │
                            ▼
                    Budget Policy
                            │
                            ▼
                        Redis
                 Atomic Budget Ledger
                            │
           ┌────────────────┼─────────────────┐
           │                │                 │
           ▼                ▼                 ▼
       Model API        MCP Gateway       Sub-Agent
           │                │                 │
           │           Tool Budget            │
           │                │                 │
           └────────────────┼─────────────────┘
                            │
                            ▼
                     Usage Settlement
                            │
                            ▼
                    Structured Logging
                            │
                            ▼
                       Cost Dashboard

Provider Account
      │
      └── Project hard spend limit

Deploying the Budget Service

If your agent runtime runs on multiple Cloud Run instances, Kubernetes pods, or VMs:

do not keep budgets in process memory

Bad:

current_spend = {}

because each instance has a different copy.

Use:

Redis

or

another strongly coordinated shared store

for live reservations.

For Cloud Run architecture, see:

Deploy Python & Express Apps to Google Cloud Run


Budget Data and Long-Term Analytics Can Use Different Stores

Redis is excellent for:

live atomic reservations

You may still write final results to:

PostgreSQL

BigQuery

data warehouse

for analytics.

Architecture:

Redis
=
real-time enforcement

Warehouse
=
historical reporting

Do not make a slow analytics database part of every budget authorization if low latency matters.


Reconcile Budget Records Asynchronously

After a run ends:

application ledger

vs

provider usage records

can be reconciled.

Look for:

missing usage

underestimated cost

unexpected tool billing

pricing configuration drift

If discrepancies appear, alert before allowing the problem to affect many runs.


Alert on Reservation Underestimation Immediately

This event should be rare:

ACTUAL_EXCEEDED_RESERVATION

Treat it as:

high-severity operational incident

because it means your hard-budget assumption may no longer be valid.

Possible automated response:

kill affected run

disable pricing configuration

activate model kill switch

alert on-call engineer

Pricing Configuration Should Be Tested

Create automated tests such as:

known model
→ pricing exists

all enabled processing modes
→ pricing exists

all paid tools
→ max cost defined

all tool limits
→ finite

Fail deployment if a paid model or tool has:

unknown price

instead of silently treating it as free.


Unknown Price Should Mean Deny

Bad:

price = (
    pricing.get(model)
    or 0
)

That means:

unknown model
=
free

Correct:

if model not in pricing:
    raise RuntimeError(
        "Pricing not configured."
    )

Fail closed.


Final Example Policy

A robust research agent might use:

Maximum run cost:
$0.25

Final-answer reserve:
$0.04

Model calls:
8

Tool calls:
12

Web searches:
3

Database reads:
5

Emails:
0

Sub-agent delegations:
3

Agent depth:
2

Retries:
2 per operation

Runtime:
120 seconds

That is dramatically safer than:

Keep going until you finish.

Internal DigitalStackRoom Guides

For securing the tools an agent can call:

Build a Secure MCP Gateway for AI Agents

For multi-agent orchestration:

MCP vs A2A: Build a Multi-Agent System in Practice

For long-running context and memory:

Build AI Agent Memory with Redis and Vector Search

For retry handling:

REST API Rate Limiting and Exponential Backoff in Python

For production monitoring:

Structured Logging and Monitoring for Cloud Services

For API security:

Secure API Endpoints with JWT, CORS & Environment Variables

For deploying the runtime:

Deploy Python & Express Apps to Google Cloud Run

For LLM tracing and cost observability:

Langfuse vs Arize Phoenix vs Weave


Final Thoughts

The biggest mistake in AI cost control is thinking:

track usage

and:

limit usage

are the same thing.

They are not.

If your architecture is:

call model

↓

calculate cost

↓

discover that budget was exceeded

you have built monitoring.

A real enforcement architecture works in the opposite order:

calculate maximum possible cost

↓

atomically reserve that amount

↓

execute only if reservation succeeds

↓

measure actual usage

↓

settle reservation

↓

continue only while budget remains

The same principle applies to:

model calls

web searches

paid APIs

browser sessions

sub-agents

code execution

image generation

any other billable operation

Then add independent behavioral limits:

maximum model calls

maximum tool calls

maximum retries

maximum agent depth

maximum runtime

Finally, add kill switches that operators can use when something goes wrong.

The complete pattern becomes:

                         Agent
                           │
                           ▼
                     Next Action
                           │
                           ▼
                    Kill Switch?
                      │         │
                    Yes         No
                      │         │
                    Stop        ▼
                            Call Limit?
                              │     │
                            Hit     OK
                              │     │
                            Stop    ▼
                           Budget Reserve
                              │
                       ┌──────┴──────┐
                       │             │
                     Denied       Approved
                       │             │
                      Stop           ▼
                                  Execute
                                     │
                                     ▼
                                Settle Cost
                                     │
                                     ▼
                                Continue

That turns cost control from:

"Please don't use too many tokens."

into deterministic infrastructure.

And that is what AI agents need.

Models can decide:

what they want to do next

but trusted application code should decide:

whether the organization is willing to pay for it.

Official Documentation and Technical References

OpenAI API

OpenAI API Pricing

OpenAI Responses API

OpenAI Input Token Counting API

OpenAI Token Counting Guide

OpenAI Agents SDK

OpenAI Agents SDK — Running Agents

OpenAI Agents SDK — Usage Tracking

OpenAI Spend Controls

OpenAI — Managing API Projects and Spend Limits

OpenAI — Troubleshooting API Usage and Spend Limits

Redis

Redis Documentation

redis-py

Redis Transactions

Redis Scripting with Lua

Scroll to Top