Appearance
Full Merlino Audit SOP Crown Jewel
This is the master SOP for running a complete Merlino-style SEO audit. No stone unturned. Every endpoint listed. Every data point pulled. Agents reference this page when executing an audit.
Audit cost estimate: $12–$22 per client domain (depends on backlink index size and crawl depth)
Execution time: 8–15 minutes (with parallel execution)
Output format: Static TypeScript files at clients/{slug}/ consumed by the audit dashboard
Pre-Audit Checklist
- [ ] Confirm
DATAFORSEO_LOGINandDATAFORSEO_PASSWORDenv vars are set - [ ] Confirm
GOOGLE_PLACES_API_KEYis set (for GMB verification) - [ ] Collect: target domain, 4–6 target keywords, all GMB business names and CIDs
- [ ] Confirm client location (city, state, zip) for local SERP accuracy
- [ ] Set
location_code: 2840(US) or override to specific metro code - [ ] Confirm output directory:
clients/{slug}/
Execution Groups
Run all calls within a group in parallel. Wait for each group to complete before starting the next.
Group 1 (parallel): Backlinks Summary + Labs Domain Overview + Domain Technologies + WHOIS + Content Analysis Summary
Group 2 (parallel): All SERP calls (4 target keywords × organic + maps + local finder + news + images + AI overview)
Group 3 (parallel): Keywords Data (volume + keywords for site + keywords for keywords + trends)
Group 4 (parallel): Labs (ranked keywords + competitors + historical rank overview + bulk traffic + keyword difficulty + search intent)
Group 5 (parallel): Backlinks Detail (backlinks list + anchors + referring domains + domain pages + timeseries)
Group 6 (parallel): Business Data (GMB info + reviews for each location)
Group 7 (sequential): On-Page crawl task post → poll for completion → retrieve pages + errors + links + duplicates
Group 8 (parallel): AI Optimization (LLM mentions brand + service keywords, ChatGPT/Claude/Gemini/Perplexity responses)DataForSEO Pulls
Backlinks API — 12 Endpoints
All backlinks endpoints are Live-only. Run as early as possible — largest dataset.
| # | Endpoint | Payload Key Fields | Cost Est. | Group |
|---|---|---|---|---|
| 1 | POST /v3/backlinks/summary/live | target, include_subdomains: true | ~$0.01 | 1 |
| 2 | POST /v3/backlinks/history/live | target, date_from, date_to | ~$0.01 | 5 |
| 3 | POST /v3/backlinks/backlinks/live | target, limit: 100, order_by: ["rank,desc"] | ~$0.02–$0.05 | 5 |
| 4 | POST /v3/backlinks/anchors/live | target, limit: 50 | ~$0.005 | 5 |
| 5 | POST /v3/backlinks/referring_domains/live | target, limit: 100, order_by: ["rank,desc"] | ~$0.01–$0.05 | 5 |
| 6 | POST /v3/backlinks/domain_pages/live | target, limit: 50 | ~$0.01 | 5 |
| 7 | POST /v3/backlinks/domain_pages_summary/live | target, limit: 50 | ~$0.01 | 1 |
| 8 | POST /v3/backlinks/referring_networks/live | target, limit: 50 | ~$0.01 | 5 |
| 9 | POST /v3/backlinks/competitors/live | target, limit: 10 | ~$0.01 | 5 |
| 10 | POST /v3/backlinks/bulk_spam_score/live | targets: [competitor_domains] | ~$0.002/domain | 5 |
| 11 | POST /v3/backlinks/bulk_ranks/live | targets: [domain, competitors...] | ~$0.002/domain | 1 |
| 12 | POST /v3/backlinks/timeseries_referring_domains/live | target, date_from, date_to, group_by: "month" | ~$0.01 | 5 |
Python — Backlinks Summary:
python
import os, requests
from requests.auth import HTTPBasicAuth
auth = HTTPBasicAuth(os.environ["DATAFORSEO_LOGIN"], os.environ["DATAFORSEO_PASSWORD"])
BASE = "https://api.dataforseo.com"
def backlinks_summary(target):
r = requests.post(
f"{BASE}/v3/backlinks/summary/live",
auth=auth,
headers={"Content-Type": "application/json"},
json=[{"target": target, "include_subdomains": True}]
)
return r.json()["tasks"][0]["result"][0]SERP API — All Tabs, 4+ Keywords
Run for each target keyword. Cover every SERP tab.
| # | Endpoint | When to Use | Cost Est. | Group |
|---|---|---|---|---|
| 1 | POST /v3/serp/google/organic/live/advanced | All 4+ target keywords | ~$0.005/kw | 2 |
| 2 | POST /v3/serp/google/maps/live/advanced | Primary local query (city + service) | ~$0.003 | 2 |
| 3 | POST /v3/serp/google/local_finder/live/advanced | Same local query | ~$0.003 | 2 |
| 4 | POST /v3/serp/google/news/live/advanced | Brand name query | ~$0.003 | 2 |
| 5 | POST /v3/serp/google/images/live/advanced | Brand name + primary service query | ~$0.003 | 2 |
| 6 | POST /v3/serp/google/ai_overview/live | Primary service keyword | ~$0.005 | 2 |
| 7 | POST /v3/serp/google/ai_mode/tasks_post | Primary service keyword (async) | ~$0.01 | 2 |
| 8 | GET /v3/serp/google/autocomplete/live/advanced | Brand name, primary service | ~$0.001 | 2 |
Standard organic SERP payload:
python
def serp_organic(keyword, location_code=2840, language_code="en", depth=20):
r = requests.post(
f"{BASE}/v3/serp/google/organic/live/advanced",
auth=auth,
headers={"Content-Type": "application/json"},
json=[{
"keyword": keyword,
"location_code": location_code,
"language_code": language_code,
"depth": depth,
"calculate_rectangles": False
}]
)
return r.json()["tasks"][0]["result"][0]Keywords Data API — 5 Endpoints
| # | Endpoint | Payload | Cost Est. | Group |
|---|---|---|---|---|
| 1 | POST /v3/keywords_data/google_ads/search_volume/live | All target keywords (batch) | ~$0.0005/kw | 3 |
| 2 | POST /v3/keywords_data/google_ads/keywords_for_site/live | Client domain | ~$0.01 | 3 |
| 3 | POST /v3/keywords_data/google_ads/keywords_for_keywords/live | Primary service keywords as seeds | ~$0.005 | 3 |
| 4 | POST /v3/keywords_data/google_trends/explore/live | Primary 2–3 keywords | ~$0.001/kw | 3 |
| 5 | POST /v3/keywords_data/clickstream/search_volume/live | All target keywords | ~$0.001/kw | 3 |
DataForSEO Labs API — 9 Endpoints
| # | Endpoint | Payload | Cost Est. | Group |
|---|---|---|---|---|
| 1 | POST /v3/dataforseo_labs/google/ranked_keywords/live | Client domain, limit: 100 | ~$0.01 | 4 |
| 2 | POST /v3/dataforseo_labs/google/competitors_domain/live | Client domain, limit: 10 | ~$0.01 | 4 |
| 3 | POST /v3/dataforseo_labs/google/domain_rank_overview/live | Client domain | ~$0.01 | 1 |
| 4 | POST /v3/dataforseo_labs/google/historical_rank_overview/live | Client domain | ~$0.01 | 4 |
| 5 | POST /v3/dataforseo_labs/google/keyword_suggestions/live | Primary service keyword | ~$0.005 | 4 |
| 6 | POST /v3/dataforseo_labs/google/related_keywords/live | Primary service keyword | ~$0.005 | 4 |
| 7 | POST /v3/dataforseo_labs/google/keyword_difficulty/live | All target keywords (batch) | ~$0.002/kw | 4 |
| 8 | POST /v3/dataforseo_labs/google/search_intent/live | All target keywords (batch) | ~$0.001/kw | 4 |
| 9 | POST /v3/dataforseo_labs/google/bulk_traffic_estimation/live | Client domain + top 5 competitors | ~$0.005 | 4 |
AI Optimization API — 8+ Endpoint Calls
Brand and service keyword visibility across all major LLMs.
| # | What to Call | Endpoint | Cost Est. | Group |
|---|---|---|---|---|
| 1 | LLM Mentions — brand name | POST /v3/ai_optimization/llm_mentions/search/live | ~$0.005 | 8 |
| 2 | LLM Mentions — primary service keyword | POST /v3/ai_optimization/llm_mentions/search/live | ~$0.005 | 8 |
| 3 | LLM Mentions — secondary service keyword | POST /v3/ai_optimization/llm_mentions/search/live | ~$0.005 | 8 |
| 4 | ChatGPT response — "best [service] in [city]" | POST /v3/ai_optimization/chatgpt/live/tasks_post | ~$0.01 | 8 |
| 5 | Claude response — "best [service] in [city]" | POST /v3/ai_optimization/claude/live/tasks_post | ~$0.01 | 8 |
| 6 | Gemini response — "best [service] in [city]" | POST /v3/ai_optimization/gemini/live/tasks_post | ~$0.01 | 8 |
| 7 | Perplexity response — "best [service] in [city]" | POST /v3/ai_optimization/perplexity/live | ~$0.01 | 8 |
| 8 | AI Keyword Data — primary keywords | POST /v3/ai_optimization/ai_keyword_data/live | ~$0.005 | 8 |
LLM Mentions payload:
python
def llm_mentions(keyword, location_code=2840):
r = requests.post(
f"{BASE}/v3/ai_optimization/llm_mentions/search/live",
auth=auth,
headers={"Content-Type": "application/json"},
json=[{
"keyword": keyword,
"location_code": location_code,
"language_code": "en"
}]
)
return r.json()["tasks"][0]["result"][0]On-Page API — Full Site Crawl
| # | Step | Endpoint | Cost Est. | Group |
|---|---|---|---|---|
| 1 | Start crawl | POST /v3/on_page/task_post | ~$0.004/page × pages | 7a |
| 2 | Poll status | GET /v3/on_page/summary/{task_id} | Free | 7b |
| 3 | Get pages | POST /v3/on_page/pages/{task_id} | Free (included) | 7c |
| 4 | Get errors | POST /v3/on_page/errors/{task_id} | Free | 7c |
| 5 | Get links | POST /v3/on_page/links/{task_id} | Free | 7c |
| 6 | Duplicate tags | POST /v3/on_page/duplicate_tags/{task_id} | Free | 7c |
| 7 | Duplicate content | POST /v3/on_page/duplicate_content/{task_id} | Free | 7c |
| 8 | Resources | POST /v3/on_page/resources/{task_id} | Requires load_resources: true | 7c |
| 9 | Instant page scan | POST /v3/on_page/instant_pages | ~$0.004 | 7c |
| 10 | Lighthouse audit | POST /v3/on_page/lighthouse/live/json | ~$0.02 | 7c |
Crawl task payload:
python
def start_onpage_crawl(domain, max_pages=100):
r = requests.post(
f"{BASE}/v3/on_page/task_post",
auth=auth,
headers={"Content-Type": "application/json"},
json=[{
"target": domain,
"max_crawl_pages": max_pages,
"load_resources": False,
"enable_javascript": False,
"enable_browser_rendering": False,
"store_raw_html": False,
"check_spell": False
}]
)
return r.json()["tasks"][0]["id"]Business Data API — GMB + Reviews
Run for every client GMB location.
| # | Endpoint | Payload | Cost Est. | Group |
|---|---|---|---|---|
| 1 | POST /v3/business_data/google/my_business_info/live | Business name + city | ~$0.01/location | 6 |
| 2 | POST /v3/business_data/google/reviews/task_post | Business CID or name | ~$0.01–$0.02 | 6 |
| 3 | POST /v3/business_data/google/my_business_updates/task_post | Business CID | ~$0.01 | 6 |
If client is in hospitality, add:
POST /v3/business_data/tripadvisor/reviews/task_postPOST /v3/business_data/trustpilot/reviews/task_post
Domain Analytics API — 2 Endpoints
| # | Endpoint | Payload | Cost Est. | Group |
|---|---|---|---|---|
| 1 | POST /v3/domain_analytics/technologies/domain_technologies/live | Client domain + top 3 competitors | ~$0.005/domain | 1 |
| 2 | POST /v3/domain_analytics/whois/overview/live | Client domain | ~$0.005 | 1 |
Content Analysis API — 3 Endpoints
| # | Endpoint | Payload | Cost Est. | Group |
|---|---|---|---|---|
| 1 | POST /v3/content_analysis/search/live | Brand name, limit: 50 | ~$0.005 | 1 |
| 2 | POST /v3/content_analysis/summary/live | Brand name | ~$0.002 | 1 |
| 3 | POST /v3/content_analysis/sentiment_analysis/live | Brand name | ~$0.002 | 1 |
Non-DataForSEO Pulls
These run in parallel with Group 1 or Group 2.
| # | Source | What to Pull | How |
|---|---|---|---|
| 1 | Direct HTTP | Homepage HTML — verify schema markup, meta tags, GHL/LeadConnector detection | curl -s https://domain.com |
| 2 | Google Places API | Verified GBP data — CID, categories, hours, photos count | GET https://maps.googleapis.com/maps/api/place/details/json |
| 3 | ReddAPI | Reddit brand mentions and sentiment | GET https://api.reddapi.com/... |
| 4 | DomDetailer API | DA, TF, CF, Trust Flow, Spam Score | POST https://www.domdetailer.com/api/... |
| 5 | Queen browser agent | Social media follower counts (Facebook, Instagram, YouTube) | Browser visit — NEVER use scraped estimates |
| 6 | Queen browser agent | YouTube channel presence — subscribers, videos, last post date | Browser visit |
| 7 | Queen browser agent | LSA (Local Services Ads) presence check — "Google Guaranteed" badge | Browser visit + screenshot |
| 8 | Queen browser agent | Google Ads presence check — paid listings in SERP screenshots | Browser visit |
| 9 | Direct HTTP | robots.txt, sitemap.xml — parse and validate | curl -s https://domain.com/robots.txt |
Output Files (Static TypeScript)
Write these files to clients/{slug}/:
| File | Contents |
|---|---|
audit.ts | Top-level audit summary, score, action items, technologies |
backlinks-data.ts | Backlinks summary, referring domains, anchors, history, timeseries |
serp-data.ts | Organic rankings, map pack, local finder, AI overview results |
keywords.ts | Search volume, difficulty, intent, trends for all target keywords |
onpage-data.ts | Crawl summary, page issues, Lighthouse scores, errors |
gmb-data.ts | GMB info, reviews, ratings, Q&A for each location |
llm-data.ts | LLM mentions, AI engine responses, AI keyword data |
competitors-data.ts | Competitor domains, traffic estimates, keyword overlap |
geogrid-data.ts | Geogrid rank positions (if geogrid service used) |
All exports must be typed TypeScript — no any, no untyped objects. Dashboard components import directly.
Cost Estimate by Category
| Category | Endpoints | Cost Range |
|---|---|---|
| Backlinks (12 endpoints) | summary, history, backlinks, anchors, domains, pages, networks, competitors, bulk spam, bulk ranks, timeseries | $0.15–$0.50 |
| SERP (8 calls × 4 keywords) | organic, maps, local finder, news, images, AI overview, AI mode, autocomplete | $0.20–$0.40 |
| Keywords Data (5 endpoints) | volume, for site, for keywords, trends, clickstream | $0.05–$0.10 |
| Labs (9 endpoints) | ranked keywords, competitors, rank overview, historical, suggestions, related, difficulty, intent, traffic | $0.10–$0.15 |
| On-Page (crawl 50 pages) | task post, summary, pages, errors, links, dupes, Lighthouse | $0.20–$0.40 |
| Business Data (2 locations) | GMB info, reviews per location | $0.04–$0.08 |
| Domain Analytics | technologies, WHOIS | $0.01–$0.02 |
| Content Analysis | search, summary, sentiment | $0.01 |
| AI Optimization (8 calls) | LLM mentions × 3, LLM responses × 4, AI keyword data | $0.07–$0.10 |
| Total | ~48 DataForSEO calls | $0.83–$1.75 |
Plus non-DataForSEO: Google Places (~$0.01), DomDetailer (~$0.01), Queen browser time (~$0).
Total per full audit: approximately $1–$2 in API costs.
QA Checklist — 3 Rounds
Round 1: Data Completeness
- [ ] All 48+ DataForSEO calls returned
status_code: 20000 - [ ] No
Noneor empty result arrays in critical fields - [ ] All TypeScript files compile without errors (
tsc --noEmit) - [ ] All export names match what dashboard components import
- [ ]
backlinks-data.tshas real integer values forbacklinks,referring_domains,rank - [ ]
keywords.tshas real integersearch_volume(not strings, not 0 for all) - [ ]
serp-data.tshas at least 1 organic result for each keyword - [ ]
gmb-data.tshas correct business name, address, rating, review count - [ ]
llm-data.tshasllm_mentionsentries withse_typefield
Round 2: Data Accuracy
- [ ] Domain rank (DataForSEO) matches DomDetailer DA within 20%
- [ ] Review count in GMB data matches visible Google count (Queen verifies)
- [ ] Top-ranking keywords in
ranked_keywordsmatch known real rankings - [ ] Social follower counts are browser-verified — NOT from any API estimate
- [ ] Schema markup detected on homepage matches what's actually in the HTML
Round 3: Dashboard Render
- [ ]
npm run buildpasses 30/30 pages with 0 errors - [ ] All 7 dashboard tabs render without blank panels
- [ ] Recharts / chart components have data (not empty arrays)
- [ ] No console errors in browser (Queen screenshot)
- [ ] All numbers visible in UI match source TypeScript data
Parallel Execution Script (Python Skeleton)
python
import os
import asyncio
import aiohttp
from aiohttp import BasicAuth
LOGIN = os.environ["DATAFORSEO_LOGIN"]
PASSWORD = os.environ["DATAFORSEO_PASSWORD"]
BASE = "https://api.dataforseo.com"
auth = BasicAuth(LOGIN, PASSWORD)
async def post(session, path, payload):
async with session.post(f"{BASE}{path}", json=payload) as r:
return await r.json()
async def run_group(session, calls):
"""Run a list of (path, payload) tuples in parallel."""
tasks = [post(session, path, payload) for path, payload in calls]
return await asyncio.gather(*tasks)
async def full_audit(domain, keywords, location_code=2840):
async with aiohttp.ClientSession(auth=auth, headers={"Content-Type": "application/json"}) as session:
# Group 1 — Domain-level data
group1 = [
("/v3/backlinks/summary/live", [{"target": domain}]),
("/v3/backlinks/domain_pages_summary/live", [{"target": domain, "limit": 50}]),
("/v3/backlinks/bulk_ranks/live", [{"targets": [domain]}]),
("/v3/dataforseo_labs/google/domain_rank_overview/live", [{"target": domain, "location_code": location_code, "language_code": "en"}]),
("/v3/domain_analytics/technologies/domain_technologies/live", [{"target": domain}]),
("/v3/domain_analytics/whois/overview/live", [{"target": domain}]),
("/v3/content_analysis/summary/live", [{"keyword": domain.replace(".com", "")}]),
("/v3/content_analysis/sentiment_analysis/live", [{"keyword": domain.replace(".com", "")}]),
]
results_g1 = await run_group(session, group1)
# Group 2 — SERP for all keywords
group2 = [
("/v3/serp/google/organic/live/advanced", [{"keyword": kw, "location_code": location_code, "language_code": "en", "depth": 20}])
for kw in keywords
] + [
("/v3/serp/google/maps/live/advanced", [{"keyword": keywords[0] + " near me", "location_code": location_code, "language_code": "en"}]),
("/v3/serp/google/local_finder/live/advanced", [{"keyword": keywords[0], "location_code": location_code, "language_code": "en"}]),
("/v3/serp/google/ai_overview/live", [{"keyword": keywords[0], "location_code": location_code, "language_code": "en"}]),
]
results_g2 = await run_group(session, group2)
# ... continue for groups 3-8
return {"group1": results_g1, "group2": results_g2}
if __name__ == "__main__":
domain = "example.com"
keywords = ["roofing contractor dallas", "roof replacement dallas", "emergency roof repair", "metal roof installation"]
asyncio.run(full_audit(domain, keywords))Key Rules
- Never use search snippets or scraped estimates for data — DataForSEO API is the mandatory primary source for all SEO metrics.
- Social follower counts must be browser-verified by Queen — no API ever returns accurate follower counts.
- All TypeScript output must be static — no API calls from the dashboard. Data is pre-fetched and committed to the repo.
- Mark clearly when data is unavailable — if an endpoint returns no result, set the field to
nulland add a comment. Never fabricate values. - Retry each failed call once — if still failing after retry, log the error and continue. Don't let one bad endpoint block the entire audit.
- Cost logging is mandatory — log
data.costfrom every response. Alert if any single call exceeds $0.50.