Enterprise SEO tools charge $200-500/month for dashboards you cannot customize. A SERP API feeding your own dashboard gives you exactly the metrics you need at a fraction of the cost. This tutorial builds a Python rank tracking backend that checks keyword positions daily, stores history in SQLite, and exports CSV for any frontend. Tracking 200 keywords daily costs about $1/day on Scavio.
Prerequisites
- Python 3.8+ installed
- requests and sqlite3 (built-in)
- A Scavio API key from scavio.dev
- Keywords and your target domain
Walkthrough
Step 1: Set up the database and configuration
Create a SQLite database for daily rank snapshots. Define your target domain and keyword list.
import os, sqlite3, requests, json
from datetime import date
API_KEY = os.environ['SCAVIO_API_KEY']
H = {'x-api-key': API_KEY, 'Content-Type': 'application/json'}
DOMAIN = 'mysite.com'
KEYWORDS = ['serp api python', 'web scraping api', 'tiktok data api', 'seo rank tracking api']
def init_db(path='seo_dashboard.db'):
conn = sqlite3.connect(path)
conn.execute('''CREATE TABLE IF NOT EXISTS rankings (
id INTEGER PRIMARY KEY, date TEXT, keyword TEXT,
position INTEGER, url TEXT, title TEXT)''')
conn.commit()
return conn
db = init_db()Step 2: Fetch rankings for each keyword
Query Google for each keyword and find your domain's position in the organic results.
def check_keyword(keyword):
resp = requests.post('https://api.scavio.dev/api/v1/search',
headers=H, json={'query': keyword, 'country_code': 'us'})
for r in resp.json().get('organic_results', []):
if DOMAIN in r.get('link', ''):
return {'position': r['position'], 'url': r['link'], 'title': r.get('title', '')}
return {'position': None, 'url': '', 'title': ''}
def run_daily_check():
today = date.today().isoformat()
for kw in KEYWORDS:
r = check_keyword(kw)
db.execute('INSERT INTO rankings (date,keyword,position,url,title) VALUES (?,?,?,?,?)',
(today, kw, r['position'], r['url'], r['title']))
status = f"#{r['position']}" if r['position'] else 'NOT FOUND'
print(f' {kw}: {status}')
db.commit()
print(f'Cost: ${len(KEYWORDS) * 0.005:.3f}')
run_daily_check()Step 3: Query history and detect rank changes
Pull rank history from SQLite to identify trends, drops, and improvements.
def get_movers(days=7):
cursor = db.execute('''
WITH recent AS (
SELECT keyword, position, date,
ROW_NUMBER() OVER (PARTITION BY keyword ORDER BY date DESC) as rn
FROM rankings WHERE position IS NOT NULL
)
SELECT a.keyword, a.position as current_pos, b.position as prev_pos,
b.position - a.position as change
FROM recent a JOIN recent b ON a.keyword = b.keyword
WHERE a.rn = 1 AND b.rn = ? AND a.position != b.position
ORDER BY change DESC
''', (min(days, 30),))
for kw, curr, prev, change in cursor.fetchall():
d = 'UP' if change > 0 else 'DOWN'
print(f' [{d}] {kw}: #{prev} -> #{curr} ({change:+d})')
def export_csv(output='dashboard.csv'):
import csv
rows = db.execute('SELECT date, keyword, position, url FROM rankings ORDER BY date DESC, keyword').fetchall()
with open(output, 'w', newline='') as f:
w = csv.writer(f)
w.writerow(['date', 'keyword', 'position', 'url'])
w.writerows(rows)
print(f'Exported {len(rows)} rows to {output}')
get_movers(7)
export_csv()Step 4: Schedule daily checks
Add a cron job to run the checker daily with duplicate prevention.
def already_checked_today():
today = date.today().isoformat()
return db.execute('SELECT COUNT(*) FROM rankings WHERE date = ?', (today,)).fetchone()[0] > 0
if __name__ == '__main__':
db = init_db()
if already_checked_today():
print('Already checked today.')
else:
print(f'{date.today()}: Running daily check...')
run_daily_check()
get_movers(7)
# crontab -e
# 0 6 * * * cd /path/to && python seo_dashboard.py >> dashboard.log 2>&1Python Example
import os, sqlite3, requests
from datetime import date
API_KEY = os.environ['SCAVIO_API_KEY']
H = {'x-api-key': API_KEY, 'Content-Type': 'application/json'}
DOMAIN = 'mysite.com'
KEYWORDS = ['serp api python', 'web scraping api']
db = sqlite3.connect('seo.db')
db.execute('CREATE TABLE IF NOT EXISTS ranks (date TEXT, keyword TEXT, position INT)')
for kw in KEYWORDS:
data = requests.post('https://api.scavio.dev/api/v1/search',
headers=H, json={'query': kw, 'country_code': 'us'}).json()
pos = next((r['position'] for r in data.get('organic_results', []) if DOMAIN in r.get('link', '')), None)
db.execute('INSERT INTO ranks VALUES (?,?,?)', (date.today().isoformat(), kw, pos))
print(f'{kw}: #{pos}' if pos else f'{kw}: not found')
db.commit()JavaScript Example
const Database = require('better-sqlite3');
const API_KEY = process.env.SCAVIO_API_KEY;
const H = { 'x-api-key': API_KEY, 'Content-Type': 'application/json' };
const DOMAIN = 'mysite.com';
const KEYWORDS = ['serp api python', 'web scraping api'];
const db = new Database('seo.db');
db.exec('CREATE TABLE IF NOT EXISTS ranks (date TEXT, keyword TEXT, position INT)');
async function run() {
const today = new Date().toISOString().slice(0, 10);
const insert = db.prepare('INSERT INTO ranks VALUES (?, ?, ?)');
for (const kw of KEYWORDS) {
const data = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST', headers: H, body: JSON.stringify({ query: kw, country_code: 'us' })
}).then(r => r.json());
const match = (data.organic_results || []).find(r => r.link.includes(DOMAIN));
insert.run(today, kw, match ? match.position : null);
console.log(match ? `${kw}: #${match.position}` : `${kw}: not found`);
}
}
run().catch(console.error);Expected Output
2026-05-18: Running daily check...
serp api python: #4
web scraping api: #7
tiktok data api: #2
seo rank tracking api: #11
Cost: $0.020
[UP] tiktok data api: #5 -> #2 (+3)
[DOWN] seo rank tracking api: #8 -> #11 (-3)
Exported 28 rows to dashboard.csv