# ASO Rank Tracker API — App Store & Google Play Change Alerts (`kaz_kakyo/aso-rank-delta`) Actor

Monitor Apple App Store and Google Play keyword rankings via API, MCP, or schedule — emits only material rank changes, with confirmation runs to suppress rank jitter. Per-country checks, persistent baselines, uncharged change alerts. $1 per 1,000 rank checks. No browser, no API key.

- **URL**: https://apify.com/kaz\_kakyo/aso-rank-delta.md
- **Developed by:** [Heim AI](https://apify.com/kaz_kakyo) (community)
- **Categories:** SEO tools, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 rank 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/platform/actors/running/actors-in-store#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

## ASO Rank Tracker API — App Store & Google Play Change Alerts

Keyword rank monitoring that emits **only material changes**. Give it a list of `(keyword, appId, store, country)` checks and a `monitorId`; each run fetches App Store / Play search rankings, compares against a persistent per-monitor snapshot, and pushes dataset rows only when an app moves, enters, or drops from results (plus a one-time baseline and a run summary). Schedule it daily; attach a webhook on `SUCCEEDED` and consume only `type:"change"` rows.

### Quick start

**Console:** paste the prefill `checks` (Calm on Google Play + Apple for "meditation") and run. First run → 2 `baseline` rows + 1 `summary`.

**CLI:**

```bash
apify call kaz_kakyo/aso-rank-delta -i '{
  "checks": [
    {"keyword":"meditation","appId":"com.calm.android","store":"google","country":"us"},
    {"keyword":"meditation","appId":"571800810","store":"apple","country":"us"}
  ],
  "monitorId": "default"
}'
```

**REST:**

```bash
curl -X POST "https://api.apify.com/v2/acts/kaz_kakyo~aso-rank-delta/runs?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"checks":[{"keyword":"meditation","appId":"com.calm.android","store":"google","country":"us"}],"monitorId":"prod"}'
```

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `checks` | array | required | `{keyword, appId, store, country}`. store ∈ `apple`|`google`. Apple `appId` = trackId digits or bundleId. Max 500 unique / run. |
| `monitorId` | string | `default` | Snapshot namespace; monitors do not interact. |
| `materialityThreshold` | int 1–100 | `3` | Min |Δrank| vs last **reported** rank. Enter/exit always material. |
| `confirmationRuns` | 1|2 | `1` | `2` = hold until same direction on a second run. |
| `appleSearchDepth` | int 10–200 | `100` | iTunes Search `limit` (may serve fewer). |
| `emitBaselines` | bool | `true` | First observation emits `baseline`. |
| `proxyConfiguration` | proxy | Apify DC | Datacenter only; local runs work without it. |

### Output (uncharged rows)

**`type:"change"`** — material rank move after confirmation rules:

```json
{
  "type": "change",
  "monitorId": "default",
  "store": "google",
  "country": "us",
  "keyword": "meditation",
  "appId": "com.calm.android",
  "appName": "Calm",
  "prevRank": 10,
  "newRank": 6,
  "delta": -4,
  "direction": "up",
  "materialityThreshold": 3,
  "confirmationRuns": 1,
  "confirmed": true,
  "firstSeenAt": null,
  "searchDepth": 23,
  "servedResults": 23,
  "observedAt": "2026-08-01T00:00:00.000Z",
  "prevReportedAt": "2026-07-31T00:00:00.000Z",
  "runId": "..."
}
```

`direction` ∈ `up` | `down` | `entered` | `dropped`. `delta` is null for entered/dropped.

**`type:"baseline"`** — first observation of a check:

```json
{
  "type": "baseline",
  "monitorId": "default",
  "store": "apple",
  "country": "us",
  "keyword": "meditation",
  "appId": "571800810",
  "appName": "Calm",
  "rank": 2,
  "searchDepth": 100,
  "servedResults": 94,
  "observedAt": "2026-08-01T00:00:00.000Z",
  "runId": "..."
}
```

**`type:"summary"`** — exactly one, last row of every run (`checksObserved`, `changes`, `baselines`, `pendingHolds`, `errors`, …).

**`type:"error"`** — uncharged; reasons: `invalid-input`, `fetch-failed`, `charge-limit`, `cap-exceeded`, `snapshot-reset`, `snapshot-entry-dropped`, `no-valid-checks`, `monitor-busy`, `timeout-budget`, `resume-stranded` (rare: a check interrupted mid-charge whose durable state predates this build, so it cannot be replayed — never re-run and never charged again). Invalid input never fails the run. Identity fields on error rows are included only when they are primitive strings (truncated to 120 chars).

### How change detection works

Each check has a **committed** (last reported) rank in the named KV store `aso-rank-delta-state`. Observations are compared to that committed rank, not to the previous raw observation — slow drifts accumulate until they cross `materialityThreshold`.

- No entry yet → baseline (optional) + commit the observation.
- Material and `confirmationRuns=1` → emit `change`, commit.
- Material and `confirmationRuns=2` → first run stores a pending hold (no row); second run with the same direction confirms and emits `change` with `firstSeenAt`. Direction flip restarts the hold; jitter back under threshold clears it.

### Scheduling + webhook

1. Create a task with your `checks` + `monitorId`.
2. Schedule cron (e.g. `0 6 * * *`).
3. Webhook on `ACTOR.RUN.SUCCEEDED` → fetch dataset items filtered to `type=change` (or filter client-side).
4. MCP / agent callers: same input schema; read only change rows for alerts.

Example task body:

```json
{
  "actId": "kaz_kakyo/aso-rank-delta",
  "input": {
    "checks": [{"keyword":"meditation","appId":"com.calm.android","store":"google","country":"us"}],
    "monitorId": "calm-us",
    "materialityThreshold": 3,
    "confirmationRuns": 2
  }
}
```

### Honest limits

- **Apple**: iTunes Search API ordering is a proxy for App Store search, depth ≤200, and the API may return fewer than requested. Not device-personalized. A parsed response with `resultCount: 0` is a legitimate billable not-found.
- **Google Play**: the real web SERP serves only ~20–30 apps; there is **no deeper pagination** (legacy batchexecute RPC is dead). Rank is null beyond what is served. `hl` is fixed to `en`; `gl` (country) drives ranking. A response is billable **only** when the `ds:4` blob parses and yields ≥1 app entry — a missing/unparseable `ds:4` or a zero-app parse is treated as `fetch-failed` (uncharged), because an empty Play SERP is indistinguishable from a layout change or consent page. Rare keywords with a genuinely empty Play SERP therefore come back as `fetch-failed` error rows, not billed not-found.
- **Concurrent runs** of the same `monitorId`: the second run exits `SUCCEEDED` with a single uncharged `monitor-busy` error row and performs no checks/charges.
- **Platform migration mid-run**: a check whose charge or delivery was interrupted is written ahead (state + output row) before it is charged, so the resumed run replays it — re-delivering the row only when the dataset cannot prove it already landed, and never charging for it again. In the rare unprovable case (dataset unreadable) a row may appear twice; it is never charged twice and never dropped.
- Ranks can jitter day-to-day — use `confirmationRuns: 2` for alerts.
- Not personalized / not identical to every device locale.

### Pricing

Pay-per-event: **$0.005** per run start + **$0.001** per successfully observed check (found or definite not-found). Change, baseline, summary, and error rows are free.

Worked example: 10 keywords × 2 stores = 20 checks/day → `$0.005 + 20×$0.001 = $0.025/day` = **$0.75/month**.

# Actor input Schema

## `checks` (type: `array`):

Required. Array of {keyword, appId, store, country}. store is "apple" or "google". country is ISO alpha-2 (e.g. us). For apple, appId may be numeric trackId or bundleId; for google, package id (com.example.app). Max 500 unique checks per run. Invalid items become type:error rows; the run still succeeds.

## `monitorId` (type: `string`):

Names the persistent snapshot in the aso-rank-delta-state key-value store. Separate monitorIds do not interact. Default: default.

## `materialityThreshold` (type: `integer`):

Minimum absolute rank change vs the last reported (committed) rank before a change row is emitted. Enter/exit (null↔rank) are always material. Default: 3.

## `confirmationRuns` (type: `integer`):

1 = emit change on first material observation. 2 = require the same direction on a second run before emitting (suppresses rank jitter). Default: 1.

## `appleSearchDepth` (type: `integer`):

iTunes Search API limit (10–200). Apple may return fewer results than requested. Default: 100.

## `emitBaselines` (type: `boolean`):

When true (default), first observation of a check emits a type:baseline row. Set false to seed state silently.

## `proxyConfiguration` (type: `object`):

Apify Proxy (datacenter). Used for Google Play fetches and Apple retries. No residential group.

## Actor input object example

```json
{
  "checks": [
    {
      "keyword": "meditation",
      "appId": "com.calm.android",
      "store": "google",
      "country": "us"
    },
    {
      "keyword": "meditation",
      "appId": "571800810",
      "store": "apple",
      "country": "us"
    }
  ],
  "monitorId": "default",
  "materialityThreshold": 3,
  "confirmationRuns": 1,
  "appleSearchDepth": 100,
  "emitBaselines": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `overview` (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 = {
    "checks": [
        {
            "keyword": "meditation",
            "appId": "com.calm.android",
            "store": "google",
            "country": "us"
        },
        {
            "keyword": "meditation",
            "appId": "571800810",
            "store": "apple",
            "country": "us"
        }
    ],
    "monitorId": "default"
};

// Run the Actor and wait for it to finish
const run = await client.actor("kaz_kakyo/aso-rank-delta").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 = {
    "checks": [
        {
            "keyword": "meditation",
            "appId": "com.calm.android",
            "store": "google",
            "country": "us",
        },
        {
            "keyword": "meditation",
            "appId": "571800810",
            "store": "apple",
            "country": "us",
        },
    ],
    "monitorId": "default",
}

# Run the Actor and wait for it to finish
run = client.actor("kaz_kakyo/aso-rank-delta").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "checks": [
    {
      "keyword": "meditation",
      "appId": "com.calm.android",
      "store": "google",
      "country": "us"
    },
    {
      "keyword": "meditation",
      "appId": "571800810",
      "store": "apple",
      "country": "us"
    }
  ],
  "monitorId": "default"
}' |
apify call kaz_kakyo/aso-rank-delta --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=kaz_kakyo/aso-rank-delta",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

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