# Rank Tracking Snapshot Comparator (`junipr/rank-tracking-snapshot-comparator`) Actor

Compare two or more SERP snapshots and report position changes, URL swaps, domain movements, feature gains/losses, and query-level volatility

- **URL**: https://apify.com/junipr/rank-tracking-snapshot-comparator.md
- **Developed by:** [junipr](https://apify.com/junipr) (community)
- **Categories:** SEO tools, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $11.50 / 1,000 rank snapshot compareds

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

## Rank Tracking Snapshot Comparator

Compare an earlier and later set of query-ranked URLs. The actor detects position movement, entrants, exits, URL swaps, and result-feature changes while preserving the source evidence behind every comparison.

### What you get

Each dataset row represents one query-domain pair and includes:

- baseline and current positions;
- signed `positionDelta`, where a positive number is an improvement;
- current or most recent URL;
- URL-change detection for the same query and domain;
- feature gained and feature lost flags;
- a bounded volatility score;
- movement type such as `entered`, `exited`, `url_swap`, `improved`, `major_improvement`, `declined`, `major_decline`, `feature_change`, or `unchanged`;
- source URL, evidence, issue code, scan time, and recommendation.

Owned domains are recognized in recommendations so an analyst can distinguish movements that directly affect controlled properties. With reports enabled, the actor writes Markdown, CSV, issue JSON, evidence JSON, run metadata, and a digest artifact to key-value storage.

### Required evidence

Supply both `baselineSerpResults` and `currentSerpResults`. Each array may contain individual records or snapshot objects with a `query` and nested `results`. Every usable record needs:

- `query`;
- positive numeric `position` or `rank`;
- public `url` or `resultUrl`;
- optional `domain`, `resultType`, and `sourceUrl`.

Invalid, missing, zero, or negative positions are ignored. If either side has no accepted records, the actor returns a diagnostic row rather than inferring movement.

Comparison controls:

- `ownedDomains`: domains highlighted as controlled properties.
- `compareFeatures`: compare organic versus explicitly typed result features.
- `rankDepth`: boundary used to score entrances and exits.
- `movementThresholds.minor` and `.major`: configure movement labels.
- `maxQueries`: cap distinct queries.
- `maxSnapshots`: cap snapshot containers read from each side.
- `maxResultsPerQuery`: cap result records accepted for each query and side.
- `maxComparisons`: cap emitted query-domain rows.
- `maxChargeUsd`: stop before a paid event would exceed the ceiling.

### Example input

```json
{
  "baselineSerpResults": [
    {
      "query": "seo tools",
      "sourceUrl": "https://example.com/serp/seo-tools/previous",
      "results": [
        { "position": 5, "url": "https://junipr.io/seo", "resultType": "organic" },
        { "position": 2, "url": "https://competitor.example/old-page", "resultType": "organic" }
      ]
    }
  ],
  "currentSerpResults": [
    {
      "query": "seo tools",
      "sourceUrl": "https://example.com/serp/seo-tools/current",
      "results": [
        { "position": 2, "url": "https://junipr.io/seo", "resultType": "organic" },
        { "position": 2, "url": "https://competitor.example/new-page", "resultType": "organic" },
        { "position": 4, "url": "https://new.example/list", "resultType": "featured_snippet" }
      ]
    }
  ],
  "ownedDomains": ["junipr.io"],
  "compareFeatures": true,
  "rankDepth": 10,
  "movementThresholds": { "minor": 2, "major": 5 },
  "maxQueries": 10,
  "maxSnapshots": 20,
  "maxResultsPerQuery": 20,
  "maxComparisons": 100,
  "includeReport": true,
  "maxChargeUsd": 35
}
```

### Example output

```json
{
  "recordType": "rank_tracking_snapshot_comparator",
  "recordName": "seo tools | junipr.io",
  "issueCode": "rank_tracking_snapshot_comparator_rank_change",
  "severity": "medium",
  "query": "seo tools",
  "domain": "junipr.io",
  "url": "https://junipr.io/seo",
  "baselinePosition": 5,
  "currentPosition": 2,
  "positionDelta": 3,
  "urlChanged": false,
  "featureGained": false,
  "featureLost": false,
  "volatilityScore": 30,
  "movementType": "improved",
  "sourceUrl": "https://example.com/serp/seo-tools/current",
  "scannedAt": "2026-07-10T12:00:00.000Z"
}
```

### Comparison semantics

Records are grouped by query and normalized domain. When multiple URLs from a domain appear for the same query, the best position on each side is compared. A different best URL becomes `url_swap`. A domain seen only in the current evidence becomes `entered`; one seen only in the baseline becomes `exited`. The configured rank depth supplies the comparison boundary for those one-sided records.

Feature comparison uses `resultType`. Organic-to-feature changes set `featureGained`; feature-to-organic changes set `featureLost`. Disable `compareFeatures` when capture providers do not use compatible result-type labels.

The volatility score is a deterministic aid, not a prediction. It combines absolute position movement with bounded additions for URL and feature changes.

### Pay-per-event pricing

Platform usage pass-through is off, and charges occur before related output.

| Event | Price |
| --- | ---: |
| `actor-start` | $0.0350 |
| `rank-snapshot-compared` | $0.0115 |
| `url-position-record-checked` | $0.0115 |
| `rank-change-detected` | $0.0225 |
| `digest-generated` | $0.0900 |
| `executive-report-generated` | $0.1800 |

The default maximum charge is $35. Set `maxChargeUsd` to zero to prevent paid processing and output.

### Practical uses

- compare weekly or monthly rank exports from the same provider;
- detect URL swaps after migrations or internal-link changes;
- identify newly visible and disappearing competitor domains;
- track result-feature ownership separately from numeric rank;
- produce a capped movement digest for SEO reporting.

### Limits

Use consistent query, locale, device, provider, and collection depth on both sides. The actor compares supplied observations and does not fetch historical rankings or reconstruct missing records. Query-domain grouping intentionally reports the best observed URL for each side.

# Actor input Schema

## `startUrls` (type: `array`):

Optional explicit public URLs; private and non-HTTP targets are rejected.

## `sitemapUrls` (type: `array`):

Optional permitted public sitemap URLs.

## `htmlInputs` (type: `array`):

Deterministic HTML objects with sourceId, sourceUrl, and query context.

## `allowedDomains` (type: `array`):

Optional hostname allowlist for network fetches.

## `maxPages` (type: `integer`):

Overall HTML target cap.

## `maxDepth` (type: `integer`):

Fixed at zero because discovered links are not crawled.

## `includeEvidence` (type: `boolean`):

Include source evidence excerpts in dataset rows.

## `includeRawSnapshots` (type: `boolean`):

Store capped raw HTML in key-value storage.

## `requestDelayMs` (type: `integer`):

Delay between permitted public fetches.

## `timeoutMs` (type: `integer`):

Per-request timeout in milliseconds.

## `userAgentMode` (type: `string`):

Standard or compatibility request header.

## `baselineSerpResults` (type: `array`):

Earlier query/result records or snapshot objects.

## `currentSerpResults` (type: `array`):

Later query/result records or snapshot objects.

## `ownedDomains` (type: `array`):

Optional owned domains retained for downstream segmentation.

## `compareFeatures` (type: `boolean`):

Compare organic versus feature result types.

## `rankDepth` (type: `integer`):

Rank boundary used to score entrants and exits.

## `movementThresholds` (type: `object`):

Position deltas for minor and major movement classifications.

## `maxQueries` (type: `integer`):

Maximum distinct query comparisons.

## `maxSnapshots` (type: `integer`):

Maximum snapshot containers read from each side.

## `maxResultsPerQuery` (type: `integer`):

Maximum records accepted per query and side.

## `maxComparisons` (type: `integer`):

Maximum query-domain rows emitted.

## `maxChargeUsd` (type: `number`):

Maximum accepted PPE spend before paid output stops.

## `includeReport` (type: `boolean`):

Write Markdown, CSV, JSON evidence, and run metadata.

## Actor input object example

```json
{
  "startUrls": [],
  "sitemapUrls": [],
  "htmlInputs": [],
  "allowedDomains": [],
  "maxPages": 1,
  "maxDepth": 0,
  "includeEvidence": true,
  "includeRawSnapshots": false,
  "requestDelayMs": 250,
  "timeoutMs": 15000,
  "userAgentMode": "standard",
  "baselineSerpResults": [],
  "currentSerpResults": [],
  "ownedDomains": [
    "junipr.io"
  ],
  "compareFeatures": true,
  "rankDepth": 10,
  "movementThresholds": {
    "minor": 2,
    "major": 5
  },
  "maxQueries": 1,
  "maxSnapshots": 2,
  "maxResultsPerQuery": 10,
  "maxComparisons": 50,
  "maxChargeUsd": 35,
  "includeReport": true
}
```

# Actor output Schema

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

No description

## `report` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("junipr/rank-tracking-snapshot-comparator").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("junipr/rank-tracking-snapshot-comparator").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 '{}' |
apify call junipr/rank-tracking-snapshot-comparator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,junipr/rank-tracking-snapshot-comparator"
        }
    }
}
```

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/5dUc3Th5Csh0HdOiw/builds/mQ8lD7aVNdIavY7dP/openapi.json
