Tutorial

How to Track Competitor SERP Ranking Changes with Diff Alerts

Monitor competitor ranking changes across target keywords. Build a diff-based tracker that alerts when competitors gain or lose SERP positions.

Knowing when a competitor gains or loses rankings is actionable intelligence for SEO teams, content strategists, and product marketers. Most rank tracking tools batch-process once a week and bury competitor data in dashboards. This tutorial builds a lightweight tracker that queries your target keywords through the Scavio API, records which competitors appear and at what position, diffs against the previous snapshot, and prints alerts when a competitor enters the top 10, drops out, or moves more than three positions. The entire state fits in a single JSON file, and the script runs in under a minute for 20 keywords.

Prerequisites

  • Python 3.8 or higher installed
  • requests library installed
  • A Scavio API key from scavio.dev
  • A list of target keywords and competitor domains

Walkthrough

Step 1: Define keywords and competitor domains

List the keywords you are tracking and the competitor domains you want to monitor. The script will flag any position changes for these domains specifically.

Python
COMPETITORS = ["competitor-a.com", "competitor-b.com", "competitor-c.com"]
KEYWORDS = [
    "best crm software 2026",
    "crm for small business",
    "free crm tool",
    "crm vs spreadsheet"
]

Step 2: Fetch SERP data and extract competitor positions

For each keyword, query the SERP and record the position of each competitor domain that appears in the top 10.

Python
import os
import requests

API_KEY = os.environ.get("SCAVIO_API_KEY", "your_scavio_api_key")
ENDPOINT = "https://api.scavio.dev/api/v1/search"

def get_competitor_positions(query: str, competitors: list[str]) -> dict:
    r = requests.post(
        ENDPOINT,
        headers={"x-api-key": API_KEY},
        json={"query": query, "country_code": "us"}
    )
    r.raise_for_status()
    positions = {}
    for item in r.json().get("organic_results", []):
        link = item.get("link", "")
        for comp in competitors:
            if comp in link:
                positions[comp] = item["position"]
    return positions

Step 3: Diff against previous snapshot and generate alerts

Compare current positions to the last saved snapshot. Alert on new entries, exits, and significant position changes.

Python
def diff_positions(keyword: str, current: dict, previous: dict) -> list[str]:
    alerts = []
    all_domains = set(list(current.keys()) + list(previous.keys()))
    for domain in all_domains:
        old_pos = previous.get(domain)
        new_pos = current.get(domain)
        if new_pos and not old_pos:
            alerts.append(f"[NEW] {domain} entered top 10 at #{new_pos} for '{keyword}'")
        elif old_pos and not new_pos:
            alerts.append(f"[GONE] {domain} dropped out of top 10 for '{keyword}' (was #{old_pos})")
        elif old_pos and new_pos and abs(old_pos - new_pos) >= 3:
            direction = "up" if new_pos < old_pos else "down"
            alerts.append(f"[MOVE] {domain} moved {direction} #{old_pos} -> #{new_pos} for '{keyword}'")
    return alerts

Step 4: Run the tracker and persist state

Execute the full tracking cycle, print alerts, and save the current state for the next run.

Python
import json
from datetime import date

STATE_FILE = "competitor_serp_state.json"

def load_state() -> dict:
    try:
        with open(STATE_FILE, "r") as f:
            return json.load(f)
    except FileNotFoundError:
        return {}

def run_tracker() -> None:
    state = load_state()
    new_state = {}
    all_alerts = []
    for kw in KEYWORDS:
        positions = get_competitor_positions(kw, COMPETITORS)
        previous = state.get(kw, {})
        alerts = diff_positions(kw, positions, previous)
        all_alerts.extend(alerts)
        new_state[kw] = positions
    for alert in all_alerts:
        print(alert)
    if not all_alerts:
        print("No significant changes detected.")
    with open(STATE_FILE, "w") as f:
        json.dump(new_state, f, indent=2)
    print(f"State saved. Checked {len(KEYWORDS)} keywords.")

run_tracker()

Python Example

Python
import os
import json
import requests
from datetime import date

API_KEY = os.environ.get("SCAVIO_API_KEY", "your_scavio_api_key")
ENDPOINT = "https://api.scavio.dev/api/v1/search"
STATE_FILE = "competitor_serp_state.json"

COMPETITORS = ["competitor-a.com", "competitor-b.com"]
KEYWORDS = ["best crm software 2026", "crm for small business", "free crm tool"]

