Keyword gap analysis shows which queries competitors rank for that you do not. SEO tools charge $100+/month for this feature. This tutorial builds the same analysis using raw SERP data at $0.005/keyword. Feed in your domain and competitor domains, and get a prioritized list of content opportunities.
Prerequisites
- Python 3.8+
- requests library
- A Scavio API key from scavio.dev
- Your domain and 2-3 competitor domains
Walkthrough
Step 1: Generate keyword candidates from competitor SERPs
Find keywords by checking what competitors rank for in your niche.
import os, requests, json
from collections import defaultdict
API_KEY = os.environ['SCAVIO_API_KEY']
SH = {'x-api-key': API_KEY, 'Content-Type': 'application/json'}
MY_DOMAIN = 'yourdomain.com'
COMPETITORS = ['competitor1.com', 'competitor2.com']
SEED_KEYWORDS = ['search api', 'serp api', 'web search api', 'mcp search', 'ai agent search']
def check_rankings(keyword, domains):
data = requests.post('https://api.scavio.dev/api/v1/search',
headers=SH, json={'query': keyword, 'country_code': 'us'}).json()
organic = data.get('organic_results', [])
rankings = {}
for i, r in enumerate(organic):
link = r.get('link', '')
for domain in domains:
if domain in link and domain not in rankings:
rankings[domain] = i + 1
return rankings
results = []
for kw in SEED_KEYWORDS:
all_domains = [MY_DOMAIN] + COMPETITORS
rankings = check_rankings(kw, all_domains)
results.append({'keyword': kw, 'rankings': rankings})
my_pos = rankings.get(MY_DOMAIN, '-')
comp_pos = {c: rankings.get(c, '-') for c in COMPETITORS}
print(f' {kw:30} | You: {str(my_pos):5} | {", ".join(f"{c.split(".")[0]}: {v}" for c, v in comp_pos.items())}')
print(f'\nCost: ${len(SEED_KEYWORDS) * 0.005:.3f}')Step 2: Identify keyword gaps
Find keywords where competitors rank but you do not.
def find_gaps(results, my_domain, competitors):
gaps = []
for r in results:
kw = r['keyword']
rankings = r['rankings']
my_rank = rankings.get(my_domain)
comp_ranks = {c: rankings.get(c) for c in competitors if rankings.get(c)}
if not my_rank and comp_ranks:
best_comp = min(comp_ranks.items(), key=lambda x: x[1])
gaps.append({
'keyword': kw,
'best_competitor': best_comp[0],
'competitor_position': best_comp[1],
'num_competitors_ranking': len(comp_ranks),
'difficulty': 'easy' if best_comp[1] > 5 else 'medium' if best_comp[1] > 2 else 'hard'
})
gaps.sort(key=lambda g: g['competitor_position'])
print(f'\n=== Keyword Gaps ===')
print(f' You are missing from {len(gaps)} keywords where competitors rank:')
for g in gaps:
print(f' [{g["difficulty"]:6}] "{g["keyword"]}" - {g["best_competitor"]} at #{g["competitor_position"]}')
return gaps
gaps = find_gaps(results, MY_DOMAIN, COMPETITORS)Step 3: Expand gaps with related keywords
Use People Also Ask data to find more gap opportunities.
def expand_gaps(gaps):
expanded = []
for gap in gaps[:5]: # Expand top 5 gaps
kw = gap['keyword']
data = requests.post('https://api.scavio.dev/api/v1/search',
headers=SH, json={'query': kw, 'country_code': 'us'}).json()
paa = data.get('people_also_ask', [])
related = [q.get('question', '') for q in paa if q.get('question')]
if related:
expanded.append({'seed': kw, 'related': related[:4]})
print(f'\n=== Expanded Gap Opportunities ===')
total_new = 0
for e in expanded:
print(f'\n "{e["seed"]}" -> {len(e["related"])} related keywords:')
for r in e['related']:
print(f' - {r[:55]}')
total_new += 1
print(f'\n New keyword opportunities: {total_new}')
print(f' Expansion cost: ${len(expanded) * 0.005:.3f}')
print(f' Total cost: ${(len(SEED_KEYWORDS) + len(expanded)) * 0.005:.3f}')
return expanded
expand_gaps(gaps)Python Example
import os, requests
SH = {'x-api-key': os.environ['SCAVIO_API_KEY'], 'Content-Type': 'application/json'}
def gap_check(keyword, my_domain, competitors):
data = requests.post('https://api.scavio.dev/api/v1/search',
headers=SH, json={'query': keyword, 'country_code': 'us'}).json()
organic = data.get('organic_results', [])
my_rank = next((i+1 for i, r in enumerate(organic) if my_domain in r.get('link', '')), None)
comp_ranks = {c: next((i+1 for i, r in enumerate(organic) if c in r.get('link', '')), None) for c in competitors}
is_gap = not my_rank and any(comp_ranks.values())
print(f'{"GAP" if is_gap else "OK":3} | {keyword:30} | You: {my_rank or "-"} | Comps: {comp_ranks}')
gap_check('search api python', 'scavio.dev', ['tavily.com'])
print('Cost: $0.005')JavaScript Example
const SH = { 'x-api-key': process.env.SCAVIO_API_KEY, 'Content-Type': 'application/json' };
async function gapCheck(keyword, myDomain, 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());
const organic = data.organic_results || [];
const myRank = organic.findIndex(r => r.link?.includes(myDomain));
const compRank = organic.findIndex(r => competitors.some(c => r.link?.includes(c)));
console.log(`${keyword}: You ${myRank >= 0 ? '#'+(myRank+1) : 'absent'} | Comp ${compRank >= 0 ? '#'+(compRank+1) : 'absent'}`);
}
await gapCheck('search api python', 'scavio.dev', ['tavily.com']);Expected Output
search api | You: 2 | competitor1: 4, competitor2: 7
serp api | You: - | competitor1: 3
web search api | You: 5 | competitor1: 2, competitor2: 6
mcp search | You: - | competitor1: 1, competitor2: 5
ai agent search | You: 3 | competitor1: -, competitor2: 8
Cost: $0.025
=== Keyword Gaps ===
You are missing from 2 keywords where competitors rank:
[hard ] "mcp search" - competitor1.com at #1
[medium] "serp api" - competitor1.com at #3
=== Expanded Gap Opportunities ===
"mcp search" -> 4 related keywords:
- What is MCP protocol for AI agents
- Best MCP search servers 2026
New keyword opportunities: 8