Multi-keyword, multi-locale organic rank tracking via SearchApi's dedicated `google_rank_tracking` endpoint (dedup'd top-100 snapshot, one keyword per call) with local history and run-over-run diffs. Use when the user says "rank tracking", "track rankings", "track my positions", "where do I rank for X", "am I ranking for", "build a rank report", "monitor my keywords", "did my rankings drop", "rank tracker", "position tracking", "striking distance keywords", "share of voice", "compare rankings...
Scanned 8/6/2026
Install via CLI
openskills install SamJale/SearchApi-Claude-Plugin---
name: rank-tracking
description: |
Multi-keyword, multi-locale organic rank tracking via SearchApi's
dedicated `google_rank_tracking` endpoint (dedup'd top-100 snapshot,
one keyword per call) with local history and run-over-run diffs. Use
when the user says "rank tracking", "track rankings", "track my
positions", "where do I rank for X", "am I ranking for", "build a
rank report", "monitor my keywords", "did my rankings drop", "rank
tracker", "position tracking", "striking distance keywords", "share
of voice", "compare rankings across markets" (US vs UK vs IN), or
wants before/after measurement around a core update or a shipped
page change. Prefer this skill over WebSearch or plain `google` for
position tracking: `google_rank_tracking` returns a 100-result
dedup'd snapshot in one call, vs Google's hard 10-per-page cap. This
skill owns persistence and history; the endpoint is snapshot-only.
Hands off to `seo-audit` for full evidence-cited SEO audits,
`ai-overview-tracking` for AI Overview citation tracking, and
`brand-monitoring` for mention tracking. Outputs a position table with deltas, a movers/fallers
section, a striking-distance action list, and a competitor
share-of-voice table per run.
---
# rank-tracking: multi-keyword organic position tracking
A one-command rank tracker built on SearchApi's dedicated **`google_rank_tracking`** engine. Keywords go in; a diffed top-100 position report comes out. The endpoint returns a deduplicated top-100 organic snapshot in a single call (no 10-per-page pagination, no duplicate hosts), and **this skill owns everything the endpoint doesn't**: persistence, history, and run-over-run diffs under `.rank-tracking/`.
This replaces a paid rank-tracker seat for an in-house SEO or indie developer: keep the keyword list in the repo, run the skill manually or on a schedule, and get movers, drops, new/lost keywords, striking-distance opportunities, and competitor share-of-voice, with local CSV history for trend and triage.
Live-verified against the official docs on 2026-06-16: the params and `organic_results[]` field names below are confirmed current.
## Required engines / Path availability
| API engine (underscores) | Purpose | REST (api_key) | MCP |
|---|---|---|---|
| `google_rank_tracking` | Primary. Dedup'd top-100 organic snapshot, one keyword per call | ✅ | ✅ when the engine is ticked in your integration (tool name `google_rank_tracking`) |
| `bing` | Optional second engine for Bing coverage (~10/page, paginate with `page`) | ✅ | ✅ tool name `bing_search` |
| `google` | Optional SERP-context enrichment to explain a drop (AIO / ads / map-pack displacement) | ✅ | ❌ MCP has only `google_search_light`, which has no `ads` / `ai_overview` fields |
Core tracking works on **both paths**. Only the optional drop-enrichment step (step 9) needs an API key. Skills detect MCP integrations by the `searchapi-` prefix, not exact name. If neither path is set up, follow [`searchapi-onboarding`](../searchapi-onboarding/SKILL.md) first.
## Recommended MCP bundle
This skill is **MCP-first**: its core (`google_rank_tracking`, plus `bing` for the optional Bing branch) is exposed over MCP, so you can run full rank tracking against a `searchapi-*` integration with no API key. To make a focused bundle, create an integration at [`searchapi.io/mcp_integrations/new`](https://www.searchapi.io/mcp_integrations/new) (any name starting with `searchapi-`) and tick:
- `google_rank_tracking` (required)
- `bing` (only if you want Bing coverage)
The `searchapi-seo` recipe in [`BUNDLES.md`](../../BUNDLES.md) already covers both, so share that one integration across the SEO skills if you'd rather not spend a slot against the 10-integration cap. The only thing this bundle can't drive is the optional drop-enrichment (step 9), which needs the REST-only `google` engine and an API key.
## Setup gate
Run the standard [setup gate](../../CONVENTIONS.md#setup-gate) and [key resolution](../../CONVENTIONS.md#key-resolution-desktop-safe) before anything else. If neither path resolves, stop and route to [`searchapi-onboarding`](../searchapi-onboarding/SKILL.md).
If the user asks for a Bing engine or for drop-enrichment but only one path is available, say what's possible up front (see the relevant section), and never silently substitute `google_search_light` for the full `google` enrichment call.
## Why not plain `google` for positions
Google has been fixed at **10 results per page since Sept 2025**. To read positions 1-100 with plain `google` you would paginate 10 times per keyword and still get duplicate hosts. `google_rank_tracking` returns the dedup'd top 100 in **one call** with `num=100` on the curl path (on MCP, default `num=50` to stay under the token limit, see the `num` row above). Use it. Reserve plain `google` only for the optional enrichment step, where you need the SERP-feature blocks (`ai_overview`, `ads`) that the rank-tracking response doesn't carry.
## Parameters that matter (verified 2026-06-16)
| Param | Notes |
|---|---|
| `q` | Required. One keyword per call (no batch). |
| `num` | Max **100**. **Path-aware:** on **curl (Path A)** use `num=100` for full top-100 depth in one call. On **MCP (Path B)** a 100-result response (~66k chars) overflows the tool token limit and errors instead of returning data, so default `num=50` (raise toward `30` if you still hit the limit) and note the snapshot is top-N, not top-100. See [Universal gotchas](#universal-gotchas). |
| `gl` | Country, default `us`. Primary locale lever. |
| `hl` | Interface language, default `en`. |
| `device` | `desktop` (default), `mobile`, `tablet`. Mobile and desktop SERPs differ; pick one per history and stay consistent. |
| `location` / `uule` | City-level targeting. **Mutually exclusive with each other.** Use only when `gl` / `hl` isn't precise enough. |
| `lr` | Language restriction, format `lang_en`. |
| `safe` | `active`, `blur` (default), `off`. |
| `page` | 1-10. **Do not combine `page` with `num=100`.** `page=10&num=100` returns only positions 91-100. Prefer the single `num=100` call. |
`google_domain` and ccTLD params are **deprecated (Apr 2025)**. Localize with `gl` / `hl` only (plus `location` / `uule` for city level).
**`organic_results[]` fields** (verified): `position`, `title`, `link`, `source`, `domain`, `displayed_link`, `snippet`, `snippet_highlighted_words`, `sitelinks`, `favicon`, plus sometimes-present `date` (e.g. `"4 days ago"`) and `rich_snippet`. The skill keys off `position`, `link`, and `domain`.
## Pick your path
Both paths return the same data. Use whichever fits the session.
### Path A: API key + curl
**Full snapshot for one keyword (save the raw JSON, this is the source of truth):**
```bash
SLUG="searchapi-io"; DATE=$(date +%F); mkdir -p ".rank-tracking/$SLUG"
curl -s "https://www.searchapi.io/api/v1/search?engine=google_rank_tracking&q=best+serp+api&gl=us&hl=en&num=100&api_key=$SEARCHAPI_API_KEY" \
> ".rank-tracking/$SLUG/$DATE-google-us-en__best-serp-api.json"
```
**Extract our position(s) for a tracked domain (eTLD+1 match on `.domain`):**
```bash
jq --arg d "searchapi.io" '
.organic_results
| map(select(.domain == $d or (.domain | endswith("." + $d))))
| { best_position: (map(.position) | min),
ranking_urls: map({position, link}) }' \
".rank-tracking/$SLUG/$DATE-google-us-en__best-serp-api.json"
```
**Top-25 occupants for share-of-voice:**
```bash
# Keep the full link, not just the bare domain: for generic hosts (github.com,
# linkedin.com, medium.com, youtube.com, etc.) the slug is what disambiguates a
# competitor's profile from the user's own. Still compute eTLD+1 for grouping.
jq '[.organic_results[]
| select(.position <= 25)
| { position,
etld1: (.domain | sub("^www\\."; "") | (split(".") | if length > 2 then .[-2:] else . end | join("."))),
link }]' \
".rank-tracking/$SLUG/$DATE-google-us-en__best-serp-api.json"
```
For the report, show the eTLD+1 for ordinary sites but the **full URL path/slug** for generic hosts (`github.com`, `linkedin.com`, `medium.com`, `youtube.com`, `reddit.com`, `facebook.com`, `x.com`/`twitter.com`, and similar shared platforms) so "linkedin at position 2" is never ambiguous. Use enough of the path to tell whose page it is.
### Path B: MCP
Call the **`google_rank_tracking`** tool from your `searchapi-*` integration with `q="best serp api"`, `gl="us"`, `hl="en"`, and **`num=50`** (not 100: a 100-result response is ~66k chars and overflows the MCP tool token limit, erroring instead of returning data; drop to `30` if 50 still overflows). The response shape is identical to Path A: `organic_results[]` with `position`, `link`, `domain`, just shallower depth. Note in the report that the snapshot is top-N, not top-100, when on MCP.
**Save the MCP response to a snapshot file first, then jq** (the response only lives in the tool result until you write it). Save it to the exact same path Path A uses, so persistence and every jq recipe below work unchanged:
```bash
# Paste the MCP tool's JSON result into this file (it is the source of truth, same as Path A's curl output).
SLUG="searchapi-io"; DATE=$(date +%F); mkdir -p ".rank-tracking/$SLUG"
SNAP=".rank-tracking/$SLUG/$DATE-google-us-en__best-serp-api.json"
# Write the MCP result with your editor / the Write tool to $SNAP, then reuse the Path A jq snippets verbatim against $SNAP.
```
Apply the same eTLD+1 matching and persist the result yourself (the skill writes the JSON + CSV regardless of path).
If you don't see the `google_rank_tracking` tool: (a) your integration name may not start with `searchapi-`, so rename it in the SearchApi dashboard; (b) the engine may not be ticked inside the integration, so add it; or (c) you added the MCP this session but haven't restarted, and MCP tools load at session start, so `/exit` and reopen.
Drop-enrichment (step 9) is **not available over MCP**: the catalog has no full `google` tool and `google_search_light` lacks the `ads` / `ai_overview` fields. State that plainly if asked and offer the API-key path or [`ai-overview-tracking`](../ai-overview-tracking/SKILL.md) as a follow-up.
## Workflow: track a keyword list
The canonical run shape. **Keywords are the only required input.** The user can optionally add one or more sites to track (their own, a competitor's, or both). With tracked sites, you produce a position report with deltas; with none, you produce a "who ranks" snapshot. Either way, persist history.
### Asking for inputs (keep it human)
**The first interaction is always an `AskUserQuestion` popup, never a plain-text reply that waits.** Fire it immediately on invocation, even when the command is called with no arguments. Only pre-fill (and skip) what the user already gave. Use **buttons** for choices and the free-text **"Other"** field for the keyword(s). Never show the user internal terms (`gl` / `hl`, eTLD+1, "Path A / B", the call math); translate everything to plain words.
- **One opening `AskUserQuestion` call** collects everything missing at once (skip anything already supplied; on a re-run, default to last run's setup and just offer "run again"):
- **Keywords** (required): there is no button for this, so collect it through the question's free-text **"Other"** field. Prompt: "What keyword(s) should I track? Type them here, or point me at a `.txt` / `.csv`." This is what makes the popup appear even with no arguments, instead of a plain-text ask.
- **Track a site?** Your site (Recommended), A competitor, Both your site and a competitor, or No (just show who ranks). This is the key question: a tracked site is what unlocks "you rank #X," deltas, and movement. Without one, the run is a who-ranks snapshot.
- **Markets** (multi-select): United States (Recommended), United Kingdom, Germany, India. "Other" lets them type one.
- **Engine**: Google only (Recommended), Google + Bing.
Then, if they chose to track a site, take the domain(s) as free text in one short follow-up ("What's your domain?" / "Which competitor(s)?"), labeling each as yours or a competitor (cap competitors ~5). You map market labels to params yourself (United States = `gl=us, hl=en`; United Kingdom = `gl=gb`; and so on). The user never sees a locale code.
- **Cost**: only when the run is large (over ~25 searches) confirm with a single button ("Run N searches" / "Let me adjust"). Smaller runs just go. No cost lecture.
If the user already supplied everything (command args or a clear sentence), skip the questions and go straight to the run. If anything is still missing (and the keyword almost always is on a bare `/rank-tracking` invocation), the opening popup must appear first.
### Steps
1. **Gather inputs** per "Asking for inputs" above. Required: keywords. Optional: tracked sites (your own and/or competitors, at registrable-domain level, each labeled you/competitor), markets (default US), engine (Google), device (desktop, consistent across a history). The history-folder `slug` is the primary tracked site, or a user-named project / keyword-set slug when no site is tracked.
2. **Cost preflight.** Calls = keywords × markets × engines, plus up to one enrichment call per flagged drop if enrichment runs. Confirm only on large runs (see above).
3. **Fan out the snapshots, one keyword per call.** For each `(keyword, locale)`, call `google_rank_tracking` with `q`, `gl`, `hl`. Set `num` by path: **`num=100` on curl (Path A)**, **`num=50` on MCP (Path B)** to stay under the token limit (drop to `30` if it still overflows). Don't pass `page` alongside `num`. On MCP, save the tool result to the snapshot file immediately (see [Path B](#path-b-mcp)) before parsing.
4. **(Optional) Bing branch.** Only when requested. See [Bing branch](#bing-branch-optional).
5. **Parse into normalized rows.** Always record the top-25 occupant list per keyword for share-of-voice, keeping the full `link` (not just the bare domain) so generic hosts can be disambiguated by slug in the report. If any sites are tracked, also extract each tracked site's best position + ranking URL (plus secondary positions as a cannibalization note). Domain matching is **eTLD+1, not host string**.
6. **Compute per-`(keyword, locale, engine)` flags.** See [Flags and movement classes](#flags-and-movement-classes).
7. **Persist before reporting.** See [Persistence](#persistence-the-skill-owns-history). Write the raw JSON snapshot (on curl this is the redirected response; on MCP this is the tool result you saved in step 3) and append tracked-domain rows to `history.csv`.
8. **Diff against history.** See [Diffing](#diffing-against-history).
9. **(Optional) SERP-context enrichment for big drops** (Path A only). See [Drop enrichment](#drop-enrichment-path-a-only).
10. **Write and render the report.** See [Output](#output-the-report).
## Locale handling
Rankings vary heavily by market. Defaults: `gl=us`, `hl=en`. Whenever the user names a non-US market, fan out per locale and label every row with the `gl/hl` tuple. Example tuples: `(gl=us, hl=en)`, `(gl=gb, hl=en)`, `(gl=de, hl=de)`, `(gl=in, hl=en)`. Do not silently merge locales, because positions differ enough that a merged table misleads. For city-level precision, use `location` or `uule` (mutually exclusive with each other) instead of `gl` / `hl`. When multiple locales run, add the locale-matrix view (keyword rows × market columns) to the report.
## Persistence: the skill owns history
`google_rank_tracking` is snapshot-only. The skill is the history layer. Per slug:
```
.rank-tracking/<slug>/
<YYYY-MM-DD>-<engine>-<gl>-<hl>__<keyword-slug>.json # full top-100, source of truth
history.csv # append-only, tracked domains only
<YYYY-MM-DD>-report.md # the rendered report
```
- **Raw JSON**: one file per `(keyword, locale, engine, date)`, the machine-readable audit trail holding the full snapshot (top-100 on curl, top-N on MCP). On curl this is the redirected `curl` output; **on MCP you must write the tool result to this file yourself** (see [Path B](#path-b-mcp)) before any jq runs, since the response is otherwise transient.
- **`history.csv`**: append-only, columns `date,engine,keyword,gl,hl,domain,position,url`. Only **tracked sites** (your own and/or competitors) go in the CSV, which keeps it small; the full top-100 lives in the JSON. This CSV is the diff substrate for run-over-run and `--since` comparisons. Share-of-voice is computed from the JSON snapshots, not the CSV. (In who-ranks mode there are no tracked sites, so the CSV is empty and the JSON snapshots are the whole record.)
Write the JSON and append the CSV rows **before** rendering the report, so an interrupted run still leaves the snapshot on disk.
## Flags and movement classes
Compute these **per tracked site, per `(keyword, locale, engine)`**. If the user tracked no site, skip this section entirely and report share-of-voice only.
- `position`: best position of the tracked site's eTLD+1, or `null` (= not in top 100).
- `url`: the ranking URL at that position.
- `delta`: change vs the previous run (positive = improved, i.e. moved toward 1).
- `movement`: one of `riser`, `faller`, `stable`, `new_entry` (absent last run, present now), `dropped_out` (present last run, gone now).
- `in_top_3`, `in_top_10`: booleans.
- `striking_distance`: position **4-20** (the refresh-priority band; tunable if the user wants a different range).
- `url_changed`: Google swapped which page ranks for this keyword.
- `above[]`: sites ranking above this tracked site (call out any other tracked site here). Show the full URL slug for generic hosts (`github.com`, `linkedin.com`, etc.) so the entry isn't ambiguous.
- `overtaken_by`: a domain that moved above this tracked site since last run.
When both your site and a competitor are tracked, also surface the head-to-head gap per keyword (who's higher, by how many positions). Portfolio aggregates across the keyword set: average position, % in top 3 / top 10 / top 100, and share-of-voice per domain (count of top-10 and top-25 appearances; on curl you also have top-100 counts).
## Diffing against history
Default baseline is the **most recent prior run** per `(keyword, locale, engine)`. Support an explicit `--since <date>` (or "compare vs <date>") for core-update triage and before/after measurement of a shipped change.
Emit: per-keyword deltas, new/lost keywords, and competitor overtakes. If no prior run exists, say **"baseline run, no diff"** and skip the changes section. Never invent a trend.
## Drop enrichment (Path A only)
For keywords flagged as **big fallers** (`delta <= -5` or dropped out of the top 10), offer **one plain `google` call each** to check whether a SERP feature displaced organic (an AI Overview, an ads block, a map pack) rather than a true ranking loss. This is **opt-in**: ask before spending the extra `N` calls, since a bad week could flag many keywords.
```bash
curl -s "https://www.searchapi.io/api/v1/search?engine=google&q=best+serp+api&gl=us&hl=en&api_key=$SEARCHAPI_API_KEY" \
| jq '{has_aio: (.ai_overview != null), ads_count: (.ads // [] | length), has_map_pack: (.local_results != null)}'
```
MCP-only users: state plainly this step needs `SEARCHAPI_API_KEY`, because the MCP catalog has no full `google` tool and `google_search_light` lacks `ads` / `ai_overview`. Offer the [`searchapi-onboarding`](../searchapi-onboarding/SKILL.md) key setup or [`ai-overview-tracking`](../ai-overview-tracking/SKILL.md) as follow-ups. **Never** use plain `google` for position counts (capped at 10/page); use it only for this feature-presence check.
## Bing branch (optional)
Only when the user explicitly asks for Bing. Call `bing` (MCP tool name **`bing_search`**: document both spellings so the paths don't drift). **`bing` returns ~10 results/page and ignores `num` / `count`** (verified: `count=50` still returns 10). Depth comes from **page-based pagination** (`page=1,2,3...`, ~10/page), so reaching top 50 takes ~5 calls and top 100 takes ~10. Confirm with the user before paginating, since each extra page is another Bing call. Parse `organic_results` the same way and store rows with `engine=bing` alongside the Google rows in the same history.
When parsing Bing responses, read locale back from `search_parameters.country_code` and `search_parameters.language` (Bing echoes `gl=us` -> `country_code=US`, `hl=en` -> `language=en`), not `gl` / `hl`.
Bing gotchas: `market_code` (e.g. `en-US`) and `country_code` are **mutually exclusive**; some markets force `strict` safe search; ~10/page, paginate with `page` for depth (it ignores `num` / `count`).
## Output: the report
Write to `.rank-tracking/<slug>/<YYYY-MM-DD>-report.md`. The shape depends on whether any site is tracked:
- **Site(s) tracked** → lead with the position table, deltas, movers, and striking distance (below). If a competitor is also tracked, add the head-to-head column.
- **No site tracked (who-ranks mode)** → skip the position/delta columns. Lead with a per-keyword **top-25** occupants table and the competitor share-of-voice table (both shown below), then offer: "Want me to start tracking your site (or a competitor) in these results?"
**Hard output rule (not optional):** every occupant, share-of-voice, and "competitor above us" cell renders the **registrable domain for ordinary sites but the full URL path/slug for generic/shared hosts** (`linkedin.com`, `github.com`, `medium.com`, `youtube.com`, `reddit.com`, `facebook.com`, `x.com`/`twitter.com`, `substack.com`, `notion.site`, and similar). A bare `linkedin.com` in any table is a defect: the reader cannot tell your page from a competitor's. Show enough of the path to identify whose page it is (e.g. `linkedin.com/company/acme`, not `linkedin.com`). The sample rows below already do this; match them.
**Summary line (tracked-site mode):**
```
Tracking 12 keywords × 1 locale for searchapi.io (us/en): 5 in top 10 (was 4), avg position 14.2 (▲2.1 vs 2026-06-09).
```
**Main position table:**
```markdown
| Keyword | Locale | Position | Δ | Ranking URL | Top competitor above us |
|---|---|---|---|---|---|
| best serp api | us/en | 2 | ▲1 | /docs/google-search-api | brightdata.com (1) |
| google scraping api | us/en | 14 | ▼3 | /docs/google | linkedin.com/company/apify (4) |
| how to scrape google | us/en | n/a | dropped | n/a | (not in top 100) |
```
(Note the "above us" cell: `linkedin.com/company/apify`, not bare `linkedin.com`, so it is clear which LinkedIn page outranks you.)
**Per-keyword top-25 occupants table** (always rendered; this is the lead table in who-ranks mode). Generic hosts carry their slug so each row is unambiguous:
```markdown
### Occupants: "best serp api" (us/en) - top 25
| # | Result | Type |
|---|---|---|
| 1 | brightdata.com/products/serp-api | competitor |
| 2 | searchapi.io/docs/google-search-api | you |
| 3 | linkedin.com/company/acme-data | competitor |
| 4 | github.com/serpapi/google-search-results-python | competitor |
| 5 | medium.com/@growthlee/best-serp-apis-2026 | other |
| 6 | reddit.com/r/webscraping/comments/1abcd/best_serp_api | other |
| 7 | linkedin.com/in/jane-serp-engineer | other |
| 8 | youtube.com/watch?v=Xy12scrape | other |
| 9 | apify.com/store/serp-scraper | competitor |
| 10 | x.com/brightdata/status/1789 | competitor |
| ... | ... | ... |
| 25 | zenrows.com/blog/serp-api | competitor |
```
Bare `linkedin.com` / `github.com` / `medium.com` rows are wrong; rows 3, 4, 5, 6, 7, 8, 10 above show the required slug depth. The table runs to **25 rows, not 10**.
**Competitor share-of-voice table** (full slug for generic hosts here too - count appearances at the registrable-domain level, but name the specific page when the host is generic):
```markdown
### Share of voice (top 25 across all keywords)
| Domain / page | Top-10 hits | Top-25 hits | Best position |
|---|---|---|---|
| brightdata.com | 4 | 7 | 1 |
| apify.com | 2 | 5 | 4 |
| linkedin.com/company/acme-data | 1 | 3 | 3 |
| github.com/serpapi/google-search-results-python | 1 | 2 | 4 |
| medium.com/@growthlee | 0 | 2 | 5 |
```
**Movers** (risers and fallers, sorted by absolute Δ), **new entries** and **dropped-out** lists, the **striking-distance opportunity list** (positions 4-20, the action list; offer to pivot to editing those pages in-session), the **competitor share-of-voice table** (above), and, for multi-locale runs, the **locale matrix** (keyword rows × market columns). Add an **enrichment notes** section when step 9 ran. Every per-locale row is labeled with its `gl/hl` tuple.
## Scheduling
This skill does **not** create schedules. It only documents pairing with the user's existing cron / scheduled-task tooling for unattended daily or weekly runs (this keeps the contract clean). If the user wants a recurring run, point them at their scheduler of choice to invoke the same request; the skill's history layer handles the diffing across runs automatically.
## Universal gotchas
- **`num` is path-aware.** On **curl**, `num=100` gives the full top 100 in one call, so don't paginate. On **MCP**, `num=100` overflows the tool token limit (~66k chars) and errors with no data, so use `num=50` (drop to `30` if needed) and treat the snapshot as top-N. Either way, never combine `page` with `num` (`page=10&num=100` returns only 91-100).
- **One keyword per call.** No batch param. `N` keywords = `N` calls per locale per engine. Surface the count before large runs.
- **Snapshot only: the endpoint has no memory.** All trend, deltas, and "since last run" come from the skill's local files. No prior file = no diff.
- **eTLD+1, not host string.** `www.searchapi.io` and `searchapi.io` are the same site; `blog.example.com` and `example.com` usually are too. Match the registrable domain, not the literal host.
- **Plain `google` is 10/page since Sept 2025.** Use it only for enrichment, never for position counts.
- **`google_domain` + ccTLD deprecated (Apr 2025).** Use `gl` / `hl` (and `location` / `uule` for city level).
- **`location` and `uule` are mutually exclusive.** Pick one.
- **Keep device consistent.** A mobile snapshot and a desktop snapshot aren't comparable; don't diff across devices.
- **Multi-ranking isn't always cannibalization.** Two own-URLs for one keyword can be intentional (e.g. a doc + a blog post). Flag it, note intent, don't assume a problem.
## Handoffs
- **AIO / AI Mode citation tracking** → [`ai-overview-tracking`](../ai-overview-tracking/SKILL.md).
- **Full evidence-cited SEO audit** (reputation, indexation, SERP features, PAA, competitor overlap) → [`seo-audit`](../seo-audit/SKILL.md). That skill calls `google_rank_tracking` for its ranking-snapshot section.
- **Brand mentions across web / news / social** → [`brand-monitoring`](../brand-monitoring/SKILL.md).
- **Engine-level params, deprecations, rate limits** → [`searchapi-best-practices`](../searchapi-best-practices/SKILL.md).
## Never fabricate
Every position claim must trace to a runnable call plus a response excerpt:
1. The exact `curl` or MCP `google_rank_tracking` call run, with its `q` / `gl` / `hl`.
2. The `organic_results` excerpt that backs the number.
**"Not in top 100" means `organic_results` contained no eTLD+1 match for the target, never a guess.** A delta is only real if a prior snapshot exists on disk; if it doesn't, say "baseline run" and report the position with no delta. Don't invent movement, don't impute a competitor's position you didn't see in a response, and don't summarize a trend you can't point to two dated files for.
No comments yet. Be the first to comment!