---
season: 3
season_name: "Season 3 — October 2026 (Autumn / Halloween Edition)"
day: 28
title: "Cost & Token Controls: Price the Task, Cap the Spend"
summary: "Price the task next to quality, alert in minutes on token burn, and cap agent loops, Cloud Run instances, and project spend."
tags: ["Phase 6: Cost Controls", "ADK", "Cloud Billing", "Observability"]
canonical_url: "https://adventofagents.com/2026/10/28"
markdown_url: "https://adventofagents.com/2026/10/28.md"
video_url: "https://www.youtube.com/embed/sbOWPWAcLTE"
primary_video:
  title: "Day 28: Cost & Token Controls: Price the Task, Cap the Spend"
  creator_name: "Dani Zamora, with John DeMartino & Sirisha Karra"
  duration: "11:50"
  video_url: "https://www.youtube.com/embed/sbOWPWAcLTE"
---

# 🪙 Day 28: Cost & Token Controls: Price the Task, Cap the Spend

> **Season 3 — October 2026 (Autumn / Halloween Edition)** · [Interactive Web View](https://adventofagents.com/2026/10/28?utm_source=adventofagents&utm_medium=markdown&utm_campaign=adventofagents_s3_2026&utm_content=day28) · [Raw Markdown](https://adventofagents.com/2026/10/28.md?utm_source=adventofagents&utm_medium=markdown&utm_campaign=adventofagents_s3_2026&utm_content=day28)

**Summary:** Price the task next to quality, alert in minutes on token burn, and cap agent loops, Cloud Run instances, and project spend.

**Day 28 of Google's Advent of Agents - Season 3**

I built an agent that waits for a report that never finishes: 40 model calls and 49,325 input tokens later, it still hadn't answered anyone (and by default it could have kept going up to 500 calls). So how do you see which agent is eating your tokens, hear about it in minutes, and cap the spend? I'm a developer, not a cost expert, so I asked two guides: John DeMartino for the developer view and Sirisha Karra for the platform view.

**Warm-up: how am I charged?**

Agents you build pay for model tokens (thinking included) plus the runtime. Managed agents and partner models like Claude are billed per token on the same Google Cloud bill, and Gemini Enterprise is a per-seat license.

![Four ways agents are billed on Google Cloud: agents you build, managed agents, seat-based Gemini Enterprise, and partner models in Model Garden](/s03/s3-day28-how-agents-are-billed.jpg)

**Part 1 · Build: the developer's view**

- **Price the task, not the token**: John's unit is the cost per successful task. One BigQuery query over the Cloud Trace spans showed my most expensive agent: the analyst, resending an 8,500-token semantic layer on every call.

![BigQuery over the Cloud Trace spans: input tokens per agent, with the analyst agent resending its semantic layer on every call](/s03/s3-day28-bigquery-burn-rate.jpg)

- **Put cost next to quality**: Same 8 questions, same judge, one change at a time. ADK's `static_instruction` plus low thinking kept 8 out of 8 and made each successful task 83% cheaper.
- **Bound the loop**: `RunConfig(max_llm_calls=8)` stops the report loop at 8 calls and 3,432 input tokens.

![Same 8 questions, same judge: tokens, seconds and cost index per variant, with the code to rerun every experiment](/s03/s3-day28-cost-next-to-quality.jpg)

**Part 2 · Run: the organization's view**

- **Alarm in minutes**: Billing data lands hours later, so as Sirisha says, don't wait for the bill: alert on ADK's token metrics per agent in Cloud Monitoring.
- **Attribute the dollars**: Label every model call (`labels={"agent": "sql_analyst"}`) so budgets and reports split spend per agent or team.
- **Cap the spend**: Alerts tell you, caps stop the spend. A Cloud Billing spend cap pauses new requests (set it below your real limit), and Cloud Run limits keep a loop from going wide.

![The organization's view: alert on tokens in minutes, label every model call, cap the spend, and bound the runtime](/s03/s3-day28-two-speeds-and-caps.jpg)

What I learned: cost is way more complicated than I thought, and this is just the tip of the iceberg. The good news: spend caps, cost anomaly detection and Gemini Cloud Assist keep making it more transparent.

**Resources:**

- [GitHub repo: agent-cost-controls (agents, experiments, SQL and alerts)](https://g.dev/cloud/adventofagents-season3-day28-demo)
- [Context caching overview on Agent Platform](https://g.dev/cloud/adventofagents-season3-docs-context-cache-overview)
- [Query and analyze traces in Cloud Trace and BigQuery](https://g.dev/cloud/adventofagents-season3-docs-trace-analytics)
- [Cap costs with Cloud Billing spend cap budgets](https://g.dev/cloud/adventofagents-season3-docs-billing-spend-cap)

## Code & Commands

### Step 1: Five Cost Controls on One ADK Agent

```python
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Day 28 in one file: five cost controls on one ADK agent.  Run from the repo root:  python snippets/cost_controls.py"""

import asyncio
import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from google.adk.agents import LlmAgent  # noqa: E402
from google.adk.agents.context_cache_config import ContextCacheConfig  # noqa: E402
from google.adk.agents.invocation_context import LlmCallsLimitExceededError  # noqa: E402
from google.adk.agents.run_config import RunConfig  # noqa: E402
from google.adk.apps import App  # noqa: E402
from google.adk.runners import InMemoryRunner  # noqa: E402
from google.genai import types  # noqa: E402

from cost_lab.db import run_sql  # noqa: E402
from cost_lab.semantic_layer import SEMANTIC_LAYER  # noqa: E402  (about 8.5k tokens of schema + glossary)

usage = {"calls": 0, "input": 0, "cached": 0, "thinking": 0, "output": 0}


def meter(callback_context, llm_response):  # 1. Meter every model call: price the task, not the token
    u = llm_response.usage_metadata
    if u:
        usage["calls"] += 1
        usage["input"] += u.prompt_token_count or 0
        usage["cached"] += u.cached_content_token_count or 0
        usage["thinking"] += u.thoughts_token_count or 0
        usage["output"] += u.candidates_token_count or 0


agent = LlmAgent(
    name="sql_analyst",
    model="gemini-3.8-flash",
    tools=[run_sql],
    static_instruction=SEMANTIC_LAYER,  # 2. The static part first, identical on every call: cacheable
    instruction="Caller: finance analyst (read-only).",  # the dynamic part goes after it
    generate_content_config=types.GenerateContentConfig(
        labels={"agent": "sql_analyst"},  # 3. A billing label: spend per agent in Cloud Billing
        thinking_config=types.ThinkingConfig(thinking_level="LOW"),  # 4. Right-size the reasoning
    ),
    after_model_callback=meter,
)
app = App(name="cost_controls", root_agent=agent,
          context_cache_config=ContextCacheConfig(min_tokens=4096, ttl_seconds=600))


async def main():
    runner = InMemoryRunner(app=app)
    session = await runner.session_service.create_session(app_name="cost_controls", user_id="me")
    for question in ["What was revenue in September 2026 across all stores?", "And in August 2026?"]:
        try:
            async for event in runner.run_async(
                    user_id="me", session_id=session.id,
                    run_config=RunConfig(max_llm_calls=8),  # 5. A hard iteration bound (the default is 500)
                    new_message=types.Content(role="user", parts=[types.Part(text=question)])):
                if event.is_final_response() and event.content and event.content.parts:
                    print(question, "->", event.content.parts[0].text)
        except LlmCallsLimitExceededError:  # the bound was hit: answer politely instead of looping
            print(question, "-> Sorry, that took too many steps. Try a narrower question.")
        print("   so far:", usage)
    await runner.close()


if __name__ == "__main__":
    asyncio.run(main())
```

### Step 2: Find the Hungriest Agent in BigQuery (Cloud Trace Spans)

```sql
-- burn_rate_by_agent.sql
-- Which agent is consuming the most tokens? One row per agent and model, last 24 hours.
-- Source: the Cloud Trace _Trace bucket linked to BigQuery (dataset agent_spans, zero copy, Observability Analytics).
-- ADK writes token counts on the `generate_content <model>` spans, next to gen_ai.agent.name.
-- ADK's output_tokens already include the reasoning (thinking) tokens.
-- adk.experimental.context_cache.hit is true when the call reused ADK's context cache.
SELECT
  JSON_VALUE(attributes, '$."gen_ai.agent.name"') AS agent_name,
  JSON_VALUE(attributes, '$."gen_ai.request.model"') AS model,
  COUNT(DISTINCT JSON_VALUE(attributes, '$."gen_ai.conversation.id"')) AS sessions,
  COUNT(*) AS llm_calls,
  ROUND(SAFE_DIVIDE(COUNT(*), COUNT(DISTINCT JSON_VALUE(attributes, '$."gen_ai.conversation.id"'))), 1) AS llm_calls_per_session,
  SUM(SAFE_CAST(JSON_VALUE(attributes, '$."gen_ai.usage.input_tokens"') AS INT64)) AS input_tokens,
  SUM(SAFE_CAST(JSON_VALUE(attributes, '$."gen_ai.usage.output_tokens"') AS INT64)) AS output_tokens,
  ROUND(100 * SAFE_DIVIDE(
    SUM(SAFE_CAST(JSON_VALUE(attributes, '$."gen_ai.usage.reasoning.output_tokens"') AS INT64)),
    SUM(SAFE_CAST(JSON_VALUE(attributes, '$."gen_ai.usage.output_tokens"') AS INT64))), 0) AS thinking_pct,
  ROUND(100 * COUNTIF(JSON_VALUE(attributes, '$."adk.experimental.context_cache.hit"') = 'true') / COUNT(*), 0) AS cache_hit_pct,
  ROUND(SAFE_DIVIDE(
    SUM(SAFE_CAST(JSON_VALUE(attributes, '$."gen_ai.usage.input_tokens"') AS INT64)),
    COUNT(DISTINCT JSON_VALUE(attributes, '$."gen_ai.conversation.id"')))) AS input_tokens_per_session
FROM `YOUR_PROJECT.agent_spans._AllSpans`
WHERE start_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 24 HOUR)
  AND name LIKE 'generate_content %'
GROUP BY agent_name, model
ORDER BY input_tokens + output_tokens DESC;
```

### Step 3: Run the Offline Tests, Cost Summary & Live Lab

```bash
#!/usr/bin/env bash
set -euo pipefail

git clone https://github.com/danielazamorah/agent-cost-controls.git
cd agent-cost-controls
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt pytest

# 1. Offline checks and saved 9-variant cost summary (no Google Cloud calls needed):
pytest -q
python run_lab.py summary

# 2. Run the 5-control snippet and live experiments on Agent Platform:
export GOOGLE_CLOUD_PROJECT="${GOOGLE_CLOUD_PROJECT:-your-project}"
export GOOGLE_CLOUD_LOCATION=global
export GOOGLE_GENAI_USE_VERTEXAI=TRUE
export MODEL=gemini-3.8-flash JUDGE_MODEL=gemini-3.8-flash ADK_EXPERIMENTAL_TELEMETRY=true

python snippets/cost_controls.py
python run_lab.py loop
python run_lab.py experiments baseline static_first best
```

## Resources & Links

- **[GitHub repo: agent-cost-controls (agents, experiments, SQL and alerts)](https://g.dev/cloud/adventofagents-season3-day28-demo)** — Start here: the ADK agents, the cost-next-to-quality experiments, the BigQuery burn-rate SQL and the alert policies.
- **[Context caching overview on Agent Platform](https://g.dev/cloud/adventofagents-season3-docs-context-cache-overview)** — How implicit and explicit context caching work on Agent Platform, prefix requirements, and billing discounts.
- **[Query and analyze traces in Cloud Trace and BigQuery](https://g.dev/cloud/adventofagents-season3-docs-trace-analytics)** — Link the Cloud Trace _Trace bucket to BigQuery with zero copy to query token burn rate and cache hit rate per agent.
- **[Cap costs with Cloud Billing spend cap budgets](https://g.dev/cloud/adventofagents-season3-docs-billing-spend-cap)** — Automatically pause on-demand Agent Platform, Gemini API, or Cloud Run usage in a project when estimated spend crosses your monthly cap.
