Guide ·

Spend caps for what an agent's tools charge, not only its tokens

Since 22 July 2026 the OpenAI API has hard spend limits for an organization and for each project: when tracked spend reaches a monthly cap, API requests return HTTP 429 (OpenAI API changelog, spend limits guide). OpenAI notes that enforcement is not instantaneous, so recorded spend can go slightly over the cap.

That limit counts what OpenAI bills. It does not count what an agent’s tools do on other services: a card charged through a payments tool, an email sent, a record written to a CRM, a call to a paid API. Those are tool calls, and they cost money or cannot be taken back whatever the model’s bill says.

okgate sits between the agent and its MCP servers and sees every tool call before it is sent. It counts each call against the caps in okgate.yaml, per run and per day. A call that would take a count over its cap is refused with CAP_EXCEEDED, and the server never receives it.

The caps init writes

npx -p @agentwares/agentguard okgate init writes these into okgate.yaml (from the run on 7 October 2026 with agentguard 0.1.6, okgate’s name until 8 October; the output below is that run’s, verbatim):

# Blast-radius caps. The call that would exceed a cap gets CAP_EXCEEDED with the remaining budget.
# `writes` counts every write or spend; `deletes`/`emails` count the subsets matched by `counters`.
caps:
  per_run:
    tool_calls: 400
    writes: 50
    deletes: 10
    emails: 5
    spend_usd: 25
  per_day:
    spend_usd: 200

# Dollar amounts. Declare how to read the amount from a spend tool's arguments; results reporting
# cost_usd / amount_usd are picked up automatically. Unknown spend tools cost `default_usd`.
spend:
  tools: {}
  #  stripe_create_charge: { amount_arg: amount, divisor: 100, currency_arg: currency }
  #  openai_*: { fixed_usd: 0.01 }
  default_usd: 0

per_run starts from zero for each run. A run is named by the X-Run-Id header over HTTP, else a runId in the call’s _meta, else the MCP session, else one id per proxy process. per_day adds up every run through the UTC day, in .okgate/state.json on the machine running the proxy.

Where the dollar amount comes from

When a rule names a currency_arg and the call’s currency is not USD, USDC, USDT or DAI, the call counts $0 toward spend_usd; cap such a tool by count with a counter of its own.

Two runs against a $20 daily cap

The demo CRM that ships with okgate has a crm_charge_card tool that takes amount_cents. init classified it as a spend from the verb in its name; in a dry-run it counted $0 (see what a dry-run of an agent’s MCP writes shows). Three edits to the file init wrote: enforce mode, the daily cap lowered to $20 for the demo, and one rule saying where the amount is.

mode: enforce

caps:
  per_run:
    tool_calls: 400
    writes: 50
    deletes: 10
    emails: 5
    spend_usd: 25
  per_day:
    spend_usd: 20

spend:
  tools:
    crm_charge_card: { amount_arg: amount_cents, divisor: 100, currency_arg: currency }

The scripted agent then ran twice against the fake CRM. In the first run its $12 charge went through:

$ agentguard report --run run_1
agentguard report — run run_1

55 tool calls between 2026-10-07T05:25:36.435Z and 2026-10-07T05:25:36.573Z across crm.

What actually happened upstream

50 writes, 1 send, $12.00 spent.

Where agentguard stepped in
#whencodetoolwhy
52026-10-07T05:25:36.468ZAPPROVAL_REQUIREDcrm_delete_contact"crm_delete_contact" needs a human's approval before it runs (approval apr_a6afd3a6bf)
102026-10-07T05:25:36.487ZLOOP_DETECTEDcrm_update_contact"crm_update_contact" was called 3 times with the same arguments in the last 30 calls
552026-10-07T05:25:36.573ZCAP_EXCEEDEDcrm_create_contactwrites cap for this run is 50; used 50, this call would make it 51
Calls

By class: write 52, read 2, spend 1By outcome: ok 52, pending 1, halted 1, blocked 1

toolclasscalls
crm_create_contactwrite46
crm_update_contactwrite4
crm_list_contactsread1
crm_get_contactread1
crm_delete_contactwrite1
crm_send_emailwrite1
crm_charge_cardspend1

Audit chain: 111 entries, verified (~/demo-spend/.agentguard/audit.jsonl).Other runs: agentguard report --all

In the second run the same charge would have taken the day to $24. It was refused before it reached the CRM, and the rest of the run carried on under its other limits:

$ agentguard report --run run_2
agentguard report — run run_2

56 tool calls between 2026-10-07T05:25:39.034Z and 2026-10-07T05:25:39.158Z across crm.

What actually happened upstream

50 writes, 1 send.

Where agentguard stepped in
#whencodetoolwhy
602026-10-07T05:25:39.067ZAPPROVAL_REQUIREDcrm_delete_contact"crm_delete_contact" needs a human's approval before it runs (approval apr_a6afd3a6bf)
622026-10-07T05:25:39.072ZCAP_EXCEEDEDcrm_charge_cardspend_usd cap for today is $20; used $12, this call would make it $24
652026-10-07T05:25:39.080ZLOOP_DETECTEDcrm_update_contact"crm_update_contact" was called 3 times with the same arguments in the last 30 calls
1112026-10-07T05:25:39.158ZCAP_EXCEEDEDcrm_create_contactwrites cap for this run is 50; used 50, this call would make it 51
Calls

By class: write 53, read 2, spend 1By outcome: ok 52, blocked 2, pending 1, halted 1

toolclasscalls
crm_create_contactwrite47
crm_update_contactwrite4
crm_list_contactsread1
crm_get_contactread1
crm_delete_contactwrite1
crm_send_emailwrite1
crm_charge_cardspend1

Audit chain: 111 entries, verified (~/demo-spend/.agentguard/audit.jsonl).Other runs: agentguard report --all

This is the tool result the agent got back for the charge, as the audit log has it:

{
  "code": "CAP_EXCEEDED",
  "cause": "spend_usd cap for today is $20; used $12, this call would make it $24",
  "fix": "stop for today and report to the user; a human can raise caps.per_day in agentguard.yaml",
  "retryable": false,
  "details": {
    "scope": "per_day",
    "counter": "spend_usd",
    "limit": 20,
    "used": 12,
    "attempted": 24,
    "remaining": {
      "tool_calls": {
        "per_run": 395
      },
      "writes": {
        "per_run": 47
      },
      "deletes": {
        "per_run": 10
      },
      "emails": {
        "per_run": 4
      },
      "spend_usd": {
        "per_run": 25,
        "per_day": 8
      }
    },
    "runId": "run_2"
  }
}

And the counters afterwards:

$ agentguard status --run run_2
policy   ~/demo-spend/agentguard.yaml (mode: enforce, 1 upstreams)
kill     off
http     not running (agentguard proxy --http)
audit    111 entries in ~/demo-spend/.agentguard/audit.jsonl
pending  1 approval — agentguard approvals
run      run_2
  per_run  tool_calls 52/400  writes 50/50  deletes 0/10  emails 1/5  spend_usd 0/25
  per_day  spend_usd 12/20

The other refusals in those reports come from the same file: the per-run cap of 50 writes stopped each run’s burst of creates, the loop breaker stopped a repeated update, and in enforce mode the delete returned APPROVAL_REQUIRED, which a person clears with okgate approve <id>.

What these caps do not cover

Try it

In the project whose MCP config your agent uses:

$ npx -p @agentwares/agentguard okgate init