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.
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.
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 positionsStep 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.
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 alertsStep 4: Run the tracker and persist state
Execute the full tracking cycle, print alerts, and save the current state for the next run.
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
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
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
[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.