Blog / AI agents

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.

  1. LangChain Custom Tool for Live Web Data: Python Tutorial

    Build a LangChain tool in Python for Google Search and News: typed arguments, retries on 429, 502 and 503 only, and a per-run credit cap.

  2. AI Agent API Cost Control: Cap Web Data Spend

    Cap an agent's web data spend per run: credit arithmetic for 8 and 20 calls per task, a Python budget wrapper, and why failed calls cost 0 credits.

  3. Build a Market Research Agent with Search and Trends Data

    Wire Google Search, Trends and News into one Python agent, cap it at 8 credits per question, and see a recorded Trends response.

Start with 1,000 free credits

No password to set. We email you a sign-in link, and your key works on every API.

No credit card. 1,000 free credits a month.