●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Surfacing AdMob Floor Price Candidates from Weekly Reports with Gemini 2.5 Pro — A Six-App Indie Operations Note
A practical pipeline for moving AdMob floor price tuning from gut feel to data, using Gemini 2.5 Pro to read weekly CSV exports. Notes from operating six wallpaper apps in parallel, with Function Calling to produce structured candidate values.
The first thing that surprised me about AdMob floor prices was how long it takes to know whether an adjustment was a good one. A week is the minimum honest window, and even that often gets contaminated by seasonality or weekday effects. Move floors too aggressively and you mistake noise for signal. Leave them alone and you let bidders win cheaply on impressions that should have been priced higher. Running six apps in parallel as an indie developer, I watched AdMob mediation get quietly more involved over time. Manually pawing through weekly CSV exports stopped scaling, so I shifted the floor-price reading work over to Gemini 2.5 Pro — not to have the model decide, but to have it lay out the same decision material in the same shape every week.
These notes describe the implementation I actually use to feed AdMob CSV exports into Gemini 2.5 Pro and get structured floor price candidates back. Because the ad network's eCPM numbers are inherently a black box, the goal is not to ask AI to produce the optimal value. The goal is much smaller: produce the same comparison table every week in the same shape, so a human can read it quickly and decide.
Why "gut feel" on floors loses precision over time
An AdMob floor price tells each ad unit (or each bidding participant) "skip bids below this number." Setting it does not produce instantly observable change. You need enough auction volume to accumulate before fill rate and eCPM both start moving. From the wallpaper apps I operate (DAU in the low thousands to low tens of thousands), the rules of thumb I have built up empirically look like this.
Raising a floor by 30% on rewarded units typically drops fill rate by 5 to 10 percentage points and lifts eCPM by tens of percent. The variance by unit and by country is wide
Set the floor too low (or leave it unset) and some bidding networks come in extremely cheap, dragging median eCPM down
Within one app, Open Ads, Interstitial, and Rewarded each have a different sensitivity. Open Ads has thinner demand, so raising the floor too far breaks fill quickly
Keeping all that in your head while you reconcile six apps multiplied by three or four units multiplied by five to ten major countries every week is, in practice, impossible. Color-coding a spreadsheet only takes you so far before something slips. A human still has to decide what to observe; Gemini can do the observing.
What weekly viewing looks like across six apps
Every Monday morning I download fourteen days of metrics from the AdMob console in two slices. One slice is at ad unit granularity, the other at country granularity. That produces six apps times two slices, or twelve CSV files, which I park in a dated folder like ~/admob-weekly/2026-05-20/. Before any of it touches Gemini, I open one or two CSVs by eye to make sure no unfamiliar column has appeared in the export.
Past floor changes live separately in floor-history.csv, which I edit by hand. The columns are simply date, app_id, ad_unit, country, old_floor, new_floor, reason. This file is the anchor that lets me tell Gemini "only treat last week's changes as a possible explanatory factor." The idea is to feed the model context I have committed to, rather than letting it invent reasons.
~/admob-weekly/├── 2026-05-20/│ ├── kabegami_lite_iOS_ad_unit.csv│ ├── kabegami_lite_iOS_country.csv│ ├── kabegami_lite_Android_ad_unit.csv│ ├── kabegami_lite_Android_country.csv│ ├── ...(remaining four apps x two OSes x two slices)│ └── floor-history.csv└── 2026-05-13/ └── ...
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Concrete implementation that extracts eCPM dips from AdMob weekly CSVs with Gemini 2.5 Pro and emits structured floor price candidates
✦Real-world numbers for eCPM, fill rate, and ARPDAU from operating six wallpaper apps in parallel, with a candid view of what changed and what didn't
✦Function Calling pattern that returns 'next floor to try' and 'units to hold' as JSON, ready to feed into a Slack approval flow before AdMob console edits
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Sending the CSVs through pandas and rewriting them as JSON Lines turned out to be more reliable than passing CSV directly with inline_data. Gemini 2.5 Pro can absorb very long contexts, but mixed Japanese/English column names plus the descriptive header rows AdMob inserts at the top of exports create silent failure modes — the model can mistake the explanation text for content.
# preprocess.pyimport jsonimport refrom pathlib import Pathimport pandas as pdREPORT_DIR = Path("~/admob-weekly/2026-05-20").expanduser()OUT_PATH = REPORT_DIR / "merged.jsonl"# Skip the descriptive lines AdMob inserts at the top of the exportdef load_admob_csv(path: Path) -> pd.DataFrame: text = path.read_text(encoding="utf-8") lines = text.splitlines() header_idx = next(i for i, ln in enumerate(lines) if ln.startswith("Date")) body = "\n".join(lines[header_idx:]) df = pd.read_csv(pd.io.common.StringIO(body)) df.columns = [re.sub(r"\s+", "_", c).lower() for c in df.columns] return dfrecords: list[dict] = []for csv_path in sorted(REPORT_DIR.glob("*_ad_unit.csv")): app_id = csv_path.stem.replace("_ad_unit", "") df = load_admob_csv(csv_path) use = df[[ "date", "ad_unit", "country", "impressions", "match_rate", "ecpm", "estimated_earnings" ]].copy() use["app_id"] = app_id use["report_kind"] = "ad_unit" records.extend(use.to_dict(orient="records"))df_all = pd.DataFrame(records)df_all["date"] = pd.to_datetime(df_all["date"])latest = df_all[df_all["date"] >= df_all["date"].max() - pd.Timedelta(days=6)]prior = df_all[(df_all["date"] < df_all["date"].max() - pd.Timedelta(days=6)) & (df_all["date"] >= df_all["date"].max() - pd.Timedelta(days=13))]def summarize(df: pd.DataFrame, label: str) -> list[dict]: g = df.groupby(["app_id", "ad_unit", "country"], as_index=False).agg( impressions=("impressions", "sum"), match_rate=("match_rate", "mean"), ecpm=("ecpm", "mean"), revenue=("estimated_earnings", "sum"), ) g["window"] = label return g.to_dict(orient="records")summary = summarize(latest, "last7") + summarize(prior, "prior7")with OUT_PATH.open("w", encoding="utf-8") as f: for row in summary: f.write(json.dumps(row, ensure_ascii=False, default=str) + "\n")print(f"wrote {len(summary)} rows to {OUT_PATH}")
The important move is aggregating to two windows ("last seven days" and "the seven before that") before Gemini sees the data. Passing in raw daily rows forces the model to spend reasoning capacity on summation it does not need to be doing, and the summation itself becomes a place where small errors hide. Pre-aggregating frees the model to focus on the comparison and the candidate proposal, which is what we actually want it doing.
The prompt I send to Gemini 2.5 Pro
I use the google-genai library (the successor to google-generativeai) and pass the JSON Lines as plain text rather than inline_data. For AdMob aggregates at my scale (six apps, a few hundred rows), text content is sufficient.
# analyze.pyfrom pathlib import Pathfrom google import genaifrom google.genai import typesMODEL = "gemini-2.5-pro"client = genai.Client(api_key="YOUR_GEMINI_API_KEY")DATA_PATH = Path("~/admob-weekly/2026-05-20/merged.jsonl").expanduser()HISTORY_PATH = Path("~/admob-weekly/floor-history.csv").expanduser()system_instruction = """You are an operational assistant for an indie developer using AdMob mediation.Role:- Given the AdMob weekly aggregates (last7 and prior7) and recent floor change history, propose floor price candidates per ad unit for the upcoming week.- Always reason across four axes: eCPM, match rate, impressions, and revenue.- For ad units with insufficient statistical volume (impressions summed across both windows under 5,000), label them "insufficient_data" and do not propose a floor.- Suggested values must be USD with two-decimal precision, rounded to the nearest 0.05.- "Hold" is a valid recommendation and must be used when the data does not justify movement.- Ad units that were adjusted in the last 7 days should default to "observe" rather than receiving another change, so we can let the prior adjustment play out."""user_text = f"""## Aggregates (JSON Lines){DATA_PATH.read_text(encoding='utf-8')}## Recent floor change history (CSV){HISTORY_PATH.read_text(encoding='utf-8')}## Output requirements- At most 12 proposals (ordered by expected impact)- If multiple country candidates exist for one ad_unit, collapse to the highest-impact country- Each proposal includes a 1- to 2-sentence rationale- Include an overall observation: possible seasonality, next metric worth watching"""
Explicitly authorising the model to label "insufficient_data" was one of the more useful guardrails I added. When you do not give the model a way to say "I don't have enough to recommend a change," it will recommend one anyway, and humans tend to trust those recommendations. Saying "you may decline" up front keeps the operational signal-to-noise high.
Function Calling for structured candidate values
I started with Markdown tables. They were easy to read, but transcribing values into the AdMob console by hand introduced small errors. Switching to Function Calling fixed the precision issue and made the downstream Slack notification and a possible future AdMob Network API integration much simpler.
tool_config with mode="ANY" forces Gemini to call the function. The low temperature=0.2 exists for a specific operational reason: I want similar candidates for similar inputs week over week. At 0.4 to 0.7, candidate values jitter even when the underlying data has barely moved, and that jitter undermines the continuity that makes weekly tuning credible. Floor work is sequential by nature; low temperature keeps the sequence coherent.
Slack notifications and "human-stoppable" workflow
The notification code is short, but operationally it is the most important piece. The AI emits candidates, a human looks at them, reacts with an emoji to approve or reject, and only then do changes go into the AdMob console.
Reserving exactly one stop in the pipeline that requires a human reduces my emotional dependence on Gemini being right. Sometimes the model proposes an extreme value, but it does not propagate beyond Slack until I approve. Had I wired this to write to AdMob directly, a thin-volume weekend would have produced something I regretted by Monday afternoon.
Realistic numbers I am seeing
I prefer to be conservative when reporting results, because the version of me reading this in six months will be the one who has to live with whatever I claim now. Across six apps, after running Gemini 2.5 Pro for weekly floor proposals for about eight weeks, here is the honest band of outcomes I observe.
Average eCPM on targeted units: up 5 to 12 percent. Rewarded saw the upper end (around 12 percent), Open Ads the lower end (around 5 percent)
Fill rate: down 1 to 3 percentage points on some units. Tolerable in aggregate, but visible in low-DAU countries
ARPDAU: improved by 4 to 7 percent on a weekly-median basis. Holidays and long weekends create their own noise on top of this
Implementation time (preprocessing, prompt, Function Calling glue): roughly 8 hours of actual work. Weekly operating time has dropped to under 30 minutes
I am not automating end-to-end, so roughly 60 percent of the proposals get adopted as-is and the remaining 40 percent get rejected or replaced with a different value. When that rejection rate starts trending down, I would consider nudging the temperature up to get fresh proposals or adding new columns (such as country-level count of bidding participants) to enrich the input.
Living with this for two months has also taught me that the way I read AdMob has changed. I am no longer hunting for headline movements in the dashboard. I am looking at the Slack candidates first and using the dashboard as a verification surface. That inversion of attention is the real productivity gain — not the API cost, not the time savings, but the shift in where my eyes go first on a Monday morning.
Three places I got bitten
The first was seasonality. Long weekends, holiday seasons, and end-of-year all push eCPM around hard. If you tell Gemini to compare week over week without context, it overweights the seasonal swings. I added short tags to the reason column in floor-history.csv (pre-holiday, year-end, etc.) and instructed the prompt to "downgrade impact by one step when an event tag appears for that week." The last week of December stopped producing wild recommendations after that single change.
The second was the treatment of bidding participants. AdMob's report alone cannot tell you that a bid arrived from an external SDK and got rejected by the floor. If you are running AppLovin MAX, Pangle, or Meta Audience Network on the side, you have to cross-reference their dashboards. There is no AI shortcut here. I let Gemini annotate every weekly output with "this analysis treats AdMob report as the sole source," so the limitation does not quietly vanish.
The third was setting temperature too low. At 0.0 or 0.1 the model returns almost identical candidates week after week, even when the data has clearly moved. The sweet spot in my hands has been 0.2 to 0.3 — low enough that continuity holds, high enough that the model is willing to revise.
How the prompt evolved over eight weeks
My first system_instruction was much shorter than the one above — it basically said "decide whether to raise or lower the floor." Each week, when I caught a misjudgement, I added a single line. When a line stopped being needed, I removed it. Three additions stuck.
The first was the "do not propose candidates for insufficient_data" rule. Without it, low-volume ad units kept attracting proposals out of momentum, and humans (me) would sometimes adopt them, producing oscillation the following week. Naming an impressions threshold (I use 5,000) gave the model permission to abstain.
The second was the "observe units changed in the last 7 days" rule. Adjusting a floor and then adjusting it again next week obscures the effect of the first change. Before this rule was in place, Gemini occasionally swung the same unit back and forth, producing oscillation rather than convergence.
The third was the formatting constraint of "USD, two decimals, rounded to 0.05." Without it the model sometimes returned 0.123, which a human then had to round, which introduced its own inconsistency. Letting the model round at the source removed that step entirely.
Bidding vs. waterfall — where each app sits
Three of my six apps are fully on AdMob Bidding for all participating networks. The other three are in a transitional state, with a mix of manual eCPM waterfall and bidding. Gemini has to interpret floor prices differently in each case, so I added a mediation_kind column with values bidding, waterfall, or mixed and reference it in the prompt to switch reasoning.
In practice, bidding-only apps yield much more stable proposals. The auction structure makes floor changes immediately competitive. Waterfall-heavy apps create proposals that lean on judgement the data cannot fully support, so I treat the AI output as "organised numbers" and reach across to the AppLovin or Pangle dashboards for the final decision.
Cost — is Gemini 2.5 Pro overkill for this?
Flash was an obvious thing to try. I tested it. Pro held a noticeable edge in two places: structured output reliability under Function Calling and depth of understanding for AdMob column semantics. Adherence to "do not propose for insufficient_data" was around 70 to 80 percent with Flash and over 90 percent with Pro.
Cost-wise, six apps' worth of aggregates and history come out to roughly 30,000 input tokens and 1,500 output tokens per run. I run it once a week, so four to five times a month. The total Gemini API spend in my logs sits under USD 1 per month, which is not worth optimising. If the cadence moved to daily, the calculus would change.
A Monday-morning checklist
The mental order I follow when reviewing the Slack proposals, written out:
Number of "insufficient_data" labels. A sudden jump usually means the AdMob export configuration (date range or granularity) drifted.
Whether the overall observation mentions "seasonality" or a holiday. If so, downgrade the read of every proposal one step.
The ratio of "raise" to "lower." Heavily skewed toward "raise" can hint at a bid surge in the prior week; mind the downside.
Whether the same ad_unit appears in both this week's and last week's proposals. If yes, oscillation may be starting; revisit temperature or window size.
Total revenue versus the prior week. If revenue is down, hold back on aggressive moves.
The checklist itself grew with the operation. It started as two items, and an item was added each time I got bitten by something I had not been looking at.
A step further — the AdMob Network API
AdMob exposes a Network API (Reporting endpoints are GA, settings endpoints are in beta) that can rewrite ad unit floor values programmatically. I still apply changes by hand after Slack approval, but I plan to migrate low-risk ad units to API-driven rewrites once the rejection rate stays consistently under 10 percent. The standard approach uses google-api-python-client; given the write semantics, a diff preview and a write log (CloudWatch, BigQuery, or similar) are non-negotiable.
I keep an eye on the developer reference each week, especially for bidding-related schema changes. The reference index that I treat as my starting point is the AdMob Network API documentation.
Closing — what I do next Monday
The thing that has mattered most about this pipeline is sticking to "same time, same shape, every week." You do not need to be running at billion-DAU scale for that to pay off. Even at the modest scale of six indie apps, having a fixed weekly viewing format reduces decision variance. Gemini is the colleague who keeps that viewing format alive; the call is still mine.
Next Monday, after pulling the usual fourteen days of reports, I will run preprocess.py once, analyze.py once, and notify.py once, and a set of proposals will appear in Slack. Within thirty minutes I will approve or reject each one and apply the approved ones in the AdMob console the same day. That cadence is where I have landed. If you are running a small portfolio of apps and looking for the operational footing for this kind of work, I hope these notes give you something to build on.
Thanks for reading.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.