GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-05-24Intermediate

Two Weeks of Classifying Half a Year of App Store Reviews with Gemini File API

I ran half a year of App Store Connect reviews through the Gemini File API for two weeks straight, asking it to classify and summarize them. Here is what worked, where Batch Mode fit better, and which sharp edges took me a few days to round off.

gemini-api277file-api6app-store-connect2indie-dev43admob4

Opening the App Store Connect reviews tab on a Sunday evening and trying to read every comment below three stars used to be one of those quiet weekly habits that slowly ate into my work. I have been an indie developer since 2014, and the wallpaper apps I run have crossed fifty million cumulative downloads. That means somewhere between fifty and a hundred and twenty reviews land per day across Japanese, English, Spanish, German and a few more locales. By the end of a week that easily passes seven hundred, and I would catch myself starting the week with last week's reviews still unread.

Two weeks ago I changed the routine. I now dump the reviews as a CSV, upload the file to the Gemini File API, and ask it to sort them into "needs an apology", "feature request", "ad complaint" and "OS-version bug report". I also tested Batch Mode in parallel, because indie budgets matter, but I keep coming back to the File API for the day-to-day work. This post is the log of what I learned in those two weeks.

Why I started uploading the whole CSV

My first attempt was the naive one: one review per request to generateContent. The granularity is nice when something fails, since you only retry that single row. But when I tried to re-classify the past six months in one go, around twelve thousand reviews, the rate limits became the bottleneck. Even with backoff I was looking at a full day of background work for one pass.

The File API lets you upload up to 2GB once and then reference that file from parts for the next 48 hours. One upload, then as many prompts as you want against the same payload. That fits perfectly with the way I actually use this: I want to slice the same review batch by three or four different lenses on the same day, and I do not want to pay the upload cost every time.

The other reason was token accounting. When I inlined the CSV into the prompt, my input token usage swung wildly depending on how chatty that week's reviews were. With File API references the input cost is much more predictable. For my workflow of re-reading a 500-row CSV five to eight times a day, that predictability turned out to matter more than I expected.

The three tasks I let it run

I ended up running three concrete tasks:

  1. Priority label — tag every review as critical (crashes, billing trouble), support (questions, requests, confusing UI) or neutral (vague impressions, low star with no specifics).
  2. Cluster summary — group similar complaints, so that "ad frequency is too high" or "downloading this specific wallpaper fails" show up as counts instead of as a wall of text.
  3. Reply drafts — for critical rows only, generate a short apology plus first response in both Japanese and English.

I kept (3) off for the first week and only added it once the labels in (1) and (2) felt right. Layering the tasks one by one made it much easier to see which step was burning tokens and which one was actually pulling its weight.

A minimal File API upload

The actual setup is a small Python script using the google-genai SDK. The pattern is: upload the CSV, then put the returned file handle at the start of contents.

import pathlib
from google import genai
from google.genai import types
 
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
 
csv_path = pathlib.Path("reviews_2026-05-week2.csv")
 
uploaded = client.files.upload(
    file=csv_path,
    config=types.UploadFileConfig(
        display_name="reviews_2026-05-week2",
        mime_type="text/csv",
    ),
)
 
prompt = """
The CSV is a week of App Store Connect reviews.
One row per review. Columns: row, rating, locale, title, body, app_version, os_version, created_at.
 
Return exactly one label per row as a JSON array:
- critical: crash, billing issue, data loss, purchase not honored
- support: how-to questions, feature requests, confusing UI
- neutral: vague comments, low-star reviews with no specifics, spam
 
Output JSON only.
Format: [{"row": 1, "label": "critical"}, ...]
"""
 
response = client.models.generate_content(
    model="gemini-2.5-flash",
    contents=[uploaded, prompt],
    config=types.GenerateContentConfig(
        response_mime_type="application/json",
        temperature=0.1,
    ),
)
 
print(response.text)

Setting temperature=0.1 matters here. The same CSV gives nearly identical results across runs, and for a labeling task that consistency is the whole point — it makes diffs between weeks much easier to read.

Uploaded files expire after 48 hours, so I just re-upload at the start of each batch. For the full twelve-thousand-row history I split by month into separate files, which made it much easier to retry a single month when something failed.

Three rough edges I had to round off

Two weeks of running this gave me three concrete things to share, in case you walk the same path.

