You can monitor competitor SERP positions by searching target keywords daily via API, extracting the domain position from organic results, storing it in SQLite, and alerting when a competitor's rank changes by more than a threshold.
Prerequisites
- Python 3.9+
- Scavio API key
- SQLite (stdlib)
- cron or a scheduler
Walkthrough
Step 1: Set up the tracking database
Create a SQLite table to store daily position snapshots.
import sqlite3
def init_db(path: str = "serp_tracker.db") -> sqlite3.Connection:
conn = sqlite3.connect(path)
conn.execute("""
CREATE TABLE IF NOT EXISTS positions (
date TEXT,
keyword TEXT,
domain TEXT,
position INTEGER,
PRIMARY KEY (date, keyword, domain)
)
""")
conn.commit()
return connStep 2: Extract domain positions from search results
Search the keyword and find each tracked domain in the organic results.
import requests
from datetime import date
API_KEY = "your-scavio-api-key"
def get_positions(keyword: str, domains: list[str]) -> dict:
r = requests.post(
"https://api.scavio.dev/api/v1/search",
json={"query": keyword, "num_results": 20},
headers={"x-api-key": API_KEY},
timeout=15
)
r.raise_for_status()
results = r.json().get("organic_results", [])
positions = {}
for domain in domains:
pos = next(
(i + 1 for i, res in enumerate(results) if domain in res.get("link", "")),
None
)
positions[domain] = pos
return positionsStep 3: Store and compare positions
Save today's positions and compare with yesterday's to detect changes.
def store_positions(conn, keyword: str, positions: dict):
today = str(date.today())
for domain, pos in positions.items():
conn.execute(
"INSERT OR REPLACE INTO positions VALUES (?, ?, ?, ?)",
(today, keyword, domain, pos)
)
conn.commit()
def get_yesterday_positions(conn, keyword: str, domains: list) -> dict:
rows = conn.execute(
"SELECT domain, position FROM positions WHERE keyword=? ORDER BY date DESC LIMIT ?",
(keyword, len(domains) * 2)
).fetchall()
# Get second-to-last date for comparison
dates = conn.execute(
"SELECT DISTINCT date FROM positions WHERE keyword=? ORDER BY date DESC LIMIT 2",
(keyword,)
).fetchall()
if len(dates) < 2:
return {}
prev_date = dates[1][0]
rows = conn.execute(
"SELECT domain, position FROM positions WHERE keyword=? AND date=?",
(keyword, prev_date)
).fetchall()
return dict(rows)Step 4: Alert on rank changes
Print (or send to Slack/email) when a competitor gains or loses 3+ positions.
def check_alerts(keyword: str, today_pos: dict, yesterday_pos: dict, threshold: int = 3):
alerts = []
for domain, pos in today_pos.items():
prev = yesterday_pos.get(domain)
if prev is None or pos is None:
continue
change = prev - pos # positive = rank improved (moved up)
if abs(change) >= threshold:
direction = "up" if change > 0 else "down"
alerts.append(f"{domain} moved {direction} {abs(change)} positions for '{keyword}': {prev} -> {pos}")
return alerts
# Example run
conn = init_db()
KEYWORDS = ["project management software", "task tracker online"]
DOMAINS = ["asana.com", "monday.com", "notion.so", "clickup.com"]
for kw in KEYWORDS:
positions = get_positions(kw, DOMAINS)
yesterday = get_yesterday_positions(conn, kw, DOMAINS)
store_positions(conn, kw, positions)
alerts = check_alerts(kw, positions, yesterday)
for alert in alerts:
print("ALERT:", alert)Python Example
import requests
import sqlite3
from datetime import date
API_KEY = "your-scavio-api-key"
KEYWORDS = ["project management software", "task tracker for teams", "kanban board online"]
DOMAINS = ["asana.com", "monday.com", "notion.so", "clickup.com", "linear.app"]
def init_db(path="serp_tracker.db"):
conn = sqlite3.connect(path)
conn.execute("CREATE TABLE IF NOT EXISTS positions (date TEXT, keyword TEXT, domain TEXT, position INTEGER, PRIMARY KEY (date, keyword, domain))")
conn.commit()
return conn
def get_positions(keyword, domains):
r = requests.post("https://api.scavio.dev/api/v1/search",
json={"query": keyword, "num_results": 20},
headers={"x-api-key": API_KEY}, timeout=15)
r.raise_for_status()
results = r.json().get("organic_results", [])
return {d: next((i+1 for i, res in enumerate(results) if d in res.get("link","")), None) for d in domains}
def store(conn, keyword, positions):
today = str(date.today())
for domain, pos in positions.items():
conn.execute("INSERT OR REPLACE INTO positions VALUES (?,?,?,?)", (today, keyword, domain, pos))
conn.commit()
def prev_positions(conn, keyword):
dates = conn.execute("SELECT DISTINCT date FROM positions WHERE keyword=? ORDER BY date DESC LIMIT 2", (keyword,)).fetchall()
if len(dates) < 2:
return {}
rows = conn.execute("SELECT domain, position FROM positions WHERE keyword=? AND date=?", (keyword, dates[1][0])).fetchall()
return dict(rows)
def run_tracker():
conn = init_db()
for kw in KEYWORDS:
today_pos = get_positions(kw, DOMAINS)
yesterday_pos = prev_positions(conn, kw)
store(conn, kw, today_pos)
print(f"\nKeyword: {kw}")
for domain in DOMAINS:
pos = today_pos.get(domain)
prev = yesterday_pos.get(domain)
change = f" (was {prev})" if prev and prev != pos else ""
print(f" {domain}: {pos or 'not ranking'}{change}")
conn.close()
if __name__ == "__main__":
run_tracker()JavaScript Example
const API_KEY = 'your-scavio-api-key';
const KEYWORDS = ['project management software', 'task tracker for teams'];
const DOMAINS = ['asana.com', 'monday.com', 'notion.so', 'clickup.com'];
async function getPositions(keyword, domains) {
const res = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': API_KEY },
body: JSON.stringify({ query: keyword, num_results: 20 })
});
const data = await res.json();
const results = data.organic_results ?? [];
return Object.fromEntries(domains.map(d => [
d,
(results.findIndex(r => r.link?.includes(d)) + 1) || null
]));
}
for (const kw of KEYWORDS) {
const positions = await getPositions(kw, DOMAINS);
console.log(`\nKeyword: ${kw}`);
for (const [domain, pos] of Object.entries(positions)) {
console.log(` ${domain}: ${pos ?? 'not ranking'}`);
}
}Expected Output
Keyword: project management software
asana.com: 2
monday.com: 4 (was 6)
notion.so: 7
clickup.com: 11
linear.app: not ranking
ALERT: monday.com moved up 2 positions for 'project management software': 6 -> 4