I have been building apps independently as Dolice since 2014, and the wallpaper titles in that catalog have grown into a cumulative 50 million downloads. These notes pull out only what actually worked in practice.
I could not hand-rewrite a 100-character field 40 times over
The App Store Connect keyword field gives you exactly 100 characters per locale. It is only 100 characters, but what you put there visibly changes organic discovery. The problem was that I run roughly 40 wallpaper apps, and giving each one keywords in Japanese, English, Spanish, German, Portuguese, and Traditional Chinese means filling 40 × 6 = 240 slots by hand.
For years I honestly filled only the Japanese and English locales and left the rest empty or reused English — even though more than half of my revenue comes from users outside Japan. Every time I remembered touching the internet at sixteen in 1997 and feeling that technology could connect people across languages, leaving those keyword fields un-localized felt like a quiet contradiction I kept postponing.
So at the end of April 2026 I wired Gemini 2.5 Flash into the process to draft the keywords. This is a log of the first month — not a finished optimum, but the places I stumbled, left as they were.
Why Flash rather than Pro
I tried Gemini 2.5 Pro first, and it was plainly overkill for keyword generation. Each slot takes about 200 tokens of input — the app name, category, and a few bullet-point features — and the output is under 100 characters. Cycling through 240 slots cheaply and quickly matters far more than reasoning depth.
In my measurements Flash answered in about a second per slot, against three to five seconds for Pro. Run as an overnight batch over 240 slots, that difference is not negligible. Flash was also dramatically cheaper, and even running every slot several times kept the cost to pocket change. On quality, once paired with structured output and the validation below, Flash was comfortably good enough. Not over-speccing the model relative to the difficulty of the task is one of the disciplines I hold to in production.
Receive keywords as structured output, not free text
My first mistake was asking the model in the prompt for "comma-separated keywords under 100 characters" and accepting free-form text. Flash returns keywords obediently, but it occasionally adds an explanation, slips in punctuation, or runs a few characters over. Across 240 slots, cleaning up output that is "almost right but not machine-parsable" becomes its own nightmare.
So I pinned the type with responseSchema and received the keywords as an array. I do not ask the model to control the length; I pack it myself after receiving it.
from google import genai
from google.genai import types
client = genai.Client(api_key="YOUR_GEMINI_API_KEY")
def draft_keywords(app_name: str, features: list[str], locale: str) -> list[str]:
prompt = (
f"Suggest search terms for an App Store keyword field.\n"
f"App name: {app_name}\n"
f"Features: {', '.join(features)}\n"
f"Locale: {locale}\n"
f"- Use terms users in {locale} would actually search\n"
f"- Do not include words already in the app name or category\n"
f"- 2-12 characters each, 20 terms max"
)
res = client.models.generate_content(
model="gemini-2.5-flash",
contents=prompt,
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema={
"type": "array",
"items": {"type": "string"},
},
temperature=0.4,
),
)
return [w.strip() for w in res.parsed if w.strip()]With an array, the downstream "dedupe → byte check → pack to 100 characters" runs as deterministic code. The trick was to leave none of the generation's uncertainty inside the model and instead receive it as editable material.
CJK byte counting tripped me up repeatedly
The keyword field is labeled "100 characters," but for Japanese and Traditional Chinese it is effectively counted by character, while leaving length control to Gemini produced mismatches between a len()-based count and per-locale full-width handling — and my build-time validation caught them again and again.
In the end I unified on letting my own code, not the model, finalize the length. I expect Gemini only to "over-produce candidates," and I pack toward 100 characters deterministically, in priority order.
def pack_keywords(words: list[str], limit: int = 100) -> str:
seen, packed, length = set(), [], 0
for w in words:
key = w.lower()
if key in seen:
continue
add = len(w) + (1 if packed else 0) # account for the comma
if length + add > limit:
continue
packed.append(w)
seen.add(key)
length += add
return ",".join(packed)Apple's help says keywords are comma-separated with no spaces needed, and adding spaces naively wastes precious characters. Gemini's output sometimes contains spaces too, so I strip them at the packing stage. A small detail, but in a world of only 100 characters, every character counts.
Deduping against the title and subtitle mattered most
The biggest ASO gain came not from clever keyword generation but from the dull work of deduplication. The App Store indexes the app name, subtitle, and keyword field together. If your title already contains "wallpaper," repeating "wallpaper" in the keyword field wastes part of your 100 characters.
So I tokenized each app's title and subtitle into an exclusion list, handed it to Gemini as "please don't use these," and then filtered again on my side after generation. I guarded twice because instructions alone still let about 20% of excluded words slip back in. Instruction and validation work best as a pair — that lesson kept reasserting itself.
This deduplication alone freed up space for higher-volume terms, recovering on average three to four words' worth of room per app.
Handling prohibited terms and brand names
Another pitfall is slipping in a competitor's app name or trademark. Gemini is obliging, so when a feature line carried a phrase like "popular X-style," it would sometimes propose another company's brand as a search term. Because that invites review rejection and legal risk, I keep a simple blocklist of brand and competitor app names and always strip them after generation.
This is an area you must not leave to the model's cleverness. Where a legal or policy judgment is required, I always build the last line of defense in deterministic code. Letting AI draft is efficient, but final responsibility stays with the operator.
What stayed with me after a month
Honestly, there is no flashy story of revenue jumping a month after changing keywords. In a category as crowded as wallpapers, a month of impression movement is hard to separate from seasonality and featuring effects. Still, the apps where I replaced reused-English fields with native-language keywords showed a gentle upward trend in search-driven impressions.
What mattered most was that 240 slots — work I would otherwise never have touched by hand — became something I could run with realistic effort. Gemini drafts, my code packs and validates, and I confirm with my own eyes at the end. With that division of labor in place, the psychological barrier to adding a locale all but vanished.
More than the technology itself, I value that it turned a job I had left undone into a job I could actually do. Next, I want to pull in real per-locale search-volume data and automate the prioritization of terms as well. I hope this helps anyone else whose multi-locale rollout has stalled.