◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-06-04Advanced

Don't make Gemini judge your AdMob report — confine structured output to extraction

When deciding AdMob floors (eCPM thresholds), letting Gemini make the decision itself is dangerous. Confine structured output to 'extracting a messy report into typed data,' and keep the threshold judgment in deterministic code — here is the reasoning and implementation, with the actual decision rules from running 42 groups.

Gemini API249Structured Output11AdMob9App MonetizationDesign3

✦ Premium Article

When automating the job of setting AdMob floors (eCPM thresholds) from a report, the first idea most people reach for is "hand the whole report to Gemini and have it answer the new floor for each group." I started down that path too, and stopped almost immediately. Letting an LLM do the threshold arithmetic and judgment makes that judgment unauditable. Setting a floor is an irreversible operation tied directly to revenue, and any mechanism where you cannot later reproduce "why this value" must not be put into operation.

When you run apps solo, most of your revenue ends up resting on AdMob mediation, which makes reviewing floors an unavoidable monthly chore. I keep 42 mediation groups across iOS and Android, and since you cannot move those by gut feel, I have gradually pushed the judgment into a mechanism. What settled it for me through that process is that Gemini's structured output is strong not at "judging" but at "extracting." This article shows the design that confines structured output to the extraction step and separates the judgment into code, together with the actual decision rules.

Why you must not let the LLM do the judging

The floor decision rules themselves are, in fact, fully deterministic. The rules I use are these. On iOS, the floor is based on actual eCPM × 55%; if the ratio of current floor / eCPM exceeds 65%, lower it; if the ratio is under 40% and the match rate exceeds 95%, raise it; the middle (40–65%) holds. The minimum unit is $0.50. Android is a flat $0.50, not a ratio of actuals.

These rules complete with arithmetic and threshold comparisons alone. That is, there is no room for an LLM to "decide." And yet, passing the whole ruleset to an LLM via prompt produces three problems. First, output wavers at the threshold edges (e.g., when the ratio is exactly 65.0%). Second, even on the same input it may return subtly different values per run, breaking reproducibility. Third, and biggest: you cannot verify, from the output, the basis for a judgment like "hold because ratio is 64%." With code, anyone can trace the single line ratio = floor / ecpm; an LLM's internal reasoning cannot be traced.

A mis-set floor causes no-fill if too high (fill rate drops) and collapses eCPM if too low. In fact, I once had Android INT floors set excessively high relative to actuals, which capped the match rate in the low 70% range. To verify and fix this kind of "too strong / too weak" afterward, the judgment must be deterministic.

So what do you let Gemini do — only "extraction"

On the other hand, when you receive an AdMob report as CSV or pasted text, the data is astonishingly messy. Inconsistent group-name notation, currency symbols present or absent, a match rate written as "47.53%" one time and "0.4753" another, column order shifting with export settings. Tidying this messy input into uniformly typed rows is exactly where Gemini's structured output is overwhelmingly strong.

The trick here is to include no "decision result" whatsoever in the output schema. Define only the raw observations in the schema (group name, group ID, actual eCPM, match rate, current floor), and never put the new floor or a "raise/lower" judgment into it. You confine the LLM's responsibility to "messy input → typed observations."

import os
from google import genai
from pydantic import BaseModel
 
class GroupRow(BaseModel):
    group_name: str
    group_id: str
    ecpm_usd: float        # actual eCPM (observation only)
    match_rate: float      # normalized to 0.0-1.0
    current_floor_usd: float
 
class ExtractedReport(BaseModel):
    rows: list[GroupRow]
 
client = genai.Client(api_key=os.environ["YOUR_GEMINI_API_KEY"])
 
raw_report = open("admob_mediation_report.txt", encoding="utf-8").read()
 
resp = client.models.generate_content(
    model="gemini-2.5-pro",
    contents=(
        "Extract the following AdMob mediation report, normalized to one row per group. "
        "Convert match_rate to 0.0-1.0. Do NOT output any judgment or recommended value. "
        "Observations only.\n\n" + raw_report
    ),
    config={
        "response_mime_type": "application/json",
        "response_schema": ExtractedReport,
    },
)
 
report: ExtractedReport = resp.parsed

The key is stating in the prompt, too, "Do NOT output any judgment or recommended value." Bound by the schema and limited in natural language as well, Gemini works stably as an "extractor."

✦

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
✦Why and how to split: confine Gemini structured output to 'extraction' and keep threshold judgment in code
✦A deterministic implementation of the real 42-group floor decision rules (eCPM×55%, ratio thresholds)
✦How letting an LLM compute thresholds becomes unauditable, and the typed-schema design that avoids it
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.

or
Unlock all articles with Membership →
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.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◈ API / SDK2026-06-12
Building an App Store Rejection Workflow with the Gemini API — From Structured Notices to Resolution Center Replies
How I use the Gemini API to parse App Store rejection notices into structured JSON, cross-check guidelines, draft Resolution Center replies, and run pre-submission checks as an indie developer.
◈ API / SDK2026-05-21
When responseSchema enum returns unexpected values — debugging Gemini API
Why Gemini API sometimes returns values outside the enum you defined in responseSchema, and the three-layer workaround I use in production for my wallpaper app classification pipeline.
◈ API / SDK2026-10-14
When Your config Raises "Extra inputs are not permitted", Check the Spelling of Every Key
If a google-genai call fails with a pydantic ValidationError (extra_forbidden) the moment you pass a config, it is almost always a one-letter typo. Here is the output I reproduced, a small function that checks keys before the call, and how to tell why response.text came back as None.