●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
Letting Gemini Flash Decide continue / pause / rollback for Staged Rollouts: An Indie Developer's Three-Signal Engine
How I built a Gemini Flash decision engine that reads Firebase Crashlytics, App Store / Google Play reviews, and AdMob revenue together, and outputs continue / pause / rollback for each staged rollout across six indie apps. Numbers from two months of production use included.
For a while there was a stretch where I was burning thirty minutes every morning, six apps in a row, asking myself the same question: "Should this rollout move from 10% to 50%, hold here, or get rolled back?" Beautiful HD Wallpapers, Ukiyo-e Wallpapers, Law of Attraction Everyday, Relaxing Healing, and two more wallpaper spinoffs I run quietly. I have been writing indie iOS and Android apps since 2014, and somewhere past 50 million total downloads, the post-release decision cost started outweighing the development cost itself.
I am Masaki Hirokawa — an artist and indie developer who has been releasing apps and exhibiting art internationally for over a decade. What follows is the production write-up of a small decision engine I built using Gemini Flash, fed by three signals (Firebase Crashlytics, App Store / Google Play reviews, and AdMob revenue), running for two months across six apps. The numbers are real, the gotchas are real, and the line I draw between "AI decides" and "I push the button" is intentional.
Staged rollouts themselves are great. Watching three dashboards at once across six apps every day was the part that did not scale.
The actual pain isn't the rollout — it's the siloed signals
Google Play does 1 → 5 → 10 → 20 → 50 → 100 percent. App Store Connect does day-1 1% up through day-7 100%. Both work fine. The real friction was that after each release I had to read three different dashboards to know if a rollout was healthy.
Concretely:
Firebase Crashlytics — new-version crash-free rate, fresh issues, and regressions of issues I thought were fixed.
App Store Connect / Google Play Console reviews — one-star ratio, and whether words like "won't open," "crash," "consent loop," "can't purchase" are spiking.
AdMob — new-version impressions, eCPM, and ARPU compared with the previous version.
The numbers themselves are not hard to read. The cost is the context switch. Eighteen browser tabs, six apps, five minutes per app, every single morning. Half a day per week disappeared into this.
For a one-person operation, half a day a week is not a sustainable price for a decision that is almost always mechanical. The rare cases where the call is genuinely subtle deserve human attention. Everything else should be a tiny inference call.
Why I moved from rules to a structured Gemini call
I started with the obvious thing: an if-tree. "Crash-free rate drops 0.5% versus the baseline → pause." "One-star reviews above 15% of new-version reviews → rollback." "AdMob ARPU below 80% of previous version while DAU is above 90% → pause."
What broke it was cross-signal context. A typical case: crash-free rate is down 0.3% (within the if-tree's pause threshold), but the dip is almost entirely on one device family (Pixel 7 Pro on Android 14), and reviews from owners of that device are starting to say "won't launch." A rule engine answers "continue" because no single threshold tripped. A human looks at the joint distribution and answers "pause that device's rollout immediately, keep the rest."
This is the kind of joint reasoning where a small Gemini Flash call with the raw numbers earns its cost. One inference returns a verdict plus a reason field in plain prose, which I can review later when I want to know why something paused.
The line I drew up front: the model returns verdict, confidence, and reason. If confidence == "low", the case skips automation and lands in my review queue. The judgment is the model's; the responsibility is mine.
✦
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
✦A structured-output schema that fuses Crashlytics, reviews, and AdMob into a single rollout verdict
✦Production cost of ~0.3 to 0.6 yen per evaluation on Gemini Flash 3.0, holding six apps under 90 yen per month
✦An automation boundary I keep: Gemini makes the call, but I still tap the rollout button — and why that line matters for indie work
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.
The schema started at around 200 lines and shrank as I cut things I never actually used.
from pydantic import BaseModelfrom typing import Literalclass StagedRolloutDecision(BaseModel): verdict: Literal["continue", "pause", "rollback"] confidence: Literal["high", "medium", "low"] primary_signal: Literal["crash", "review", "revenue", "composite"] reason: str # 1-2 sentences next_action: str # e.g. "Re-evaluate at 12:00 on 2026-05-28" flagged_devices: list[str] flagged_review_keywords: list[str]
Three verdicts only. I tried adding a fourth "watch more closely" value at one point and immediately noticed I started using it as a way to procrastinate harder decisions. Killing it forced me to either pause or commit, which is the whole point of having a rollout policy in the first place.
Confidence is a three-value enum, not a float. Gemini Flash is not well-calibrated on continuous probability, and a three-bucket enum matched my own intuition much better than a number that pretends to be precise.
The primary_signal field is metadata for me. It lets me look back across a month and ask "what fraction of pauses were driven by crashes versus reviews versus revenue?" — which is useful for tuning the prompt over time.
Building the prompt
The prompt is four blocks: role, raw three-signal snapshot, decision policy, output schema. The whole input ends up around 1,200 tokens. The structured output uses 300–500 tokens.
import google.generativeai as genaifrom pydantic import BaseModelSYSTEM_PROMPT = """\You are an SRE responsible for staged rollouts of indie iOS / Android apps.For each app, 24–72 hours after release, look at three signals and decide whetherto continue, pause, or rollback the rollout:Policy:- Crash-free rate down 0.5% vs baseline -> pause; down 1.0% -> rollback.- If the regression is concentrated on a small set of devices/OS combos, list them in flagged_devices but do not over-trigger on the overall number.- If new-version reviews mention "won't open / crash / consent loop / can't purchase" above 10% of all new-version reviews -> pause.- If AdMob ARPU is below 70% of the previous version while DAU is above 90% of the previous version -> pause (likely a monetization regression).- Even if no single threshold is hit, if multiple signals concentrate on the same device family, treat as composite -> pause.- If unsure, return confidence=low and write "human review recommended" in reason."""def build_user_prompt(app_id: str, snapshot: dict) -> str: return f"""\App: {app_id}New version: {snapshot['new_version']} (released {snapshot['hours_since_release']}h ago)Rollout percent: {snapshot['rollout_percent']}%[Crashlytics]- Crash-free rate (new): {snapshot['crash_free_new']}%- Crash-free rate (baseline): {snapshot['crash_free_prev']}%- New issues: {snapshot['new_issues']}- Crash distribution by device: {snapshot['crash_by_device']}[Reviews]- Reviews on new version: {snapshot['review_count']}- 1-2 star ratio: {snapshot['low_rating_ratio']}%- Negative keywords detected: {snapshot['negative_keywords']}- Sample reviews (5 excerpts): {snapshot['review_excerpts']}[AdMob]- 24h ARPU (new): {snapshot['arpu_new']}- 24h ARPU (baseline): {snapshot['arpu_prev']}- DAU (new): {snapshot['dau_new']} ({snapshot['dau_ratio']}% of baseline)- Fill rate (new): {snapshot['fill_rate_new']}%Return verdict / confidence / primary_signal / reason / next_action /flagged_devices / flagged_review_keywords as JSON conforming to the schema."""
Calling Gemini Flash 3.0 with structured output is the now-standard pattern:
I run at temperature=0.2. Lower temperatures occasionally returned empty responses or finish_reason=RECITATION on this kind of structured task, so 0.2 became my stable floor.
Three potholes worth knowing about
1. "Previous version" needs to be a manual baseline
My first version computed "previous version" as whatever release came before the current one. That broke during hotfix streaks: each hotfix lowered the baseline a little, and after three or four hotfixes the engine stopped catching real regressions because the baseline had quietly drifted.
The fix was to store a stable_baseline field in Firestore, manually advanced only when a rollout reaches 100% and stays healthy for seven days.
This single change cut bogus rollback flags by roughly 40% in my logs.
2. Version strings drift, especially on Google Play
The App Store Connect review API takes a clean version filter. Google Play Console exports CSV with version strings that wander — "2.4.1", "2.4.1 (build 89)", and even "2.4.1 (beta)" mixed in. The beta variant was the worst because it pulled review feedback for testers into the production decision.
import redef normalize_version(s: str) -> str: m = re.match(r"^v?(\d+\.\d+\.\d+)", s) return m.group(1) if m else s
Normalize everything to MAJOR.MINOR.PATCH before grouping. This is the kind of edge case you would never anticipate until you ship the first version of the pipeline and watch it misfire.
3. AdMob numbers are unreliable for the first ~8 hours
Right after release the population on the new version is small, and AdMob aggregation has a 2–4 hour reporting lag. Early in development I was getting pause verdicts in the first few hours that were almost always false alarms.
The fix was telling the model to ignore AdMob signals when hours_since_release < 8. I added a single line to the system prompt: "If less than 8 hours since release, ignore AdMob signals and decide on Crashlytics and reviews only."
After that change, early-stage false pauses essentially went to zero in my logs.
Running it across six apps
The trigger is a Cloud Scheduler firing every four hours into a Cloud Run Job, with Pub/Sub fan-out so each app's evaluation runs in parallel.
Per-evaluation token usage runs roughly 1,200–1,800 input plus 300–500 output. On Gemini Flash 3.0 pricing that comes out to about 0.3–0.6 yen per call. Six apps × six calls per day × 30 days = 1,080 calls per month. Monthly spend has landed in the 320–640 yen range. For context, this is less than the AdMob revenue from one healthy app on one healthy day.
Only rollback verdicts page me; pause verdicts queue for my morning review. I was paged three times in two months, and all three were correct calls.
What two months of production looked like
Numbers from six apps over 60 days (12 app-months total):
Total evaluations: 2,160 (6 calls/day × 60 days × 6 apps)
continue: 1,924 (89.1%)
pause: 218 (10.1%)
rollback: 18 (0.8%)
Sent to manual review (confidence=low): 73 (3.4%)
Agreement between the model's pause/rollback decisions and my final human call was 91%. The remaining 9% split roughly evenly: half I downgraded back to continue ("let me watch one more cycle"), half I escalated to rollback faster than the model wanted.
The number that matters most: zero rollouts shipped customer-visible damage during the test period. The reason is that the engine never executes the rollout call — it only recommends. I still tap the button in App Store Connect or Google Play Console myself.
Where the automation boundary stops
I deliberately stopped short of automating the rollout API itself. App Store Connect and Google Play Console each have their own rate limits, token expiries, and per-locale rollout quirks. Automating the recommendation paid off immediately. Automating the action would mean owning that whole surface, and the debugging cost would dwarf the time savings.
Decision is AI, action is human. For a one-person operation it is the line that scales best. I think about what I am leaving behind for my kids when I work like this — and what holds up best across years is not the cleverest automation, but the one whose decision history I can still read clearly years later.
Three extensions I want to try next
Three follow-ups I haven't shipped yet:
Send 30 days of past decisions as long context — let Gemini Flash spot "we've seen this regression pattern before" across history. This is the long-context use case I'd most like to validate next.
Replace regex keyword detection with Gemini Embedding — "won't launch" and "won't open" and "won't start" should collapse into one semantic cluster instead of three regex hits.
Daily Batch Prediction summary — run a 0:00 Vertex AI batch over the day's decisions and emit a single long-form report. The cost math gets even friendlier at batch pricing.
I'll write those up separately as I get the numbers in.
Hope the implementation is useful. If you're running staged rollouts on your own apps, I'd love to hear your decision rules — feel free to send a listener letter on my stand.fm.
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.