
API-backed agents are cheap to start and expensive to leave unsupervised. A single retry loop can burn hundreds of dollars of token requests before anyone notices. The reliable fix is three layers that back each other up: a provider-side spend cap, a client-side per-job ceiling checked after every response, and a launchd watchdog that unloads the agent when daily spend crosses a line.
Most API-orchestration agents are network-bound, not compute-bound. The Figment virtual Intel macOS VM at $29/month (2 vCPU, 8 GiB RAM, 80 GiB disk) is enough to run a Python or Node agent loop continuously against an LLM API. The VM runs macOS on Intel; it is a real macOS environment that supports Intel-compatible toolchains (including Xcode 26 or earlier), not an SSH jump to another machine. Access is through the browser console, and outbound HTTPS to your LLM provider works normally.
Choose an M-series Mac from $95/month when the agent needs to execute ARM-only binaries, call Vision or Core ML, or hold a local model in RAM. Specific chip, RAM and disk at that entry price are confirmed in the quote — ask before you commit. The Managed Build Mac at $249/month adds one M-series Mac with CI setup, backups, monitoring, and 30 minutes of monthly support, which is useful when the agent runs unattended overnight and you want hardware watched by someone else. Physical plans currently collect requests until supply is confirmed, so start the conversation early via /mac-hosting/start.
Every major LLM provider exposes a cap you can set before writing any agent code. Set it before you deploy, not after the first incident.
| Provider | Where to set it | Verify on vendor docs |
|---|---|---|
| OpenAI | Platform → Settings → Limits (per project / per org) | openai.com/api |
| Anthropic | Console → Plans & Billing (workspace spend limit) | anthropic.com/pricing |
| Google AI | AI Studio / Cloud billing budget on the project | ai.google.dev/pricing |
| OpenRouter | Per-API-key credit limit on a prepaid balance | openrouter.ai/docs |
Enforcement behaviour differs: prepaid balances stop at zero, metered plans may lag by a short window as usage events roll up. Read the current enforcement wording on your provider's pricing or limits page and treat the cap as a backstop, not the first line of defence. The client-side counter below is what keeps a single run from eating the entire month.
Accumulate estimated cost during the run and raise an exception when the ceiling is crossed. The example below is unexecuted illustrative code; wire your model's current per-million token prices into cost_per_m_in and cost_per_m_out from the provider's pricing page.
# budget_guard.py
PER_JOB_LIMIT_USD = 0.05 # stop this run at five cents
class BudgetGuard:
def __init__(self, ceiling: float):
self.ceiling = ceiling
self.spent = 0.0
def charge(self, input_tokens: int, output_tokens: int,
cost_per_m_in: float, cost_per_m_out: float) -> None:
cost = (input_tokens / 1_000_000) * cost_per_m_in \
+ (output_tokens / 1_000_000) * cost_per_m_out
self.spent += cost
if self.spent >= self.ceiling:
raise RuntimeError(
f"Per-job budget exceeded: ${self.spent:.5f} >= ${self.ceiling}"
)
def log_summary(self) -> str:
return f"spent={self.spent:.5f}"
Call guard.charge(...) after every API response using the token counts the provider returns in its usage field. When the ceiling is hit the agent exits non-zero rather than retrying. Write guard.log_summary() to the log file so the watchdog can parse it. If your provider returns a cost value directly in the response payload, prefer that over a client-side calculation.
Fill in the table once with your model's published per-token prices. Run one sample job first to measure realistic token counts; a ceiling built on guessed averages is the main way these setups fail in production.
| Budget item | Formula | Your value |
|---|---|---|
| Input tokens per call (avg) | measure on a sample run | — |
| Output tokens per call (avg) | measure on a sample run | — |
| Cost per call (USD) | (in/1M × rate_in) + (out/1M × rate_out) | — |
| Max calls per job run | set in your agent config | — |
| Per-job ceiling (USD) | cost_per_call × max_calls | — |
| Job runs per day | 86 400 ÷ StartInterval seconds | — |
| Daily ceiling (USD) | per_job × runs_per_day | — |
| Monthly ceiling (USD) | daily × 30 | — |
| Provider cap (USD) | monthly + headroom you tolerate | — |
Worked example. Suppose a call averages 2 000 input tokens at $3/M and 500 output tokens at $15/M: (2000/1M × 3) + (500/1M × 15) = 0.006 + 0.0075 = $0.0135 per call. Max 3 calls per job gives a per-job ceiling of $0.0405 — round up to $0.05 to leave room for a retry. One run per hour = 24 runs/day × $0.0405 = $0.972 daily, or $29.16 for a 30-day month — about the cost of the $29 VM itself, which is a useful sanity check on the agent's unit economics.
Stop conditions, act on the first one triggered:
BudgetGuard raises RuntimeError → exit 1, do not retry inside the same run.Create two plists in ~/Library/LaunchAgents/. Apple documents the full key reference in Creating Launchd Jobs.
Agent job (com.yourname.agent.plist):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>Label</key><string>com.yourname.agent</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/python3</string>
<string>/Users/agent/jobs/run_agent.py</string>
</array>
<key>StartInterval</key><integer>3600</integer>
<key>StandardOutPath</key><string>/Users/agent/logs/agent.log</string>
<key>StandardErrorPath</key><string>/Users/agent/logs/agent-err.log</string>
</dict></plist>
Daily watchdog (com.yourname.budget-watchdog.plist) parses the log and unloads the agent if daily spend crosses the line. Replace 0.97 with your daily ceiling:
#!/bin/bash
# budget_watchdog.sh
set -euo pipefail
DAILY_CEILING=0.97
LOG=/Users/agent/logs/agent.log
PLIST="$HOME/Library/LaunchAgents/com.yourname.agent.plist"
spent=$(grep "$(date +%Y-%m-%d)" "$LOG" \
| grep -oE 'spent=[0-9.]+' \
| cut -d= -f2 \
| awk '{s+=$1} END{print (s>0)?s:0}')
if awk "BEGIN{exit !($spent >= $DAILY_CEILING)}"; then
launchctl unload "$PLIST" 2>/dev/null || true
echo "$(date -u +%FT%TZ) agent paused spent=${spent} ceiling=${DAILY_CEILING}" \
>> /Users/agent/logs/watchdog.log
fi
Schedule the watchdog with its own plist using StartCalendarInterval once per day, or a short StartInterval for finer granularity. Load both with launchctl load <plist>. For a notification when the watchdog fires, wrap the final echo with a call to the macOS Shortcuts command line tool: shortcuts run "Alert Agent Paused", where the Shortcut sends you a Message or Mail.
When the watchdog has paused the agent:
watchdog.log and agent-err.log to see which stop condition fired.PER_JOB_LIMIT_USD, max calls, or StartInterval in the worksheet, not in ad hoc edits to the script.launchctl load ~/Library/LaunchAgents/com.yourname.agent.plist.The $29 Figment VM hosts this setup comfortably; move to an M-series or Managed Build plan when you need ARM execution, more RAM, or someone else watching the box. Start at /mac-hosting or request a configuration at /mac-hosting/start.
Related reading