def get_positions(query: str) -> dict:
    r = requests.post(
        ENDPOINT,
        headers={"x-api-key": API_KEY},
        json={"query": query, "country_code": "us"}
    )
    r.raise_for_status()
    positions = {}
    for item in r.json().get("organic_results", []):
        for comp in COMPETITORS:
            if comp in item.get("link", ""):
                positions[comp] = item["position"]
    return positions

def diff(keyword: str, current: dict, previous: dict) -> list[str]:
    alerts = []
    for domain in set(list(current) + list(previous)):
        old, new = previous.get(domain), current.get(domain)
        if new and not old:
            alerts.append(f"[NEW] {domain} entered #{new} for '{keyword}'")
        elif old and not new:
            alerts.append(f"[GONE] {domain} left top 10 for '{keyword}'")
        elif old and new and abs(old - new) >= 3:
            alerts.append(f"[MOVE] {domain} #{old}->{new} for '{keyword}'")
    return alerts

def track() -> None:
    try:
        with open(STATE_FILE) as f:
            state = json.load(f)
    except FileNotFoundError:
        state = {}
    new_state, alerts = {}, []
    for kw in KEYWORDS:
        pos = get_positions(kw)
        alerts.extend(diff(kw, pos, state.get(kw, {})))
        new_state[kw] = pos
    for a in alerts:
        print(a)
    with open(STATE_FILE, "w") as f:
        json.dump(new_state, f, indent=2)
    print(f"Tracked {len(KEYWORDS)} keywords, {len(alerts)} alerts")

if __name__ == "__main__":
    track()

JavaScript Example

JavaScript
const fs = require("fs");
const API_KEY = process.env.SCAVIO_API_KEY || "your_scavio_api_key";
const ENDPOINT = "https://api.scavio.dev/api/v1/search";
const STATE = "competitor_serp_state.json";
const COMPETITORS = ["competitor-a.com", "competitor-b.com"];
const KEYWORDS = ["best crm software 2026", "crm for small business"];

async function getPositions(query) {
  const res = await fetch(ENDPOINT, {
    method: "POST",
    headers: { "x-api-key": API_KEY, "Content-Type": "application/json" },
    body: JSON.stringify({ query, country_code: "us" })
  });
  const data = await res.json();
  const positions = {};
  for (const item of data.organic_results || []) {
    for (const comp of COMPETITORS) {
      if ((item.link || "").includes(comp)) positions[comp] = item.position;
    }
  }
  return positions;
}

async function track() {
  let state = {};
  try { state = JSON.parse(fs.readFileSync(STATE, "utf-8")); } catch {}
  const newState = {};
  for (const kw of KEYWORDS) {
    const pos = await getPositions(kw);
    const prev = state[kw] || {};
    for (const d of new Set([...Object.keys(pos), ...Object.keys(prev)])) {
      if (pos[d] && !prev[d]) console.log(`[NEW] ${d} entered #${pos[d]} for '${kw}'`);
      else if (prev[d] && !pos[d]) console.log(`[GONE] ${d} left top 10 for '${kw}'`);
      else if (prev[d] && pos[d] && Math.abs(prev[d] - pos[d]) >= 3)
        console.log(`[MOVE] ${d} #${prev[d]}->#${pos[d]} for '${kw}'`);
    }
    newState[kw] = pos;
  }
  fs.writeFileSync(STATE, JSON.stringify(newState, null, 2));
}

track().catch(console.error);

Expected Output

JSON
[NEW] competitor-b.com entered top 10 at #7 for 'best crm software 2026'
[MOVE] competitor-a.com moved up #8 -> #4 for 'crm for small business'
[GONE] competitor-c.com dropped out of top 10 for 'free crm tool'
State saved. Checked 4 keywords.

Related Tutorials

Frequently Asked Questions

Most developers complete this tutorial in 15 to 30 minutes. You will need a Scavio API key (free tier works) and a working Python or JavaScript environment.

Python 3.8 or higher installed. requests library installed. A Scavio API key from scavio.dev. A list of target keywords and competitor domains. A Scavio API key gives you 250 free credits per month.

Yes. The free tier includes 250 credits per month, which is more than enough to complete this tutorial and prototype a working solution.

Scavio has a native LangChain package (langchain-scavio), an MCP server, and a plain REST API that works with any HTTP client. This tutorial uses the raw REST API, but you can adapt to your framework of choice.

Start Building

Monitor competitor ranking changes across target keywords. Build a diff-based tracker that alerts when competitors gain or lose SERP positions.