# Cross-Store ASO Keyword Rank Monitor (`herazur/cross-store-aso-keyword-rank-monitor`) Actor

Monitor Apple App Store and Google Play keyword rankings across countries. Detect rank gains, drops, Top 3/10 changes, competitor overtakes, and #1 changes automatically.

- **URL**: https://apify.com/herazur/cross-store-aso-keyword-rank-monitor.md
- **Developed by:** [Furkan Toluç](https://apify.com/herazur) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 keyword market checks

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Cross-Store ASO Keyword Rank Monitor

**Know when your app gains or loses keyword visibility.**

Monitor public Apple App Store and Google Play keyword rankings across countries. Scheduled runs remember the previous successful positions and return only meaningful changes: rank gains, drops, Top 3/10 transitions, lost rankings, competitor crossovers, and changes at #1.

This Actor is intentionally a focused rank monitor, not another full search-results scraper or an AppTweak/Appfigures/Sensor Tower replacement.

### What it does

One HTTP search is made for each `store × country × keyword`. Every app in that result is then matched against all relevant tracked apps by stable ID:

- Apple apps are matched by numeric App Store ID.
- Google Play apps are matched by package name.
- App titles are never used as identity.
- No login, API key, browser, proxy, LLM, Apple developer account, or Play Console account is required.

With 10 keywords, 3 countries, and both stores, the Actor performs 60 searches—not one search per tracked app.

### Use cases

#### ASO monitoring

Track important keywords daily or several times per day with [Apify Schedules](https://docs.apify.com/platform/schedules).

#### Release and metadata impact

See whether title, subtitle, or description changes coincide with ranking movement.

#### Competitor monitoring

Mark explicitly tracked apps as `competitor` and detect when one crosses above or below your app.

#### Country monitoring

Find markets where keyword visibility improves or deteriorates.

#### Agency monitoring

Track multiple client apps without buying a full ASO SaaS suite.

### Input

```json
{
  "apps": [
    {
      "name": "HabitKit",
      "role": "own",
      "appStoreId": "6448311069",
      "googlePlayId": "com.roehl.habitkit"
    },
    {
      "name": "Competitor",
      "role": "competitor",
      "appStoreId": "123456789",
      "googlePlayId": "com.example.competitor"
    }
  ],
  "keywords": ["habit tracker", "daily habits"],
  "countries": ["us", "gb", "de"],
  "stores": ["appstore", "googleplay"],
  "googlePlayLanguage": "en",
  "maxResultsPerKeyword": 30,
  "onlyChanges": true,
  "minimumRankChange": 2
}
```

Each logical app needs `name`, `role`, and at least one store ID. Cross-store IDs are not inferred. Country values are two-letter codes. Search depth is capped at 30 because Google Play's public HTTP result depth is more limited than Apple's.

### Change-only mode

`onlyChanges` defaults to `true`. The default Dataset then contains incident rows rather than another full result dump.

```json
{
  "eventId": "2d0a...",
  "eventType": "ENTERED_TOP_10",
  "severity": "MEDIUM",
  "store": "appstore",
  "country": "us",
  "keyword": "habit tracker",
  "appName": "HabitKit",
  "appId": "6448311069",
  "role": "own",
  "previousRank": 14,
  "currentRank": 8,
  "rankChange": 6,
  "checkedDepth": 30,
  "checkedAt": "2026-08-29T10:00:00.000Z",
  "baselineRun": false
}
```

The Actor can emit:

- `RANK_GAIN`, `RANK_DROP`
- `ENTERED_TOP_3`, `LEFT_TOP_3`
- `ENTERED_TOP_10`, `LEFT_TOP_10`
- `ENTERED_RESULTS`, `DROPPED_OUT`
- `COMPETITOR_OVERTAKE`, `COMPETITOR_OVERTAKEN`
- `TOP_APP_CHANGED`

Ordinary gains and drops smaller than `minimumRankChange` are suppressed. Boundary, entry, and drop-out events are still emitted. Event IDs are deterministic and duplicate events are removed within a run.

### Rank semantics

`rankChange = previousRank - currentRank`.

- `#9 → #4` is `+5` and means improvement.
- `#4 → #9` is `-5` and means deterioration.

A null rank is never converted to depth + 1. For example:

```json
{
  "found": false,
  "rank": null,
  "checkedDepth": 30
}
```

This means **not found within the first 30 checked results**. It does not claim that the app is ranked #31 or lower.

### Baseline and persistent state

The first successful run for a tracked tuple establishes its baseline. It saves rank and #1 state in the named Apify Key-Value Store `cross-store-aso-rank-monitor-state` and writes `baselineRun: true` to the `OUTPUT` summary. With `onlyChanges=true`, an all-new baseline normally produces zero Dataset rows.

Later scheduled runs compare against the last trustworthy state. State keys are deterministic from `store | country | keyword | appId`; top-app state uses `store | country | keyword`.

Do not overlap runs of the same configuration. The MVP uses one state document and concurrent writes from overlapping runs can race.

### Snapshot mode

Set `onlyChanges` to `false` for one normalized snapshot per `app × keyword × country × store`. A not-found snapshot retains `rank: null`, `found: false`, and the checked depth. Change detection and state updates still run normally.

### Failure safety

Apple and Google responses are shape-validated before any app is considered absent. In particular, a Google page with no confidently parsed app cards is a failed job, not an empty ranking.

- 429, temporary 5xx, and network/timeout failures receive limited exponential-backoff retries.
- A failed keyword market is listed under `errors` in `OUTPUT`.
- Other keyword markets continue.
- Failed jobs do not update rank or top-app state.
- If every job fails, the run fails and no state is committed.

### Output summary

The default Key-Value Store `OUTPUT` record includes run status, baseline status, planned/succeeded/failed jobs, rank checks, detected event totals, event-type counts, timestamps, and per-job errors.

```json
{
  "status": "SUCCEEDED",
  "baselineRun": false,
  "appsTracked": 4,
  "keywords": 20,
  "countries": 3,
  "stores": 2,
  "searchJobsPlanned": 120,
  "searchJobsSucceeded": 118,
  "searchJobsFailed": 2,
  "rankChecks": 472,
  "eventsDetected": 17,
  "errors": []
}
```

### Extraction approach

Apple uses the public iTunes Search API with country, software entity, and a bounded result limit. Result array order is treated as the public storefront search order. Calls are throttled in line with Apple's documented approximate API limit.

Google Play uses the public server-rendered search HTML. Ordered app detail cards are parsed by package ID, deduplicated, shape-checked, and bounded before ranks are accepted. There is no automatic browser or proxy fallback; unexpected page structure fails clearly.

### Pay-per-event readiness

The billing adapter has one clean event: `keyword_market_checked`. One event means one successfully parsed `store × country × keyword` search, regardless of how many tracked apps were evaluated. It does not charge again per app or detected incident.

No final event price is declared in this repository. Pricing must be configured and reviewed in Apify Console before Store publication. When the current Actor pricing model is not PPE, the adapter is a no-op.

### Local development

Requires Node.js 20 or newer.

```bash
npm install
npm test
npm run build
npm run validate:live
```

The live validation performs two keywords × two countries × both stores twice, reports ranks and result counts, and checks Top 10 repeatability. It uses public HTTP only.

### Known limitations

- Public rankings can differ from personalized results on a specific device or user account.
- Apple ranks represent Apple's public storefront Search API response, which can differ from the App Store UI.
- Google Play public search depth is more limited than Apple's.
- “Not found” means not found within the configured checked depth.
- Google Play's public page and private internal structure may change; parser breakage becomes a job error rather than a false drop-out.
- This Actor measures keyword rank, not keyword search volume.
- It does not estimate downloads or revenue.
- It does not calculate keyword difficulty.
- It does not perform automatic competitor or keyword discovery.
- It is not intended to replace AppTweak, Appfigures, Sensor Tower, or a full ASO audit platform.

### Data and privacy

Input contains public app IDs and keyword markets. No credentials or user cookies are requested. Persistent ranking history remains in the Actor user's Apify Key-Value Store.

# Actor input Schema

## `apps` (type: `array`):

Tracked own and competitor apps. Provide stable store IDs; at least one ID is required per app.

## `keywords` (type: `array`):

Keyword phrases to check.

## `countries` (type: `array`):

Two-letter storefront/market codes such as us, gb, or de.

## `stores` (type: `array`):

Storefronts to monitor.

## `onlyChanges` (type: `boolean`):

Return incidents only. Disable for one normalized snapshot row per tracked app and keyword market.

## `googlePlayLanguage` (type: `string`):

Language code used for Google Play public search, for example en or de.

## `maxResultsPerKeyword` (type: `integer`):

Maximum results checked per keyword market. Google Play public search currently limits reliable HTTP depth, so the MVP maximum is 30.

## `minimumRankChange` (type: `integer`):

Minimum movement for ordinary RANK\_GAIN/RANK\_DROP events. Result and Top 3/10 boundary events are never suppressed.

## Actor input object example

```json
{
  "apps": [
    {
      "name": "HabitKit",
      "role": "own",
      "appStoreId": "6448311069",
      "googlePlayId": "com.roehl.habitkit"
    }
  ],
  "keywords": [
    "habit tracker",
    "daily habits"
  ],
  "countries": [
    "us",
    "gb",
    "de"
  ],
  "stores": [
    "appstore",
    "googleplay"
  ],
  "onlyChanges": true,
  "googlePlayLanguage": "en",
  "maxResultsPerKeyword": 30,
  "minimumRankChange": 2
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "apps": [
        {
            "name": "HabitKit",
            "role": "own",
            "appStoreId": "6448311069",
            "googlePlayId": "com.roehl.habitkit"
        }
    ],
    "keywords": [
        "habit tracker",
        "daily habits"
    ],
    "countries": [
        "us",
        "gb",
        "de"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("herazur/cross-store-aso-keyword-rank-monitor").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "apps": [{
            "name": "HabitKit",
            "role": "own",
            "appStoreId": "6448311069",
            "googlePlayId": "com.roehl.habitkit",
        }],
    "keywords": [
        "habit tracker",
        "daily habits",
    ],
    "countries": [
        "us",
        "gb",
        "de",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("herazur/cross-store-aso-keyword-rank-monitor").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "apps": [
    {
      "name": "HabitKit",
      "role": "own",
      "appStoreId": "6448311069",
      "googlePlayId": "com.roehl.habitkit"
    }
  ],
  "keywords": [
    "habit tracker",
    "daily habits"
  ],
  "countries": [
    "us",
    "gb",
    "de"
  ]
}' |
apify call herazur/cross-store-aso-keyword-rank-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,herazur/cross-store-aso-keyword-rank-monitor"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/fZm9dVWFgPrKHWcUN/builds/SbNuPhxaY7VakumZ5/openapi.json
