Build your own App Store optimization tool
Copy one prompt into Claude Code, Codex or Cursor. Your agent builds a full ASO tool on your machine: real keyword ranks in 40+ markets, demand and difficulty scores, AI keyword research, and a listing audit that publishes to App Store Connect.
> paste prompt.md
# Build a local-first App Store keyword tracker and ASO copilot
Everything runs on my machine. No accounts, no cloud backend, no telemetry...
- words
- 4,820
- words
- sections
- 13
- sections
- milestones
- 8
- milestones
- 1
Skeleton
App opens, database file created, empty state loads
- 2
Store data
Top 10 for "habit tracker" in US, DE and JP match the phone
- 3
Tracking
Real app, 5 phrases, 3 markets: ranks and scores appear
- 4
App Store Connect
Board shows which phrases sit in your keywords field
- 5
AI
10 brainstormed concepts, every phrase measured before it shows a score
- 6
Audit and publish
Diff inspected, published to a new editable version
- 7
MCP
Claude Code answers "what should I target in Germany" from the board
- 8
Polish
Auto-check, diagnostics, signing and notarization
Everything a paid ASO tool does
Keyword tracking, competitor ranks, research and listing optimization. All of it on your laptop, built from public Apple data and the official App Store Connect API.
Real App Store ranks
Runs the same search the App Store app runs on a phone, daily, and finds your app in up to 200 results. Rank history is kept forever.
40+ markets, local language
Every storefront from the US to Japan, with phrases written the way locals actually search, not literal translations.
Demand and difficulty
A 0 to 100 demand estimate from the top 50 apps plus Apple autocomplete, and a difficulty score adjusted for each market's size.
AI keyword research
Brainstorm and localize with the Claude Code, Codex, OpenCode or Ollama you already have. The AI proposes phrases. It never scores them.
Audit and publish
Three full listing options with live character counts, localized per market, then published to App Store Connect after you click confirm.
Built-in MCP server
Your agents can read the board and run checks. One-click setup for Claude Desktop, Claude Code, Cursor, Windsurf, Codex and OpenCode.
Three steps, one afternoon
- 01
Copy the prompt
Click Copy the full prompt. It is a 4,820-word build spec covering the stack, data sources, scoring, data model, AI layer, MCP server, UI and security.
- 02
Paste it into your AI coding agent
Open an empty folder in Claude Code, Codex or Cursor and paste it. The agent lists the files and library versions it will use before writing code.
- 03
Build milestone by milestone
The agent builds 8 milestones in order and runs each acceptance check before moving on. Add your App Store Connect key at milestone 4 to see your hidden keywords field.
How every keyword gets scored
Apple does not publish search volume to anyone, so every ASO tool estimates it. This one shows its math. Each number comes from one App Store search per phrase, per market, per day. A missing measurement shows as “not checked”, never as zero.
- Demand (0 to 100)
- How many of the top 50 results target the phrase in their name or subtitle, plus a boost when Apple autocomplete suggests it early. Suggested after 3 typed characters beats suggested after 12.
- Difficulty (0 to 100)
- The median rating count of the top 10 results on a log scale: 100 ratings is about 40, 1,000 is about 60, 100,000 is 100. The median keeps one giant app from skewing it.
- Market density
- Small markets have far fewer ratings than the US, which would make every one look easy. Difficulty is scaled by how many ratings the same apps have there versus in the US.
| Verdict | When | What it tells you |
|---|---|---|
| own | Rank 1 to 3 | You own this. Protect it. |
| push | Rank 4 to 10 | Close. Strengthen it in the title or subtitle. |
| target | Demand 25+, difficulty 55 or less | Real demand, beatable competition. Go after it. |
| maybe | Demand 10 to 24 | Some demand. |
| hard | Demand 25+, difficulty above 55 | Real demand, strong competition. |
| dead / swap | Demand under 10 | Nobody searches this. If it is in your listing, swap it out. |
# Build a local-first App Store keyword tracker and ASO copilot
You are a senior full-stack engineer. Build me a complete, working, local-first desktop app for App Store Optimization (ASO). It tracks keywords for my iOS apps and my competitors in every App Store market, in each market's own language. It measures real search ranks every day, estimates demand and difficulty for every phrase, uses AI to brainstorm and localize keywords, runs a full listing audit, and can publish the improved listing straight to App Store Connect. It also exposes everything to AI agents through a local MCP server.
Everything runs on my machine. No accounts, no cloud backend, no telemetry. My data lives in a local SQLite file. My App Store Connect key never leaves my computer.
Work in milestones (listed at the end). After each milestone, run it, test it against the acceptance checks, and only then move on. Do not stub anything that this spec describes as real. Never invent numbers anywhere in the product: a missing measurement is shown as "not checked", never as zero.
---
## 1. Stack
- **Desktop shell:** Electron (latest stable), main process in TypeScript, bundled to a single CommonJS file.
- **UI:** React + TypeScript + Vite. Tailwind or plain CSS, your choice. Icons from lucide-react. A clean, dense, keyboard-friendly UI, with light and dark themes.
- **Database:** SQLite via Node's built-in `node:sqlite` (or `better-sqlite3` if the Electron Node version lacks it). WAL mode. One file in the app's user-data folder.
- **IPC:** a strict preload bridge with `contextIsolation: true`, `nodeIntegration: false`, `sandbox: true`. The renderer only gets a small typed API (section 11). Every IPC handler verifies the sender is the app's own window.
- **Secrets:** Electron `safeStorage` (backed by the macOS Keychain or Windows DPAPI) for anything sensitive.
- **Packaging:** electron-builder for a signed, notarized macOS `.dmg` (arm64 + x64) and a Windows installer. Register a custom URL scheme (for example `asotracker://`) for deep links.
- **Optional voice input:** on Apple Silicon Macs, bundle `whisperkit-cli` and offer push-to-talk dictation in the chat box. Download the model on first use into the user-data folder. Skip this on other platforms.
---
## 2. Data sources (the core of the product)
All App Store data comes from public Apple endpoints plus the official App Store Connect API. No paid data provider. Implement each of these as its own small, tested module.
### 2.1 Storefront table
Build a typed table of every App Store storefront you support (start with at least 40 major ones: US, GB, CA, AU, DE, FR, ES, IT, NL, SE, NO, DK, FI, PL, PT, BR, MX, AR, CL, CO, JP, KR, CN, TW, HK, SG, IN, ID, TH, VN, MY, PH, TR, RU, UA, SA, AE, IL, ZA, NZ). Each row has:
- `code` (two-letter lowercase country, e.g. `de`)
- `storefrontId` (Apple's numeric storefront id, e.g. US = `143441`)
- `languageId` (Apple's language id for that storefront's main language)
- `language` (BCP-47 code used for keyword text, e.g. `de-DE`)
- `ascLocales` (the App Store Connect locale codes that apply to that storefront, e.g. `de-DE`)
- display name and flag
Look up the real storefront and language ids. Verify each one by making a live search request (2.3) and checking that results come back in the expected language. Do not guess ids.
The storefront header Apple expects is `X-Apple-Store-Front: <storefrontId>-<languageId>,29`.
### 2.2 Shared HTTP layer
Every Apple request goes through one function that:
- sends a normal desktop Safari user agent
- applies a 15 second timeout
- disables caching
- retries network errors and HTTP 5xx up to 3 times with quadratic backoff (0.8s, 3.2s)
- treats HTTP 403 and 429 as "rate limited": stops retrying, raises a typed `BlockedError`, and slows the whole app down (see 2.7)
- treats any other 4xx as fatal and does not retry
### 2.3 Real App Store search (rank source)
This is the same search the App Store app itself performs, and it is the only source of rank. Do **not** use `itunes.apple.com/search` for rank: its ordering differs from what users actually see.
```
GET https://search.itunes.apple.com/WebObjects/MZStore.woa/wa/search
?clientApplication=Software
&caller=com.apple.AppStore
&term=<phrase>
Header: X-Apple-Store-Front: <storefrontId>-<languageId>,29
```
Parse:
- the ordered result id list from `pageData.bubbles[0].results[].id`
- app details ("lockups") from `storePlatformData["native-search-lockup"].results[<id>]`: `name`, `subtitle`, `artistName`, `userRating.ratingCount`, `artwork.url` (an image template with `{w}`, `{h}`, `{c}`, `{f}` placeholders: fill with 128, 128, `bb`, `png`)
Rules:
- An empty `bubbles` array means zero results (valid). Missing `bubbles[0].results` is an error.
- Keep the top 50 ids for scoring. Store the full id list (up to 200) for rank.
- **Rank** = 1-based position of the tracked app's id in the full list, or null if absent (show as "200+").
- Some ids in the top 50 will have no lockup. Fill those through the lookup endpoint (2.4).
### 2.4 iTunes lookup and search (details and app finder)
```
GET https://itunes.apple.com/lookup?id=<id1,id2,...>&country=<cc>&entity=software
GET https://itunes.apple.com/search?term=<q>&country=<cc>&entity=software&limit=<n>
```
- Lookup accepts up to 100 ids per call. Batch.
- Map: `trackId`, `trackName`, `artistName` (fall back to `sellerName`), `userRatingCount`, `primaryGenreName`, `trackViewUrl` (strip the query string), icon (`artworkUrl512`, rewritten to a 128x128 png).
- Cache lookup rows in memory per `country:id` for 24 hours, capped at 100,000 entries.
- Use `search` only for the "find an app by name" picker, never for rank.
- Accept app input as a numeric id, an `apps.apple.com/.../id123456789` URL, or `?id=123456789`.
### 2.5 Public listing page (competitor name and subtitle)
The subtitle is not in the iTunes API. Fetch:
```
GET https://apps.apple.com/<cc>/app/id<id>
Header: X-Apple-Store-Front: <storefrontId>-<languageId>,29
```
- If the body is JSON, read `storePlatformData["product-dv"].results[<id>]` for `name` and `subtitle`.
- If it is HTML, take the `<h1>` text (remove any badge `<span>`), and the first `<h2>` or `<p>` whose class contains `subtitle`. Strip tags, decode HTML entities, collapse whitespace.
- Return nulls (not an error) if the markup changes, and surface a warning in diagnostics.
### 2.6 Search hints (real user queries)
Apple's autocomplete is the only free signal of what real people type. Use it for keyword discovery and as an extra demand hint.
```
GET https://search.itunes.apple.com/WebObjects/MZSearchHints.woa/wa/hints
?clientApplication=Software&term=<prefix>
Header: X-Apple-Store-Front: <storefrontId>-<languageId>,29
```
- Parse the suggestion terms from the response (handle both JSON and plist formats; detect which one comes back).
- Store each suggestion in a `hints` table with the prefix depth at which it first appeared (`depth` = number of characters typed before Apple suggested it). A phrase Apple suggests after 3 characters is far more popular than one that needs 12.
- Best effort only: on failure return an empty list.
### 2.7 Rate limiting and pacing
Apple will block a machine that hammers it. Build one global pacer with a separate lane per endpoint type and a minimum gap between calls:
| Lane | Normal gap | Fast mode gap |
|---|---|---|
| store search (2.3) | 1200 ms | 850 ms |
| lookup (2.4) | 1200 ms | 850 ms |
| iTunes search (2.4) | 3200 ms | 3200 ms |
| hints (2.6) | 250 ms | 250 ms |
| app finder search | 300 ms | 300 ms |
- All lanes share a slowdown multiplier, starting at 1. On any 403/429, double it (max 4x), at most once per 10 seconds. After 300 successful responses in a row, halve it (min 1x).
- If a check run gets blocked, stop it cleanly, record which pairs failed, and tell the user "The App Store is rate-limiting this computer. Try again in a few minutes."
- Detect offline (network error class) separately and say so.
- Log every search attempt in a `fetches` table (phrase, storefront, time, ok, result count, detail count, error) so diagnostics can show what happened.
### 2.8 App Store Connect API (my own apps)
The user connects an App Store Connect API key (Issuer ID, Key ID, `.p8` private key) from App Store Connect > Users and Access > Integrations.
- **Key file UX:** a file picker filtered to `.p8` that opens in Downloads, plus a "Found these keys" helper that lists files named `AuthKey_<KEYID>.p8` in Downloads and Desktop (filenames only, newest first, max 5). Parse the Key ID from the filename. Accept drag and drop.
- **Storage:** store the issuer id, key id and the PEM contents encrypted with `safeStorage`. Refuse to save if encryption is unavailable ("Unlock your computer before saving connected accounts").
- **Auth:** sign an ES256 JWT locally with `node:crypto` (`kid` header = key id, `iss` = issuer id, `aud` = `appstoreconnect-v1`, `exp` = now + 20 min). Cache and reuse until 1 minute before expiry.
- **Client:** base URL `https://api.appstoreconnect.apple.com`, with `get`, `all` (follows `links.next` pagination) and `send(method, path, body)`. Map 401/403 to "Your App Store Connect key was rejected", 404 to null where it means "not visible to this key".
Reads:
- `GET /v1/apps?limit=200&fields[apps]=name,bundleId` (list the account's apps; used to auto-classify a tracked app as "mine")
- `GET /v1/apps/{id}/appInfos?limit=50`, then `GET /v1/appInfos/{infoId}/appInfoLocalizations?limit=200` (name and subtitle per locale)
- `GET /v1/apps/{id}/appStoreVersions?filter[platform]=IOS&limit=200`, then `GET /v1/appStoreVersions/{versionId}/appStoreVersionLocalizations?limit=200` (**keywords field**, description, promotional text, per locale)
Pick the editable app info (the one not yet live) when there is one, else the live one. Read keywords from the newest version in an editable state, else the live version.
Writes (only from the publish flow in 6.4, always behind an explicit confirmation):
- Create the next version if none is editable: `POST /v1/appStoreVersions` with `platform: IOS`, `versionString` = the suggested next version (bump the last numeric part: 1.4.2 → 1.4.3), relationship to the app.
- For each locale: `PATCH /v1/appInfoLocalizations/{id}` (name, subtitle) or `POST /v1/appInfoLocalizations` if the locale does not exist yet.
- For each locale: `PATCH /v1/appStoreVersionLocalizations/{id}` (keywords, optionally description and promotional text) or `POST /v1/appStoreVersionLocalizations` to add it.
- Show a per-locale progress list and a final summary. Never submit for review automatically.
### 2.9 Listing text per app
Store each app's listing per locale in `listings` (name, subtitle, keywords, source `asc` or `public`, fetched time). For my apps with a connected key, use App Store Connect (2.8): this is the only way to see the hidden 100-character keywords field. For competitors, use the public page (2.5) per market; their keywords field is not visible and the UI says so.
---
## 3. Measurement and scoring
Every measurement comes from **one store search (2.3) per phrase per storefront per day**. No other source of numbers.
### 3.1 Text normalization
- Lowercase with the storefront's locale, decompose accents (NFD), strip combining marks, recompose (NFC), turn hyphens and dashes into spaces, collapse whitespace.
- Word set: all runs of Unicode letters and digits. For words longer than 3 characters ending in `s`, also add the singular form.
- For Chinese, Japanese, Korean and Thai (no spaces between words), match by substring instead of word sets.
- Keep a small stopword set of generic filler that never counts as the meaningful part of a phrase: `app`, `apps`, `free`, `best`, `new`, `pro`, articles and prepositions in each supported language (`the`, `a`, `de`, `du`, `la`, `le`, `el`, `des`, `di`, `da`, `do`, ...). Make it configurable per app category.
### 3.2 Relevance of one result app to a phrase (0, 0.5 or 1)
- **1** if the normalized phrase appears as written in `name + " " + subtitle`, or if (for multi-word, space-delimited phrases) all the phrase's words appear in the name in the same order (plural-tolerant).
- **0.5** if every phrase word appears somewhere in name + subtitle, and at least one non-stopword phrase word appears in the name (or all words are in the name if the phrase is only stopwords).
- **0** otherwise.
### 3.3 Volume (demand proxy, 0 to 100)
`volume = round(min(100, sum(relevance of top 50 results) / 25 * 100))`
Then blend in the hints signal when available: if Apple suggests the phrase at prefix depth `d`, add a boost of `max(0, 30 - 2 * d)` points (cap the total at 100). Show both parts in the tooltip.
Label the number honestly in the UI: "Estimated demand from how many top apps target this phrase and how early Apple suggests it. Apple does not publish search volume."
### 3.4 Difficulty (0 to 100)
- `median` = median rating count of the top 10 results (all results, not only matching ones; median, not mean, so one giant app does not skew it).
- `difficulty = round(min(100, log10(1 + median / density) / 5 * 100))` (100,000 ratings = 100; 1,000 ≈ 60; 100 ≈ 40).
### 3.5 Market density (fair difficulty outside the US)
Rating counts in small markets are much lower than in the US, so raw difficulty would make every small market look easy.
- For a storefront S, take every app that appears in S's results and also in the US results for any phrase checked that day, where the app has at least 100 US ratings.
- For each, compute `(ratingsInS + 1) / (ratingsInUS + 1)`.
- If there are at least 5 such apps, `density(S) = clamp(median of ratios, 0.001, 1)`. Otherwise density = 1.
- Store and show the density used.
### 3.6 Verdict per phrase (for a tracked app)
| Condition (in order) | Verdict | Meaning shown to user |
|---|---|---|
| not checked yet | `unchecked` | Not measured |
| volume is null | `unknown` | Measurement failed |
| volume < 10 | `dead` (or `swap` if the phrase is already in the listing) | Nobody searches this. If it is in your listing, swap it out |
| rank 1 to 3 | `own` | You own this. Protect it |
| rank 4 to 10 | `push` | Close. Strengthen it in title or subtitle |
| volume 10 to 24 | `maybe` | Some demand |
| difficulty ≤ 55 | `target` | Real demand, beatable competition. Go after it |
| otherwise | `hard` | Real demand, strong competition |
### 3.7 History
Every search result is stored in `serp` keyed by (phrase, storefront, UTC day): full result ids, top 50 details, difficulty, checked time. A check run skips any pair already measured today unless the user forces it. Rank history charts read from this table. Keep history forever (it is small).
---
## 4. Data model (SQLite)
Create these tables with migrations (versioned, idempotent, run at startup):
- `apps` (id = App Store numeric id or generated id for brainstorms, name, developer, icon_url, store_url, genre, markets JSON list of storefront codes, asc_connected, kind `mine` | `competitor` | `brainstorm`, brief text for brainstorms, auto_check schedule, added_at)
- `listings` (app_id, locale, name, subtitle, keywords, source, fetched_at; primary key app_id + locale)
- `concepts` (id, app_id, label, tier `core` | `secondary` | `long-tail`, source `ai` | `user` | `asc`, added_at): a concept is one idea ("habit tracking") with a phrase per market
- `terms` (id, app_id, concept_id, storefront, text, source, hidden, added_at; unique app_id + storefront + text): the actual phrase in that market's language
- `serp` (text, storefront, day, result_ids JSON, top JSON, difficulty, checked_at; primary key text + storefront + day)
- `hints` (text, storefront, depth, checked_at)
- `fetches` (text, storefront, at, ok, results, details, error)
- `jobs` (id, app_id, kind, state `queued` | `running` | `done` | `failed` | `stopped`, progress text, started_at, finished_at, error)
- `audits` (app_id, state JSON: English options, chosen option, per-locale drafts, publish result)
- `chat` (id, app_id, role, text, actions done JSON, failed, at)
- `settings` (key, value)
Wrap multi-row writes in transactions. Emit a `changed` event to the UI after every committed write so all views refresh.
---
## 5. Background work
- **Job queue:** one job per (app, kind) at a time. Kinds: `fetch` (listings), `check` (measure all tracked pairs), `brainstorm`, `localize`, `audit`. Jobs report progress text ("search 14/120"), can be stopped, and survive the window closing (not app quit).
- **Check lane:** a check run builds the list of (phrase, storefront) pairs for the app: the app's primary market first, then phrases that came from the App Store Connect keywords field, then everything else. Hidden keywords are skipped. Measure in that order through the pacer. Deduplicate across apps: if two apps track the same phrase in the same market on the same day, search once.
- **AI slots:** at most 2 AI runs at a time across the whole app; extra AI jobs queue.
- **Auto-check:** a per-app setting (`off`, `daily`, `weekly`). A timer every 15 minutes starts due checks when the app is open. Optional: launch at login and stay in the menu bar so daily checks happen.
- **Check speed setting:** `normal` or `fast` (changes the pacer gaps from 2.7).
---
## 6. AI layer
The AI never produces numbers. It only proposes phrases and copy. Every proposed phrase is measured through section 3 before the user sees a score.
### 6.1 Providers (bring your own, no API key required)
Detect and let the user pick from what is installed, in this order of preference:
1. **Claude Code CLI** (`claude`). Check sign-in with `claude auth status --json`. Run headless and locked down:
`claude -p --output-format json --json-schema '<schema>' --tools "" --strict-mcp-config --mcp-config '{"mcpServers":{}}' --settings '{"disableAllHooks":true}' --setting-sources "" --no-session-persistence --system-prompt '<system>' [--model <m>] [--effort <e>]`
Send the user prompt on stdin. Read `structured_output` from the JSON result.
2. **Codex CLI** (`codex`): `codex exec --skip-git-repo-check --ephemeral --sandbox read-only --color never -c web_search="disabled" [-c model=...] [-c model_reasoning_effort=...] --output-schema schema.json -o answer.json -` with the prompt on stdin, in a fresh temp folder.
3. **OpenCode** (`opencode run --format json --agent plan -m <provider/model>`), parsing its JSON event stream.
4. **Ollama** at `http://127.0.0.1:11434` (`/api/tags` to list models, `/api/chat` with `format` set to the JSON schema).
5. Optional: a direct API key (Anthropic or OpenRouter) stored with `safeStorage`.
CLI handling rules:
- Find executables on PATH plus the usual install folders (Homebrew, `~/.local/bin`, npm global, nvm versions, `/Applications/*.app/Contents/Resources`, Windows `%LOCALAPPDATA%` and Store apps). Fix PATH for GUI launches on macOS by reading the login shell's PATH once.
- Spawn in a new process group, enforce a timeout, kill the whole tree on cancel (SIGTERM then SIGKILL; `taskkill /T /F` on Windows).
- Cap captured output, redact anything that looks like a key or token before logging, and turn common failures into plain messages ("Sign in to Claude Code: run `claude` in Terminal", "Your Claude plan has run out for now").
- If no provider is installed, show one-click install help per provider (open Terminal with the official install command) and links to each provider's sign-in page.
- Every AI call uses a strict JSON schema and validates the answer; on a malformed answer, retry once, then fail the job with a clear message.
### 6.2 Brainstorm (new keywords)
Input: app name, subtitle, description, genre, current keywords, competitors' names and subtitles, the user's brief, and existing concepts.
Output schema: a list of concepts, each with a label, tier, and one search phrase per selected market **written the way people in that market actually search** (not literal translations; local spelling, word order and common short forms).
Then: dedupe against existing terms, add the phrases, pull Apple hints for each concept's main word in each market and add on-topic suggestions, and queue a check for all new pairs.
### 6.3 Localize
For an existing concept, generate the phrase for markets that are missing it, using the same "how locals search" rule, then measure.
### 6.4 ASO audit (own apps only)
A guided flow with these steps, each a separate action so an agent can drive it too:
1. **audit.english:** the AI reads the current listing plus all measured data (ranks, volumes, difficulties, verdicts) and proposes 3 complete English options (name ≤ 30, subtitle ≤ 30, keywords field ≤ 100 characters, comma-separated, no spaces after commas, no words repeated from name or subtitle, no plurals of words already present, no competitor brand names, no "app"). Every phrase in each option is measured before display; options show their projected coverage (sum of volume of phrases they contain, weighted by verdict).
2. **audit.more / option.add / option.update / option.remove:** generate more options or edit them by hand, with live character counters and re-measurement.
3. **audit.choose:** pick one option.
4. **audit.localize:** for each locale the app supports, the AI writes that market's name, subtitle and keywords from the chosen option, using phrases already measured in that storefront and preferring high-volume, beatable ones. Enforce all character limits in code (count Unicode graphemes), trim the keywords field to whole terms that fit, and remove duplicates of words already in name or subtitle.
5. **audit.locale.set / locale.pick:** edit or pick per locale.
6. **Export:** a readable text and CSV export per locale (current vs proposed, and the measured numbers per phrase).
7. **audit.publish:** show a full diff per locale and a "Publish to App Store Connect" confirmation. Then run the writes in 2.8. Never publish without the user's explicit click (or an explicit agent tool call the user asked for).
### 6.5 Per-app chat agent
A chat panel per app. The AI gets a compact text snapshot of the app's board (phrases with rank, volume, difficulty, verdict per market) and can answer questions and propose actions. Actions it proposes are rendered as buttons the user clicks (add keyword, hide keyword, run check, start audit). Store the conversation in `chat`. System rule: "Measurements come from one App Store search per phrase and country. Never invent numbers."
---
## 7. Local MCP server (agent access)
Expose the app to AI coding agents and desktop assistants.
- The running app listens on a Unix socket `<userData>/mcp.sock` (Windows: a named pipe `\\.\pipe\<appname>-<hash of userData path>` plus a random 32-byte token written to `<userData>/mcp-auth` with mode 0600, which the client must send first).
- Ship a tiny stdio bridge script (`mcp.cjs`) that the agent launches with the app's own binary and `ELECTRON_RUN_AS_NODE=1`. It forwards JSON-RPC over stdio to the socket. If the app is not running, it launches it and waits.
- Implement MCP `initialize`, `ping`, `tools/list`, `tools/call`.
- **Read tools:** `status` (apps, jobs, AI slots), `overview` (every app's headline numbers), `board_get` (compact grid for one app), `term_get` (one phrase's top apps in one market), `audit_get`, `asc_apps`, `store_search` (find apps by name), `markets_list`, `next_version`, `ai_status`, `wait` (blocks 5 to 50 seconds until running jobs change, so agents do not poll fast).
- **Action tools:** one tool per action in section 11's dispatch list, each with a JSON schema. Mark destructive ones (`app.remove`, `keyword.remove`, `audit.clear`, `audit.publish`, `asc.disconnect`) with `destructiveHint: true`. Long actions return immediately with a job id; the agent calls `wait` then `status`.
- Put a clear server-level instruction string in `initialize` that explains the workflow (status → board → brainstorm → check → audit), that long jobs are async, and that missing measurements are null, never zero.
- **One-click setup** in Settings: detect and add or remove the server entry in Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`), Claude Code (`claude mcp add`), Cursor (`~/.cursor/mcp.json`), Windsurf (`~/.codeium/windsurf/mcp_config.json`), Codex (`~/.codex/config.toml`, respecting `CODEX_HOME`) and OpenCode (`~/.config/opencode/opencode.json`, respecting `XDG_CONFIG_HOME`). Parse JSONC safely, preserve the user's other entries and formatting, and back up each file before writing. Also show copy-paste commands for manual setup.
---
## 8. UI
- **Sidebar:** my apps, competitors, brainstorms; add button; job status indicator per app.
- **Add app:** paste a URL or id, or search by name (2.4 finder, results with icons). Auto-select "mine" if the App Store Connect key can see the app. Choose markets (defaults: the markets the app is localized for, from App Store Connect, else US + the top 5 by the app's genre).
- **Board (main view):** rows = concepts, columns = markets. Each cell shows the phrase in the local language and rank / volume / difficulty, colored by verdict. Sort by volume, difficulty, rank or verdict; filter by verdict and tier; search box. Inline add, edit, hide, remove. Shows which phrases are already in the listing.
- **Phrase detail:** top 10 apps for that phrase in that market (icon, name, subtitle, ratings, relevance), rank history chart, hints depth, where it appears in my listing.
- **Competitors:** for any tracked phrase, show each competitor's rank side by side with mine.
- **Listing view:** current name, subtitle and keywords per locale with character counts and which tracked phrases each one covers.
- **Audit view:** the step flow from 6.4 with diffs.
- **Chat panel:** per app, with optional push-to-talk dictation.
- **Settings:** App Store Connect key, AI provider and model, check speed, auto-check, MCP setup, data folder location, export and backup, diagnostics (recent fetch log, last errors, versions).
- **Empty states** that explain what to do next. Keyboard shortcuts for the common actions. Every number has a tooltip explaining where it came from.
---
## 9. Security and privacy (non-negotiable)
- No analytics, no crash reporters, no remote logging.
- The `.p8` key and any API keys are stored only encrypted with `safeStorage`, never in plain JSON, never in logs, never sent anywhere except Apple.
- The only outbound hosts are Apple's (section 2), the AI provider the user picked, and the app's own update server if you add one.
- Renderer gets no Node access. Validate every IPC argument. `openExternal` only allows `https:` URLs.
- If you add auto-update: download, verify a SHA-256 checksum, verify the code signature (`codesign --verify --deep --strict`, `spctl --assess --type execute`, and the Team ID matches the running app) before installing.
- Any feedback or bug report feature captures only the app's own window, and only when the user clicks it, with a preview and crop before sending.
---
## 10. Optional: Google Play support
Add later as a second platform behind the same interfaces, using the `google-play-scraper` npm package: `search` for rank (up to 250 results), `app` for details and rating counts, `suggest` for hints. Same scoring, same tables with a `platform` column.
---
## 11. Renderer API (preload bridge)
Expose exactly this, typed:
- `snapshot()`, `board(appId)`, `dispatch(action)`, `onChange(cb)`
- `searchApps(term, storefront)`, `markets()`, `ascApps()`, `nextVersion(appId)`
- `pickKeyFile()`, `findKeyFiles()`, `pathForFile(file)`
- `audit(appId)`, `exportAudit(appId)`
- `aiStatus()`, `aiTerminal(provider, command)` (opens Terminal to install or sign in)
- `mcpSetup()`, `mcpAdd(client)`, `mcpRemove(client)`
- `copyText(text)`, `openExternal(url)`
- `microphoneAccess()`, `dictate(audio)` (macOS arm64 only)
`dispatch` action types: `app.add`, `app.kind`, `app.remove`, `app.markets`, `app.fetch`, `session.create`, `session.update`, `keywords.brainstorm`, `keyword.add`, `keyword.localize`, `keyword.hide`, `keyword.update`, `keyword.remove`, `term.set`, `check.run`, `asc.connect`, `asc.disconnect`, `asc.test`, `settings.save`, `agent.ask`, `agent.clear`, `audit.english`, `audit.more`, `audit.option.add`, `audit.option.update`, `audit.option.remove`, `audit.measure`, `audit.choose`, `audit.localize`, `audit.locale.set`, `audit.locale.pick`, `audit.clear`, `audit.publish`, `job.stop`.
The MCP action tools (section 7) call the same `dispatch` so the UI and agents share one code path.
---
## 12. Milestones and acceptance checks
1. **Skeleton:** Electron + React + preload bridge + SQLite migrations + settings. *Check:* app opens, DB file created, `snapshot()` returns empty state.
2. **Store data:** storefront table, HTTP layer, pacer, search, lookup, listing page, hints. *Check:* a script prints the top 10 for "habit tracker" in `us`, `de` and `jp` with names in the right language, and the order matches what the App Store app shows on a phone.
3. **Tracking:** add app (mine or competitor), markets, concepts and terms, check run, `serp` history, scoring, density, verdicts, board UI. *Check:* add a real app, add 5 phrases in 3 markets, run a check, see ranks and scores; run it again the same day and confirm it skips; force it and confirm it re-measures.
4. **App Store Connect:** key connect with `safeStorage`, JWT, read listings including the keywords field. *Check:* the board shows which phrases are already in my keywords field.
5. **AI:** provider detection, locked-down CLI runs, brainstorm, localize, chat. *Check:* brainstorm 10 concepts for a real app in 3 markets; every phrase gets measured; nothing shows a number before it is measured.
6. **Audit and publish:** full flow, limits enforced in code, export, publish with confirmation. *Check:* run on a test app, inspect the diff, publish to a new editable version, and confirm the fields in App Store Connect.
7. **MCP:** socket server, bridge script, tools, one-click client setup. *Check:* from Claude Code, ask "what keywords should I target for my app in Germany" and watch it call `status`, `board_get`, `keywords.brainstorm`, `wait`, `board_get`.
8. **Polish:** auto-check, diagnostics, dictation (optional), packaging, signing, notarization.
Write unit tests for: storefront header building, app id parsing, HTML subtitle parsing, relevance (including plurals, accents, CJK), volume, difficulty, density, verdict, keyword-field fitting (100 characters, no duplicates), version bumping, JSONC config editing. Use recorded Apple responses as fixtures so tests run offline.
---
## 13. Honest limits to show users
- Apple does not publish search volume. Volume here is an estimate from how many top apps target a phrase and how early Apple suggests it.
- The search and hints endpoints are undocumented. If Apple changes them, the app must fail loudly in diagnostics, not silently show zeros.
- Results can vary slightly by device, account and time of day. One daily check is a snapshot.
- Competitors' keywords fields are private; only their name and subtitle are visible.
Start with milestone 1 now. Before writing code, list the files you will create and the exact libraries and versions you will use.
Build it or rent it?
A big studio tracking thousands of keywords across a portfolio may still want a hosted suite. For an indie developer with a few apps, owning the tool wins on almost every line.
| Build your own | Subscription ASO tool | |
|---|---|---|
| Cost | Free. Uses the AI plan you already have | Monthly subscription, often per app or per market |
| Rank source | The same search the App Store app runs | Varies, rarely documented |
| How scores work | Every formula written down in the spec | Proprietary |
| Your data | One SQLite file on your laptop | The vendor's cloud |
| App Store Connect key | Encrypted in your keychain, sent only to Apple | Uploaded to the vendor |
| Agent access | Built-in MCP server | Sometimes, often a higher tier |
| Change anything | It is your code | Feature requests |
What it can’t do
The spec makes the tool show these limits to you in its own UI. No ASO tool can get around them, the honest ones just say it.
- Apple does not publish search volume. Demand here is an estimate, and the tool says so.
- The search and autocomplete endpoints are undocumented. If Apple changes them, the tool fails loudly in diagnostics instead of showing zeros.
- Results vary slightly by device, account and time of day. One daily check is a snapshot.
- Competitors' keyword fields are private. You see their name and subtitle, not their hidden 100 characters.
Rank an app people actually want
ASO gets an app found. It can’t fix an app nobody needs. BigIdeasDB tracks 1M+ real complaints so you can pick the problem first.
Frequently asked questions
Straight answers about building and running your own ASO tool.
For indie developers, the best free option is BigIdeasDB's build-your-own ASO tool prompt. You paste it into Claude Code, Codex or Cursor and your agent builds a full keyword tracker on your machine: real App Store ranks in 40+ markets, demand and difficulty estimates, AI keyword research and a listing audit that publishes to App Store Connect. There is no subscription and no paid data provider, because every number comes from Apple's own public search.
Yes. The prompt on this page is a complete 4,820-word spec split into 8 milestones, each with an acceptance check. Your AI coding agent builds one milestone, runs it, tests it and only then moves on, so you end up with a working desktop app rather than a demo.
It runs the same search the App Store app runs on a phone, once per phrase per market per day, and records where your app appears in up to 200 results. It deliberately avoids the public iTunes search API for rank, because that endpoint orders results differently from what users actually see.
No. Apple does not publish search volume to anyone, so every ASO tool estimates it. This one estimates demand from how many of the top 50 apps target a phrase plus how early Apple's autocomplete suggests it. A phrase Apple suggests after 3 typed characters is far more popular than one that needs 12. The UI labels the number as an estimate.
Difficulty uses the median rating count of the top 10 results on a log scale, so 100,000 ratings scores 100, 1,000 scores about 60 and 100 scores about 40. Outside the US it adjusts for market density, because rating counts in small markets are much lower and would otherwise make every small market look easy.
The data is free: public Apple endpoints plus the official App Store Connect API. The AI features run on whatever you already have installed, such as a Claude Code or Codex plan, OpenCode, or a local Ollama model. The tool never needs its own API key or account.
The spec makes it non-negotiable. The .p8 key is encrypted with the operating system keychain, never written to plain files or logs, and only ever sent to Apple. The app has no analytics, no crash reporter and no cloud backend, and your data lives in one local SQLite file.
BigIdeasDB is the best place to start, because its App Store database has analyzed 7,700+ apps and 136,000+ user reviews, part of 1M+ complaints across Reddit, G2, Capterra and the app stores. You see what users of existing apps keep complaining about, then use this ASO tool to check whether people actually search for it.
Google Play support is an optional section in the prompt. It adds Play as a second platform behind the same interfaces, with the same scoring and the same tables, so you can add it after the App Store version works.
Find the app before you rank it
1M+ real complaints from Reddit, G2, Capterra and the app stores, in one place.