Rank tracking tools charge $50-300/month and update once per day at best. A custom competitive SERP monitor checks exactly the keywords you care about, at your own frequency, and alerts on the changes that matter to your business. This tutorial builds a monitor that tracks multiple competitors across keyword sets at $0.005/check, with Slack or email alerting on position swings.
Prerequisites
- Python 3.8+
- requests library
- A Scavio API key from scavio.dev
- Competitor domains and target keywords
Walkthrough
Step 1: Define competitors and keywords
Set up the monitoring configuration.
import os, requests, json, sqlite3
from datetime import datetime
API_KEY = os.environ['SCAVIO_API_KEY']
SH = {'x-api-key': API_KEY, 'Content-Type': 'application/json'}
CONFIG = {
'competitors': ['serpapi.com', 'dataforseo.com', 'serper.dev', 'exa.ai', 'tavily.com'],
'keywords': [
'serp api', 'search api for developers', 'google search api python',
'web scraping api 2026', 'ai agent search api'
]
}
db = sqlite3.connect('serp_monitor.db')
db.execute('''CREATE TABLE IF NOT EXISTS rankings (
keyword TEXT, domain TEXT, position INTEGER,
checked_at TEXT, title TEXT
)''')
db.commit()
print(f'Monitoring {len(CONFIG["competitors"])} competitors across {len(CONFIG["keywords"])} keywords')
print(f'Daily cost: ${len(CONFIG["keywords"]) * 0.005:.3f}')Step 2: Check rankings for all keywords
Run a SERP check for each keyword and record competitor positions.
def check_rankings(keywords, competitors):
results = []
for kw in keywords:
data = requests.post('https://api.scavio.dev/api/v1/search',
headers=SH, json={'query': kw, 'country_code': 'us'}).json()
organic = data.get('organic_results', [])
now = datetime.now().isoformat()
for r in organic[:20]:
domain = r.get('link', '').split('/')[2] if r.get('link') else ''
domain = domain.replace('www.', '')
if domain in competitors:
pos = r.get('position', 99)
db.execute('INSERT INTO rankings VALUES (?,?,?,?,?)',
(kw, domain, pos, now, r.get('title', '')[:80]))
results.append({'keyword': kw, 'domain': domain, 'position': pos})
db.commit()
print(f'Checked {len(keywords)} keywords. Cost: ${len(keywords) * 0.005:.3f}')
return results
ranks = check_rankings(CONFIG['keywords'], CONFIG['competitors'])
for r in ranks[:10]:
print(f' {r["keyword"]:30} | {r["domain"]:20} | #{r["position"]}')Step 3: Detect position changes
Compare current check against previous to find movers.
def detect_changes(keyword, domain, threshold=3):
rows = db.execute(
'SELECT position, checked_at FROM rankings WHERE keyword=? AND domain=? ORDER BY checked_at DESC LIMIT 2',
(keyword, domain)).fetchall()
if len(rows) < 2: return None
current, previous = rows[0][0], rows[1][0]
change = previous - current # positive = improved
if abs(change) >= threshold:
return {'keyword': keyword, 'domain': domain,
'previous': previous, 'current': current, 'change': change}
return None
def alert_changes(keywords, competitors, threshold=3):
alerts = []
for kw in keywords:
for comp in competitors:
change = detect_changes(kw, comp, threshold)
if change: alerts.append(change)
if alerts:
print(f'\n{len(alerts)} position changes detected:')
for a in alerts:
direction = 'UP' if a['change'] > 0 else 'DOWN'
print(f' {a["domain"]:20} | {a["keyword"]:25} | #{a["previous"]} -> #{a["current"]} ({direction} {abs(a["change"])})')
else:
print('No significant position changes.')
return alerts
alert_changes(CONFIG['keywords'], CONFIG['competitors'])Step 4: Generate competitive dashboard data
Build a summary view of the competitive landscape.
def dashboard(competitors, keywords):
print(f'\n{"Keyword":30} | ', end='')
for c in competitors[:5]: print(f'{c[:12]:12} | ', end='')
print()
print('-' * (32 + 15 * len(competitors[:5])))
for kw in keywords:
print(f'{kw[:30]:30} | ', end='')
for comp in competitors[:5]:
row = db.execute(
'SELECT position FROM rankings WHERE keyword=? AND domain=? ORDER BY checked_at DESC LIMIT 1',
(kw, comp)).fetchone()
pos = f'#{row[0]}' if row else '-'
print(f'{pos:12} | ', end='')
print()
total_checks = len(keywords)
print(f'\nMonthly cost estimate: ${total_checks * 0.005 * 30:.2f} (daily) or ${total_checks * 0.005 * 7:.2f} (weekly)')
# Run a check first, then display
check_rankings(CONFIG['keywords'], CONFIG['competitors'])
dashboard(CONFIG['competitors'], CONFIG['keywords'])Python Example
import os, requests
SH = {'x-api-key': os.environ['SCAVIO_API_KEY'], 'Content-Type': 'application/json'}
def check(keyword, competitors):
data = requests.post('https://api.scavio.dev/api/v1/search',
headers=SH, json={'query': keyword, 'country_code': 'us'}).json()
for r in data.get('organic_results', [])[:20]:
domain = r.get('link', '').split('/')[2].replace('www.', '')
if domain in competitors:
print(f'{keyword}: {domain} at #{r["position"]}')
check('serp api', ['serpapi.com', 'dataforseo.com', 'serper.dev'])JavaScript Example
const SH = { 'x-api-key': process.env.SCAVIO_API_KEY, 'Content-Type': 'application/json' };
async function check(keyword, competitors) {
const data = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST', headers: SH,
body: JSON.stringify({ query: keyword, country_code: 'us' })
}).then(r => r.json());
for (const r of (data.organic_results || []).slice(0, 20)) {
const domain = new URL(r.link).hostname.replace('www.', '');
if (competitors.includes(domain))
console.log(`${keyword}: ${domain} at #${r.position}`);
}
}
check('serp api', ['serpapi.com', 'dataforseo.com', 'serper.dev']).catch(console.error);Expected Output
Monitoring 5 competitors across 5 keywords
Daily cost: $0.025
Checked 5 keywords. Cost: $0.025
Keyword | serpapi.com | dataforseo.c | serper.dev | exa.ai | tavily.com |
-----------------------------------------------------------------------------------------------
serp api | #3 | #7 | #5 | #12 | #15 |
search api for developers | #4 | #8 | #6 | #9 | - |
google search api python | #2 | #5 | #4 | - | - |
web scraping api 2026 | #6 | #3 | #8 | - | - |
ai agent search api | #5 | - | #7 | #4 | #11 |
Monthly cost estimate: $3.75 (daily) or $0.88 (weekly)