--- # Quickstart > openaso is an ASO data service built agent-first: keyword ranks, real Apple Search Ads > popularity, listing audits, reviews, modeled revenue bands, and managed daily rank > tracking — every capability an MCP tool with a stable JSON contract. One endpoint. One key. Three steps: key, connect, ask. Your agent handles everything after that — the server teaches it how App Store search works. ## 1. Get a key Create an account at https://openaso.ai/login (email + password), then mint an API key in the dashboard at https://openaso.ai/dashboard. The key is shown **only once** — the dashboard also shows usage and rotates or revokes keys. Free while in early access: 500 tool calls per day and 500 tracked keyword × app × storefront combinations. ## 2. Connect your client Claude Code: ```sh claude mcp add openaso --transport http https://mcp.openaso.ai/mcp \ --header "Authorization: Bearer YOUR_KEY" ``` Cursor, Windsurf, or anything MCP — same endpoint, same header: ```json { "mcpServers": { "openaso": { "type": "http", "url": "https://mcp.openaso.ai/mcp", "headers": { "Authorization": "Bearer YOUR_KEY" } } } } ``` `x-api-key: YOUR_KEY` works too if your client cannot set an Authorization header. The server is stateless Streamable HTTP — POST only, no session juggling. ## 3. Ask for something real Good first prompts, in rough order of ambition: - "Where does app `6446901002` rank for `habit tracker` in the US and UK?" — one `rank` call. - "Audit the App Store listing for \ in the US and propose better metadata" — the `audit` tool gathers listing, word pool, and scored keyword candidates in one call, then your agent drafts against `coverage`. - "Track our top 20 keywords daily and tell me when anything drops" — `track_config` + the managed daily snapshot run, then `history dropsOnly=true` any morning. There is also a packaged `optimize_metadata` prompt on the server — the full playbook from audit to tracked verification. --- More: [tool catalog](https://openaso.ai/tools.md) · [how App Store search matches metadata](https://openaso.ai/docs/concepts.md) · [limits & data](https://openaso.ai/docs/limits.md) --- # How App Store search matches metadata This is the model the openaso server teaches your agent. It is also the part of ASO most tooling gets wrong — worth understanding even if an agent does the work. ## One pool, three fields For each localization, Apple indexes the **title** (30 characters), the **subtitle** (30), and the hidden **keyword field** (100) as **one combined word pool**. An app can rank for any query assembled from words across those fields: `photo` in the title plus `resize` in the keyword field can rank for *"resize photo"*. - Title words weigh most, then subtitle, then keyword field. - Word order and case do not gate what can rank — a phrase kept intact in the title or subtitle is a mild extra relevance signal, nothing more. - Repeating a word in a second field adds nothing. That budget is wasted. - Storefronts may index extra localizations — the US storefront also indexes Spanish (Mexico) — expanding the usable pool beyond one localization's budgets. ## Keyword-field mechanics Commas count toward the 100 characters, and spaces around commas are pure waste: `a,b` not `a, b`. A space that is a word's only separator costs the same as the comma replacing it. ## Optimize the pool, not phrases A word's search value is the sum of every phrase it can combine into — `photo` serves *photo editor*, *photo maker*, and *take photo* at once. But a phrase's value accrues to each of its words, so compare words by **marginal unlock and at-risk value**, not by summing phrase totals. The `coverage` tool computes all of this locally for a draft: pool, duplicates, budgets, keyword-field waste, per-phrase coverage, and per-word value when you pass volume-weighted phrases. ```json { "phrase": "resize photo", "covered": true, "via": { "resize": "keywords", "photo": "title" }, "note": "cross-field combinations tend to rank weaker than same-field phrases — verify with rank" } ``` ## Caveats the tools surface - `rank` covers the top 200 search results — the public API's cap. - `volume` returns Apple's real Search Ads popularity when `source: "apple"`; where Apple floors low-demand keywords or carries no signal, `source: "proxy"` is a transparent independent estimate with per-signal components. - The live keyword field and subtitle are not exposed by any public API — tools infer around that honestly rather than pretending to read them. - `revenue` is a modeled order-of-magnitude band from public grossing ranks. Agents are told to report it as an estimate, never as fact. - Google Play is not covered. --- # Limits & data Early-access limits exist to protect shared Apple rate-limit headroom, not to upsell you. They are generous for real use and enforced per account. | limit | detail | | --- | --- | | tool calls | 500 executed calls per account per UTC day; over-quota calls return a tool error naming the limit and the 00:00 UTC reset | | tracked set | 500 keyword × app × storefront combinations per account, snapshotted daily by the managed run | | history | per-series stats windows cover the newest 365 snapshots | | account creation | 3 per IP per day | | rank depth | top 200 search results per storefront (the public API's cap) | --- # openaso tool catalog > 13 callable contracts, generated from the live MCP registry — what you read > here is what your agent gets from `tools/list`, schema for schema. Endpoint: `https://mcp.openaso.ai/mcp` (MCP Streamable HTTP, POST only) · auth `Authorization: Bearer ` (or `x-api-key`) · get a key: [quickstart](https://openaso.ai/docs.md) - `rank` — Keyword rank (not read-only) - `suggest` — Keyword suggestions - `competitors` — Top apps for a keyword - `volume` — Keyword demand (Apple Search Ads popularity) - `app` — App lookup - `audit` — ASO research bundle - `coverage` — Metadata keyword coverage - `reviews` — Recent customer reviews - `charts` — Top-chart positions - `revenue` — Modeled revenue estimate - `track_config` — Manage tracked keywords (not read-only) - `track` — Track configured keywords (not read-only) - `history` — Rank history report ## rank — Keyword rank _not read-only_ App's position in App Store search results for a keyword (top 200), per storefront. Apps rank not only for literal metadata phrases: queries match the combined word pool of title + subtitle + hidden keyword field (see the coverage tool). With save=true, appends a snapshot to a JSONL history file and reports the change since the previous snapshot for the same keyword/app/storefront. Relative historyFile paths resolve against $OPENASO_HOME or the server's working directory. ### Input - `keyword` (string, required) — App Store search term - `appId` (integer, required) — App Store trackId of the app - `countries` (string[], default ["US"]) — 2-letter storefront codes, e.g. ["US", "GB", "JP"]. Each storefront is queried separately; at most 25 per call. - `save` (boolean, default false) — Append a rank snapshot to history and report the change - `historyFile` (string) — History JSONL path (default .openaso-history.jsonl); requires save=true ### Example call ```json { "name": "rank", "arguments": { "keyword": "habit tracker", "appId": 6446901002 } } ``` ### Output - `keyword` (string, required) - `appId` (integer, required) - `historyFile` (string) — resolved absolute path; only with save - `storefronts` (object[], required) - `country` (string, required) - `appName` (string | null, required) - `rank` (integer | null, required) — 1-based position, null = not in top 200 - `totalResults` (integer, required) - `prev` (integer | null) — previous saved rank (null = was not ranked); only with save - `change` (string) — human-readable movement; only with save --- ## suggest — Keyword suggestions _read-only_ App Store search-autocomplete suggestions for a seed term, per storefront — the terms Apple itself suggests to users typing in the store, in suggestion order. Use for keyword discovery: feed promising suggestions into rank/volume/competitors. ### Input - `keyword` (string, required) — Seed term to autocomplete - `countries` (string[], default ["US"]) — 2-letter storefront codes, e.g. ["US", "GB", "JP"]. Each storefront is queried separately; at most 25 per call. ### Example call ```json { "name": "suggest", "arguments": { "keyword": "habit tracker" } } ``` ### Output - `keyword` (string, required) - `storefronts` (object[], required) - `country` (string, required) - `suggestions` (string[], required) --- ## competitors — Top apps for a keyword _read-only_ Top-ranking apps for a keyword, per storefront: position, ratings, genre, and whether the keyword appears in the app's title. ### Input - `keyword` (string, required) — App Store search term - `limit` (integer, default 25) — How many top apps to return per storefront - `countries` (string[], default ["US"]) — 2-letter storefront codes, e.g. ["US", "GB", "JP"]. Each storefront is queried separately; at most 25 per call. ### Example call ```json { "name": "competitors", "arguments": { "keyword": "habit tracker" } } ``` ### Output - `keyword` (string, required) - `storefronts` (object[], required) - `country` (string, required) - `total` (integer, required) — apps matching per the API (capped at 200 by the search limit) - `returned` (integer, required) — rows returned (min of total and limit) - `results` (object[], required) - `rank` (integer, required) — 1-based search position - `trackId` (integer, required) - `name` (string, required) - `developer` (string, required) - `rating` (number | null, required) - `ratings` (integer, required) - `genre` (string, required) - `keywordInTitle` (boolean, required) - `url` (string | null, required) --- ## volume — Keyword demand (Apple Search Ads popularity) _read-only_ Keyword demand (popularity 0-100) plus competition count, per storefront. popularity is Apple's real Search Ads Search Popularity (5-100) when source is "apple". Apple floors low-demand keywords at 5 and returns nothing for terms below its threshold or in storefronts outside Apple Ads coverage (~95 of 175 storefronts have it); source is then "proxy" and popularity is an independently-derived estimate blending autocomplete-prefix probing, rating mass of top apps, competition depth and keyword-in-title saturation (per-signal values in components). floored marks keywords Apple carries no signal for; applePopularity echoes Apple's raw value (5 when floored, null when unavailable); estimate is always the independent number. Costs one search plus up to 3 autocomplete probes per storefront (throttled ~1/s), plus a cached Apple Ads lookup when configured. competition saturates at 200 (the search API's cap). ### Input - `keyword` (string, required) — App Store search term - `countries` (string[], default ["US"]) — 2-letter storefront codes, e.g. ["US", "GB", "JP"]. Each storefront is queried separately; at most 25 per call. ### Example call ```json { "name": "volume", "arguments": { "keyword": "habit tracker" } } ``` ### Output - `keyword` (string, required) - `storefronts` (object[], required) - `country` (string, required) - `popularity` (integer, required) — headline demand — Apple's real popularity (source apple) or the estimate (source proxy) - `source` ("apple" | "proxy", required) — apple = real Apple Search Ads popularity; proxy = independent estimate - `applePopularity` (integer | null, required) — raw Apple Search Ads popularity 5-100; 5 when floored, null when Apple has no data - `floored` (boolean, required) — Apple floored the keyword at 5 (or had no data); popularity fell back to the estimate - `estimate` (integer, required) — the independent blended demand estimate (always computed) - `label` ("Low" | "Medium" | "High" | "Very high", required) - `competition` (integer, required) — apps surfaced by the search (capped at 200) - `topAppsConsidered` (integer, required) - `medianTopRatings` (integer, required) - `components` (object, required) — per-signal 0-100 scores feeding the estimate - `autocomplete` (integer | null, required) — null when no probes ran - `ratings` (integer, required) - `competition` (integer, required) - `titleMatch` (integer, required) - `autocompleteProbes` (object[], required) — prefixes probed against App Store autocomplete, shortest first; failed probes are omitted (the score renormalizes over the rest) - `prefix` (string, required) - `weight` (number, required) — rank weight of this prefix in the score - `matched` (boolean, required) - `position` (integer | null, required) — 1-based suggestion position; null when not matched - `note` (string, required) --- ## app — App lookup _read-only_ Metadata for one app (by trackId) per storefront, including whether the app is available in that storefront at all. With full=true returns the complete iTunes lookup record (description, genres, release dates, minimum OS version, ...) instead of the summary. With subtitle=true also scrapes the App Store web listing for the app's subtitle (which the iTunes API omits) — one extra request per storefront; the keyword field is never exposed by any source and is not returned (evaluate metadata drafts, keyword field included, with the coverage tool). ### Input - `appId` (integer, required) — App Store trackId of the app - `countries` (string[], default ["US"]) — 2-letter storefront codes, e.g. ["US", "GB", "JP"]. Each storefront is queried separately; at most 25 per call. - `full` (boolean, default false) — Return the full iTunes lookup record (description included) - `subtitle` (boolean, default false) — Also fetch the App Store subtitle from the web listing (extra request per storefront) ### Example call ```json { "name": "app", "arguments": { "appId": 6446901002 } } ``` ### Output - `appId` (integer, required) - `storefronts` (object[], required) - `country` (string, required) - `available` (boolean, required) - `app` (object | null, required) — Summary of the listing (or the full iTunes lookup record, description included, when full=true); null when unavailable - `trackId` (integer) - `trackName` (string) - `subtitle` (string | null) — App Store subtitle from the web listing; only present when subtitle=true (null if none/unreadable) - `developer` (string | null) - `genre` (string | null) - `rating` (number | null) - `ratings` (integer) - `version` (string | null) - `price` (string | number | null) — formatted price string (summary) or numeric price (full record) - `languages` (array | null) — ISO language codes the listing supports - `url` (string | null) --- ## audit — ASO research bundle _read-only_ One-call ASO research bundle for an app, per storefront: the current listing (title + web-scraped subtitle), its visible word pool, keyword candidates discovered from App Store autocomplete seeded with that pool (plus any supplied phrases), and for every candidate a demand number (popularity, as in the volume tool — real Apple Search Ads popularity when popularitySource is "apple", else an estimate), competition, incumbent strength (medianTopRatings) and this app's current rank — each candidate scored from a single shared search. Feed the result into coverage ({phrase, value: popularity} weights) to draft metadata, confirm real positions with rank, and register the target set with track for daily movement; the optimize_metadata prompt packages the whole playbook. Costs one lookup, one listing scrape, one autocomplete request per seed and one search plus up to 3 autocomplete probes per candidate, all throttled ~1/s — expect roughly seeds + 4 x candidates seconds per storefront; progress notifications are sent when requested. Discovery is autocomplete-only: mine reviews for user vocabulary and competitors for who wins each phrase. ### Input - `appId` (integer, required) — App Store trackId of the app - `countries` (string[], default ["US"]) — 2-letter storefront codes, e.g. ["US", "GB", "JP"]. Each storefront is queried separately; at most 25 per call. - `phrases` (string[]) — Target phrases always scored alongside discovered candidates (e.g. phrases the app already ranks for or should) - `seeds` (string[]) — Autocomplete seed terms; default: the substantive words of the current title + subtitle - `limit` (integer, default 12) — Max discovered candidates scored per storefront (supplied phrases do not count against this) ### Example call ```json { "name": "audit", "arguments": { "appId": 6446901002 } } ``` ### Output - `appId` (integer, required) - `storefronts` (object[], required) - `country` (string, required) - `available` (boolean, required) - `app` (object | null, required) — Summary of the listing (or the full iTunes lookup record, description included, when full=true); null when unavailable - `trackId` (integer) - `trackName` (string) - `subtitle` (string | null) — App Store subtitle from the web listing; only present when subtitle=true (null if none/unreadable) - `developer` (string | null) - `genre` (string | null) - `rating` (number | null) - `ratings` (integer) - `version` (string | null) - `price` (string | number | null) — formatted price string (summary) or numeric price (full record) - `languages` (array | null) — ISO language codes the listing supports - `url` (string | null) - `pool` (object[], required) — visible word pool (title + subtitle, strongest field first; the hidden keyword field is not public) - `word` (string, required) - `field` ("title" | "subtitle", required) - `stopword` (boolean, required) - `seeds` (string[], required) — seed terms whose autocomplete suggestions were harvested - `candidates` (object[], required) — scored keyword candidates, sorted by popularity descending - `keyword` (string, required) - `source` ("supplied" | "discovered", required) — how the candidate was found (distinct from popularitySource) - `suggestedBy` (object[], required) — autocomplete evidence: seeds whose suggestions surfaced this keyword, with 1-based positions (Apple's lists are demand-ordered) - `seed` (string, required) - `position` (integer, required) - `popularity` (integer, required) - `popularitySource` ("apple" | "proxy", required) — apple = real Apple Search Ads popularity; proxy = estimate - `applePopularity` (integer | null, required) — raw Apple popularity 5-100; 5 floored, null when unavailable - `floored` (boolean, required) - `estimate` (integer, required) - `label` ("Low" | "Medium" | "High" | "Very high", required) - `competition` (integer, required) — apps surfaced by the search (capped at 200) - `medianTopRatings` (integer, required) — median rating count of the top-10 apps — incumbent strength - `rank` (integer | null, required) — this app's position (top 200), null = not ranked - `note` (string, required) --- ## coverage — Metadata keyword coverage _read-only_ Which search phrases a metadata draft can rank for, computed locally (no network). Apple indexes the title (30 chars), subtitle (30) and hidden keyword field (100) of a localization as ONE combined word pool: an app can rank for any query assembled from words across those fields — "photo" in the title plus "resize" in the keyword field can rank for "resize photo". Title words weigh most, then subtitle, then keyword field; word order and case do not gate what can rank (a phrase kept intact in the title/subtitle is a mild extra signal); repeating a word in a second field adds nothing. Reports per-field character budgets, the combined pool, cross-field duplicates (reclaimable characters), and for each target phrase whether it is covered — verbatim in the title/subtitle, assembled cross-field, or missing words. In the keyword field, commas count toward the 100 characters; redundant characters (spaces around commas, duplicated words, stopwords) are reported in keywordFieldWaste. Weight phrases with {phrase, value} (e.g. value = popularity from the volume tool) and wordValues aggregates each word's total search value across every phrase it combines into — "photo" serves "photo editor", "photo maker" and "take photo" at once. totalValue credits a phrase to all of its words (not additive across words); use unlockValue (absent words whose addition alone covers new phrases) and atRiskValue (pool words whose removal breaks covered phrases) for marginal decisions. Evaluates one localization; storefronts may index extra localizations (e.g. the US storefront also indexes Spanish (Mexico)). Cross-field combinations tend to rank weaker than same-field phrases, so confirm real positions with rank. ### Input - `title` (string, required) — App title draft (30-char limit) - `subtitle` (string) — Subtitle draft (30-char limit) - `keywords` (string) — Hidden keyword field draft, comma-separated (100-char limit; commas count, spaces are waste) - `phrases` (object[]) — Target search phrases to check coverage for; weight with {phrase, value} to aggregate per-word search value in wordValues ### Example call ```json { "name": "coverage", "arguments": { "title": "…" } } ``` ### Output - `fields` (object[], required) - `field` ("title" | "subtitle" | "keywords", required) - `text` (string, required) - `chars` (integer, required) - `limit` (integer, required) — Apple's limit: title 30, subtitle 30, keywords 100 - `remaining` (integer, required) — characters left; negative = over the limit - `overLimit` (boolean, required) - `tokens` (string[], required) - `pool` (object[], required) — combined indexable word pool, attributed to the strongest field containing each word - `word` (string, required) - `field` ("title" | "subtitle" | "keywords", required) — strongest field containing the word - `stopword` (boolean, required) — filler word Apple's search largely ignores — reclaimable - `alsoIn` (("title" | "subtitle" | "keywords")[], required) — weaker fields repeating the word (reclaimable characters) - `duplicates` (object[], required) — words spent in more than one field — repeating adds no ranking benefit - `word` (string, required) - `fields` (("title" | "subtitle" | "keywords")[], required) - `phrases` (object[], required) - `phrase` (string, required) - `covered` (boolean, required) — every substantive word is available in the combined pool (stopwords assumed ignored; false when the phrase has no substantive match at all) - `exactIn` ("title" | "subtitle" | null, required) — whole phrase appears verbatim in this field (never keywords — that field is unordered); a kept-intact phrase is a mild extra relevance signal - `crossField` (boolean, required) — covered only by combining substantive words from different fields (ranks, but tends to rank weaker) - `words` (object[], required) - `word` (string, required) - `match` ("exact" | "plural" | "stopword" | null, required) — "plural" is a suffix heuristic; null when missing - `field` ("title" | "subtitle" | "keywords" | null, required) — strongest field supplying the match; null for stopword/missing - `missing` (string[], required) — words in no field — the pool alone cannot produce the phrase (Apple can still rank it via developer name, in-app purchase names or query expansion) - `value` (number, required) — the phrase's relative search value (input weight; 1 when unweighted) - `keywordFieldWaste` (object | null, required) — keyword-field characters that deleting the waste would actually free (null when no keywords draft was given) - `spaces` (integer, required) — whitespace around commas or at the ends — a space that is a word's only separator is char-neutral and not counted - `duplicates` (integer, required) — chars of words already in title/subtitle or repeated in the field - `stopwords` (integer, required) — chars of filler words - `separators` (integer, required) — commas/spaces freed by removing the wasted words - `total` (integer, required) - `wordValues` (object[], required) — per-word aggregate search value across all target phrases, sorted by totalValue. A word serves every phrase it can combine into, but a phrase's value is credited in full to EACH of its words — totals are not additive across a word set; budget with unlockValue/atRiskValue - `word` (string, required) - `inPool` ("title" | "subtitle" | "keywords" | null, required) — strongest field containing — or plural-matching — the word; null if absent - `chars` (integer, required) — characters of the word itself; a keyword-field entry additionally costs one separating comma - `phrases` (string[], required) - `totalValue` (number, required) — summed value of every target phrase containing the word - `unlockValue` (number, required) — absent words: summed value of phrases covered by adding just this word - `atRiskValue` (number, required) — in-pool words: summed value of covered phrases that would actually lose coverage if the word were dropped from every field (a surviving plural variant keeps a phrase safe) - `note` (string, required) --- ## reviews — Recent customer reviews _read-only_ Recent customer reviews for an app from Apple's public RSS feed, per storefront: rating distribution, average of the fetched reviews, and review texts (for mining user phrasing as keyword candidates). Pages 1-10, ~50 most-recent reviews per page. ### Input - `appId` (integer, required) — App Store trackId of the app - `countries` (string[], default ["US"]) — 2-letter storefront codes, e.g. ["US", "GB", "JP"]. Each storefront is queried separately; at most 25 per call. - `page` (integer, default 1) — RSS feed page - `limit` (integer, default 10) — Max review texts included per storefront ### Example call ```json { "name": "reviews", "arguments": { "appId": 6446901002 } } ``` ### Output - `appId` (integer, required) - `page` (integer, required) - `storefronts` (object[], required) - `country` (string, required) - `count` (integer, required) — reviews fetched from this page - `averageRating` (number | null, required) - `distribution` (object, required) - `1` (integer, required) - `2` (integer, required) - `3` (integer, required) - `4` (integer, required) - `5` (integer, required) - `reviews` (object[], required) - `id` (string, required) - `rating` (integer, required) - `title` (string, required) - `text` (string, required) - `author` (string, required) - `version` (string | null, required) - `date` (string | null, required) - `note` (string, required) --- ## charts — Top-chart positions _read-only_ The app's position on the overall top-free, top-paid and top-grossing charts (top 100), per storefront. null = not on that chart. Free/paid come from Apple's public marketing-tools feed; grossing comes from the legacy iTunes RSS feed (the only public source of grossing ranks). topGrossing is the one public monetization signal — feed it into the revenue tool for a modeled revenue band. ### Input - `appId` (integer, required) — App Store trackId of the app - `countries` (string[], default ["US"]) — 2-letter storefront codes, e.g. ["US", "GB", "JP"]. Each storefront is queried separately; at most 25 per call. ### Example call ```json { "name": "charts", "arguments": { "appId": 6446901002 } } ``` ### Output - `appId` (integer, required) - `chartSize` (integer, required) - `storefronts` (object[], required) - `country` (string, required) - `topFree` (integer | null, required) — 1-based chart position, null = not in top 100 - `topPaid` (integer | null, required) — 1-based chart position, null = not in top 100 - `topGrossing` (integer | null, required) — 1-based chart position, null = not in top 100 (unless grossingUnavailable) - `grossingUnavailable` (boolean) — present (true) only when the legacy grossing feed could not be read — topGrossing is then unknown, not "not in top 100" --- ## revenue — Modeled revenue estimate _read-only_ Order-of-magnitude revenue estimate for an app, per storefront, MODELED from its public top-grossing chart rank (overall chart, falling back to the app's genre chart) — Apple publishes no revenue data for arbitrary apps, and grossing rank is the only public monetization signal. daily/monthly are gross consumer spend bands in USD (low/mid/high; developer proceeds ~70-85% of gross): a power-law fit calibrated on published rank-revenue datapoints, scaled per storefront and genre. Apps on no consulted chart (top 100) get only upperBoundDaily — they earn less than a rank-100 app, possibly far less. Treat bands as honest uncertainty, not precision: same-rank apps differ several-x when revenue is whale-driven, and subscription renewals move grossing rank without downloads. Costs one lookup plus up to two chart fetches per storefront (throttled ~1/s). Confirm chart movement over time with charts. ### Input - `appId` (integer, required) — App Store trackId of the app - `countries` (string[], default ["US"]) — 2-letter storefront codes, e.g. ["US", "GB", "JP"]. Each storefront is queried separately; at most 25 per call. ### Example call ```json { "name": "revenue", "arguments": { "appId": 6446901002 } } ``` ### Output - `appId` (integer, required) - `chartSize` (integer, required) - `currency` ("USD", required) - `storefronts` (object[], required) - `country` (string, required) - `available` (boolean, required) - `appName` (string | null, required) - `genreId` (integer | null, required) — Apple primary genre id, e.g. 6014 = Games - `genreName` (string | null, required) - `topGrossing` (integer | null, required) — overall top-grossing position, null = not in top 100 - `genreGrossing` (integer | null, required) — position on the app's own genre grossing chart, null = not in top 100 / no genre - `estimate` (object | null, required) — null when the app is not available in this storefront - `basis` ("overall-chart" | "genre-chart" | "not-charted", required) — which rank fed the model; not-charted = upper bound only - `rankUsed` (integer | null, required) — the grossing rank the estimate is computed from - `daily` (object | null, required) — daily gross consumer spend band, USD; null when not-charted - `low` (number, required) - `mid` (number, required) - `high` (number, required) - `monthly` (object | null, required) — monthly gross consumer spend band, USD; null when not-charted - `low` (number, required) - `mid` (number, required) - `high` (number, required) - `upperBoundDaily` (number | null, required) — not-charted only: the rank-100 value of the narrowest consulted chart, padded to the band's high edge (bandMultiplier x the model midpoint) so "earns less than this" stays honest — possibly far less - `countryMultiplier` (number, required) — storefront scale vs US = 1.0 - `genreMultiplier` (number | null, required) — genre-chart scale vs the overall chart; null unless a genre chart was consulted (it scales the genre-chart estimate, or the not-charted ceiling) - `bandMultiplier` (number, required) — band half-width K (low = mid/K, high = mid*K): 2 for overall-chart estimates, 2.5 when a genre multiplier is in play (it adds uncertainty) - `note` (string, required) --- ## track_config — Manage tracked keywords _not read-only_ Manage this account's tracked set (the hosted replacement for the local track config file): every (app x keyword x storefront) combination is snapshotted by the daily managed run and on demand via the track tool. action=add inserts the cross product of apps x keywords x countries (cap 500 combinations per account); action=remove deletes combinations matching the given appId/keyword/country filters (at least one filter required); action=list just returns the current set. ### Input - `action` ("list" | "add" | "remove", required) - `apps` (integer[]) — trackIds to add (action=add) - `keywords` (string[]) — keywords to add (action=add) - `countries` (string[], default ["US"]) — 2-letter storefront codes, e.g. ["US", "GB", "JP"]. Each storefront is queried separately; at most 25 per call. - `appId` (integer) — remove filter: only this app - `keyword` (string) — remove filter: only this exact keyword - `country` (string) — remove filter: only this storefront ### Example call ```json { "name": "track_config", "arguments": { "action": "list" } } ``` ### Output - `tracked` (object[], required) - `appId` (integer, required) - `keyword` (string, required) - `country` (string, required) - `total` (integer, required) - `added` (integer) — rows inserted; only for action=add - `removed` (integer) — rows deleted; only for action=remove --- ## track — Track configured keywords _not read-only_ Snapshot ranks for every (app x keyword x storefront) in this account's tracked set (manage it with track_config) and report movement per combination. Snapshots are stored server-side per account; a managed run also happens daily, so calling this is only needed for an immediate refresh. One throttled search per keyword x storefront (~1s each); progress notifications are sent when requested. ### Input _no parameters_ ### Example call ```json { "name": "track", "arguments": {} } ``` ### Output - `apps` (integer[], required) - `keywords` (string[], required) - `countries` (string[], required) - `tracked` (integer, required) — combinations in the tracked set - `results` (object[], required) - `appId` (integer, required) - `keyword` (string, required) - `country` (string, required) - `appName` (string | null, required) - `rank` (integer | null, required) - `totalResults` (integer, required) - `change` (string | null, required) --- ## history — Rank history report _read-only_ Read this account's server-side rank history (written by track and the daily managed run): one series per (keyword, app, storefront) with snapshot count, current/previous/best/worst rank and time range. dropsOnly=true returns only series whose latest snapshot is worse than the previous one (rank-drop check). Filterable; no network. Series stats cover each series' most recent 365 snapshots. ### Input - `keyword` (string) — Only series for this exact keyword - `appId` (integer) — Only series for this app - `country` (string) — Only series for this storefront - `since` (string) — Only records at/after this ISO date/timestamp (date-only values are interpreted as UTC midnight) - `records` (integer) — Include the last N raw records per series - `dropsOnly` (boolean, default false) — Only series that dropped (rank got worse, or fell out of the top 200) ### Example call ```json { "name": "history", "arguments": {} } ``` ### Output - `series` (object[], required) - `keyword` (string, required) - `appId` (integer, required) - `country` (string, required) - `snapshots` (integer, required) - `current` (integer | null, required) — rank in the latest snapshot - `previous` (integer | null) — rank in the snapshot before the latest; absent with one snapshot - `best` (integer | null, required) — best (lowest) rank ever; null if never ranked - `worst` (integer | null, required) - `firstRank` (integer | null, required) - `firstTs` (string, required) - `lastTs` (string, required) - `records` (object[]) — most recent raw records, oldest first; only when records > 0 was requested - `ts` (string, required) - `keyword` (string, required) - `appId` (integer, required) - `country` (string, required) - `rank` (integer | null, required)