In part 1 we took one of Google's Agent Development Kit (ADK) examples, a team of agents that improves product titles for an online shop, and ran it on Cloud Run inside our product, cloudcost.info. The shop example was never the point. The point was its shape: a coordinator, specialists with tools, data in BigQuery.
In this part the shop agents become a Cost Advisor: an agent team that reads a Google Cloud bill, finds where the money goes, and compares what you pay with cheaper ways to buy the same thing. We kept the same graph and replaced what each agent does, one agent at a time.
This part covers the data and the first two specialists. The recommendations team (a generator and a critic) comes in part 3.
From shop agents to cost agents
Here is how each agent of the example maps to the Cost Advisor:
| Example agent | Cost Advisor agent | Tool | Data |
|---|---|---|---|
| coordinator | cost_advisor | - | - |
| keyword agent | spend_analysis_agent: where the money goes, what changed | get_billing_summary | Cloud Billing export in BigQuery |
| browser agent (Computer Use) | pricing_agent: list prices and cheaper options | get_gcp_list_prices | Google's official price list |
| comparison agent + generator + critic | the recommendations team (part 3) | - | - |
Each specialist saves its result in the session state under its output_key (spend_analysis, pricing_findings), and the next one reads it straight from its instruction with a placeholder such as {spend_analysis?}. The question mark makes it optional, so the agent still runs if the value is missing. That is how the agents pass work to each other without passing it through the coordinator's prompt.
Step 0: the billing data
An agent that explains a bill needs the bill. Google Cloud can export billing data to BigQuery automatically, several times a day. There are three exports: standard usage cost (cost per day, service, SKU, project, region, credits), detailed usage cost (the same plus each resource) and pricing (the prices of your billing account, including discounts). We turned on standard and pricing.
Two things surprised us.
It is console-only. There is no gcloud command or API to turn the export on; it is a setting in the Billing console, and it needs billing administrator rights. The dataset must exist first (we created billing_export in the EU multi-region).
An old export was already on, and useless. The console showed the standard export as enabled, writing to a dataset called test in a project we no longer had access to, from an old experiment. If you have had a billing account for a while, check where your export actually goes before you build on it.
The export does not bring your history; it fills up from the day you turn it on. So while we waited, we wrote sample billing data in exactly the export's columns (service.description, sku.description, project.id, cost, credits, usage, invoice.month and so on) into a separate dataset. The agent reads one setting, BILLING_TABLE; switching to the real export later is one line.
We made the sample data tell a story, so the agents have something real to find: always-on development VMs, BigQuery query costs that more than double in one month, backups that grow in the most expensive storage class, steady compute that only gets the automatic discount, and a highly available Cloud SQL database.
Step 1: a billing tool the model cannot misuse
The first idea for "let the agent read the bill" is to give it SQL. We did not. The tool takes two typed arguments, and the SQL is fixed:
def get_billing_summary(
group_by: Literal["service", "sku", "project", "region"] = "service",
months: int = 2,
) -> dict:
"""Summarizes the cloud bill: net cost per month and the top 10 lines."""
group_by picks a column from a fixed list in code, months is capped at 6, the month boundary goes in as a query parameter, and the table name is checked against a strict pattern. The model cannot read another table, run a different query or produce a huge bill: the worst it can do is pick a less useful grouping.
The tool also returns a summary, not rows: net cost per month, the 10 biggest lines with their change since last month, and a note when the latest month is not over yet. Our sample has 671 rows; the summary is a couple of kilobytes. Everything a tool returns goes into the next prompt, so a small result means faster, cheaper and more focused answers.
Two things we saw in the first runs:
- Parallel tool calls. Asked "why did our bill go up?", the spend agent called the tool three times at once: by service, by SKU and by project. Gemini can request several independent function calls in one turn, and ADK runs them all.
- The month-to-date trap, avoided. It was 20 October. The tool's note said the current month had data until the 19th, and the prompt says never to compare a partial month with a full one. The agent compared per day (about €72 a day in September, €114 in October) and projected the full month (+63%), instead of reporting that October was cheaper than September.
A wrong answer that was the tool's fault
In one run the answer put the Cloud SQL costs in the wrong project. The model had not misread anything: the SKU lines did not say which project they belonged to, so it guessed. We fixed the tool, not the prompt: SKU lines are now "project · service / SKU". The next run named the right project.
This is a pattern worth remembering: when an agent gets a fact wrong, first check whether the tool gave it the fact at all.
Step 2: a pricing tool on Google's real price list
The browser agent of the example, which drove a real browser with Gemini Computer Use, became the pricing agent. Its tool reads the Cloud Billing Catalog API, the official list prices, and returns at most 15 matching SKUs with their price type: on demand, 1-year commitment, 3-year commitment or spot. That is enough for the agent to compare what is paid with the cheaper ways to buy the same thing.
Reading the real price list taught us more than any documentation:
- It is big. Compute Engine alone has about 33,500 SKUs, and reading them takes 10 to 15 seconds. The tool keeps the price list in memory for a day (it changes rarely) and nothing else.
- SKU names are not what you expect. Our sample data first used names like "N2 Instance Core running in Belgium". The real name is "N2 Instance Core running in EMEA": compute is priced per area. Commitments are "Commitment v1: N2 Cpu in EMEA for 1 Year", and Cloud SQL is "Cloud SQL for Postgres: Regional - Enterprise N4 vCPU in Belgium". We changed the sample data to the price list's exact names and EUR prices, which is what a real export contains anyway.
- Substring search is wrong. A search for egress ("internet data transfer out") also matched "from South America". The tool now matches whole words and puts the SKU that contains the exact phrase first.
- The bill and the price list do not use the same names. Internet egress appears under Networking on the bill but under Compute Engine in the price list, and a BigQuery dataset in the EU multi-region is europe in the price list. The tool handles both, so the agent can use the names it sees on the bill.
With that, the pricing agent finds the cheaper option for each big line and does the arithmetic. These are list prices in EUR at the time of writing:
| Line | Now | Cheaper option |
|---|---|---|
| N2 cores, steady load | €0.0306 per core-hour | €0.0193 with a 1-year commitment (-37%), €0.0138 with 3 years (-55%) |
| E2 development VMs, always on | €0.0211 per core-hour, 24/7 | spot €0.0108 (-49%), or working hours only |
| Cloud SQL, high availability | regional vCPU €0.0727 per hour | zonal €0.0363 (-50%) where failover is not needed |
| Backups in Cloud Storage | Standard €0.0176 per GiB-month | Nearline €0.0088, Coldline €0.0035 |
| BigQuery queries | €5.50 per TiB scanned (1 TiB free) | fewer bytes scanned: partitioning, clustering, byte limits |
Every price in the answer came from a tool call, and we checked them against the catalog. That check found one mistake: for the always-on development VMs, which are E2 machines, the answer quoted the spot discount of N2 machines (-77%). The E2 spot price is a smaller saving (-49%). The tool had returned both; the model picked the wrong family. No prompt rule fully prevents that kind of slip, which is exactly why the next part adds a critic agent whose only job is to check each saving against the numbers the tools returned. The prompt tells the agent to say so when a price is not found rather than guess one, and to mark savings as estimates with their condition ("if it runs all year", "if read less than once a month").
Removing the browser
Dropping the Computer Use agent removed more than an agent: Playwright, the Chromium download in the container image, the wait workaround from part 1, and the security concern of an agent that can open any URL. The container went from 2 GiB of memory to 1 GiB, and the image is much smaller, which helps the cold start.
Where it stands
| Shop example (part 1) | Cost Advisor so far | |
|---|---|---|
| A full run | about 1 to 4 minutes | 104 seconds with both specialists |
| Model calls | 15 to 26 | about 10 tool calls in 4 hand-offs |
| Container | 2 GiB, with a browser | 1 GiB, no browser |
The live Agent Lab shows the same graph as before, now with cost_advisor, spend_analysis_agent and pricing_agent, and numbered arrows for the hand-offs.
A faster loop for us
One side effect of building in small steps is many deploys, and ours took over 15 minutes, half of it browser tests running one at a time on a 2-CPU build machine. We gave the build 8 CPUs, ran independent steps side by side (the new version uploads without traffic while the tests run, and gets the traffic only when they pass), and skip the build's own browser-test run when the same code, by fingerprint, has just passed them on a laptop. The live smoke test with automatic rollback still runs every time. A frontend deploy now takes about 4.5 minutes.
What to consider
- Check where your billing export goes before you build on it, and turn it on early: it does not bring history.
- Give the model typed choices, not query languages. Fixed SQL behind a
Literalargument is safer and more predictable than generated SQL. - Return summaries. Size tool results for the prompt, not for completeness.
- When an agent gets a fact wrong, read the tool's output first. Often the fact was never there.
- Test against the real API early. Our invented SKU names would have passed every unit test and failed on the first real bill.
- Cache what is big and slow to change (the price list), and nothing else.
Next
In part 3 the last two agents of the example become the recommendations team: a generator that turns the findings into concrete savings, each with an amount, effort and risk, and a critic that checks every number against the state before anything reaches the user. They run in a fixed order with a workflow agent, and return a structured report the product can use.