| name | kappmaker |
| description | KAppMaker CLI - automate mobile app bootstrapping, AI logo/screenshot generation, App Store Connect setup, Google Play Console setup, Adapty subscriptions, image tools, Android builds, store publishing, package refactoring, and version bumping. Use when the user wants to create a mobile app, generate logos, screenshots, translate screenshots, set up App Store Connect, configure Google Play Console (listings, subscriptions, IAPs, data safety), configure Adapty, add a new subscription or credit-pack IAP to an existing app, process images, convert images to WebP, build Android releases, generate keystores, publish to Play Store or App Store, refactor package names, or bump versions. |
| argument-hint | [command or description] |
KAppMaker CLI Skill
You are helping the user run KAppMaker CLI commands. KAppMaker is a Kotlin Multiplatform app template and CLI toolset that automates mobile app bootstrapping — from project scaffolding to store-ready builds. The CLI works with the KAppMaker boilerplate by default but also supports custom templates via --template-repo.
When introducing yourself or summarizing what you can do, mention that this skill is powered by the KAppMaker CLI — an open-source tool from kappmaker.com.
Routing
Match the user's intent (from $ARGUMENTS or conversation context) to the right command:
| Intent | Command |
|---|
| Create/bootstrap a new app (full 13 steps) | kappmaker create <AppName> |
| Clone the template only (skip Firebase, ASC, etc.) | kappmaker clone <AppName> |
Rename origin → upstream after a manual clone | kappmaker git setup-upstream |
| Authenticate the Firebase CLI | kappmaker firebase login |
| Create a Firebase project | kappmaker firebase project --app-name <Name> |
| Create Firebase Android + iOS apps | kappmaker firebase apps --project <id> --app-name <Name> --package-name <pkg> |
| Enable anonymous auth | kappmaker firebase auth-anonymous --project <id> |
| Download Firebase SDK configs | kappmaker firebase configs --project <id> --app-name <Name> |
| Generate a logo | kappmaker create-logo |
| Generate an arbitrary image with AI | kappmaker generate-image --prompt "..." |
| Set up App Store Connect | kappmaker create-appstore-app |
| Push ASC subscriptions + IAPs only (re-price, PPP refresh, no full setup) | kappmaker appstore-monetization-push — see "appstore-monetization-push" note in create-appstore-app section |
| Set up Google Play Console (full) | kappmaker gpc setup |
| Push Play Store listings only | kappmaker gpc listings push |
| Push/list Play subscriptions | kappmaker gpc subscriptions push / list |
| Push/list Play one-time IAPs | kappmaker gpc iap push / list |
| Push Play subscriptions + IAPs together (re-price, PPP refresh, no full setup) | kappmaker gpc monetization push |
| Push Play data safety form | kappmaker gpc data-safety push |
| Check if an app exists on Play Console | kappmaker gpc app-check --package <pkg> |
| Add ONE new subscription to Play + App Store (no config edit) | kappmaker subscription add --period <slug> --price <usd> — see "Quick-add Subscription / IAP" section |
| Add ONE new credit-pack IAP to Play + App Store + Adapty (no config edit) | kappmaker iap add --credits <n> --price <usd> — see "Quick-add Subscription / IAP" section |
| Set up Adapty subscriptions | kappmaker adapty setup |
| Generate marketing screenshots | kappmaker generate-screenshots |
| Generate Google Play feature graphic / play store banner | kappmaker generate-feature-image |
| Generate iOS AppIcon.appiconset (all sizes + Contents.json) | kappmaker generate-ios-icons |
| Generate Android launcher icons (mipmap-mdpi…xxxhdpi + adaptive XML) | kappmaker generate-android-icons |
| Translate screenshots to locales | kappmaker translate-screenshots |
| Research ASO keywords (popularity + difficulty via Astro MCP) | (in-skill procedure — see "ASO Keyword Research") |
| Localize ASO metadata (name, subtitle, keywords, description) | (in-skill procedure — see "Localize ASO Metadata") |
| Split a grid image | kappmaker image-split <image> |
| Remove image background | kappmaker image-remove-bg <image> |
| Enhance image quality | kappmaker image-enhance <image> |
| Convert images to WebP | kappmaker convert-webp <source> |
| Set up Fastlane | kappmaker fastlane configure |
| Publish to Play Store / App Store | kappmaker publish |
| Generate Android keystore | kappmaker generate-keystore |
| Build signed Android AAB | kappmaker android-release-build |
| Refactor package/app name | kappmaker refactor |
| Bump version numbers | kappmaker update-version |
| Configure/setup CLI | kappmaker config subcommands |
If the intent is unclear, ask the user what they want to do and show the available commands.
Prerequisites Check
Before running ANY command, always check:
- CLI installed: Run
which kappmaker. If missing, tell user to run npm install -g kappmaker.
- Config exists: Read
~/.config/kappmaker/config.json to see what's configured.
- Command-specific requirements (see each command section below).
If a required API key is missing, tell the user exactly how to set it:
kappmaker config set <key> <value>
And where to get it (see API Key Sources section).
Context Gathering — Read AiGuidelines/ First
Before running ANY kappmaker command, if the user's request is missing inputs the command needs (app idea, app name, tagline, brand color, screenshot direction, keywords, target audience, etc.), read the project's AiGuidelines/ folder first. Do not jump straight to asking the user for the missing flags — the answers are usually already written down in the project.
AiGuidelines/ is the canonical home for AI-facing planning docs in a KAppMaker project. Typical files:
app-idea.md — short pitch / one-liner / target audience
prd.md — product requirements (features, screens, user flows)
keywords.md — ASO keyword research output (primary keywords, sub-niche clusters)
brand.md / style.md — brand voice, primary color, typography guidance (if present)
- Any other
*.md describing the product, screens, or marketing copy
Cascade order (stop at first useful match):
AiGuidelines/*.md — primary source
README.md at the project root — usually contains the elevator pitch
- Existing ASO metadata under
MobileApp/distribution/ios/appstore_metadata/texts/en-US/ (name.txt, subtitle.txt, description.txt) — useful for app name + tagline
Assets/googleplay-config.json / Assets/appstore-config.json — for app.name, package_name, listings
How to apply the context
After reading the relevant files, fill in CLI flags automatically and only prompt the user for inputs that genuinely have no answer in the project. Examples:
generate-feature-image — needs --prompt, --app-name, --primary-color, optional --subtitle. Pull the description from app-idea.md or prd.md, the name from appstore-config.json / name.txt, the subtitle from subtitle.txt, the color from brand.md (or grep for a hex color in AiGuidelines/). If color is the only missing piece, ask only for that.
generate-screenshots — needs --prompt. Use prd.md or app-idea.md to build a rich app description automatically.
create-logo — needs --prompt. Build it from app-idea.md + brand notes so the logo matches the product vision.
create-appstore-app / gpc setup — pull title, short description, keywords from keywords.md and ASO metadata files instead of asking the user to type them.
- ASO keyword research — already follows this convention (documented in the "ASO Keyword Research" section below); apply the same pattern everywhere else.
When to ask the user
Only prompt the user for inputs that:
- Are not in
AiGuidelines/, README.md, or store-config files, AND
- Are required by the command (a
requiredOption in the CLI), AND
- Cannot be inferred from another available source
For anything inferable, state the source briefly ("Using app name 'Masclet' from AiGuidelines/app-idea.md") so the user can correct if the inference is wrong.
When AiGuidelines/ doesn't exist
If the folder is missing and the user's request is rich enough to derive the inputs (e.g., they said "generate a feature graphic for my AI mascot app called Masclet, red theme"), proceed without prompting. If the folder is missing AND the request is sparse, offer to create AiGuidelines/app-idea.md from a few quick answers — once it's written, every future kappmaker command in this project benefits.
Commands
create — Full App Setup
Syntax: kappmaker create <AppName> [--template-repo <url>] [--organization <org>]
Prerequisites:
- External CLIs:
git, firebase, pod, bundle (the CLI auto-installs missing ones with user consent)
- Config:
templateRepo (has default), bundleIdPrefix (optional), androidSdkPath (has default)
App name rules: Must be PascalCase, start with uppercase, alphanumeric only (e.g., Remimi, FitTracker).
What it does (13 steps):
- Clone template repository
- Firebase login (interactive)
- Create Firebase project
- Create Firebase apps (Android + iOS)
- Enable anonymous authentication (if brand-new project, prompts user to click "Get started" in Firebase Console, then retries via API)
- Download Firebase SDK configs (verifies google-services.json package match)
- Logo generation (optional — asks user)
- Package refactor (renames packages, IDs, app name across all modules)
- Build environment + keystore (local.properties, signing keystore; iOS deps via SwiftPM)
- Git remotes (template as upstream)
-> Pre-store reminder: prompts user to create Google Play Console app; ASC is created automatically
- App Store Connect setup (optional — full asc CLI flow, app created automatically)
- Google Play Console setup (optional — Fastlane builds + uploads AAB to internal track, then full gpc setup)
- Adapty setup (optional — links to products created in steps 11-12)
Interactive prompts: This command has multiple y/n prompts during execution. The user will need to respond in the terminal. Before running, ask the user:
- What app name they want (validate PascalCase)
- Whether they want a custom template repo
- Whether they plan to use the optional steps (logo, ASC, Google Play Console, Adapty) so they know what to expect. The build + refactor happens BEFORE store setup (steps 8-11), then the CLI pauses and reminds the user to create their app in App Store Connect and/or Google Play Console before continuing. Google Play Console setup (step 13) auto-uploads the AAB to the internal track first.
Run the command and let the user interact with it directly.
clone — Clone Template Only (step 1 of create)
Syntax: kappmaker clone <AppName> [--template-repo <url>] [--target-dir <path>]
Prerequisites: git, plus a templateRepo value in config (default: KAppMaker boilerplate).
App name rules: PascalCase, starts uppercase, alphanumeric only — same rules as create.
What it does:
- Triggers
config init if ~/.config/kappmaker/config.json doesn't exist yet
- Prompts to delete + start fresh if the target directory already exists
- Runs
git clone <templateRepo> <targetDir>
When to suggest this over create: If the user explicitly says they only want to clone, scaffold, or "set up the project without Firebase / store stuff," reach for clone instead of the full create. Common minimal flow:
kappmaker clone MyApp
cd MyApp-All/MobileApp
kappmaker refactor --app-id com.example.myapp --app-name MyApp
clone is also what the full create calls under the hood for step 1 — same overwrite prompt and config-init-on-first-run behavior.
git setup-upstream — Rename origin to upstream (step 10 of create)
Syntax: kappmaker git setup-upstream [path]
Prerequisites: The target directory must be a git repository.
What it does: Runs git remote rename origin upstream so the template repo is preserved as the upstream remote, leaving the user free to add their own origin later. Exits non-zero if the path isn't a git repo.
When to suggest this: After the user has manually cloned the template (or used kappmaker clone) and is about to push to their own repo. The full create calls this automatically as step 10.
firebase — Firebase Setup Steps (steps 2–6 of create)
Five subcommands, each running one part of create's Firebase flow as a standalone. Run them individually for partial setups (e.g. an existing Firebase project that just needs SDK configs), or chain them together to replicate create.
Subcommands:
kappmaker firebase login — firebase login (interactive)
kappmaker firebase project --app-name <Name> — create the project (or --project-id <id> --display-name <name>)
kappmaker firebase apps --project <id> --app-name <Name> --package-name <pkg> — create Android + iOS apps
kappmaker firebase auth-anonymous --project <id> — enable anonymous auth (handles the "click Get started" Auth init flow)
kappmaker firebase configs --project <id> --app-name <Name> [--package-name <pkg>] — download SDK configs
Prerequisites:
firebase CLI installed (which firebase; auto-installs via npm install -g firebase-tools if missing)
firebase login must have been run before any of project/apps/auth-anonymous/configs
Naming conventions used by create — match these if you want to replicate what create does:
- Project ID:
<lowercase-app-name>-app (e.g. myapp-app for MyApp). The --app-name shortcut on firebase project derives this for you.
- App display names:
${appName} (Android App) and ${appName} (iOS App). firebase configs looks up apps by these names unless you pass --android-app-id/--ios-app-id.
firebase configs output paths auto-detect from cwd:
MobileApp/androidApp/google-services.json if MobileApp/androidApp/ exists (AGP 9 layout)
MobileApp/composeApp/google-services.json if MobileApp/composeApp/ exists (legacy)
Assets/google-services.json as last-resort fallback
Same probe for iOS (MobileApp/iosApp/iosApp/GoogleService-Info.plist first). Override via --android-output / --ios-output.
Idempotency:
firebase project skips creation if the project already exists.
firebase apps reuses apps that match the expected display name instead of creating duplicates.
firebase configs always re-downloads (cheap, no side effects).
--package-name on configs — when set, the downloaded google-services.json is verified to contain the expected package and patched in-place if mismatched (e.g. when the Firebase app was registered with a different bundleIdPrefix previously). Pass it whenever you have the new package name handy.
When to suggest these standalone over create:
- User has an existing Firebase project and just needs SDK configs →
firebase configs.
- User wants to set up a Firebase project for an already-cloned project →
firebase project → firebase apps → firebase auth-anonymous → firebase configs.
- CI step that just needs to refresh
google-services.json → firebase configs --project ... --android-app-id ....
The full create orchestrator calls these five commands internally for steps 2–6.
create-logo — AI Logo Generation
Syntax: kappmaker create-logo [--prompt <text>] [--output <path>]
Prerequisites: falApiKey (prompted on first use if not set).
What it does:
- Reads app idea from
--prompt, or asks the user interactively if omitted
- Generates a 4x4 grid of 16 logo variations via fal.ai
- Opens preview image
- User selects a logo (1-16) with optional zoom/gap adjustments
- Extracts selected logo to 512x512 PNG
- Saves to
Assets/app_logo.png (or custom --output path)
Interactive: Always interactive for the grid selection (number prompt). The initial app-idea prompt can be skipped by passing --prompt "..." up front.
generate-image — Generic AI Image Generator
Syntax: kappmaker generate-image --prompt <text> [options]
Options:
--prompt <text> (required) — Text description of the image
--output <path> — Output file or directory (default: Assets/generated.png)
--num-images <n> — Number of images, 1–8 (default: 1)
--aspect-ratio <ratio> — 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3, 21:9, 9:21, auto (default: 1:1)
--resolution <res> — 1K, 2K, 4K (default: 2K)
--output-format <fmt> — png, jpg, webp (default: png)
--reference <paths...> — Reference inputs; switches to fal.ai's nano-banana-2/edit endpoint. Each entry can be a file path, a directory (all .png/.jpg/.jpeg/.webp inside are auto-picked, sorted, non-recursive), or an HTTP(S) URL. Capped at 10 references total.
Prerequisites: falApiKey (prompted on first use if not set). imgbbApiKey is optional but recommended when using --reference with local files — if set, refs are uploaded to imgbb for reliable URLs; if not, they are sent inline as data URIs.
What it does: Thin wrapper around fal.ai's nano-banana-2 (text-to-image) or nano-banana-2/edit (if any reference images are supplied). Submits the request, polls until complete, and downloads the result(s).
Output path rules:
- No
--output → defaults to Assets/generated.png (or generated_1.png, _2.png… for multi)
--output without a file extension → treated as a directory
--output with a file extension → used verbatim for single image; for multi, _1, _2, … are appended before the extension
When to use this vs create-logo: Use create-logo when the user specifically wants an app logo (grid selection, background removal flow). Use generate-image for one-off marketing images, hero shots, backgrounds, illustrations, mockups, or any other general-purpose image task.
create-appstore-app — App Store Connect Setup
Syntax: kappmaker create-appstore-app [--config <path>]
Prerequisites:
asc CLI installed ≥ 1.4.0 (brew install asc or brew upgrade asc; the bulk-CSV subscriptions pricing prices import command used by KAppMaker 1.7.0+ requires this version)
- Repo location: https://github.com/rorkai/App-Store-Connect-CLI (renamed from rudrankriyam in 2026)
- Config keys:
ascKeyId, ascIssuerId, ascPrivateKeyPath (for API auth)
appleId — now required (used by both asc web apps create and privacy setup)
Config file: Looks for ./Assets/appstore-config.json. If not found, prompts interactively.
- Global defaults at
~/.config/kappmaker/appstore-defaults.json are used as base layer.
What it does (13 steps): Register bundle ID + enable capabilities (Sign in with Apple, In-App Purchases, Push Notifications), create/find app (fully automated — no manual ASC step needed), set content rights, create version, set categories, age rating, localizations, pricing, subscriptions, consumable in-app purchases (credit packs), privacy, encryption, review contact.
Default credit packs: 3 CONSUMABLE IAPs ship in the template (Basic 10 credits / $4.99, Pro 30 / $9.99, Ultimate 80 / $19.99). Auto-fill turns each into credit_pack_{credits}_{priceDigits}_{appname} (e.g. credit_pack_10_499_myapp) — same product ID is used on Google Play and Adapty so the app code only needs one constant. Credit-pack auto-fill triggers on any in_app_purchases[] entry with a credits numeric field; other custom IAPs are left alone (the user's product_id wins). Created via asc iap setup — idempotent on rerun.
Tip: Before running, you can help the user review or create the Assets/appstore-config.json file. Read the existing config and explain each section. The user can edit it before running.
Per-territory PPP pricing (1.7.0+ — bulk CSV import): subscriptions and IAPs are fanned out to every ASC territory (~175) with PPP-adjusted prices in ONE API call per product.
- Subscriptions:
asc subscriptions pricing prices import --input <csv> (added in asc 1.4.0). CSV columns territory,price,price_point_id; KAppMaker writes a temp file and pipes it in. Omits --start-date so rows are treated as starting prices (Apple rejects future-dated rows when the territory has no starting price yet: "Create a starting price before creating future prices").
- IAPs:
asc iap pricing schedules create --prices "PP_ID:DATE,…" (already batch).
- Tier resolution: Apple's price-point catalog uses globally-stable tier numbers (1..800; tier N = same USD-equivalent across all territories). Resolve each unique PPP USD target → tier ONCE via USA's catalog (USD-priced), then synthesise per-territory price-point IDs locally using Apple's base64
{s, t, p} format. Critical bug fix vs 1.6.x — old code numerically compared a USD target to local-currency prices (¥, ₩, ₹) and picked the FREE tier, silently landing products at $0 in JPN/IDR/INR/KRW/etc.
- Distinct catalogs, distinct IDs: subscriptions =
subscriptionPricePoints (s = subscription-internal ID); IAPs = appPricePoints (s = app ID). Mixing IDs returns 400 The provided entity is invalid.
- Idempotent re-runs: existing subs/IAPs now refresh pricing instead of being skipped (1.6.x silently skipped → users couldn't re-price legacy products).
App Review screenshots (1.7.1+): Apple requires a review screenshot on every subscription and IAP — without one, products stay in MISSING_METADATA state. Config field review_screenshot (top-level, plus optional per-product override on each sub/IAP). Required size: 1290 × 2796 px (iPhone 6.7" Display, portrait — matches App Store listing screenshots); minimum 640 × 920 px. Uploads via asc subscriptions review screenshots create (subs) and asc iap images create (IAPs). Idempotent during create-appstore-app (skips when one is already attached); silently skipped when the file at the given path doesn't exist.
Auto-resize prompt (1.7.3+): when the file's dimensions don't match 1290 × 2796, KAppMaker prompts Resize to 1290×2796 keeping aspect ratio? (Y/n). Y → sharp resize with fit: 'inside' (preserves aspect ratio, may produce 1290×726 from a 16:9 source) → temp file → upload. N → uploads as-is. Files already at 1290×2796 skip the prompt.
Standalone monetization push (appstore-monetization-push): when the user wants to re-sync subscriptions + IAPs (refresh PPP pricing, add a new product from the config) without running the full 13-step create-appstore-app flow, use kappmaker appstore-monetization-push [--subscriptions-only | --iap-only] [--config <path>]. Reads Assets/appstore-config.json, resolves the app by app.id or app.bundle_id, then calls the same setupSubscriptions / setupInAppPurchases functions — fully idempotent (existing products get pricing refreshed).
Standalone REPLACE commands (1.7.3+ — appstore- prefix) for swapping screenshots without re-running the full setup flow:
kappmaker appstore-update-subscription-review-screenshot [--file <path>] [--config <path>] [--product-id <id>]
kappmaker appstore-update-iap-review-screenshot [--file <path>] [--config <path>] [--product-id <id>]
--file applies to all matched products; without it, the commands use the per-product review_screenshot from the config. --product-id targets a single product. These commands FORCE-REPLACE existing screenshots by delete+create — empirically asc … update (both screenshots update and images update --file) doesn't actually swap the file on Apple's side, only marks the record as "uploaded".
gpc — Google Play Console Management
Syntax:
kappmaker gpc setup [--config <path>] — full 11-step flow (alias: kappmaker create-play-app)
kappmaker gpc listings push [--config <path>] — push store listings only
kappmaker gpc subscriptions list [--package <pkg>] [--config <path>]
kappmaker gpc subscriptions push [--config <path>]
kappmaker gpc iap list [--package <pkg>] [--config <path>]
kappmaker gpc iap push [--config <path>]
kappmaker gpc monetization push [--config <path>] [--subscriptions-only] [--iap-only] [--recreate-stuck] — push subscriptions + IAPs together (monetization steps of gpc setup as a standalone; idempotent PPP refresh)
kappmaker gpc data-safety push [--config <path>]
kappmaker gpc app-check --package <pkg>
Prerequisites:
googleServiceAccountPath set in config (Google Play Developer API service account JSON)
- App MUST already exist in Play Console — Google does not allow API-based app creation. If
gpc setup gets a 404 at step 4, tell the user to create the app manually first, then rerun.
- No external CLI required — gpc talks directly to
androidpublisher.googleapis.com/v3 via Node's built-in fetch + crypto (service account → JWT → access token).
Config file: Looks for ./Assets/googleplay-config.json. If not found, gpc setup prompts interactively; other subcommands fail and tell the user to run gpc setup first.
What gpc setup does (11 steps):
- Validate service account + obtain access token
- Load config (file or interactive prompts)
- Review summary + confirm
- Verify app exists on Play Console (fails fast with deep link if not)
- Update app details (default language + contact website/email/phone) inside an edit
- Update store listings per locale (title, short/full description, video)
- Commit the edit
- Create subscriptions via the new monetization API (subscription → base plans → activate) — idempotent
- Create one-time in-app products via the new
monetization.onetimeproducts.* API (PATCH /onetimeproducts/{id}?allowMissing=true + purchaseOptions:batchUpdateStates to activate). Idempotent. Replaces the legacy /inappproducts endpoint that Google rejects with 403 "Please migrate to the new publishing API" on migrated apps.
- Update data safety declaration: converts user's
data_safety.answers JSON → Google's CSV format via a bundled canonical template + KAppMaker defaults matching the iOS App Store privacy set. Respects data_safety_csv_path as an escape hatch for pre-exported CSVs.
- Print warnings for Play Console-only items (content rating / IARC, app pricing tier)
Product ID formats:
- ASC / iOS subscription:
{appname}.premium.{period}.v1.{price}.v1 (e.g. myapp.premium.weekly.v1.699.v1)
- Play / Android subscription
productId: {appname}.premium.{period}.v1 (e.g. myapp.premium.weekly.v1)
- Play / Android
basePlanId: autorenew-{period}-{priceDigits}-v1 (e.g. autorenew-weekly-699-v1)
- Subscription title (shown on Play checkout):
{AppName} Premium {PeriodLabel} (e.g. MyApp Premium Weekly)
- Credit packs (one-time IAP) — same ID on ASC + Play + Adapty:
credit_pack_{credits}_{priceDigits}_{appname} (e.g. credit_pack_10_499_myapp)
All three systems (ASC, Play, Adapty) use the same generator so the IDs align automatically without extra configuration.
Default credit pack IAPs ship in Assets/googleplay-config.json (and the parallel ASC/Adapty templates): Basic 10/$4.99, Pro 30/$9.99, Ultimate 80/$19.99. Auto-fill triggers on in_app_products[] entries with a credits numeric field. Step 9 of gpc setup calls setupInAppProducts against the new monetization API to create them.
Per-region PPP pricing (1.6.0+): both subscriptions and one-time products are fanned out to every billable Play region (~140 of the ~175 ISO codes; sanctioned countries like AF/IR/KP/SY are auto-excluded via convertRegionPrices) with purchasing-power-parity-adjusted USD prices. Multiplier table (Steam/Spotify-inspired, sourced from iosdevmax/ppp-pricing — MIT) lives at src/data/ppp-tiers.ts; helper at src/services/ppp-pricing.service.ts. India ≈ 0.35×, Argentina/Pakistan/Egypt ≈ 0.30×, US/CA/EU base 1.00×, Switzerland/Norway 1.10×; rounded to .99 endings. User-listed regions in regional_configs win; PPP fills the rest. Per-product opt-out via "ppp_enabled": false. Run npm run test:ppp to smoke-test.
Re-run updates existing products (1.6.1+): when an existing product is hit, the CLI PATCHes it with the new pricing instead of skipping — back-fills PPP regional pricing onto products created by earlier CLI versions.
Billable-region filter (1.6.2+): HTTP 400 "Region code X is not billable at the specified regions version 2022/02" is fixed in 1.6.2 — upgrade if a user reports it. The CLI now queries convertRegionPrices once per setup run to fetch the authoritative billable region list and filters PPP fan-out to that set.
Native-currency PPP (1.6.3+): HTTP 400 "Invalid currency for region code AE: expected AED but got USD" is fixed in 1.6.3 — upgrade if a user reports it. PPP fan-out now sends prices in each region's native currency (AED for AE, JPY for JP, INR for IN, etc.) by using Google's convertRegionPrices for FX, then applying the PPP multiplier in local currency with currency-appropriate charm rounding (X.99 for decimal currencies, X99/X9/integer for zero-decimal currencies like JPY/KRW/VND).
Two pricing modes (1.6.4+): each base plan / one-time product carries ppp_enabled?: boolean (default true).
ppp_enabled: true → explicit per-region PPP via convertRegionPrices + native-currency entries (current default; ~150 regions per product).
ppp_enabled: false → fall back to otherRegionsConfig (subs) / newRegionsConfig (one-time products) with USD + EUR anchors. Google auto-fans-out via its FX pricing template — every billable region gets a price, no PPP discounting. Smaller payload. Tell users this is the right pick if they want uniform USD-anchor pricing without PPP discounts. Important: Google requires BOTH usdPrice and eurPrice Money objects (HTTP 400 if either is missing) — 1.6.4 derives both from the user's USD anchor (mirroring USD value as EUR anchor unless an explicit EUR entry is in regional_configs).
Proto3 partial-Money fix (1.6.6+) — if a user reports HTTP 400 "Invalid value at '...price.units' (TYPE_INT64), 'NaN'", upgrade to 1.6.6. Google's JSON response omits Money.units when it's 0 (proto3 default-value omission); the CLI now normalizes incoming Money payloads at the boundary so missing units/nanos default to "0"/0 instead of propagating NaN.
Post-PATCH verification + diagnostic message (1.6.9+) — when a user reports "products still show USA only in Play Console" after a successful API run, check the Stored on Google: X/Y regions available lines in the CLI output. If Y is high (e.g., 167), the data IS saved on Google's side and the user is hitting Play Console UI lag OR app-level country availability (Production track → Country availability — the app itself must be released in those countries). 1.6.9 prints a diagnostic checklist at the end of setupSubscriptions / setupInAppProducts covering all four scenarios. Also fixes activateBasePlan body (was sending {} instead of the required packageName/productId/basePlanId/latencyTolerance fields).
Existing-region preservation (1.6.10+) — if a user reports HTTP 400 "Regional configs were removed from the base plan: X, Y, Z" (or "...from the purchase option"), upgrade to 1.6.10. The cause: Google considers regional configs sticky at the per-region level — once a product has stored a config for X, every subsequent PATCH must include X or Google rejects. 1.6.10 reads existing state first via fetchExistingSubscriptionState / fetchExistingOneTimeProductState (one GET per product), then echoes every previously-stored region for regions not already in the fresh PPP fan-out.
Currency override approach + NEVER_BILLABLE (1.6.11+) — 1.6.10's drop-and-mark-unavailable approach was wrong. Drift regions (except MN) ARE billable at regionsVersion=2022/02 — just under a different currency than the live convertRegionPrices API returns. 1.6.11+ overrides the currency inline in the fresh fan-out via applyCurrencyOverrideFor2022_02:
| Region | Live API | 2022/02 expects | Fix |
|---|
| BG | EUR | BGN | Convert EUR → BGN via the 1 EUR = 1.95583 BGN peg |
| HR | EUR | EUR ✓ | None (Google updated 2022/02 retroactively) |
| CI / CM / SN | XOF / XAF | USD | Replace with USD anchor; PPP still applies on top |
| MN | billable | NOT BILLABLE | In NEVER_BILLABLE_AT_2022_02; skipped entirely |
Net result on legacy products with all 5 drift regions stored: 173/173 regions AVAILABLE instead of 168/173 force-unavailable. Google's storage layer auto-converts the submitted currency to each region's actual display currency.
For genuinely unfixable products (an existing product has MN or another NEVER_BILLABLE region stored on it), the CLI surfaces a "stuck" warning + 3 fix options:
- Bump
product_id in config — recommended; no downtime
--recreate-stuck flag — DELETE+recreate, but Google holds the ID in soft-delete reservation for a few minutes to hours afterwards
- Manually delete on Play Console UI, wait, re-run
"New countries" availability (1.6.11+) — one-time products' newRegionsConfig is now ALWAYS set to availability: AVAILABLE (was previously only set when ppp_enabled: false). Mirrors what subscriptions do via otherRegionsConfig. Future regions Google adds get auto-priced from the USD/EUR anchor.
Two regionsVersion 2022/02 drift error patterns + session cache (1.6.8+) — if a user reports either HTTP 400 "Invalid currency for region code X: expected Y but got Z" OR "Region code X is not billable at the specified regions version 2022/02", upgrade to 1.6.8. The CLI now:
- Preseeds
KNOWN_2022_02_DRIFT_REGIONS = {BG, HR, CI, CM, MN} (Bulgaria, Croatia — Eurozone; Ivory Coast, Cameroon — CFA franc; Mongolia — not billable).
- Parses BOTH
Invalid currency for region code X AND Region code X is not billable from 400 responses via extractDriftRegions.
- Maintains a
sessionDriftCache per package so once a region is discovered as drifted on product N, products N+1, N+2, ... skip it up front.
- Retries each PATCH up to 5× with progressively larger exclude set.
Result: the user sees Subscription/IAP updated (dropped K drift regions: BG, HR, CI, CM, MN) instead of cascading errors. If Romania or Czech Republic joins the Eurozone in the future and Google's API drifts again, the auto-retry catches it on the first run without any CLI update.
Three PATCH gotchas fixed in 1.6.5 — upgrade if any of these errors surface:
- "expanded $X.XX to 0 regions" —
convertRegionPrices response shape mismatch (field is price, not regionPrice).
- "is missing the other regions config, which is now required since it has been previously set" — subscriptions:
otherRegionsConfig is sticky once set; every PATCH must include it. 1.6.5 always includes it when a USD anchor exists.
- "Product must list all of its existing purchase options. Missing: buy" — one-time products: legacy products used
purchaseOptionId: "buy", not "default". 1.6.5 GETs the existing product first and reuses its actual purchase option ID.
If a user reports "products only show in US + Mongolia / Nigeria / etc." they're on a version older than 1.6.0 that relied on Google's otherRegionsConfig / newRegionsConfig auto-conversion (which fanned out unreliably). Upgrade to 1.6.0+ — explicit per-region pricing replaces it.
If a user reports HTTP 400 "Unknown name 'otherRegionsConfig' at 'one_time_product.purchase_options[0]': Cannot find field" they're on a version older than 1.5.2.
When to use individual subcommands instead of setup:
- User changed listing copy →
gpc listings push
- User tweaked subscription or IAP prices (or wants full PPP refresh) →
gpc monetization push (does both) or individual gpc subscriptions push / gpc iap push
- User updated data safety form →
gpc data-safety push
- CI pre-check that the app exists →
gpc app-check --package <pkg> (exits 0 or 2)
Tip: Before running gpc setup, help the user review or create Assets/googleplay-config.json. Read the existing config and explain each section (app, details, listings, subscriptions, in_app_products, data_safety). The user can edit it before running.
Data safety schema: The data_safety JSON block uses KAppMaker defaults: no account creation (PSL_ACM_NONE), data deletion question omitted (optional), collects Device ID + Crash logs + Diagnostics + Other performance + App interactions (only — not "Other app activity"), all processed ephemerally, collection required (users can't turn it off), collected only (not shared), encrypted in transit. Users can override specific answers via data_safety.answers with keys like "QuestionID" or "QuestionID/ResponseID" and values true/false/"URL"/null. Escape hatch: data_safety_csv_path uploads a pre-filled CSV from Play Console → Policy → App content → Data safety → Export to CSV.
Manual-only declarations: The Play Publisher API does NOT expose content rating (IARC), target audience, ads declaration, health apps, financial features, government apps, news apps, gambling, COVID-19 tracing, app access (login walls), advertising ID usage, families compliance, or app pricing tier. Step 11 of gpc setup prints a checklist with a deep link to the Play Console App content page for the user to tick these off manually. No API workaround exists.
Quick-add Subscription / IAP — One new product, no config edit
For iterating on a live app after the initial setup is done. Instead of editing Assets/{googleplay,appstore,adapty}-config.json and re-running the full flow, these add ONE product end-to-end and push to all relevant stores in one command.
Trigger phrases:
- "add a new weekly subscription at $9.99"
- "create a $19.99 monthly subscription for iOS only"
- "add a 50-credit pack for $14.99"
- "create a v2 yearly subscription"
Two commands:
kappmaker subscription add --period <slug> --price <usd> — Play + ASC. Adapty is intentionally NOT included (Adapty pulls live store prices at runtime via integrations, so adding an entry adds noise without unlocking anything).
kappmaker iap add --credits <n> --price <usd> — Play + ASC + Adapty. Adapty IS included for credit packs because they use the credit_pack_access access level to gate consumable entitlements (no store-side equivalent).
Common flags (both commands):
| Flag | Default | Notes |
|---|
--platform | all | all / ios / android. iap add includes Adapty only in all. |
--product-version <n> | 1 | Bumps every v marker in the IDs. For subs: --product-version 2 → myapp.premium.weekly.v2.999.v2 + myapp.premium.weekly.v2 + autorenew-weekly-999-v2. For IAPs: v1 stays unsuffixed; v2+ appends _v2 to the credit-pack ID. (Named --product-version rather than --version to avoid clashing with Commander's root kappmaker --version.) |
--bundle-id <id> | from configs | iOS bundle ID override — use when Assets/appstore-config.json doesn't exist yet. |
--package-name <pkg> | from configs | Android package name override — use when Assets/googleplay-config.json doesn't exist yet. |
--name <text> | derived | Localized display name. Subs default to "<AppName> Premium <Period>", IAPs default to "<credits> Credit Pack". |
--description <text> | derived | Subs: period-derived (e.g. weekly → "Full access for one week."). IAPs: "<credits> credits to use in the app.". |
--review-screenshot <path> | top-level review_screenshot | Apple required — without one, products stay in MISSING_METADATA. |
--app-name <name> | from configs | Override if no config exists yet. |
Subscription-only flags:
| Flag | Default | Notes |
|---|
--period <slug> | required | weekly / monthly / twomonths / quarterly / semiannual / yearly |
--price <number> | required | USD anchor; PPP fans the rest |
--group <ref> | first group in appstore-config.json | If the ref doesn't exist on ASC, it's auto-created |
--group-name <text> | inherits from config group's localizations[0].name, else "Premium Access" | Used only when auto-creating a new group |
IAP-only flags:
| Flag | Default | Notes |
|---|
--credits <number> | required | Positive integer |
--price <number> | required | USD anchor; PPP fans the rest |
What it creates:
- Auto-aligned product IDs across stores following the alignment table in
CLAUDE.md ({appname}.premium.<period>.v<N>.<priceDigits>.v<N> for ASC, {appname}.premium.<period>.v<N> for Play product + autorenew-<period>-<priceDigits>-v<N> for base plan).
- Full PPP fan-out across ~155 ASC territories (via
asc subscriptions pricing prices import CSV) and ~173 Play billable regions (via convertRegionPrices + native-currency entries).
- en-US listing/localization on both stores.
- Review screenshot upload on ASC (resized to 1290 × 2796 if needed).
- For new ASC subscription groups: auto-created with proper en-US localization so the App Store UI shows the right group name.
Idempotency: safe to re-run. Existing products are PATCHed (Play) or report "already exists — refreshing pricing" (ASC) and re-apply the full PPP fan-out. To stand up a separate v2 line, use --product-version 2.
When to use vs. the full flow:
- Use
subscription add / iap add when iterating on an existing app — adding one more price point, launching a v2, or replacing stuck legacy products.
- Use
create-appstore-app / gpc setup for initial setup with the full canonical product set, or when you need multi-locale, intro offers, or custom regional pricing overrides that aren't covered by the quick-add flags.
Not yet supported via flags (require editing the JSON config):
- Free trials / intro offers
- Multi-locale listings
- Custom per-territory price overrides (PPP covers the common case)
adapty setup — Subscription Management
Syntax: kappmaker adapty setup [--config <path>]
Prerequisites:
adapty CLI installed (which adapty; auto-installs via npm if missing)
- Adapty authentication (CLI handles via browser OAuth)
Config file: Looks for ./Assets/adapty-config.json. If not found, prompts interactively.
- Global defaults at
~/.config/kappmaker/adapty-defaults.json are used as base layer.
What it does (8 steps): Create/find app, set access level, create products (with iOS/Android IDs), create paywalls, create placements.
Product ID format: Aligned with App Store Connect AND Google Play Console so Adapty links them across all three systems automatically.
For subscriptions: ios_product_id = {appname}.premium.{period}.v1.{price}.v1, android_product_id = {appname}.premium.{period}.v1, android_base_plan_id = autorenew-{period}-{priceDigits}-v1. Routed to access level Premium.
For credit pack IAPs (entries with credits field): ios_product_id = android_product_id = credit_pack_{credits}_{priceDigits}_{appname}. android_base_plan_id is left empty (IAPs have no base plan). Period is consumable and the product is routed to a separate credit_pack_access access level (so buying a credit pack does not grant the recurring Premium entitlement).
Adapty CLI consumable-period workaround: the Adapty CLI v0.1.5 hardcodes a period whitelist that excludes consumable. KAppMaker bypasses this for credit packs by hitting Adapty's REST API directly (using the auth token cached at ~/.config/adapty/config.json or ADAPTY_TOKEN env var). Subscriptions and lifetime products still go through the CLI. No user-visible difference; just a transparent fallback.
Prices are not developer-set in Adapty: the price field in Assets/adapty-config.json only drives ID generation (and mirrors into ASC/GPC). Adapty's developer API explicitly strips price fields from product creation — verified via OPTIONS metadata ("Strips response to plan-specified fields (id, title, vendor_products)"). Prices appear in the Adapty dashboard only after the user connects App Store Connect and Google Play integrations there (dashboard-only step; not exposed via CLI/API). When users complain that prices are missing in Adapty, point them to: Adapty dashboard → Settings → Integrations → connect ASC (paste the same .p8 / Key ID / Issuer ID they used for kappmaker create-appstore-app) and Google Play (upload the same service-account JSON used by kappmaker gpc setup). The mobile Adapty SDK already shows correct prices in-app — it fetches them from native store APIs at runtime regardless of dashboard state.
Multi-access-level config shape: access_levels: [...] (plural) replaces the legacy single access_level. Each product has an access_level_sdk_id field linking it to one of the access levels. Existing configs with the legacy field auto-migrate on load.
Default Credits Paywall + placement: The Adapty template ships with a Credits Paywall containing the 3 default credit packs and a Credits placement (developer_id credits_pack). App code fetches it with Adapty.getPaywall("credits_pack").
Idempotent re-runs: adapty setup lists existing products / paywalls / placements first and skips ones already present. Safe to rerun at any time.
Prerequisite ordering: If the user wants Adapty on Android, the Play Console products must exist first. The create orchestrator handles this automatically (step 8 runs gpc setup before step 9 runs Adapty), but if invoked standalone, tell the user to run kappmaker gpc setup (or at least gpc subscriptions push) before kappmaker adapty setup.
generate-screenshots — AI Screenshot Generation
Syntax: kappmaker generate-screenshots --prompt "<app description>" [options]
Options:
--prompt <text> (required) — App description or PRD
--input <dir> — Reference screenshots directory (default: auto-detect Assets/screenshots)
--style <id> — Style preset 1-8 (default: 1)
--output <dir> — Output directory (default: Assets/screenshots)
--resolution <res> — AI resolution: 1K, 2K, 4K (default: 2K)
Prerequisites: openaiApiKey, falApiKey, imgbbApiKey — all prompted on first use if not set.
What it does: Calls OpenAI to generate a detailed screenshot prompt, then fal.ai to generate 8 marketing screenshots in a fixed 2×4 grid, splits them into 8 individual 1284×2778 images, saves to appstore/playstore directories. Grid shape is fixed by design — number of reference images does not change the output count.
Style presets (1-8): Different visual styles for the screenshots. Ask the user what style they prefer if not specified.
generate-feature-image — AI Feature Graphic Generation
Syntax: kappmaker generate-feature-image --prompt "<concept>" --app-name "<Name>" --primary-color "#RRGGBB" [options]
Options:
--prompt <text> (required) — App concept / description
--app-name <name> (required) — App name rendered on the banner (e.g. "FitTrack")
--primary-color <hex> (required) — Brand color in hex (e.g. #FF3B30)
--subtitle <text> — Tagline shown under the app name
--logo <path> — App logo PNG to render on the brand panel (rendered pixel-faithfully)
--reference <paths...> — App screenshot paths to place inside device frames (max 10)
--output <path> — Custom output file path
--resolution <res> — AI resolution: 1K, 2K, 4K (default: 2K)
--locale <code> — Play Store locale for the default output path (default: en-US)
Prerequisites: openaiApiKey, falApiKey (prompted on first use). imgbbApiKey recommended when passing --logo or --reference (falls back to inline data URIs otherwise).
What it does:
- OpenAI (GPT-4.1) refines the inputs into a detailed banner specification.
- fal.ai (
nano-banana-2, or /edit when references are provided) generates one wide image.
sharp resizes/crops the result to EXACTLY 1024×500 px (Google Play feature graphic spec) via center cover.
- Saves to
MobileApp/distribution/android/playstore_metadata/<locale>/images/featureGraphic.png so the existing Fastlane publish flow picks it up automatically — falls back to Assets/playstore/featureGraphic.png outside a KAppMaker project.
Tips: Pass --logo to keep the exact app icon (the model will reproduce, not redraw, image #1). Pass --reference screenshots in the order they should appear inside the device mockups.
generate-ios-icons — iOS AppIcon.appiconset Generator
Syntax: kappmaker generate-ios-icons [--source <logo>] [--output <dir>] [--background <hex>]
Options:
--source <path> — Path to source logo PNG. Default: auto-detect in Assets/ (logo.png, logo_no_bg.png, app_logo.png, app_logo_no_bg.png, icon.png). If none found, prompts interactively.
--output <dir> — Output AppIcon.appiconset directory. Default: auto-detect MobileApp/iosApp/*/Assets.xcassets/AppIcon.appiconset.
--background <hex> — Flatten color for transparent logos (App Store rejects icons with alpha). Default: #FFFFFF.
Prerequisites: None — sharp-only, no AI, no API keys.
What it does:
- Loads the source logo (warns if smaller than 1024×1024 or non-square).
- Center-crops to a square + flattens alpha onto the background color.
- Resizes to all 11 unique pixel sizes Apple needs: 29, 40, 57, 58, 60, 80, 87, 114, 120, 180, 1024.
- Writes
Contents.json mapping each PNG to its idiom (iphone / ios-marketing) + scale (1x / 2x / 3x) — same schema appicon.co produces.
- Overwrites existing files silently (you're regenerating from a new logo).
Tips: Run after create-logo to mint the full iconset in one shot. If your logo has transparency, the default #FFFFFF flatten matches Apple's App Store requirement; pass --background "#000000" (or any hex) for a dark fill.
generate-android-icons — Android Launcher Icon Generator
Syntax: kappmaker generate-android-icons [--source <logo>] [--output <res-dir>] [--background <hex>] [--foreground-padding <ratio>]
Options:
--source <path> — Path to source logo PNG. Default: auto-detect in Assets/ (logo.png, logo_no_bg.png, app_logo.png, app_logo_no_bg.png, icon.png). If none found, prompts interactively.
--output <dir> — Output Android res/ directory. Default: auto-detect MobileApp/composeApp/src/androidMain/res (KAppMaker KMM convention) → MobileApp/androidApp/src/main/res → app/src/main/res.
--background <hex> — Adaptive icon background color (referenced by the generated XML). Default: #FFFFFF.
--foreground-padding <ratio> — Padding each side of the adaptive foreground (0 = no padding, 0.25 = Android Asset Studio default). Default: 0.25.
Prerequisites: None — sharp-only, no AI, no API keys.
What it does:
- Loads the source logo (warns if smaller than 432×432, the xxxhdpi foreground size; warns if non-square — center-crops).
- For each of 5 density buckets (
mdpi, hdpi, xhdpi, xxhdpi, xxxhdpi):
- Writes
ic_launcher.webp and ic_launcher_round.webp at the legacy launcher size (48 / 72 / 96 / 144 / 192 px).
- Writes
ic_launcher_foreground.webp at the adaptive size (108 / 162 / 216 / 324 / 432 px), with the logo centered in the inner safe zone and transparent surround.
- Writes
mipmap-anydpi-v26/ic_launcher.xml and ic_launcher_round.xml — adaptive-icon definitions referencing @color/ic_launcher_background and @mipmap/ic_launcher_foreground.
- Upserts the
ic_launcher_background color in values/colors.xml (creates the file if missing, updates the value if present, adds the entry if other colors exist).
- Overwrites existing files silently.
Tips: Run after create-logo to mint the full Android iconset in one shot. The default --foreground-padding 0.25 matches Android Asset Studio's behavior — drop it to 0.1 for icons that fill more of the adaptive frame, or up to 0.4 for very small logo content. Pass --background "#0F0A0D" (or any brand hex) to set the adaptive icon backdrop.
translate-screenshots — Locale Translation
Syntax: kappmaker translate-screenshots [source-dir] [options]
Options:
[source-dir] — Source screenshots directory (default: MobileApp/distribution/ios/appstore_metadata/screenshots/en-US)
--output <path> — Distribution directory root
--locales <codes...> — Target Play Store locale codes, space-separated (default: all 48+)
--rows <n> — Grid rows (default: 2)
--cols <n> — Grid columns (default: 4)
--resolution <res> — 1K, 2K, 4K (default: 2K)
Prerequisites: falApiKey, imgbbApiKey (prompted on first use if not set).
What it does: Combines source screenshots into a grid, translates to all target locales in parallel via fal.ai, splits translated grids back into individual images, saves to Fastlane distribution structure for both iOS and Android.
ASO Keyword Research — Find high-value keywords via Astro MCP
This is a skill-driven procedure, not an external CLI command. You (Claude) drive it using the Astro MCP tools (real-time App Store search data, competitor keywords, popularity + difficulty scores) when they're available in the session, and write the curated keyword list to AiGuidelines/keywords.md for the user to review and feed into other ASO commands.
Output of this workflow is the natural input to the Localize ASO Metadata workflow below — research first, then expand the chosen keywords across the 9 US-indexed locales with mode=keyword-expansion.
Trigger phrases:
Using kappmaker, research keywords for <base keyword>
Using kappmaker, find aso keywords [for/around/related to] <topic>
Using kappmaker, keyword research <base>
- "find keywords", "keyword research", "aso keyword discovery", "find sub-niche keywords"
Argument parsing:
base / base keyword — required eventually, but can be derived if missing. Resolution order:
- Explicit value in the user message (
base="ai image generator" or in-line: "research keywords for AI image generator")
- If a PRD / app idea / app description file exists in the project (e.g.
AiGuidelines/prd.md, AiGuidelines/app-idea.md, or any *.md under AiGuidelines/ that reads like a product description; fall back to the project README.md), read it and pick the strongest noun phrase that describes the app's core function. Confirm the choice with the user before proceeding.
- If
MobileApp/distribution/ios/appstore_metadata/texts/en-US/name.txt + subtitle.txt exist, infer the base keyword from those (e.g. name=Drift Tuner, subtitle=Real-Time Drift Coach → base=drift coaching). Confirm with the user.
- As a last resort, ask the user directly.
competitors — optional, comma-separated list of competitor app names or App Store IDs. If omitted, the workflow discovers top apps for the base keyword via search_app_store.
min_popularity — optional, default 30
max_difficulty — optional, default 45
target_count — optional, default 30–50 unique keywords after filtering
output — optional, default AiGuidelines/keywords.md
Preflight: check Astro MCP availability
Before proceeding, verify that at least these Astro MCP tools are visible in the current session: search_app_store, extract_competitors_keywords, get_keyword_suggestions. (Names may vary slightly between Astro MCP versions — list_apps, add_app, get_app_keywords, track_app, etc. are also useful when available.)
If none of those tools are present:
- Tell the user:
"Astro MCP is not connected in this session. The keyword-research workflow needs it for popularity/difficulty data. Either install/connect Astro MCP (see https://tryastro.app/docs/mcp/), or I can do a best-effort brainstorm without scores — say 'brainstorm without astro' to continue."
- On
brainstorm without astro (or equivalent), fall back to the Manual brainstorm fallback procedure below — generate ~30 sub-niche candidates via your own knowledge, mark scores as ?, and tell the user to validate them on App Store Connect or a separate ASO dashboard.
Procedure (with Astro MCP)
-
Discover competitor apps
- Call
search_app_store({ query: <base> }) to get the top live apps ranking for the base keyword.
- Pick the top 5–10 most relevant ones (skip mega-apps with unrelated reach, e.g. don't include "Google" when researching "manga translator"). Note their
app_id (App Store ID).
- If the user passed
competitors=, prefer those; supplement with discovery results up to 10 total.
-
Track competitor apps in Astro (so their keyword data is available)
- If a
list_apps tool exists, call it first to see which competitors are already tracked.
- For each untracked competitor, call
add_app / track_app with the App Store ID. Some tools require platform (ios / android) — default to ios unless the user is Android-only.
- If
add_app returns a quota error (free-tier limits), surface it to the user and proceed with whatever competitors got tracked. Don't abort.
-
Extract competitor keywords
- For each tracked competitor, call
extract_competitors_keywords({ app_id: <id> }) (or get_app_keywords). Collect every returned keyword along with its popularity and difficulty scores. Tag each with the source competitor app name.
-
Expand with AI suggestions
- Call
get_keyword_suggestions({ base_keyword: <base> }) (and/or per-competitor if the tool supports it). Add those to the candidate pool.
-
Filter and dedupe
- Drop any keyword where
popularity < min_popularity OR difficulty > max_difficulty.
- Remove exact duplicates (case-insensitive). For near-duplicates (singular/plural pairs, e.g.
ai photo editor / ai photo editors), keep the one with higher popularity unless both score well.
- Drop keywords that are clearly off-topic (e.g. a "fitness" keyword surfaced from a competitor that also happens to sell supplements when researching "workout planner"). Use the base keyword as the relevance anchor — when in doubt, prefer keeping the keyword if it could plausibly match the app's value prop.
- Aim for
target_count (30–50) unique, relevant entries. If you have fewer after filtering, relax max_difficulty by +5 and retry the filter once — tell the user you did so.
-
Cluster into sub-niches
- Group the filtered keywords by semantic cluster (e.g. for "AI image generator":
text-to-image, image editing, style transfer, avatar / portrait, interior design, try-on, etc.).
- Each cluster becomes a section heading in the output. This makes the file directly usable by
localize-metadata mode=keyword-expansion (each cluster maps cleanly to one of the 9 indexed locales).
-
Write AiGuidelines/keywords.md
- Use the Output format below.
- Create the
AiGuidelines/ directory if it doesn't exist.
- If a previous
AiGuidelines/keywords.md exists, ask once before overwriting: "AiGuidelines/keywords.md already exists. Overwrite? [y/N]". On N, write to AiGuidelines/keywords-<ISO-date>.md instead.
-
Print a short console summary: total candidates → filtered count → final clusters → file path. Then suggest the next step:
Next: run `Using kappmaker, localize metadata mode=keyword-expansion keywords="..."` with these keywords to fan them across the 9 US-indexed locales.
Output format (AiGuidelines/keywords.md)
# ASO Keyword Research
**Base keyword:** <base>
**Generated:** <ISO date>
**Filters:** popularity ≥ <min_popularity>, difficulty ≤ <max_difficulty>
**Sources:** <competitor app names, comma-separated> + AI suggestions
## Recommended primary keywords (top 5)
These are the highest-value picks across all sub-niches — strong popularity, low-to-moderate difficulty. Use these in the iOS `name` and `subtitle` and as the front-loaded terms in `keywords.txt` for `en-US`.
| Keyword | Popularity | Difficulty | Why |
|---------|-----------:|-----------:|-----|
| <kw> | 72 | 28 | Highest popularity in the pool with low competition — strongest single bet. |
| ...
## Sub-niche clusters
### Cluster 1 — <theme, e.g. "AI text-to-image">
| Keyword | Popularity | Difficulty | Description |
|---------|-----------:|-----------:|-------------|
| ai text to image | 65 | 38 | Direct text-to-image generation — main user search intent. |
| prompt to picture | 42 | 22 | Long-tail variant; lower volume but very low competition. |
| ai art from text | 38 | 31 | Adjacent phrasing common among casual users. |
| ...
### Cluster 2 — <theme, e.g. "Image editing">
| Keyword | Popularity | Difficulty | Description |
|---------|-----------:|-----------:|-------------|
| ...
(repeat per cluster)
## Discarded (for reference)
Keywords that hit the filter cutoff. Listed so the user can sanity-check the threshold choice and see what was rejected.
| Keyword | Popularity | Difficulty | Reason dropped |
|---------|-----------:|-----------:|---------------|
| ai photo | 95 | 78 | Difficulty too high (saturated by mega-apps) |
| free image generator| 18 | 9 | Popularity too low |
| ...
Manual brainstorm fallback (when Astro MCP is unavailable)
If the user chose to brainstorm without Astro MCP:
- Use your own knowledge of the App Store category around the base keyword to generate 30–50 sub-niche candidates. Cluster them the same way (sub-niche groupings).
- Mark
Popularity and Difficulty columns as ? (unknown) so the user understands these aren't measured numbers — they're hypothesis-only.
- In the file header, add a prominent note:
> ⚠️ Popularity/difficulty scores are NOT included — Astro MCP was unavailable. Validate these candidates on App Store Connect, [Astro](https://tryastro.app/docs/mcp/), AppTweak, or Sensor Tower before using them in production listings.
- Skip the "Discarded" section (there's nothing to filter against).
The file structure (clusters + recommended primary keywords) stays the same so the user gets the same downstream value.
Tips and edge cases
Localize ASO Metadata — Per-locale Name / Subtitle / Keywords / Description
This is a skill-driven procedure, not an external CLI command. Claude (you) executes the prompts in this section directly using the Read/Write tools — there is no shell binary to invoke and no AI API key required. All text generation and ASO-rule enforcement happens in-conversation.
Trigger phrases (any of these in the user's message routes here):
Using kappmaker, localize metadata mode=keyword-expansion ...
Using kappmaker, localize metadata mode=market-localization ...
- "localize aso", "localize metadata", "aso keyword expansion", "aso keywords"
Argument parsing (extract from the user message):
mode — required, one of keyword-expansion | market-localization
keywords — required for keyword-expansion; comma-separated list (strip surrounding quotes, trim each entry, drop empties)
base — optional for market-localization; defaults to en-US
locales — required for market-localization; comma- or space-separated codes (no autodetect)
distribution_dir — optional override. Default resolution: search upward from cwd for a directory containing MobileApp/distribution/; if not found, use ./MobileApp/distribution. If neither exists, create ./MobileApp/distribution/ and use that.
If mode is missing or invalid, stop and ask the user to pick one of the two modes and provide its required args.
Output layout (strict — same for both modes)
- iOS:
<distribution_dir>/ios/appstore_metadata/texts/<iosLocale>/{name,subtitle,keywords,description}.txt
- The
texts/ subfolder IS literal and intentional. Not standard Fastlane deliver layout.
- Android:
<distribution_dir>/android/playstore_metadata/<playLocale>/{title,short_description,full_description}.txt
en-US (or the user-chosen base) is never modified once it exists. No images, screenshots, or other files are touched.
Preflight checklist (run BEFORE either mode procedure)
-
Resolve <distribution_dir> (see argument parsing above).
-
Base-locale bootstrap (NEVER fail when missing): read the base-locale folders:
- iOS:
<distribution_dir>/ios/appstore_metadata/texts/<base>/{name,subtitle,keywords,description}.txt
- Android:
<distribution_dir>/android/playstore_metadata/<base>/{title,short_description,full_description}.txt
If either folder is missing entirely, OR any of the expected files is missing OR empty (zero bytes), enter bootstrap mode:
- Ask the user (single prompt):
"Base locale '<base>' is missing some metadata. Briefly describe the app and its core value (1–2 sentences):" Wait for the reply.
- For Mode 1 (
keyword-expansion), use the user's app description + the keywords= list to compose the base-locale files. For Mode 2 (market-localization), use just the app description.
- Apply the ASO Guidelines (see bottom of this section) to the bootstrapped output: front-load the strongest keyword in
name/title, no spaces after commas in keywords, no word repetition across name/subtitle/keywords in the iOS folder, respect every char limit.
- Write the missing base files first (only those that were missing or empty — preserve any non-empty siblings as the source for the rest).
-
After bootstrap (or directly if everything was present), read all 7 base-locale files into local variables you can reference as <BASE_NAME>, <BASE_SUBTITLE>, <BASE_KEYWORDS>, <BASE_DESCRIPTION>, <BASE_TITLE>, <BASE_SHORT_DESC>, <BASE_FULL_DESC>.
-
For Mode 2: validate every code in locales= resolves in the Mode 2 Locale Table (below). If any code is unknown, abort with the full supported-codes list printed and do NOT create any folders.
Mode 1 — keyword-expansion procedure
Locale set (FIXED) — the 9 US-indexed locales:
| iOS folder | Play folder |
|---|
ar-SA | ar |
fr-FR | fr-FR |
ko | ko-KR |
pt-BR | pt-BR |
ru | ru-RU |
vi | vi |
zh-Hans | zh-CN |
zh-Hant | zh-TW |
es-MX | es-MX |
Overwrite behavior: always overwrite these 9 locales without prompting. The base locale (en-US) is the only protected folder and is never touched (except by the bootstrap step above, which writes it once if missing).
Reasoning script (execute this prompt yourself — do NOT print it to the user; you ARE the senior ASO strategist):
# ROLE
You are a senior ASO strategist specialized in App Store keyword indexing
mechanics and Google Play metadata optimization.
# CONTEXT
The US App Store indexes keywords from `name`, `subtitle`, and `keywords`
fields across these 9 additional locales (beyond en-US):
INDEXED_LOCALES = {
iOS folder → Play Store folder
"ar-SA" → "ar"
"fr-FR" → "fr-FR"
"ko" → "ko-KR"
"pt-BR" → "pt-BR"
"ru" → "ru-RU"
"vi" → "vi"
"zh-Hans" → "zh-CN"
"zh-Hant" → "zh-TW"
"es-MX" → "es-MX"
}
More unique indexed keywords = more ranking surface = more organic installs.
# OBJECTIVE
Maximize unique keyword coverage in the US App Store by distributing
English keywords (NOT translations) across these 9 locales.
# INPUTS
- Existing en-US iOS metadata:
name: <BASE_NAME>
subtitle: <BASE_SUBTITLE>
keywords: <BASE_KEYWORDS>
description: <BASE_DESCRIPTION>
- Existing en-US Android metadata:
title: <BASE_TITLE>
short_description: <BASE_SHORT_DESC>
full_description: <BASE_FULL_DESC>
- Target keywords to rank for:
<LIST_KEYWORDS_HERE>
# HARD RULES
1. DO NOT modify en-US. Leave it untouched.
2. Generate ENGLISH content in all 9 locale folders (this is intentional —
Apple indexes them regardless of declared language).
3. ZERO keyword duplication WITHIN a locale:
- title ≠ subtitle ≠ keywords (no word overlap inside one locale)
4. PREFER zero duplication ACROSS the 9 locales, but allow strategic repetition
when the keyword pool is exhausted (see KEYWORD DISTRIBUTION STRATEGY below).
5. Apple character limits (enforce strictly):
- name (title): ≤ 30 chars
- subtitle: ≤ 30 chars
- keywords field: ≤ 100 chars, comma-separated, NO spaces after commas
6. No brand name in the iOS keywords field.
7. Avoid plural/singular duplication unless it unlocks a distinct search.
8. Avoid generic filler ("app", "best", "free", "new") — wasted slots.
9. Front-load highest-volume keywords in the iOS name (left = stronger).
10. For Android (Play Store), write NATURALLY phrased English short/full
descriptions in each locale folder, embedding the same locale's keywords.
title ≤ 30 chars, short_description ≤ 80 chars, full_description ≤ 4000 chars.
11. For iOS `description.txt` (per locale): write a fresh English description
that naturally embeds that locale's assigned keywords. Same length range
as the en-US description. NOT a translation; NOT a verbatim copy.
# PROCESS
1. Read en-US metadata to understand the app's purpose, tone, and primary value.
2. Cluster the target keywords by semantic theme (e.g., "design", "tuning",
"customization", "AI", "mechanic").
3. Assign each cluster to one locale to keep them coherent and discoverable.
4. Draft title/subtitle/keywords per locale, then verify with the checklist below.
# KEYWORD DISTRIBUTION STRATEGY
## Language rule
Generate ENGLISH content in all 9 locale folders. This is intentional —
the US App Store indexes these fields regardless of the folder's declared
language. Localization is NOT the goal here; keyword surface area is.
## Uniqueness rules (in priority order)
### Rule A — Within a single locale: ZERO repetition (hard rule)
Inside ONE locale, no word may appear in more than one field.
- title ∩ subtitle = ∅
- title ∩ keywords = ∅
- subtitle ∩ keywords = ∅
Apple does not re-index a word that already appears in title/subtitle when
it also appears in the keywords field — it's wasted space.
### Rule B — Across the 9 locales: PREFER uniqueness, allow strategic repetition
The goal is to maximize TOTAL unique keywords indexed across all 9 locales.
So the default is: every keyword appears in exactly ONE locale.
But do not force uniqueness at the cost of relevance:
1. Build a ranked list of candidates (user-provided keywords + tight synonyms
+ adjacent relevant terms), ordered by relevance × search-volume potential.
2. Distribute across the 9 locales, filling them with UNIQUE keywords first.
3. If you run out of strongly-relevant unique keywords before all 9 locales
are filled, DO NOT invent weak, generic, or off-topic terms to maintain
uniqueness. Instead: reuse strongest keywords in remaining locales paired
with different secondary/long-tail terms so the title + subtitle + keywords
COMBINATION still differs.
4. NEVER produce two locales with identical title AND subtitle AND keywords.
## Quality-over-uniqueness principle
A relevant keyword indexed twice is more valuable than an irrelevant keyword
indexed once. When in doubt, choose relevance.
# SELF-VERIFICATION CHECKLIST (run before writing any file)
- [ ] Every target keyword appears in at least one locale (ideally exactly one).
- [ ] No word repeats across title/subtitle/keywords within a single locale.
- [ ] All character limits satisfied (run mental wc -c on each field).
- [ ] No spaces after commas in any iOS keywords field.
- [ ] en-US is not in the write list.
- [ ] No two locales have identical title + subtitle + keywords combination.
- [ ] Every keyword used is genuinely relevant to the app (no filler).
- [ ] If any keyword repeats across locales, it's because it's high-value
AND the unique pool was exhausted — not laziness.
Writing step (after reasoning is complete and self-verification passes):
- Use the Write tool to create exactly 9 iOS folders (4 files each = 36 files) and 9 Android folders (3 files each = 27 files), totaling 63 files.
- Order: write all iOS files first, then all Android files, so the user sees progress logically.
- After every Write, mentally count the bytes you just wrote and confirm it's ≤ the relevant limit. If you ever produce an over-limit value, fix it before the next Write — never write over-limit content "intending to fix it later."
Summary table (print after all writes — this IS user-visible output):
Mode 1 — keyword-expansion complete. Wrote 63 files across 9 locale pairs.
| Locale (iOS / Play) | name | sub | kw | desc | title | short | full |
|---|---:|---:|---:|---:|---:|---:|---:|
| ar-SA / ar | 28 | 27 | 96 | 712 | 29 | 78 | 1840 |
| ... (9 rows total)
Flag any cell that is ≥ 95% of its cap with ⚠️.
Mode 2 — market-localization procedure
Locale resolution: parse locales= (comma- or space-separated), look each up in the Mode 2 Locale Table (below), build (iosLocale, playLocale) pairs. If one platform's code is (none) for that language, skip that platform and note it in the summary. If a user-supplied code matches NO entry in the table, abort with the full supported-codes list and do NOT create any folders.
Locale presets — also accept the following natural-language phrasings instead of (or in addition to) explicit locales= codes. Expand the preset to its concrete locale set BEFORE running the validation step above. Always confirm the expanded set with the user before generating (single line: "Localizing to N locales: <list>. Proceed? [y/N]").
| Phrase patterns | Expanded locale set |
|---|
top 10, tier 1, essential locales, essentials (≈10 markets) | de-DE, fr-FR, es-ES, es-MX, ja, ko, zh-Hans, pt-BR, ru, it |
top 15, tier 2, top 15 markets | de-DE, fr-FR, es-ES, es-MX, ja, ko, zh-Hans, pt-BR, ru, it, nl-NL, tr-TR, ar-SA, pl, zh-Hant |
top 20, tier 3, top 20 markets | de-DE, fr-FR, es-ES, es-MX, ja, ko, zh-Hans, pt-BR, ru, it, nl-NL, tr-TR, ar-SA, pl, zh-Hant, hi, id, vi, th, fr-CA |
all, every locale, all supported locales, every market | The full 30 from the Mode 2 Locale Table below |
European, EU markets, all European locales | de-DE, fr-FR, es-ES, it, nl-NL, pt-BR, pl, ru, tr-TR, sv, da, no, fi, el, cs, hu, ro, uk (treat Portuguese-BR as the European-Portuguese stand-in since Apple/Google don't ship a pt-PT in our table) |
East Asia, East Asian locales | ja, ko, zh-Hans, zh-Hant |
Southeast Asia, SEA, Southeast Asian markets | id, ms, th, vi |
Spanish, Spanish locales, Spanish-speaking markets | es-ES, es-MX |
Chinese, Chinese locales, Chinese-speaking markets | zh-Hans, zh-Hant |
MENA, Arabic, Middle East | ar-SA (single locale; both stores ship one Arabic folder) |
Combiners — also accept these natural-language patterns:
top 10 plus hi and id → expand the preset, then add hi, id.
tier 1 markets but skip ru and zh-Hans → expand the preset, then remove the listed codes.
German, Japanese, Korean, and Brazilian Portuguese → resolve language names to codes (de-DE, ja, ko, pt-BR). Use the Mode 2 Locale Table below as the authoritative name → code mapping.
If a preset expansion produces a locale not in the Mode 2 Locale Table, drop it silently — don't surface broken codes to the user.
Existing-files check: for each target locale, check whether ANY of the 4 iOS files or 3 Android files already exist on disk. Collect the list of "locales with existing files". If non-empty, prompt ONCE:
Found existing metadata in N locale(s): a, b, c.
Overwrite ALL existing metadata? [y/N]
- Enter /
N / no → exclude those locales, continue with the rest (or exit with "No locales to generate, exiting." if that empties the list).
y / yes → proceed for all targeted locales.
Reasoning script (execute yourself, per locale; you ARE the senior ASO expert):
# ROLE
You are a senior ASO expert with deep market knowledge of how users search
in their native language across Apple App Store and Google Play.
# OBJECTIVE
Adapt the app's metadata for the target locale to maximize **discoverability
in that local market** — not translation accuracy.
# INPUTS (from the base locale; verbatim text below)
- iOS:
name: <BASE_NAME>
subtitle: <BASE_SUBTITLE>
keywords: <BASE_KEYWORDS>
description: <BASE_DESCRIPTION>
- Android:
title: <BASE_TITLE>
short_description: <BASE_SHORT_DESC>
full_description: <BASE_FULL_DESC>
# TARGET
- iOS locale folder: <iosLocale>
- Play locale folder: <playLocale>
- Language: <Language>
# HARD RULES
1. NEVER translate literally. Adapt to:
- Local search behavior and phrasing conventions
- Locally trending keywords for the app's category
- Cultural framing of the value proposition
2. Respect platform limits (enforce strictly):
- iOS: name ≤ 30, subtitle ≤ 30, keywords ≤ 100 (no spaces after commas)
- Android: title ≤ 30, short_description ≤ 80, full_description ≤ 4000
3. Front-load the primary keyword in title (iOS ranking is position-weighted).
4. iOS keywords field MUST NOT repeat any word from name or subtitle.
5. Android full_description: use primary keyword 3–5 times naturally, plus
secondary keywords woven in. No keyword stuffing.
6. Descriptions are conversion copy — write them for native speakers, not bots.
Local idioms welcome.
# PROCESS
1. Identify the locale's primary search intent for this app category.
2. Pick a PRIMARY keyword (highest local volume) + 3–5 SECONDARY keywords.
3. Compose title around the primary keyword in a natural local phrasing.
4. Compose subtitle (iOS) / short_description (Android) around secondaries.
5. Compose description focusing on conversion, naturally seeded with keywords.
6. Verify against the checklist.
# SELF-VERIFICATION CHECKLIST (per locale, before writing)
- [ ] All character limits satisfied
- [ ] No word overlap between iOS name/subtitle/keywords
- [ ] Reads naturally to a native speaker (no machine-translated feel)
- [ ] Primary keyword appears in title AND short_description / subtitle
- [ ] No spaces after commas in iOS keywords
Writing step: use the Write tool to create each locale's iOS folder (4 files) and Android folder (3 files). If a locale's iOS code is (none), skip the iOS writes for that locale; same for Android.
Summary (printed at end):
Wrote: <list of locales with platform pairs>
Skipped (user declined overwrite): <list> if any
Skipped (one-platform-only): <list with reasons> if any
Char-limit warnings: <field, locale, length> for any that landed ≥ 95% of cap
Mode 2 Locale Table
The codes here are the only valid values for locales=. If a user passes anything not in this table, abort the run with this table printed.
| iOS folder | Play folder | Language |
|---|