The first was that multi-line review bodies make Gemini lose track of row numbers. If a reviewer puts an emoji-laden newline inside their body, the model would sometimes re-count rows internally. The fix was to encode the CSV with csv.writer(quoting=csv.QUOTE_ALL) so every field is wrapped in double quotes, plus carrying an explicit row column.

import csv
 
with open("reviews_2026-05-week2.csv", "w", newline="", encoding="utf-8") as f:
    writer = csv.writer(f, quoting=csv.QUOTE_ALL)
    writer.writerow(["row", "rating", "locale", "title", "body", "app_version", "os_version", "created_at"])
    for i, r in enumerate(reviews, start=1):
        writer.writerow([i, r.rating, r.locale, r.title, r.body, r.app_version, r.os_version, r.created_at])

Asking Gemini to return the row value from the CSV itself, instead of asking "which row is this", made downstream joining trivially correct.

The second thing was label skew. For the first few days I was getting fewer critical labels than my gut expected. Looking into it, short English reviews like "Crashed once" or "Couldn't restore purchase" were being treated as neutral. One added sentence in the prompt — "short reports of a crash or purchase failure count as critical even without details" — brought the ratio back to where my hand sample said it should be.

The third was the reply drafts ending up too formulaic. Left to its own devices, Gemini produced the same shape: apology, ask for repro, thanks for understanding. I do not personally enjoy long apologies, so instead of writing more rules into the prompt I replaced two example replies with two of my own past responses (with personal info stripped). Two or three real examples in the prompt shifted the tone faster than any amount of rule writing did.

Where Batch Mode fits, and where it does not

Batch Mode runs at half the cost and is great for "kick it off overnight, look at the answers in the morning". But the latency contract is "within 24 hours", and for "I want the critical reviews triaged before I sit down for coffee" that contract is too loose.

After two weeks I have settled into a split:

  • File API with synchronous generateContent — same-day reviews, classify and summarize before the day ends. Mostly gemini-2.5-flash.
  • Batch Mode — historical passes (six months, one year) and full re-labels when I change a label definition. gemini-2.5-pro for those, to lean into accuracy.

The combined flow is: a nightly batch refreshes labels across the whole history, and during the day I run the File API path to draft replies for that day's critical rows. The number of reviews I personally read fell from around seven hundred a week to about ninety. I open the critical candidates, tweak roughly twenty percent of Gemini's draft, and send.

What I want to add in the next two months

Three things are on the immediate list. First, hooking into the App Store Connect API to fully automate the CSV export — right now I still do that step by hand on the weekend, and removing it would let "today's critical drafts" show up in Slack without me touching anything.

Second, putting Google Play reviews through the same pipeline. I run the Android side of the wallpaper apps too, and the Reply to Reviews API on that side is friendlier than what Apple offers.

Third, measuring whether replies actually move the star rating. If I can track whether a replied-to reviewer later raises their rating, I will have a real signal for how to tune the drafts. I still remember being a sixteen-year-old in 1997 teaching myself to code in order to talk to strangers across the world, and replying to a review feels like answering one of those messages — only this time with a few thousand of them waiting. That is exactly the reason I want a way to look back and ask "did this answer actually land".

If you are also an indie developer drowning in reviews, I hope some of this is useful. 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.

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

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API / SDK2026-05-30
Two Months of Turning App Store Connect Daily Sales into a Slack Digest with Gemini 2.5 Flash
Notes from two months of running App Store Connect Sales/Trends data through Gemini 2.5 Flash and posting a short morning digest to Slack. Why Flash beat Pro for this job, how AdMob and store revenue stopped colliding, and what a single 'normal/check' label changed.
API / SDK2026-05-25
Running In-App Help Translation on Gemini 2.5 Flash for Three Months — An Indie Developer's Notes
After three months running my iOS and Android in-app help through a Gemini 2.5 Flash translation pipeline, here are the operational notes — when to fall back to Pro, how glossaries help, and the small lift it added to AdMob revenue.
API / SDK2026-05-13
Building an AdMob Revenue Anomaly Detector with Gemini API Function Calling
Learn how to build an automatic AdMob revenue anomaly detection system using Gemini API Function Calling — with real Python code, practical tips from 10+ years of indie app development, and Slack alerting integration.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →