storebound logo

# storebound **Website: [storebound.dev](https://storebound.dev) · npm: [storebound](https://www.npmjs.com/package/storebound) · `bunx storebound init`** [![npm](https://img.shields.io/npm/v/storebound?label=npm&color=cb3837)](https://www.npmjs.com/package/storebound) [![license: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![version 0.6.0](https://img.shields.io/badge/version-0.6.0-informational)](CHANGELOG.md) [![runtime: Bun](https://img.shields.io/badge/runtime-Bun%201.1%2B-f9f1e1)](https://bun.sh) [![runtime dependencies: 0](https://img.shields.io/badge/runtime%20dependencies-0-brightgreen)](package.json) [![Agent Skills: 25](https://img.shields.io/badge/Agent%20Skills-25-8a63d2)](skills) [![MCP: 2026-07-28](https://img.shields.io/badge/MCP-2026--07--28-black)](#mcp-server) **Take a mobile app or game from nothing to "Submit for review" on Google Play and the App Store: app records, listings in every language, in-app products and subscriptions with regional prices, every questionnaire, account deletion and review access, privacy, icons, screenshots and previews, with one config and your AI coding agent.** Hey, I'm CRTkafa. I built this while getting my own game through both stores and hit every wall on the way: draft apps, edits that die when you touch the Console, the "first IAP must ship with a version" dance, Data safety CSVs that silently need one more answer, an IARC questionnaire, target audience, App Privacy labels that are saved but not published, screenshots in 7 sizes x 14 languages... I wrote each fix down as code or as a checklist. None of those walls is game-only: a to-do app with accounts and a subscription hits the same ones, plus a few of its own (a demo account for the reviewer, account deletion, subscription disclosures), so storebound covers apps and games alike. It works for me, so I open-sourced it. Use it too. No account, no sign-up, no telemetry. A small Bun CLI that talks to the official store APIs, Agent Skills that walk your AI coding tool (or you) through the parts that are still console clicks, and an MCP server so agents can call the CLI safely. **storebound never presses Submit, Release or Publish for you.** Pick a profile and start from honest templates: `init --profile app` (utility / productivity app, optional account, analytics, no ads; the default), `--profile subscription-app` (accounts + auto-renewable subscriptions) or `--profile game` (free-to-play with ads and IAP). It keeps going after launch: TestFlight groups and testers, crash and ANR vitals against the store thresholds, review replies you approve before they go out, prices for many products and regions from one CSV, store analytics next to your listing changes, and App Store product page tests from your screenshot variants. ![The journey: config, app records, products, listings, questionnaires, privacy and icons, store art, validate, then you press Submit](https://git.crt.fyi/CRT/storebound/raw/branch/main/docs/img/journey.svg) ## The journey Everything an app or game needs before review, in the order I do it. For each step: what storebound does through the store APIs (or on your machine), what the skill walks your agent through where the stores have no public API, and what stays your own click. - **storebound**: a CLI command (also an MCP tool). Every write has `--dry`; MCP write tools default to a dry run. - **skill + agent (UI)**: no public API exists for it. The named skill has the exact navigation and the prepared answers. An agent with browser tools (a Chrome extension, computer use, a Playwright MCP ...) can click through it while you watch; without one, you click with the skill as your checklist. - **you**: public, legal or money steps. The skills tell every agent to stop and hand these to you. | # | step | storebound (API / local) | skill + agent (UI) | you | |---|---|---|---|---| | 1 | **Set up** | `init --profile app\|game\|subscription-app` (config, `store/*.md`, declarations, App Privacy and privacy templates per profile), `install --agent `, `check` | Play: service account + API access invite; App Store: API team key (`play-first-release`, `appstore-first-release`) | developer accounts; Paid Applications agreement, banking and tax; Play payments profile | | 2 | **App records and bundle id** | `apple bundle` registers the bundle id, `apple status` finds the app record | App Store Connect > New App (no API); Play Console > Create app; the very first AAB upload when Play needs it from the Console | the declarations you accept when creating the Play app | | 3 | **Builds and testers** | `play next-version-code`, `play build`, `play upload --track internal`, `apple signing ensure`, `apple build`, `apple upload` (waits for processing), `apple encryption`, `apple testflight groups / what-to-test / info / distribute` (internal groups) | Play internal testers list, Apple sandbox testers (`play-first-release`, `testflight`) | `apple testflight testers` (Apple emails the invitations), external groups and Beta App Review (`distribute --submit-review`) | | 4 | **In-app products and subscriptions** | `play products` (regional prices from USD + your overrides, activated), `apple iap` (consumable, non-consumable, non-renewing; localizations, price schedule, availability), `apple iap-review`, `apple subscriptions` (groups, levels, equalized prices, intro and promotional offers, offer codes), `play subscriptions` (base plans, regional prices, offers), `catalog diff` (both stores against one catalog) | Play license testers, Apple sandbox testers, "Add for Review" of the first IAPs and subscriptions into the version's submission, Play promo codes (`store-iap-setup`, `store-subscriptions`) | custom offer codes (`apple subscriptions --codes` asks first) | | 5 | **Store listings, every language** | `play listing` (title, short / full description, contact email + website, promo video URL), `apple listing --version x --build N` (name, subtitle, description, keywords, promo text, URLs, privacy URL, manual release), `listing translate` (drafts per language), `whats-new`, `check` (limits), `check --aso` | Play store settings: category and tags (`play-first-release`) | | | 6 | **Play questionnaires** | `play data-safety`: the whole Data safety form from `dataSafety` in the config (CSV import through the API) | Policy and programs > App content, answers prepared in `store/declarations.md` (`store-declarations`): privacy policy, app access with the demo account (`review-access`), delete-account URL (`account-deletion`), ads, content rating (IARC), target audience and content, advertising ID, news, government, financial and health features; Data safety summary check | the answers themselves are your statements: the agent asks when the code does not prove them | | 7 | **App Store questionnaires** | `apple appinfo` (categories, age rating declaration, content rights, Free price, availability), `apple encryption` (export compliance from `apple.usesNonExemptEncryption`, never guessed), `apple review-details` (contact, demo account from `.storebound/`, notes) | App Privacy nutrition labels from `store/app-privacy.md`, then **Publish** (`store-declarations`); encryption documentation when the answer is yes (`export-compliance`); the first availability record if the API refuses it | the export compliance answer (a legal statement, set once in the config) | | 8 | **Privacy and accounts** | privacy policy page template (`init`), `privacy-manifest --ios ` (PrivacyInfo.xcprivacy, required-reason APIs, SDK manifests), `validate` checks the policy, delete-account and terms URLs answer | host the pages, link them in both consoles and inside the app; in-app account deletion (`privacy-policy`, `privacy-manifest`, `account-deletion`) | | | 9 | **Icons** | `icons`: App Store 1024 without alpha, Play 512, adaptive + legacy Android launcher icons, web icons, Capacitor sources | | | | 10 | **Screenshots and previews** | `shots capture / render / check / gallery`, `play images`, `apple screenshots`, `shots record / video`, `apple previews`, `apple ppo create / upload` (App Store product page tests from `shots` variants; see [Store art](#store-art)) | Play store listing experiments (optional, `store-screenshots`) | uploading the promo video to YouTube; `apple ppo start / stop` | | 11 | **Validate** | `scan` (offline: rejection risks in the project files, see [Catch rejections before you submit](#catch-rejections-before-you-submit)), `check` (offline) and `validate` (read-only on both stores: every blocker in fix order, plus the local renders) | the UI-only items `validate` cannot see (`store-preflight` lists them) | | | 12 | **Submit** | `play release --version-code N --production draft`, `apple status` | Play: countries / regions, open the production draft and save it (`play-first-release`) | **Play: Send changes for review. App Store: Submit for Review.** | | 13 | **After review** | `apple status --wait-review`, `apple release --phased on / pause`, `play rollout --halt`, reply drafts (`review-rejections`) | | releasing the approved version, resuming or completing a phased release or rollout (terminal commands that ask first, `release-management`) | | 14 | **Live app** | `health [--weekly]` (crash / ANR vitals vs Play's thresholds, App Store diagnostics; also in `validate`), `reviews list / draft`, `analytics snapshot / compare`, `prices export`, `prices import --dry`, `apple ppo status` | `store-health`, `review-replies`, `store-analytics`; Play statistics come from the reports bucket (no API) | `reviews send` (approved drafts only), `prices import` (asks first) | New personal Play developer accounts also need a closed test with 12+ testers for 14 days before production; the `play-first-release` skill covers it. ### API or console, honestly - **Through the API** (storebound does it): bundle id, builds on tracks, versions and build selection, listings in every language, graphics, screenshots, app previews, in-app products and prices (also from one CSV), Play Data safety, Apple age rating, categories, content rights, price and availability, export compliance answer, App Review details with the demo account, subscriptions (Apple groups, levels, prices, introductory and promotional offers, offer codes; Play base plans, regional prices, offers), phased release and staged rollout, TestFlight groups / testers / What to Test / Test Information / build processing / expiring builds, crash and ANR vitals and App Store diagnostics, TestFlight feedback, review replies, App Store analytics reports, sales and finance reports, App Store Product Page Optimization tests, IAP and subscription review submissions, offer codes for one-time purchases, win-back offers, promoted IAPs, pre-orders, custom product pages and in-app events, and pulling live listing texts back. - **Console only** (no public API; the skill + your agent's browser, or you): creating the App Store Connect app record and the Play app, Play App content (IARC content rating, target audience, ads, advertising ID, app access, government / financial / health / news, app access with the demo account), Play category and tags, Play countries, App Privacy labels and their Publish, Play promo codes, Play pre-registration, Play store listing experiments, test results of both stores' experiments, promotional images of promoted IAPs. Play store statistics have no API either: storebound reads the monthly exports from your Play reports bucket. - **Only you**: agreements, banking and tax, and the final Send for review / Submit for Review (apps, IAPs, custom product pages, in-app events). Releases and rollouts after approval, Beta App Review, tester invitations, sending review replies, applying price changes, starting / stopping a product page test, creating offer codes, win-back offers, switching on promoted IAPs, pre-orders and expiring builds too. None of these is an MCP tool; the terminal commands ask first. ## Catch rejections before you submit App Store rejections cost days: you wait for review, read one sentence in the Resolution Center, fix it, and wait again. On Google Play a policy problem can mean a blocked update, a removal or a strike on the account. Most of the usual ones are visible in the project long before that. `storebound scan` reads the project itself, offline and read-only (Xcode project, Info.plist and build settings, entitlements, PrivacyInfo.xcprivacy, Podfile.lock, Package.resolved, Cartfile, Gradle files and version catalogs, AndroidManifest.xml (and the merged manifest of a local build), res/xml and strings.xml, CMake / ndk-build files, Unity ProjectVersion.txt, package.json, Expo app.json, capacitor.config, Cordova config.xml, pubspec, Unity Packages/manifest.json, ProjectSettings and External Dependency Manager files, Godot export presets), plus your config and listing files. It works out the stack and the SDKs (ads, analytics, attribution, crash, sign-in, payments, push, with versions and the file:line they come from) and reports every rejection risk it can prove, each with the guideline, the evidence, the fix, a severity and the skill to open. On the teaser fixture (`tests/fixtures/scan/teaser`): ```text $ storebound scan Rejection risks: 2 blocker, 3 high, 0 medium, 0 low 1. BLOCKER 2.1(a) · App Store · Sign-in is required but there is no demo account for App Review at storebound.config.ts:12 reviewAccess: { signInRequired: true, instructions: 'Sign in with the demo account.' }, at storebound.config.ts:11 accounts: { enabled: true }, at Podfile.lock:3 Google Sign-In: GoogleSignIn 7.1.0 2. BLOCKER 5.1.1(v) · App Store · Accounts can be created but no in-app account deletion was found at storebound.config.ts:11 accounts: { enabled: true }, at Planner/LoginView.swift:11 Button("Create account") { Task { try await AccountApi.signUp() } } at Planner/LoginView.swift:17 static func signUp() async throws { 3. HIGH 4.8 · App Store · Third-party login (Google / Facebook / ...) without Sign in with Apple or another equivalent login option at Podfile.lock:3 Google Sign-In: GoogleSignIn 7.1.0 at Planner/LoginView.swift:9 GIDSignIn.sharedInstance.signIn(withPresenting: rootViewController()) { _, _ in } 4. HIGH 3.1.1 · App Store · Restorable purchases (non-consumables or subscriptions) but no Restore Purchases path was found at storebound.config.ts:16 com.example.planner.pro (non_consumable) 5. HIGH 5.1.2(i) · App Store · Tracking-capable code without App Tracking Transparency (Google AdMob, IDFA) at Podfile.lock:2 Google AdMob: Google-Mobile-Ads-SDK 11.10.0 at Planner/Ads.swift:7 print(ASIdentifierManager.shared().advertisingIdentifier) ``` Every finding also has a `fix:` line and the skill. Below the risks comes the declarations proposal: the Play Data safety and App Store App Privacy answers, App Tracking Transparency and privacy manifest entries the detected SDKs and code imply, each marked **detected (file:line)** or **ask owner**, as a diff against `dataSafety` / `appPrivacy` in the config. Nothing is written unless you run `storebound scan --write` (only detected answers; questions stay questions). The MCP tool `scan` is read-only and cannot write; `validate` includes the open scan findings; the desktop app shows them on the Checks tab. A finding you decided is fine goes into the config with its reason: `scan: { ignore: [{ rule: 'demo-account', reason: '...' }] }`; a rule with several checks (e.g. `play-metadata`) takes `match: ''` to accept just one of them. | rule | App Store | Google Play | what scan proves | |---|---|---|---| | `demo-account`, `play-app-access` | 2.1(a) | App access | a sign-in (config, auth SDKs, sign-in code) without `reviewAccess` or without the demo account in `.storebound/` | | `account-deletion`, `play-deletion-url` | 5.1.1(v) | User Data: account deletion | sign-up or social login without a deletion path in code or `accounts.inApp`; no `accounts.deletionUrl` | | `login-services` | 4.8 | | Google / Facebook / other third-party login without Sign in with Apple or another equivalent option | | `restore-purchases`, `external-purchase`, `play-payments` | 3.1.1 | Payments | non-consumables or subscriptions without a restore path; card / PayPal SDKs next to in-app products; "buy on our website" (Play: unless `play.billingPrograms` lists user choice billing or external offers) | | `subscription-info` | 3.1.2(c), 3.1.2 | | no Terms of Use URL, no Terms link or renewal wording in the description, no legal links or "Manage subscription" in the app | | `att` | 5.1.2(i) | | tracking-capable SDKs or IDFA without ATT; the prompt without its purpose string; the string without a prompt | | `purpose-strings`, `unused-permission` | 5.1.1(ii), 5.1.1(iv), ITMS-90683 | | a used permission without its NS*UsageDescription; empty or vague strings; declared but unused | | `privacy-manifest` | ITMS-91053 / 91061 | | no PrivacyInfo.xcprivacy, required-reason APIs not declared, SDKs older than their first manifest version | | `privacy-policy-link`, `play-privacy-policy` | 5.1.1(i) | User Data: privacy policy | no privacy link in the app's sources (Play: every app; medium when it collects data), an `http://` link | | `privacy-labels` | 5.1.2 | User Data: Data safety | SDK data types missing from `dataSafety`; Play also: an answer narrower than the SDK's disclosure (purposes, sharing), SDKs with no `dataSafety` at all | | `play-prominent-disclosure` | | User Data: prominent disclosure, Location permissions | background location without a disclosure that says "location" and "background"; location, contacts, calendar or phone number access without any disclosure or rationale text in sources or `strings.xml` | | `play-background-location`, `play-photo-video`, `play-all-files-access`, `play-accessibility`, `play-install-packages`, `play-health-connect`, `play-full-screen-intent`, `play-vpn` | | Location permissions, Photo and Video Permissions, All files access, AccessibilityService API, REQUEST_INSTALL_PACKAGES, Health Connect, Full-screen intent, VpnService | the restricted permission or service in the manifest (or the merged manifest of a local build), each with the core-use test and the Play Console declaration it needs; `isAccessibilityTool` lowers the accessibility finding to medium | | `play-fgs`, `play-exact-alarm`, `play-query-all-packages`, `play-sms-call-log` | | Foreground service types, exact alarms, package visibility, SMS and Call Log | foreground services without a type or its permission (and the Console declaration); USE_EXACT_ALARM; QUERY_ALL_PACKAGES; SMS and call log permissions | | `kids-ads`, `play-families-ads`, `play-families-ad-id`, `play-families-location` | 1.3 | Families | a Kids Category age band with third-party ads or analytics SDKs; `play.targetAges` under 13 with an ad SDK that is not Families self-certified (or older than its certified version), the AD_ID permission in a child-only app (mixed audience: ad / attribution SDKs need child-directed treatment), location in a child-only app | | `play-ad-id` | | Advertising ID | targetSdk 33+ with ad / attribution SDKs and no AD_ID permission (medium when the merged manifest proves it is missing; Google Mobile Ads adds it itself) | | `play-16kb` | | 16 KB page sizes (updates blocked from 2027-02-01) | Unity before 6000.0.38f1 / 2022.3.56f1 / 2021.3.48f1, React Native before 0.77, NDK before r28 for the app's own C/C++ without the 16 KB flags, AGP before 8.5.1 with uncompressed native libraries; else prebuilt `.so` files to check | | `play-ads-launch` | | Ads, Better Ads Experiences | an interstitial shown from `onCreate` of the launcher activity (Android native; other engines: review-only) | | `play-testing-requirement` | | Testing for new personal accounts | `play.accountType: 'personal'` created after 2023-11-13 (12 testers, 14 days of closed testing before production); low when the account type is not set | | `other-platforms` | 2.3.10 | | "Android", "Google Play" and other stores in the App Store text or screenshot captions | | `metadata-keywords`, `kids-terms`, `play-metadata` | 2.3.7, 2.3.8 | Metadata | other apps' names, prices and "best / #1" in name, subtitle or keywords; keyword lists; "For Kids" outside the Kids Category. Play title over 30 characters, emoji, ALL CAPS, rank / price / promotion terms; other apps in the title or short description; repeated keywords or keyword lists in the full description | | `play-impersonation` | | Impersonation | a well-known brand in the package name or app name (low: a check, not a verdict) | | `placeholder-text` | 2.1(a) | Metadata | lorem ipsum, TODO, template lines in the listings | | `beta-wording` | 2.2 | | "beta", "demo", "trial version", "test build", "early access" in the app name (config, CFBundleDisplayName) or the App Store name, subtitle, keywords, promotional text and description ("demo account" is fine) | | `originality` (with `--originality`) | 4.3 | Repetitive content | a near-duplicate of one of your own apps (`storebound originality` scores: name, listing text, icon and screenshot hashes, project structure) | | `min-functionality` | 4.2 | | Capacitor `server.url` / Cordova `` on a remote site; a few native files around a remote WKWebView | | `deprecated-api`, `private-api` | 2.5.1 (ITMS-90809) | | UIWebView; `App-Prefs:` / `prefs:root` URL schemes | | `ipv6`, `ats` | 2.5.5, App Transport Security | | IPv4 literals in server URLs; NSAllowsArbitraryLoads | | `export-compliance` | export compliance | | no ITSAppUsesNonExemptEncryption and no `apple.usesNonExemptEncryption`, or the two disagree (never guessed) | | `review-prompt` | 5.6.1, 3.2.2(x) / 5.6.3 | | a custom "rate us" prompt without the system API; rewards for ratings | | `play-target-sdk` | | Target API level | below API 36 (Wear OS / Automotive 35, TV / XR 34) for new apps and updates since 2026-08-31 | | `play-package-name` | | Play Console upload | the reserved `com.example` prefix | `storebound scan --originality [--against ]` adds the Guideline 4.3 / Play repetitive content check against your other apps to the same report (folders default to `originality.against` in the config; `storebound originality` stays the command with every score and suggestion). Rules cite Apple's App Review Guidelines and the Play Policy Center / Play Console Help pages they come from (checked 2026-10-11, in the code comments of `cli/lib/scan/rules.ts` and `cli/lib/scan/play-rules.ts`); the SDK table with each vendor's data disclosure page and Google's Families self-certified ads SDK list is `cli/lib/scan/sdks.ts`. Play config the rules read: `play.targetAges` (Target audience age groups), `play.accountType` / `play.accountCreated`, and `play.billingPrograms`. When the project was built locally, scan also reads the merged manifest under `build/intermediates` (the permissions libraries add); otherwise it says that it could not. Not a rule because no Play policy page covers it: device admin apps (BIND_DEVICE_ADMIN); not a policy at all: Play Integrity / anti-tamper. **Checked at review, not by scan** (no file shows them): crashes and hangs on the reviewer's device (2.1, `store-health`), links that are dead at review time (`validate` checks the configured URLs), screenshots and descriptions that do not match the app (2.3, `store-screenshots`), unfinished UI (4.0), "too simple" and spam / template apps (4.2, 4.3, `portfolio-originality`), IAPs that fail in the sandbox (2.1(b), `store-iap-setup`), the content of the privacy policy (`privacy-policy`), local law (5.2). On Google Play: whether a restricted permission really serves the app's core purpose (the Console declaration and its video decide), the wording and timing of a prominent disclosure, ad placements in Unity, Flutter or React Native code, the Health apps declaration answers, and the alignment inside a prebuilt `.so` (scan lists them; `zipalign -c -P 16` and `check_elf_alignment.sh` check the built app). The `review-rejections` skill covers what to do when one of these comes back. ## 5-minute quick start You need [Bun](https://bun.sh) 1.1+ (and `ffmpeg` for icons, screenshots and video). ```sh cd my-app bun add -d storebound playwright # or: bun add -d git+https://git.crt.fyi/CRT/storebound.git playwright bunx storebound init # --profile app (default) | subscription-app | game: config, store/*.md, declarations, privacy policy, .storebound/ (gitignored) # fill in storebound.config.ts and the store/ files bunx storebound check # offline: config, copy limits, Data safety answers, products, files ``` Add credentials (below), then walk the journey with dry runs first: ```sh bunx storebound apple bundle --dry bunx storebound play listing --dry bunx storebound play products --dry && bunx storebound apple iap --dry bunx storebound apple subscriptions --dry && bunx storebound play subscriptions --dry bunx storebound apple review-details --dry # contact + demo account from .storebound/ (masked) + notes bunx storebound play data-safety --dry --csv store/data-safety.csv bunx storebound apple appinfo --dry bunx storebound icons --dry bunx storebound shots capture && bunx storebound shots render && bunx storebound shots gallery bunx storebound validate # read-only: what still blocks a submission, per store, in fix order bunx storebound catalog diff # read-only: products + subscriptions on both stores vs the config, prices per region ``` After launch: ```sh bunx storebound apple testflight what-to-test --build 42 --dry # then distribute --build 42 --group Team bunx storebound health --weekly # vitals + diagnostics, .storebound/health/.md bunx storebound reviews draft # reply drafts in .storebound/replies/, then reviews send bunx storebound prices export && bunx storebound prices import --dry bunx storebound analytics snapshot && bunx storebound analytics compare ``` Or ask your agent: "take my app to Google Play and the App Store", "fill in the store questionnaires", "set up the in-app purchases and a yearly subscription with a free trial on both stores", "do both stores sell the same catalog", "the reviewer could not sign in", "make store screenshots in 5 languages", "is my app ready to submit", "why was my update rejected", "put build 42 on TestFlight", "how are our crash rates", "answer the new reviews", "did the new screenshots help". The matching skill takes over. ### Coming from fastlane `storebound import fastlane` turns an existing fastlane setup into storebound files. It only reads (no Ruby is run), never overwrites anything and prints what it could not map: ```sh bunx storebound import fastlane --dry # the plan + report; default --dir: fastlane/, ios/fastlane/, android/fastlane/ bunx storebound import fastlane # writes the files below; an existing file gets a .imported. copy bunx storebound init # adds the remaining templates, keeps the imported files bunx storebound check ``` | fastlane | storebound | |---|---| | Appfile `app_identifier`, `package_name` | `apple.bundleId`, `play.packageName` | | deliver `metadata//*.txt` (+ `default/`) | `store/appstore.md` per App Store locale; URLs, copyright, categories and review notes in the config | | supply `metadata/android//*.txt`, newest `changelogs/.txt` | `store/listing.md` per Play language | | deliver and supply language folders | `languages[]` with both store codes (`tr` + `tr-TR`, `zh-Hans` + `zh-CN`, `he` + `iw-IL`) | | `screenshots//`, `images/phoneScreenshots/`, `tenInchScreenshots/` | `store/shots/frames///NN.png` with a `folder` capture source | | supply `icon`, `featureGraphic` | `play.icon`, `store/renders//feature/1024x500.*` | Keys and accounts are never copied: `json_key_file`, the API key, team ids, the App Review contact and demo account are named in the report with where they go (`.storebound/credentials.json`). Submission and release flags, `price_tier`, the age rating config, lanes and signing (`match`) are listed with their storebound counterpart. The `migrate-from-fastlane` skill walks an agent through it. ### Credentials (never in the repo) Read from environment variables, or from `.storebound/credentials.json` in your project (gitignored by `init`): | name | what | |---|---| | `PLAY_SERVICE_ACCOUNT` | path to the Google Cloud service account JSON key (or the JSON itself) with access to your app in Play Console | | `ASC_KEY_ID`, `ASC_ISSUER_ID` | App Store Connect API team key (Users and Access > Integrations) | | `ASC_P8` | path to `AuthKey_.p8` (default `~/.appstoreconnect/private_keys/AuthKey_.p8`) | | `ASC_VENDOR_NUMBER` | vendor number for `reports apple-sales` / `apple-finance` (Payments and Financial Reports) | | `ANDROID_KEYSTORE`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS`, `ANDROID_KEY_PASSWORD` | upload key for `play build` (path to the .jks / .p12 and its passwords; handed to Gradle through the environment) | | `reviewContact` | (file only) App Review contact name / phone / email | | `reviewDemoAccount` / `REVIEW_DEMO_USER`, `REVIEW_DEMO_PASSWORD` | demo account for App Review (`reviewAccess.signInRequired`); never in the config or the repo, masked in logs | Several store accounts on one machine: credential profiles (`~/.storebound/credentials/.json`, the same names except the per-app upload keystore) picked with `--account ` or by a workspace ([Many apps and studios](#many-apps-and-studios)). ## Install for your AI tool Every route gives the agent the same skills and the same MCP tools. Nothing here requires Claude: the skills are plain Markdown and the CLI is a normal command. | tool | install | |---|---| | Claude Code | `/plugin marketplace add https://git.crt.fyi/CRT/storebound.git` then `/plugin install storebound@crtkafa` (skills, MCP server, `storebound` on PATH). Or per project: `bunx storebound install --agent claude` | | OpenAI Codex | `bunx storebound install --agent codex` (skills in `.agents/skills`, MCP in `.codex/config.toml`; Codex reads that file for trusted projects). The repo is also a Codex plugin (`.codex-plugin/plugin.json`, skills only) | | Gemini CLI | `gemini extensions install https://git.crt.fyi/CRT/storebound.git` (skills, MCP server, `GEMINI.md`). Or `bunx storebound install --agent gemini` | | Cursor | `bunx storebound install --agent cursor` (skills in `.agents/skills`, `.cursor/mcp.json`); the repo's `.cursor/rules/storebound.mdc` is an agent-requested rule you can copy | | VS Code / GitHub Copilot | `bunx storebound install --agent copilot` (skills in `.github/skills`, `.vscode/mcp.json`) | | Windsurf (Devin Desktop) | `bunx storebound install --agent windsurf` (skills in `.agents/skills`, MCP in your global `mcp_config.json`) | | OpenCode | `bunx storebound install --agent opencode` (skills in `.opencode/skills`, `opencode.json`) | | anything that reads `AGENTS.md` | copy or link `AGENTS.md` (rules + skill index) and use the CLI | `install` takes several agents at once (`--agent codex,cursor`), `--dry` to preview, `--no-skills` / `--no-mcp`, and `--command ""` to choose how the MCP server starts. It only adds or updates the `storebound` entry and its own skill folders; everything else in those files stays as it is, and re-running it is safe. ## Commands Every write command takes `--dry`; re-runs are idempotent; output is one timestamped line per step, so long jobs can run in the background with a log (`storebound shots render > .storebound/render.log 2>&1 &`).
Setup and checks | command | kind | what | |---|---|---| | `init [--profile app\|game\|subscription-app]` | local | copy the templates of a profile (default app), gitignore `.storebound/` | | `import fastlane [--dir fastlane] [--profile ...]` | local | config, `store/appstore.md`, `store/listing.md` and screenshot frames from a fastlane setup; never overwrites, reports what is not mapped | | `install --agent ` | local | skills + MCP entry for an AI tool | | `mcp` | local | stdio MCP server wrapping the commands | | `serve --stdio` | local | JSON-RPC session for the storebound desktop app (see [Desktop machine interface](#desktop-machine-interface)) | | `inspect [--json]` | local | offline summary of the config: name, store ids, languages, products | | `check` | local | config, copy limits, data safety, products, subscriptions, profile (delete-account URL, demo account, terms URL), files; warns on untranslated release notes | | `catalog diff [--store] [--threshold 10]` | API, read-only | one catalog vs both stores: missing / extra / mismatched products and subscriptions, US and override prices, locales, offers, per-region price differences between the stores; exit 1 on mismatches | | `check --aso` | local | keyword audit: App Store keyword field rules, Play stuffing / claims | | `scan [--store apple\|play] [--json] [--write] [--originality --against ]` | local, read-only | rejection risks in the project itself (stack, SDKs, Info.plist, manifest, listings) with guideline, evidence file:line, fix and skill; Data safety / App Privacy / ATT / privacy manifest proposal as a diff (`--write` adds the detected answers to the config); exit 1 on blockers | | `validate [--apple] [--play]` | API, read-only | readiness report with ordered blockers plus the local renders checks (`--no-local` skips); exit 1 while any blocker is left | | `originality --against [--json]` | local, read-only | Guideline 4.3 check vs your other apps: name, listing text, icon and screenshot perceptual hashes, project structure; scores, near-duplicate flags, suggestions; exit 1 when flagged | | `whats-new [--since ]` | local | draft player-facing release notes from git log into both listing files | | `whats-new --languages en,de,tr` / `check` / `apply` | local | one file per language (`store/whats-new/`) for your agent (or a `--translate` hook of yours) to translate, per-store limits (Play 500, App Store 4000), then into both listing files | | `listing translate --to de,ja [--from en] [--translate ""]` | local | per-language listing sections in both listing files (App Store name, subtitle, promotional text, keywords, description; Play title, short, full) for your agent (or a hook) to translate, marked as drafts until reviewed; keywords cleaned up | | `listing check` / `listing reviewed --lang ` | local | every language section against the store limits and keyword rules, untranslated and unreviewed drafts (exit 1 on a problem) / remove the draft marker after a human read it | | `flow list \| validate \| run [NAME=value] [--dry \| --apply] [--from \| --resume]` | local + whatever the steps call | repo-local workflows (`store/workflows/*.yml`): steps in order with progress; writes run as dry runs unless `--apply`, owner steps are listed as your click and never run (see [Repo-local workflows](#repo-local-workflows)) | | `catalog pull [--write]` | API, read-only + local | IAP and subscription texts on the stores vs the config: a proposal diff (`.storebound/pull/catalog.json`); the config is never rewritten | | `ads scan --ios ` | local | AdAttributionKit / SKAdNetwork: ad network ids, postback copy endpoints, attribution windows and cooldown, conversion value updates; exit 1 on a blocker | | `privacy-manifest --ios ` | local | PrivacyInfo.xcprivacy, required-reason APIs, SDKs without manifests | | `api list \| coverage \| apple\|play ` | API | every App Store Connect / Google Play operation (see [Every endpoint](#every-endpoint)); GET runs, writes are dry unless `--yes` in a terminal | | `desktop [--download]` | local | open the storebound desktop app (in an app folder: add or select it there); not installed: the download for this system, `--download` saves it to ~/Downloads after checking its signature (never installs) | | `doctor [--json]` | local | which local tools are here (ffmpeg, Chrome, playwright, Xcode, adb, git) and how to install the missing ones | | `icons` | local | App Store 1024 (no alpha), Play 512, web, Capacitor sources, Android legacy launcher icons |
Google Play | command | kind | what | |---|---|---| | `play listing [--include-drafts]` | API | app details, title / short / full description per language, promo video URL (`play.video`); prints the plan first (old -> new per field, draft languages skipped), `--dry` stops there, the real run asks and sends only changes | | `play listing pull [--write]` | API, read-only + local | live listings back into `store/listing.md`: diff first, writes only with `--write` | | `play images [--variant b]` | API | icon, feature graphic, phone / 7" / 10" screenshots, one edit per language | | `play products` | API | one-time products: regional prices from `convertRegionPrices` + your overrides, activate | | `play subscriptions [--csv]` | API | subscriptions: listings + benefits, one auto-renewing base plan, regional prices, intro and extra offers, activate (plan first, asks) | | `play data-safety` | API | Data safety form (CSV import); `--csv` writes the file | | `play build [--version-code N] [--flavor f]` | local + API read | Gradle `bundleRelease` (Flutter `flutter build appbundle`; Capacitor, React Native, Unity export found) with the next free versionCode and the upload key from env / `.storebound` (passed through the environment, never argv); AAB checked and copied to `build/android/` | | `play upload [] --track internal\|alpha\|beta` | API | AAB to a testing track with release notes; versionCode checked first; `--dry` is read-only, a real run asks | | `play release ` | API | upload + tracks (`--internal completed --production draft`, `--production inProgress --fraction 0.05`), release notes | | `play rollout` | API | `--fraction x`, `--halt`, `--resume`, `--complete` (asks first, except halt) | | `play next-version-code` | API, read-only | next free versionCode | | `play status` | API, read-only | tracks, listings, products |
App Store | command | kind | what | |---|---|---| | `apple bundle` | API | register the bundle id, find the app record | | `apple listing --version x.y.z [--build N] [--include-drafts]` | API | prints the plan first (version: keep / update / create / refuse with the reason; per locale create / update, old -> new per field; draft locales skipped), `--dry` stops there, the real run asks and applies exactly it: version (manual release), the build with exactly that number, texts per locale (name: the locale's `### name`, else `apple.name`, else `app.name`) | | `apple listing pull [--version x.y.z] [--write]` | API, read-only + local | live texts back into `store/appstore.md`: diff first, writes only with `--write` | | `apple screenshots [--variant b]` | API | 6.9" + 6.5" (+ 13" iPad with `apple.ipad`, + 6.3" with `apple.iphoneMedium`) per locale, MD5-idempotent | | `apple previews [--poster 5]` | API | app preview videos per locale, checked first, MD5-idempotent, poster frame | | `apple iap` / `apple iap-review` | API | IAPs (consumable, non-consumable, non-renewing), localizations, price schedules, availability / review screenshots | | `apple iap-submit [--with-version] [--no-submit]` | API | IAP / subscription / group versions into a review submission (the first of a type together with the app version); asks before adding and before submitting | | `apple offer-codes [--codes]` | API | offer code campaigns for one-time purchases; custom and one-time-use codes only with `--codes` (asks; codes saved to `.storebound/offer-codes/`) | | `apple win-back` | API | win-back offers for lapsed subscribers (asks: they go live on their start date) | | `apple promoted [--enable]` | API | promoted in-app purchases: create (disabled), order, switch on (asks) | | `apple preorder status \| enable --release-date \| end` | API | pre-orders per territory (enable / end ask first) | | `apple cpp list \| create \| upload \| submit \| visible` | API | custom product pages from the config and a shots variant; submit / visible ask first | | `apple events list \| create \| upload \| submit` | API | in-app events with schedule, localizations, card and details images; submit asks first | | `apple subscriptions [--csv] [--codes]` | API | groups, subscriptions, levels, localizations, availability, equalized prices + overrides + CSV (scheduled changes), introductory and promotional offers, offer code campaigns, review screenshots (plan first, asks; custom codes only with `--codes`) | | `apple appinfo` | API | categories, age rating, content rights, Free, territories; plan first (old -> new), only changes are sent | | `apple review-details` | API | App Review contact, demo account (from `.storebound/`, masked), notes (+ sign-in steps, where account deletion is) | | `apple encryption` | API | export compliance answer from `apple.usesNonExemptEncryption` (never guessed) | | `apple release --phased on\|pause\|resume\|complete`, `--manual-release` | API | phased release, release an approved version (resume / complete / release ask first) | | `apple signing status` | API, read-only | bundle id capabilities, distribution certificates (with / without a private key on this Mac), App Store profiles per bundle id, installed or not | | `apple signing ensure [--keychain] [--no-install]` | API | missing capabilities, a distribution certificate from a key made here (kept 0600 in `.storebound/signing/`, never printed), App Store profiles installed for Xcode; dry first, asks; Developer ID needs the Account Holder (403 explained) | | `apple build [--version x.y.z] [--build-number N]` | local + API read | xcodebuild archive + exportArchive with a generated ExportOptions.plist (app-store-connect, team, signing style, profiles) and the API key; next build number; Capacitor, React Native, Flutter, Unity projects found | | `apple upload [] [--via api\|altool]` | API | IPA through the App Store Connect API build upload (or altool), build number checked, waits until VALID (exit 0 / 1 / 7); dry first, asks | | `apple next-build` | API, read-only | next safe build number | | `apple status` | API | versions, builds, IAPs, submissions; `--wait-review` (exit 0 / 1 / 7); `--cancel-submission ` | | `apple ppo status \| create --variant b \| upload --variant b` | API | Product Page Optimization test from `shots` variants: treatments, localizations, screenshot sets | | `apple ppo start \| stop` | API | start / stop the test (asks first; a stopped test cannot be restarted) |
TestFlight | command | kind | what | |---|---|---| | `apple testflight status` | API, read-only | groups, testers and builds per group, recent builds with internal / external beta states | | `apple testflight groups` | API | create the groups of `apple.testflight.groups` that are missing | | `apple testflight testers [--csv f] [--remove] [--invite]` | API | testers from a CSV in `.storebound/` into their groups (asks first; emails masked) | | `apple testflight what-to-test --build N` | API | "What to Test" per locale from `store/appstore.md` (`whatToTest`, else `whatsNew`) | | `apple testflight info` | API | Beta App Description, feedback email, URLs, Beta App Review contact and notes | | `apple testflight distribute --build N --group a,b [--submit-review]` | API | the build to groups; external groups and Beta App Review ask first | | `apple testflight wait --build N [--timeout 60m]` | API, read-only | until processing ends, with progress; exit 0 VALID / 1 FAILED / 7 timeout | | `apple testflight expire --keep N \| --older-than D` | API | expire old builds (never attached or processing ones); dry run, then asks |
After launch | command | kind | what | |---|---|---| | `health [--weekly] [--days 7]` | API, read-only | Play vitals (user-perceived crash / ANR vs 1.09 % / 0.47 %, devices, top issues), App Store metrics, diagnostics, TestFlight crashes; `--weekly` writes a Markdown report | | `reviews list [--unanswered]` | API, read-only | recent reviews of both stores | | `reviews draft` | API + local | one reply draft per unanswered review in `.storebound/replies/` | | `reviews send` | API | sends drafts with `approved: true` (checks length and incentives, asks first) | | `prices export [--no-subscriptions]` | API + local | current prices of every product and subscription (Apple current + scheduled, Play base plan regions), both stores, as `store/prices.csv`; subscription rows feed `apple subscriptions --csv` / `play subscriptions --csv` | | `prices import [--reconvert]` | API | diff table, then (asks first) Apple price schedules incl. future-dated changes and Play regional prices | | `analytics setup` | API | App Store analytics report request (once, Admin key) | | `analytics snapshot [--days 28]` | API + local | impressions, page views, downloads (App Store), visitors, acquisitions, installs (Play reports bucket) -> `.storebound/analytics/` | | `analytics compare` | local | snapshots vs the git history of the store files: before / after each listing change | | `reports apple-sales \| apple-finance \| play-earnings \| play-sales` | API, read-only + local | sales / finance reports into `.storebound/reports/`, summary per product and currency, CSV export | | `triage [--days 14]` | API, read-only | TestFlight crash and screenshot feedback, Play error reports: grouped by signature with devices, OS versions, builds | | `aso rank [--storefronts us,de]` | network + local | App Store rank per keyword and storefront plus competitors from the public iTunes Search API (rate limited) -> `.storebound/aso/` | | `aso competitors [--term ] [--storefront us,tr] [--top 10]` | network + local | top apps per keyword (name, developer, rating count, first release, last update, localized genre; no subtitles: not in any public API) and the words in their names you do not cover, same rate limit -> `.storebound/aso/competitors/` | | `aso suggest [--locale en-US] [--write]` | local (+ Apple Ads API with credentials) | keyword candidates (own text, competitor names, localized category names, Apple Ads keyword suggestions with their relative 0-100 popularity) scored by relevance and competition, and a 100-byte keyword field per locale as a diff; `--write` puts it into `store/appstore.md` as a draft for review. Popularity is never estimated | | `aso audit` | local | one report: `check --aso`, free keyword room, rank trend arrows, competitor gaps, Play short description and density, App Store conversion from the analytics snapshots, PPO / Play experiment ideas and the next 3 actions |
Screenshots and video | command | kind | what | |---|---|---| | `shots capture [--source web\|ios-sim\|android-emu\|folder] [--concurrency N] [--only-missing \| --force]` | local | raw frames from your running app or game; web: parallel contexts, optional one boot per language (`capture.session`), skips unchanged frames, timing per step | | `shots fonts` | network | font subsets for your copy (Noto for CJK / Arabic / Cyrillic / Devanagari ...), your own local font files, and the glyph coverage check per language | | `shots render [--variant b \| --variants] [--concurrency N]` | local | every store size and language in the game or app style, feature graphic, contact sheet, overflow report | | `shots jpeg` | local | JPEG q92 copies of the Play renders (about 4x faster uploads) | | `shots check [--variants] [--json]` | local, read-only | store rules: sizes, alpha, format, bytes, counts, overflow, fallback / missing glyphs, videos, folder frames | | `shots gallery` | local | `/index.html` review page | | `shots record [--source ...]` | local | app or gameplay clips: web (Chromium screencast), simulator, emulator, folder | | `shots video [--target iphone,ipad,youtube]` | local | app previews + promo video with captions, poster, checks |
Many apps and release trains | command | kind | what | |---|---|---| | `ws init` | local | find the app folders below and write `storebound.workspace.json` (globs like `games/*`) | | `ws list [--all]` | local | apps (folders + store-only apps), studios, accounts and whether each credential profile has its keys | | `ws status [--offline] [--apps a,b]` | API, read-only | portfolio table: every app x both stores (live / rollout % / next version), catalog parity, check; parallel | | `ws check`, `ws validate [--apps a,b]` | local / API, read-only | the offline check / the readiness report of every app in one table | | `ws run --apps a,b \| --all [--apply]` | per command | any read command in every app; writes as dry runs unless `--apply`; never submit / release / rollout / send | | `ws apps import --account ` | API, read-only + local cache | the apps a store account sees, paired with the folders (the desktop's rules) | | `ws desktop export [--out f \| --merge [--apply]]`, `ws desktop import` | local | the workspace to and from the desktop app's state (labels, never keys) | | `release status [--app a,b] [--versions]` | API, read-only | both stores on one train per version: build > testing > review > live > rollout % | | `release plan [--app a]` | API, read-only | what is left for that version on each store, in fix order, with the owner's next step |
## Growth extras Optional commands for after (or around) the first release. Each has `--dry` and `--json`; store and API writes ask in a terminal first and are never offered over MCP. - **Purchasing power prices:** `storebound prices ppp [--base 4.99] [--source worldbank|bigmac|file]` proposes a price per region from the bundled World Bank ICP PPP conversion factors (`cli/data/ppp-worldbank.json`, CC BY 4.0, dated and cited; refresh with `bun tools/update-ppp.ts`), clamped to `prices.ppp.floor`..`ceiling` of the US price and snapped to each store's price points. It writes a sheet in the `prices import` format (default `store/prices-ppp.csv`) and shows the diff; `storebound prices import --csv store/prices-ppp.csv --dry` applies it. Skill: `ppp-pricing`. - **Apple Ads:** `storebound ads apple status | campaigns | report [--by campaign|keyword] [--csv out.csv]`: spend, taps, installs, CPA, read-only (OAuth client credentials from env / `.storebound/`, never printed). `bid`, `budget` and `pause` print old -> new and ask first. Skill: `apple-ads`. - **Web presence:** `storebound web init [--landing]` writes `apple-app-site-association`, `.well-known/assetlinks.json` (Play app signing SHA-256), the Smart App Banner tag and an optional minimal landing page (store marks from simple-icons, privacy / support links from the config). `storebound web check ` verifies the live files: 200 without redirects, JSON content type, valid JSON, ids match the config. Skill: `web-presence`. - **Ad-hoc and internal sharing:** `storebound apple adhoc devices | register | profile | options | export` (UDIDs masked; registering asks first) and `storebound play share --aab app.aab` (Play internal app sharing; asks first, prints the install link). Skill: `adhoc-distribution`. - **RevenueCat (optional):** `storebound revenuecat diff | sync` mirrors products, subscriptions, entitlements and a default offering into a RevenueCat project (REST API v2, secret key from env / `.storebound/`). sync only creates and attaches, never deletes. Runtime entitlements, receipts and paywalls stay RevenueCat's job. Skill: `revenuecat-sync`. - **Console forms without an API, agent-first:** `storebound console plan
[--json]` gives your AI agent the start URL (deep links when the ids are known), the steps, the answer sheet from the config (each answer certain or "ask owner", with the console's exact labels), the buttons it must never press and the UI facts verified on the real consoles; the agent drives the page with its own browser tool (Claude in Chrome, a built-in browser, Playwright), stops before the final button and tells you what to press. `storebound console verify [--from-text -]` then checks the page against the config (App Privacy per-type summaries, Play Create app input states, summaries, or the App Store Connect API) and exits 1 on a mismatch. Both are read-only MCP tools (`console_plan`, `console_verify`). - **Scripted fast path:** `storebound console fill ` (`apple-app-record`, `apple-age-rating`, `apple-app-privacy`, `play-app-record`, `play-content-rating`, `play-target-audience`) opens a visible browser, goes to the form by itself (Apps > + > New App, the app's App Privacy, Play All apps > Create app, Play app list > App content > Start / Manage; navigation clicks only, never Save / Submit / Publish / Create / Next), fills what the config proves, highlights the rest and stops before the final button: you click. **Sign-in:** storebound's own profile (one per store, `~/.storebound/console-profile/`): you sign in there once by hand, it is kept between runs (session restore on launch, graceful close; tick "Keep me signed in" / "Stay signed in"). storebound starts that Chrome itself as a normal window (no `--enable-automation`, `navigator.webdriver` false) so Google accepts the sign-in; if Google still answers `/signin/rejected` it says so. A Chrome running with remote debugging is used in place. **It never copies your Google cookies by default:** Google binds them to your device (Chrome's Device Bound Session Credentials) and signs out sessions whose cookies look copied, which can sign you out of your real Chrome. `--use-chrome-session google|apple` is an explicit opt-in (a temporary copy of only that store's cookies, deleted at the end) that prints this warning; see the console-autofill skill. It detects sign-in pages by URL, frame and text (Apple's starts with no password field), waits while you sign in and then opens the form from step 1; after printing manual steps it keeps watching and picks up where you are. **One login, many studios:** it finds the Play developer account (by `play.packageName`, or `play.developerId`) and the App Store Connect team (account menu; `apple.teamId`, or by bundle id) that has the app and remembers it in `~/.storebound/console-accounts.json` (ids only). **Play Create app** (`play-app-record`, the Play Developer API cannot create apps): the developer account comes from `play.developerId`, the workspace account, or `play.developerAccount` (its name on "Choose a developer account"); with several accounts and none set it stops and lists them. It fills App name (`play.name` or `app.name`, 30 characters), Default language, App or game (the profile) and Free or paid (`play.free`, else Free for IAP / ads: an owner check, a free app can never become paid). It never ticks the Developer Program Policies or US export laws declarations and never presses the form's Create app: you do. With `play.developerId` it opens the form's own URL (`/console/u/0/developers//create-new-app`); it fills App package name from `play.packageName` and presses the read-only "Check availability", and clicks the inner `[role=radio]` of Play's material radios (the native input does not update the form). After Create app, add the new app to your service account yourself (Users and permissions > the service account > Add app): app-level permissions do not cover apps created later. When a page has changed it falls back to step-by-step guidance with copyable answers. **Console pages change; this is AI-assisted: review every field and save it yourself.** Desktop: "Fill in console" on the App info tab. Never over MCP. Skill: `console-autofill`. MCP gets only the safe parts: `prices_ppp`, `web_init`, `apple_adhoc_options` (local files, dry run by default) and the reads `ads_apple_status`, `ads_apple_campaigns`, `ads_apple_report`, `web_check`, `apple_adhoc_devices`, `revenuecat_diff`. ## Many apps and studios A studio with twenty games, or a developer with apps under two store accounts, works from one folder above the app folders. `storebound ws init` writes `storebound.workspace.json`: ```json { "apps": ["games/*", { "path": "apps/notes", "apple": "solo-apple" }], "studios": [ { "name": "Acme Games", "color": "indigo", "apps": ["games/*"], "accounts": [{ "store": "apple", "profile": "acme-apple" }, { "store": "play", "profile": "acme-play" }] } ], "accounts": [{ "store": "apple", "profile": "solo-apple", "label": "Solo" }], "ignore": ["games/old-*"], "overrides": { "pairs": [], "unpairs": [], "links": [], "appStudio": [] }, "concurrency": 4 } ``` - **apps**: folders or globs (only folders with a `storebound.config.*` count; `node_modules` and dot folders are skipped). An object entry names the folder's own credential profiles. - **studios**: name, colour (the desktop's set), the store accounts and the app folders (globs) that belong to it. - **accounts**: store accounts without a studio. An account is a store and a **credential profile** name, never a key. - **ignore**: folder globs, `app:` and `account:`, as in the desktop's ignore list. - **overrides**: the desktop's pairing corrections (`pairs`, `unpairs`, `links` of a store app to a folder, `appStudio`), with folders named by their path. A `storebound.workspace.ts` with `export default { ... }` works too. Which keys an app uses: its own entry's profiles, else the single account of that store in its studio, else the single account of that store in the workspace, else the folder's own credentials (env or `.storebound/`). **Credential profiles** keep many accounts apart on one machine: `~/.storebound/credentials/.json` with the account names of `.storebound/credentials.json` (`PLAY_SERVICE_ACCOUNT`, `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_P8`, `ASC_VENDOR_NUMBER`; the Android upload keystore belongs to the app and stays in the project; relative key paths are relative to that folder, which `STOREBOUND_CREDENTIALS_DIR` can move), or env `STOREBOUND_PROFILE__` (for CI). Any command takes `--account `. `ws list` shows per account whether its profile has keys; no command prints a key. `ws status`, `ws check`, `ws validate` and `ws run` read the apps a few at a time (`concurrency`, 1-8, or `--concurrency`), each app with its own keys handed to its child process only. `ws run` takes any read command and the MCP tool set; write commands get `--dry` unless `--apply`, and submit, release, rollout, send, start / stop, testers and `--yes` are refused: those stay with the owner in the app folder. `ws apps import --account ` lists the apps a store account sees (as `apps list`) into `.storebound/workspace/` and pairs them with the folders by the desktop's rules (`cli/lib/identity.ts`, the same code): a folder takes the store apps its config names, the remaining iOS and Android apps pair by id, close id or name, `unpairs` keep two apart, and the rest are store-only apps that `ws status` reads too. **The desktop app** keeps the same model (studios, accounts, folders, overrides). `ws desktop import` turns its `state.json` (or `--state `) into a workspace file and lists the credential profiles to create: the desktop's keychain is never read. `ws desktop export` prints the workspace as a desktop state (`--out `), and `--merge --apply` adds it to the desktop's `state.json` while the app is closed (a backup is kept; nothing is removed; new accounts get their keys in the desktop's Accounts screen). MCP: `workspace_list` (offline) and `workspace_status` (read-only). ## Release trains One version, both stores, one track: **build > testing > review > live > rollout**. The CLI and the desktop's Releases tab share the code (`cli/lib/train.ts`, `cli/lib/portfolio.ts`), so both show the same stage for the same store state. ``` $ storebound release status app version train App Store Google Play ------ ------- ------- ---------------------- ----------------------------- Puzzle 2.3.0 current rollout 20% (build 45) rollout 10% (build 230) Puzzle 2.4.0 next build testing (build 240, internal) ``` `release plan ` lists what is left for that version on each store in fix order: config problems (`check`), the version on the store (how to create it when it is missing), store blockers (`validate`, while nothing is submitted), the next move and the warnings. Owner steps (submit for review, promote to production, release an approved version, raise or complete a rollout) are named with where the owner does them; storebound does not do them. A store it cannot read is "unknown", never "not there". Both commands work in an app folder (that app) and in a workspace (every app, or `--app a,b`); `--json` gives the structured result. MCP: `release_status`, `release_plan` (read-only). ## Repo-local workflows Your release routine as a file in the repo, run step by step. `storebound init` writes two to start from (`store/workflows/release.yml`, `beta.yml`; app and game versions): ```yaml # store/workflows/release.yml (app profile, shortened) params: VERSION: "1.0.0" steps: - id: next-build if: apple run: apple next-build capture: BUILD - id: build if: apple run: apple build --version ${VERSION} --build-number ${BUILD} - id: upload if: apple run: apple upload --no-wait # wait, play-build, play-upload, whats-new, listing, validate, scan, plan ... - id: submit owner: "App Store Connect: Submit for Review (version ${VERSION}, build ${BUILD}) ..." ``` The plan of that file (`--dry`: nothing runs; without it the reads run and `${BUILD}` is filled in): ``` $ storebound flow run release VERSION=1.2.0 --dry [flow] 1/12 next-build: storebound apple next-build (plan only) [flow] 2/12 build: storebound apple build --version 1.2.0 --build-number ${BUILD} --dry (plan only) [flow] 3/12 upload: storebound apple upload --no-wait --dry (plan only) [flow] 4/12 wait: storebound apple testflight wait --build ${BUILD} --version 1.2.0 --dry (plan only) [flow] 5/12 play-build: storebound play build --version-name 1.2.0 --dry (plan only) [flow] 6/12 play-upload: storebound play upload --track internal --dry (plan only) [flow] 7/12 whats-new: storebound whats-new apply (if exists store/whats-new: false) [flow] 8/12 listing: storebound apple listing --version 1.2.0 --build ${BUILD} --dry (plan only) [flow] 9/12 validate: storebound validate (plan only) [flow] 10/12 scan: storebound scan (plan only) [flow] 11/12 plan: storebound release plan 1.2.0 (plan only) [flow] 12/12 submit: YOUR CLICK App Store Connect: Submit for Review (version 1.2.0, build ${BUILD}). Play Console: promote the internal release and Send changes for review ``` - Read steps (check, validate, scan, release plan, next-build, status) run; **write steps run with `--dry`** unless `--apply`, and even then each command asks its own confirmation; wait steps check once; `sh:` steps (your own build commands) run only with `--apply`. `--dry` prints the plan without running anything. - Owner steps (submit, release, phased release, rollout, review replies, price imports, tester invitations, Beta App Review) are never executed, also when written as `run:`; the flow ends with the list of your clicks. - `capture:` turns a step's last output line into a variable (`${BUILD}`), `if:` skips a step (`apple`, `play`, `exists `, `${VAR} == x`), `flow:` reuses another flow (`private: true` sub-flows), `before` / `after` / `onError` are hooks, `continueOnError` keeps going. - A failed step stops the flow; `--resume` starts again at that step with the captured variables (`.storebound/flows/.json`), `--from ` anywhere. `flow validate` checks commands, variables, duplicate ids and sub-flow cycles. Several flows can also live in `storebound.workflows.yml|json|ts`. - MCP: `flow_list` and `flow_run` (the dry mode only; `--apply` stays a terminal command). Skill: `release-flows`. ## Listing in more languages `storebound listing translate --to de,ja` adds a section per language to `store/appstore.md` (name, subtitle, promotional text, keywords, description) and `store/listing.md` (title, short and full description) with the source text and a marker, ``. Your agent translates in place (`listing-localization` skill: native wording, researched keywords, the store limits) and sets `status=draft`; or `--translate ""` runs a command of yours once per field (source text on stdin, `STOREBOUND_TARGET_LANG`, `STOREBOUND_FIELD`, `STOREBOUND_LIMIT` in the environment). storebound itself calls no translation service. `listing check` enforces each store's limits (App Store subtitle 30, promotional text 170, keywords 100 without duplicates or name words, description 4000; Play title 30, short 80, full 4000) and lists drafts; `storebound check` warns while one is left. After a human read a draft, `listing reviewed --lang de` removes the marker. Reviewed sections are never overwritten without `--force`. MCP: `listing_translate`, `listing_check`. ## Every endpoint The commands above cover the journey. For anything else, `storebound api` calls **every operation** of the App Store Connect API (1303 operations, OpenAPI 4.5.1) and of the Google Play Developer APIs (Android Publisher v3: 145, Play Developer Reporting v1beta1: 25), generated from the official specs pinned in `specs/` (source URLs, dates and SHA-256 in `specs/SOURCES.json`). Same credentials, same retries on 429 / 5xx (Retry-After honoured), JSON:API documents on the Apple side. ```sh storebound api list --store apple --search "customerReviews" # find an operation storebound api apple apps_getCollection --param 'filter[bundleId]=com.example.app' storebound api apple GET /v1/apps/123/appStoreVersions --all-pages # METHOD path works too storebound api play reviews.list --param packageName=com.example.app --all-pages --json storebound api play POST /v1beta1/apps/com.example.app/crashRateMetricSet:query --body query.json storebound api coverage # counts per API and resource, what commands wrap ``` - **GET runs right away** (so do the Reporting API `:query` calls and other POSTs that only read). - **Every write is a dry run by default**: it prints the exact request (method, URL, body; the credential is never shown) and is only sent with `--yes` from an interactive terminal. - **Owner-only operations** (submit, release, delete, cancel, refunds, prices, testers and users, review replies, product page tests, Play `edits.commit`, app signing; the list is in `cli/api/gate.ts`) also ask you to type the operation id before anything is sent. - **Agents read, never write**: MCP has one tool for this, `api_get` (GET operations only). Under MCP or the desktop app no write goes out, whatever the flags. - Small on purpose: one table row per operation plus generic helpers (`cli/api/`), typed from the rows (path and required parameters, JSON:API request and response shapes). `bun tools/gen-api.ts` regenerates the tables and [docs/api-coverage.md](docs/api-coverage.md) (every operation, its gate and the commands that already call it); CI fails when they are stale. ## Store art Screenshots and previews are one step of the journey, but the one that takes the longest by hand, so storebound does a lot here. Two looks: `shots.style: 'game'` (sunburst, sparkles, badges) and `shots.style: 'app'` (calm gradient, a clean device frame, headline + subline). Everything below was made by storebound itself from two tiny fictional demos: [a game](examples/demo-game) ("Prism Puddle", procedural art) and [an app](examples/demo-app) ("Tidy Lists", one static page with sample data): raw frames captured from the web build, captions in several languages, every store size, a review gallery and an app preview video. No store account needed to try it. ![App style: four calm screenshots and the feature graphic of the demo list app, rendered by storebound](https://git.crt.fyi/CRT/storebound/raw/branch/main/docs/img/app-style.jpg) ![Store screenshots rendered by storebound: four scenes in English plus the hero scene in Turkish, Japanese and Arabic](https://git.crt.fyi/CRT/storebound/raw/branch/main/docs/img/showcase.jpg) | Play feature graphic | App preview (App Store, 886 x 1920) | |---|---| | ![Play feature graphic, 1024 x 500](https://git.crt.fyi/CRT/storebound/raw/branch/main/docs/img/feature.jpg) | ![A few seconds of the app preview video](https://git.crt.fyi/CRT/storebound/raw/branch/main/docs/img/preview.gif) | ![The review gallery: every language, size, scene and variant on one local page, store check issues highlighted](https://git.crt.fyi/CRT/storebound/raw/branch/main/docs/img/gallery.jpg)
Contact sheet of one language (every size, packed into a grid) ![Contact sheet: iPhone 6.9" and 6.5", 13" iPad, Play phone, 7" and 10" tablets, feature graphic](https://git.crt.fyi/CRT/storebound/raw/branch/main/docs/img/sheet.jpg)
### How it works 1. **Frames.** Give your app or game a few switches (language, demo data or a fixed seed, no ads, jump to a screen, freeze, autoplay) and let `shots capture` take the frames: Playwright for a web build, `simctl` for anything that runs in the iOS Simulator (Unity, Godot, Flutter, React Native, native, Capacitor), `adb` for an Android emulator or device, or bring a folder of your own frames. The status bar is cropped off and redrawn clean (9:41, full battery). 2. **Copy.** Per language and scene: eyebrow, two-line title, subtitle, badges. Fonts are subset from Google Fonts to exactly your characters, so Japanese, Chinese, Korean, Arabic (right to left) and Hindi just work. `shots fonts` checks every character of every language against the font's cmap (TTF / OTF / WOFF / WOFF2, read in plain TypeScript): a glyph the font lacks gets an automatic Noto fallback (logged), no fallback is a clear error, and a render that drew any glyph with a fallback you did not choose, or with a system font, fails `shots check`. | script | title / body font (default stack) | |---|---| | Latin incl. Latin Extended (tr, pl, cs, ro, hu ...) and Vietnamese | your `shots.fonts`; default game Paytone One + Montserrat, app Inter (all cover them) | | Cyrillic, Greek | Noto Sans (whole line) | | Arabic, Hebrew, Devanagari, Bengali, Thai | Noto Sans Arabic / Hebrew / Devanagari / Bengali / Thai (whole line) | | Japanese, Korean, Chinese | Noto Sans JP / KR / SC (TC for zh-Hant, TW, HK) (whole line) | | a character the stack lacks | Noto Sans Display (titles) or Noto Sans (body), Noto Sans <Script>, then Noto Sans Symbols 2 / Symbols / Math: an ERROR in `shots check` unless listed in `shots.fonts.fallback` | Some display fonts lack Latin Extended: Lilita One has no ş ğ İ ą ę ń ś ź ż ć ă ș ț č ř ě ů ő ű or Vietnamese, so Turkish, Polish, Czech, Romanian, Hungarian or Vietnamese titles need another title font (Paytone One, Baloo 2, Rowdies) or `fonts: { ..., fallback: ['Noto Sans Display'] }` to accept the mixed glyphs. **Your own brand font.** `shots.fonts.title` / `body` / `fallback[]` also take a local file (relative to the config; TTF, OTF, WOFF or WOFF2, checked by extension and signature) or `{ family, file, weight?, style?, license? }`: ```ts fonts: { title: { family: 'ER Display', file: 'apps/game/public/assets/fonts/er-display.woff2', license: 'OFL-1.1' }, body: 'Montserrat', fallback: ['Noto Sans Display'], // tried in order, before the automatic Noto fallbacks }, ``` `shots fonts` runs the same cmap check on the file (`pl title: ę ł ą Ł not in ER Display -> Noto Sans Display`), prints its size, glyph count, sha256 and license, and records it in `fonts.json`. The file is never copied: render (and `shots video` captions) serve it from where it is, and a changed file changes `fonts.css` (the capture cache misses) and makes `shots render` ask for `shots fonts` again. `shots check` counts the local font and listed fallbacks as configured, not as fallbacks, and warns when a local font has no `license` note. 3. **Render.** Every store size at exact pixels: iPhone 6.9" and 6.5", 13" iPad (if your app runs on iPad), Play phone, 7" and 10" tablets, the 1024 x 500 feature graphic. 24-bit RGB, no alpha (both stores reject it). 4. **Check and review.** `shots check` applies the store rules locally (sources cited in `cli/shots/spec.ts`); `shots gallery` puts everything on one page with problems outlined. `validate` repeats the checks next to the store state. 5. **Variants.** Other first screenshot, order or copy as `shots.variants`, rendered side by side, for a Play store listing experiment or an App Store Product Page Optimization test (set up in the consoles; storebound makes and checks the images). 6. **Video.** `shots record` films the autoplay mode, `shots video` cuts 15-30 s App Store previews (886 x 1920, 1200 x 1600) and a 9:16 / 16:9 promo for YouTube with captions in every language, `apple previews` uploads them. Play takes a YouTube URL in `play.video`. Details: the [store-screenshots](skills/store-screenshots/SKILL.md) and [app-previews](skills/app-previews/SKILL.md) skills. ## Skills In journey order. Each one says which steps are API (storebound) and which are console clicks, with the exact navigation. | skill | for | |---|---| | `migrate-from-fastlane` | `storebound import fastlane`: ids, listings, languages and screenshots from fastlane; keys and accounts stay out | | `play-first-release` | Google Play from zero to "In review" | | `appstore-first-release` | App Store from API key to "Waiting for review", incl. xcodebuild with the API key | | `build-and-sign` | API signing (capabilities, certificate, profiles), `apple build` / `apple upload`, `play build` / `play upload`, for native, Capacitor, React Native, Flutter and Unity projects | | `testflight` | beta groups, testers from a CSV that never enters git, What to Test, Test Information, Beta App Review, waiting for processing, expiring old builds | | `store-iap-setup` | in-app products on both stores (consumable, non-consumable, non-renewing), prices and the price sheet CSV, catalog diff, testers, review submissions through the API and the first-IAP-with-version rule | | `store-subscriptions` | subscription groups, levels, base plans, prices, intro and promotional offers, offer codes, disclosure and Terms (3.1.2), first submission | | `review-access` | demo account for reviewers (App Review fields, Play App access), never committed | | `account-deletion` | Apple 5.1.1(v) in-app deletion, Play delete-account web page and Data safety | | `aso-basics` | keyword field, subtitle, cross-localization, Play placement without stuffing, keyword ranks, competitors, keyword field proposals and the ASO audit (iTunes Search API, Apple Ads popularity with credentials) | | `app-store-promotion` | IAP offer codes, win-back offers, promoted IAPs, pre-orders (Play pre-registration: console guide), custom product pages, in-app events | | `store-declarations` | every questionnaire: Play Data safety (API) and App content (IARC, target audience, ads, advertising ID, app access, government / financial / health), Apple App Privacy labels and age rating | | `export-compliance` | encryption answer, Info.plist key, declarations; never exempt by default | | `privacy-policy` | one policy page for all your apps with a per-app services table (templates per profile) | | `privacy-manifest` | PrivacyInfo.xcprivacy, required-reason APIs, SDK manifests (ITMS-91053 / 91061) | | `app-icons` | one artwork to every icon, adaptive / legacy launcher pitfalls | | `store-screenshots` | capture from any engine, copy, fonts, render, checks, gallery, variants, App Store product page tests, Play experiments, upload | | `app-previews` | app or gameplay clips, App Store previews, Play promo video, checks, upload | | `store-preflight` | run and read `check`, `check --aso`, `validate`, `privacy-manifest` and `release plan` before submitting; `ws check` / `ws validate` for many apps | | `release-management` | updates: release trains (`release status`, `release plan`), Apple phased + manual release, Play staged rollout, halt / pause (not a rollback), review wait | | `store-preflight` | run and read `check`, `check --aso`, `validate`, `privacy-manifest` before submitting | | `store-api` | any App Store Connect / Google Play operation without a command: find it, read freely, writes dry until the owner sends them | | `release-management` | updates: Apple phased + manual release, Play staged rollout, halt / pause (not a rollback), review wait, pulling console edits back | | `store-health` | crash / ANR vitals vs Play's bad behavior thresholds, App Store diagnostics, weekly report, crash and feedback triage | | `review-replies` | reply drafts, tone, no incentives, store limits, the owner approves and sends | | `store-analytics` | App Store analytics reports, Play reports bucket, before / after listing changes, sales / finance / earnings reports | | `whats-new` | player-facing release notes from git, one file per language for your agent to translate, store limits checked | | `listing-localization` | the listing in more languages: drafts per language, native wording and keyword research, store limits, review before push | | `release-flows` | repo-local workflows (`store/workflows/*.yml`): release and beta routines with dry runs, resume and the owner's clicks | | `review-rejections` | triage a rejection (fix / clarify / push back / appeal), reply template, the guidelines apps and games hit | | `ads-mediation-checks` | AdMob, AppLovin MAX and LevelPlay: ids, debug tools, adapters, app-ads.txt, SKAdNetwork, AdAttributionKit (`ads scan`), no-fill (`ads-levelplay-checks` stays as a pointer) | | `portfolio-originality` | Guideline 4.3 gate for developers shipping many similar apps or games, with `storebound originality` scores | ## MCP server `storebound mcp` speaks MCP over stdio, tools only: spec revision **2026-07-28** (stateless: `server/discover`, per-request `_meta`, `resultType`, cacheable `tools/list`) and, on the same process, clients that still open with `initialize` (2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05). It runs each tool as a CLI call in the project directory (the server's working directory, or `STOREBOUND_PROJECT`) and returns the output. - One tool per command. Read-only: `scan`, `check`, `check_aso`, `aso_competitors` (no snapshot saved), `aso_suggest` (never writes), `aso_audit`, `validate`, `catalog_diff`, `health`, `reviews_list`, `prices_diff`, `analytics_compare`, `apple_status`, `apple_next_build`, `apple_testflight_status`, `apple_ppo_status`, `play_status`, `play_next_version_code`, `privacy_manifest`, `shots_check`, `workspace_list`, `workspace_status`, `release_status`, `release_plan`, `flow_list`, `flow_run` (dry mode only), `listing_check`. Local writes: `import_fastlane`, `whats_new`, `listing_translate`, `icons`, `play_status`, `play_next_version_code`, `privacy_manifest`, `shots_check`, `api_get` (any App Store Connect or Google Play GET operation by id). Local writes: `import_fastlane`, `whats_new`, `icons`, `health_report`, `reviews_draft`, `prices_export`, `analytics_snapshot`, `shots_capture`, `shots_render`, `shots_gallery`, `shots_record`, `shots_video`. Store writes: `play_listing`, `play_images`, `play_products`, `play_data_safety`, `play_subscriptions`, `play_release_testing`, `apple_bundle`, `apple_iap`, `apple_iap_review`, `apple_subscriptions`, `apple_listing`, `apple_screenshots`, `apple_previews`, `apple_appinfo`, `apple_review_details`, `apple_encryption`, `apple_phased_release`, `apple_testflight_groups`, `apple_testflight_what_to_test`, `apple_testflight_info`, `apple_testflight_distribute` (internal groups only), `apple_ppo_create`, `apple_ppo_upload`, `analytics_setup`. - Annotations say what is read-only, destructive (replaces data), idempotent and network-bound. - No MCP SDK: a small newline-delimited JSON-RPC server (server/discover, initialize, ping, tools/list, tools/call, cancellation), so storebound keeps zero runtime dependencies. Manual config for a tool that `install` does not know: command `bunx` with args `["storebound", "mcp"]` (or the absolute path of `bin/storebound` plus `mcp`). ### Desktop machine interface `storebound serve --stdio` is what the desktop app (`apps/desktop`, in progress) runs as its sidecar: newline-delimited JSON-RPC on the same core as the MCP server (tool list, validation, forbidden flags, child-process runner), with typed methods (`check`, `validate`, `apple.status`, `play.listing`, ... plus `project.inspect` and `portfolio.status` over many project folders), structured results (the commands' `--json` output), progress notifications and cancellation. The first request is `session.hello` with a per-session secret only the host knows. Write methods are dry runs unless the call carries a single-use confirm token (HMAC over method, arguments and expiry) that the host mints after the owner confirmed a native dialog showing that dry run. There is no method that submits, releases or publishes; Play testing releases and phased release changes stay out of the desktop profile too. Types: `cli/lib/desktop-protocol.ts`. `bun tools/build-sidecars.ts` compiles the CLI for macOS, Linux and Windows (`bun build --compile`). Design, safety model and the Dependencies screen (tool sources and licenses): [docs/desktop.md](docs/desktop.md). ## Safety model - **Credentials** live in env vars or the gitignored `.storebound/credentials.json`, never in the config or the repo. storebound only talks to the official store APIs (plus Google Fonts, your own Play reports bucket and your own URLs); see [SECURITY.md](SECURITY.md). - **Dry run everywhere.** Every write command takes `--dry`; every MCP write tool has `dry_run`, **default true**. Raw API writes (`storebound api`) are dry unless `--yes` is given in an interactive terminal; owner-only operations also need the operation id typed, and MCP only gets the read-only `api_get`. - **Nothing public or irreversible through an agent.** Submitting for review (App Review and Beta App Review), releasing a version, production releases, resuming or completing a rollout or phased release, canceling a submission, deleting, inviting or removing testers, sending review replies, applying prices and starting or stopping a product page test and creating custom offer codes are not MCP tools. Subscription syncs through MCP only create what is new; price changes of live subscriptions wait for a terminal run. They stay terminal commands that ask for confirmation, and the skills tell every agent to ask the owner first ([docs/agents-core.md](docs/agents-core.md)). - **Personal data stays local.** Tester lists, the App Review demo account, reply drafts, health reports and analytics snapshots live in the gitignored `.storebound/` (the demo account is masked in every log line); tester emails are masked in every log line, and the config refuses a tester CSV path outside `.storebound/`. - **Idempotent writes.** Re-running a command converges (MD5 checks for screenshots and previews, one Play edit per language), so a failed upload is fixed by running it again. - **Simulators and emulators**: storebound shuts down only a simulator it booted itself and never boots, wipes or reboots an Android emulator or device. ## FAQ **Is it only for games?** No. It started with my game, but every command is about store records, not engines or genres. `init --profile app` and `--profile subscription-app` give you templates for a utility app with accounts and for a subscription app (Data safety, App Privacy, declarations, listing and screenshot copy), the `app` screenshot style, account deletion and demo account checks, and subscriptions on both stores. Game-only advice (IARC game questions, gameplay footage, ad mediation) is marked as such in the skills. **Do I need Claude (or any AI tool)?** No. The CLI is a normal command and the skills are Markdown checklists you can read yourself. The AI integrations are optional adapters generated from the same files. **Does it fill the questionnaires?** The ones with an API, yes: Play Data safety (the whole form from `dataSafety` in the config; the import replaces the form, so the config is the single truth), Apple age rating, content rights, categories, price and availability, the export compliance answer and the App Review details. The stores have no public API for Play's App content cards (IARC content rating, target audience, ads, advertising ID, app access, government / financial / health / news) or for Apple's App Privacy labels. For those, `init` gives you prepared answers (`store/declarations.md`, `store/app-privacy.md`, per profile: a utility app with an account, a subscription app, or a free-to-play game with ads and IAP), and the `store-declarations` skill has the exact clicks: an agent with browser tools can fill them in while you watch, or you copy them in yourself. Either way the answers are your statements to the stores: change what does not match your app, and the agent asks you whenever the code does not prove an answer. `validate` cannot see these forms, so `store-preflight` lists them as a manual check. **My app or game is Unity / Godot / Flutter / React Native / native, not web.** Screenshots and clips come from the iOS Simulator (`ios-sim`), an Android emulator or device (`android-emu`) or a folder you fill with any tool; only the rendering of the store images uses a headless browser. The store commands do not care about the engine at all: they work on your AAB / build numbers and listing files. **Is my store account safe?** storebound uses the official APIs with keys you create and can revoke, never your password. Store writes are dry runs until you say otherwise, and the public steps (submit, release) stay with you. Use an App Store Connect key with the App Manager role and a Play service account limited to your app. **Why Bun?** One fast runtime with TypeScript, `fetch`, a test runner and a static server built in: no build step and zero runtime dependencies. It needs Bun installed (it does not run on plain Node). **Does it upload my video to YouTube?** No. Play only takes a YouTube URL, and uploading to YouTube needs your Google account; `shots video` makes the file, you upload it and paste the URL into `play.video`. **Can it run store experiments for me?** On the App Store, mostly: `apple ppo create / upload` builds a Product Page Optimization test from your `shots` variants (treatments, locales, screenshots) and you start it (`apple ppo start` asks first). Results per treatment are only shown in App Store Connect. Play store listing experiments have no API: storebound renders and checks the variant images, you set the experiment up in Play Console. **Does it answer reviews by itself?** No. It drafts one file per review, your agent helps write the replies, and only the drafts you mark `approved: true` are sent, by you, after a confirmation. Replies that offer something for a rating are refused. ## Roadmap Wave 2 (TestFlight, vitals, review replies, price sheets, analytics, MCP 2026-07-28, Product Page Optimization) shipped in 0.4.0. 0.5.0 made storebound app-first (profiles, app screenshot style, account deletion, review access) and brought the full in-app catalog: subscriptions on both stores with offers and offer codes, non-renewing products and the cross-store `catalog diff`. Next, roughly in this order: - Play prepaid base plans for `non_renewing`. - The App Asset Library (API 4.5.1) for screenshots, custom product pages and in-app events once the screenshot sets it replaces stop working. - Play store listing experiments and their results, as soon as Google offers an API. - App preview treatments and alternate icons in Product Page Optimization tests. - The iPhone Duo screenshot classes Apple lists for apps built with the iOS 27.1 SDK (required from April 2027). ## Contributing Issues and pull requests are welcome: store rule updates (with a source), new capture sources, adapters for more AI tools. See [CONTRIBUTING.md](CONTRIBUTING.md); `bun run ci` must be green.
Agent support: formats and what is not verified Checked against each tool's own docs in October 2026: - **Agent Skills**: frontmatter is only `name` (lowercase-hyphen, equals the folder) and `description`; the generator fails if a skill adds anything else. - **Claude Code**: plugin manifest + root `.mcp.json` with `${CLAUDE_PLUGIN_ROOT}`; `claude plugin validate .` passes. - **Codex**: `.codex-plugin/plugin.json` with `"skills": "./skills/"`. Not verified: whether Codex starts the root `.mcp.json` from a plugin and expands `${CLAUDE_PLUGIN_ROOT}` there, so use `install --agent codex` for the MCP server. OpenAI also documents a newer portable root `plugin.json`; not shipped yet. - **Gemini CLI**: `gemini-extension.json` with `${extensionPath}${/}`, `GEMINI.md` as context, `skills/` inside the extension. Google now points unpaid users to Antigravity CLI; not tested there. The `.gemini/skills` folder used by `install --agent gemini` is not verified (the extension route is). - **Windsurf**: the current docs name `~/.config/devin/mcp_config.json`; `install` keeps using `~/.codeium/windsurf/mcp_config.json` when that file already exists (older installs). - **MCP**: built for 2026-07-28 (final, modelcontextprotocol.io) and dual-era: `initialize` clients get 2025-11-25 (or 2025-06-18 / 2025-03-26 / 2024-11-05). Tested with both handshakes on one stdio process, not yet against every client listed above. - **Not verified against a live account yet** (no credentials during development; request builders are tested with fixtures from the official docs): the scale of the Play Reporting API rates (read as fractions, 0.0109 = 1.09 %), the App Store review reply limit (5970 characters is commonly reported; Apple documents none, storebound only warns), the TestFlight "What to Test" limit (4000 assumed), whether an analytics segment checksum covers the gzip file or its content (both are accepted), that a new App Store price schedule replaces the old one (Apple's example implies it), stopping a Product Page Optimization test with `started: false`, and `APP_IPHONE_61` for the 6.3" iPhone set. From 0.5.0 (request bodies follow Apple's and Google's published schemas, tested with fixtures, not yet run against a live account): free promotional offers and offer codes sent with territories but no price point, subscription prices and introductory offers created inline through `PATCH /v1/subscriptions/{id}`, Play pay-as-you-go offers as a `relativeDiscount`, Play "developer determined" offers as offers without targeting plus an offer tag, the ISO 8601 strings for Play billing periods (Google lists them only in words), the Play subscription benefit length (40, from Play Console), the Data safety sign-up method ids (`PSL_ACM_*`, Google publishes none: compare with a Console export) and the delete-account URL question id (use `dataSafety.extra`). From the store operations (built from the App Store Connect API 4.5.1 OpenAPI spec and the cited Help pages, tested with recorded answers, not yet run live): the first IAP of a type sent through `reviewSubmissionItems` next to the app version (`--no-submit` leaves the draft for the UI if Apple refuses it), free IAP offer code and win-back prices without a price point, the order request for promoted purchases, the Play earnings / sales zip names beyond the documented prefix (storebound lists the prefix), and where Xcode stores `AttributionCopyEndpoint` (top level or inside an `AdAttributionKit` dictionary: both are read). - **Apple IAP description**: 45 characters (App Store Connect Help, "In-app purchase information"); an older App Store marketing page says 55. storebound checks 45.
Develop ```sh bun install bun run ci # adapters fresh (gen-adapters --check), tests, typecheck, secrets / personal data scrub bun tools/gen-adapters.ts # after editing skills/*/SKILL.md or docs/agents-core.md ``` `skills/*/SKILL.md` and `docs/agents-core.md` are the single source; `AGENTS.md`, `GEMINI.md`, the plugin manifests, `.github/copilot-instructions.md` and `.cursor/rules/storebound.mdc` are generated and committed, and CI fails when they are stale.
## Thanks and idea sources Some skills were sparked by other people's open work. I wrote my own text; the ideas deserve the credit: - [rorkai/app-store-connect-cli-skills](https://github.com/rorkai/app-store-connect-cli-skills) (MIT): App Store Connect workflow ideas. - [rshankras/claude-code-apple-skills](https://github.com/rshankras/claude-code-apple-skills) (MIT): Apple platform and App Store topic ideas. Built with Bun and plain `fetch` against the Google Play Developer API v3, the Play Developer Reporting API and the App Store Connect API. Screenshot fonts come from Google Fonts at render time (Noto, Paytone One, Montserrat, Inter; SIL Open Font License 1.1). Not affiliated with Google or Apple. ## License MIT, see [LICENSE](LICENSE). Copyright (c) 2026 CRTkafa.