Scheduled AI Agent Runs: Budgeting Credits per Run
Budget a scheduled agent by dividing your monthly credits by the number of runs, then enforcing the result as a hard call cap inside each run. On 1,000 free credits, an hourly agent gets 1 call per run and a daily agent gets 32. A small Python ledger that reads the X-Credits-Used header keeps every run inside that cap.
How do you set a credit budget for each agent run?
Start from the monthly credits you are willing to spend and divide by the number of scheduled runs in the month. Round down, and that is the most calls one run may make. A successful call costs the credits shown on its endpoint, which is 1 credit for every Google Search API endpoint, so the call cap and the credit cap are the same number.
runs per month = runs per day × days in the month
calls per run = floor(monthly credits ÷ runs per month)
monthly spend = runs per month × calls per run
Failed calls cost nothing, so a cap based on attempts is slightly conservative. That is the right direction to be wrong in. If you want the broader picture of spend limits for agents, AI agent API cost control covers it; this article is the per-run, scheduled case.
What does an hourly or daily schedule cost against the free credits?
Every account gets 1,000 free credits a month after verifying an email address, with no credit card. Spread over a 31-day month, that is enough for one call on each of 744 hourly runs, or 32 calls on each of 31 daily runs. The table shows the arithmetic for other schedules and for prepaid packs.
| Schedule | Runs in a 31-day month | Credits available | Calls per run | Credits used |
|---|---|---|---|---|
| Daily | 31 | 1,000 (free) | 32 | 31 × 32 = 992 |
| Every 6 hours | 124 | 1,000 (free) | 8 | 124 × 8 = 992 |
| Hourly | 744 | 1,000 (free) | 1 | 744 × 1 = 744 |
| Every 15 minutes | 2,976 | 1,000 (free) | 0 | Not possible on the free credits |
| Daily | 31 | 10,000 (Starter pack) | 322 | 31 × 322 = 9,982 |
| Hourly | 744 | 10,000 (Starter pack) | 13 | 744 × 13 = 9,672 |
| Hourly | 744 | 50,000 (Build pack) | 67 | 744 × 67 = 49,848 |
Two things stand out. Frequency matters far more than depth: hourly runs are 24 times as frequent as daily ones, so the per-run allowance falls from 32 calls to 1 on the free credits, and from 322 to 13 on the Starter pack. And a 15-minute schedule needs at least 2,976 credits a month before it can make a single call per run. Pricing has what credits cost; purchased credits last 12 months, so an unspent month doesn't vanish.
Which request will the agent spend its credits on?
For a research agent, the workhorse is GET /v1/google-search/search, which returns organic results, related searches, local results and more as JSON for 1 credit. The parameters below are the ones an agent usually sets. The full list is on the Search endpoint page.
| Parameter | Type | Required | Notes |
|---|---|---|---|
query |
string | yes | The search text. |
hl |
string | no | Interface language, up to 2 characters. |
gl |
string | no | Country, up to 2 characters. |
safe |
string | no | One of active, off. |
page |
integer | no | Result page. |
amount |
integer | no | Integer parameter, see the endpoint page. |
A single call looks like this:
curl "https://api.perceptlayer.com/v1/google-search/search?query=best+restaurants+in+Berlin&hl=en&safe=off" \
-H "X-API-Key: $API_KEY"
The response is a JSON object with named fields. A recorded response, trimmed:
{
"search_information": {
"query_displayed": "best restaurants in Berlin",
"organic_results_count": 6
},
"organic_results": [
{
"position": 1,
"title": "The Best Restaurants in Berlin - Food Guide & Map",
"link": "https://andershusa.com/best-restaurants-berlin-germany-guide/",
"snippet": "Dec 3, 2025 — Tim Raue is the most internationally renowned restaurant in Berlin. …",
"source": "Anders & Kaitlin"
},
…
],
"related_searches": […],
…
}
The cost is on the response headers, not in the body: X-Credits-Used is what this call cost and X-Credits-Remaining is the balance afterwards. Those two headers are all the ledger below needs. The credits documentation describes them.
How do you enforce a per-run call cap in Python?
Wrap every request in one function that checks the cap before the call and records the cost after it. Three limits do the work: a maximum number of attempts per run, a monthly credit limit read from a local ledger, and a reserve you never touch. When any of them trips, the function raises an exception instead of making the request.
import os
import sqlite3
import uuid
import requests
BASE = "https://api.perceptlayer.com"
KEY = os.environ["API_KEY"]
class BudgetExceeded(Exception):
pass
class Ledger:
def __init__(self, path="credits.db"):
self.db = sqlite3.connect(path)
self.db.execute(
"""CREATE TABLE IF NOT EXISTS calls (
ts TEXT, run_id TEXT, path TEXT,
status INTEGER, credits INTEGER, remaining INTEGER)"""
)
def add(self, run_id, path, status, credits, remaining):
self.db.execute(
"INSERT INTO calls VALUES (datetime('now'), ?, ?, ?, ?, ?)",
(run_id, path, status, credits, remaining),
)
self.db.commit()
def month_spent(self):
row = self.db.execute(
"SELECT COALESCE(SUM(credits), 0) FROM calls "
"WHERE ts >= date('now', 'start of month')"
).fetchone()
return row[0]
class RunBudget:
def __init__(self, ledger, max_calls, month_limit, reserve=0):
self.ledger = ledger
self.max_calls = max_calls
self.month_limit = month_limit
self.reserve = reserve
self.run_id = uuid.uuid4().hex[:8]
self.calls = 0
self.remaining = None # last balance seen in a response header
def get(self, path, **params):
if self.calls >= self.max_calls:
raise BudgetExceeded(f"call cap of {self.max_calls} reached")
if self.ledger.month_spent() + 1 > self.month_limit:
raise BudgetExceeded("monthly credit limit reached")
if self.remaining is not None and self.remaining <= self.reserve:
raise BudgetExceeded("balance is at the reserve")
self.calls += 1
try:
r = requests.get(
BASE + path,
params=params,
headers={"X-API-Key": KEY},
timeout=30,
)
except requests.Timeout:
# The reply was lost; the call may have succeeded and been billed.
self.ledger.add(self.run_id, path, 0, 1, None)
raise
used = int(r.headers.get("X-Credits-Used", 0))
left = r.headers.get("X-Credits-Remaining")
if left is not None:
self.remaining = int(left)
self.ledger.add(self.run_id, path, r.status_code, used, self.remaining)
if r.status_code == 402:
raise BudgetExceeded("insufficient_credits")
r.raise_for_status()
return r.json()
The timeout branch is the part most budget code gets wrong. Requests have no idempotency key, so a client timeout can hide a call that went through. The ledger records it at the endpoint's 1 credit, the worst case, and the next successful response's X-Credits-Remaining corrects the picture. The timeout=30 argument bounds how long requests waits for the server (Requests timeouts). Retry web API calls without paying twice goes deeper on that.
The ledger's "start of month" is the calendar month. Treat the ledger as a local guard rail and X-Credits-Remaining as the source of truth. SQLite's date functions are documented in the SQLite date and time functions reference if you want to move the window.
How do you hand the capped function to an agent?
Expose one tool to the model, backed by RunBudget.get, and make the cap visible in the tool's result. When the budget is spent, return a plain message instead of raising, so the model finishes with what it has instead of looping.
def make_search_tool(budget):
def search_web(query: str) -> str:
try:
data = budget.get(
"/v1/google-search/search", query=query, hl="en"
)
except BudgetExceeded as e:
return f"No more searches this run ({e}). Answer from what you have."
except requests.RequestException as e:
return f"Search failed ({type(e).__name__}). Answer from what you have."
rows = data.get("organic_results", [])[:5]
return "\n".join(
f"{r['position']}. {r['title']} - {r['link']}\n {r.get('snippet', '')}"
for r in rows
)
return search_web
ledger = Ledger()
budget = RunBudget(ledger, max_calls=32, month_limit=992, reserve=8)
search_web = make_search_tool(budget)
Passing only five rows with four fields also keeps the model's context small, which is a separate saving covered in trimming API responses for LLM context. For the tool definition itself, see the tool schema checklist.
If you connect the agent through the MCP server instead, the same rule applies. search_endpoints and get_endpoint only look up the catalogue and cost nothing; call_endpoint spends credits on success and reports the credits used and remaining, so your harness can tally those numbers into the same ledger and stop the loop at the cap. The MCP vs function calling comparison helps you choose between the two.
What should happen when a run hits a limit?
End the run, log why, and let the next scheduled run start fresh. Don't retry inside the same run to "finish the job", because that is how a capped run turns into an uncapped one. Each limit maps to a different response, and the error reference lists the codes.
| Signal | Meaning | What the run does |
|---|---|---|
BudgetExceeded from the call cap |
The run used its allowance | Return a partial answer and exit |
402 insufficient_credits |
Balance is empty; nothing more is debited | Stop, alert a human, no retry |
429 rate_limited |
Too many requests this minute | End the run and let the next one start fresh |
503 temporarily_unavailable |
The service is briefly unavailable | End the run and let the next one start fresh |
| Timeout | Reply lost, may have been billed | Log at 1 credit, do not repeat blindly |
The 429 and 503 responses carry a Retry-After header with the seconds to wait. Free accounts get 1 request a second (60 a minute); rate limits explains how the per-minute counting works, and concurrent requests in Python covers the parallel case. For Python error handling in general, see handling API errors.
How do you stretch a budget without lowering the cap?
Reduce repeated work rather than the number of runs. If an hourly agent asks the same query every time and the answer is unlikely to change within the hour, a local cache with a sensible TTL answers repeats without making a call, so they spend no credits; caching API responses in Python shows the TTL choices. Next, spend the allowance where it matters: let the agent plan its queries first, run the one or two that answer the question, and skip the rest.
Last, match frequency to how fast the data changes. Search results for a stable topic don't need an hourly check. Moving from hourly to every 6 hours on the free credits takes the allowance from 1 call to 8 per run. For a worked example of an agent that combines several searches, read the market research agent.
To try the numbers on your own schedule, get a free API key, paste in the ledger above and set max_calls from the table.
APIs in this post
Frequently asked questions
How many credits does a scheduled agent use per month?
Runs per month multiplied by calls per run, times the credits per call. Hourly runs in a 31-day month are 744; at 3 calls of 1 credit each, that is 744 × 3 = 2,232 credits, which is above the 1,000 free credits. The same agent on a daily schedule uses 31 × 3 = 93 credits.
Do failed calls count against the agent's budget?
Not in credits. A failed call costs nothing and returns X-Credits-Used: 0. The ledger above still counts every attempt against the per-run call cap, which keeps a run from looping on errors. The one exception worth recording is a client timeout, which may have been billed even though you saw no reply.
What happens when the agent runs out of credits?
The service is prepaid with no automatic top-up. When the balance reaches zero, calls return insufficient_credits with HTTP 402 and nothing more is debited. Your agent should treat that as a stop signal, not a retry case. Buy a pack or wait for the monthly free credits, then let the next run proceed.
Can I read the remaining balance without making a billable call?
Every response carries X-Credits-Remaining, including errors, so the balance arrives with the calls you already make. The search_endpoints and get_endpoint MCP tools only look up the catalogue and cost nothing. For your balance, rely on the header from your last call.