example

Find out who you're actually up against

Competitors ranked by real keyword overlap from the SERP archive — not who you assume they are.

Who you think you compete with and who shares your SERPs are usually two different lists. Three endpoints — five requests — build the shortlist, and the whole page runs on our archive's real data for semrush.com, not a mocked shape.

language
prerequisites

Export your key once.

$ setupcurl
export SEOFETCH_KEY=sof_live_…
# the scripts below use jq -- brew install jq / apt-get install jq
step 1

Ask the archive who ranks where you rank.

→ requestcurl
curl https://api.seofetch.com/v1/domains/competitors \
  -H "Authorization: Bearer $SEOFETCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "semrush.com", "engine": "google", "location": 2840, "language": "en", "device": "desktop", "limit": 25}' \
  -o competitors.json
← response200
{
  "id": "doma_mjff5imq36refjlhknksn6ly",
  "request_id": "req_k5sgjyoybnb7vbroskyhsoydq4",
  "object": "domain_competitors",
  "created_at": "2026-08-09T10:06:21Z",
  "elapsed_ms": 180,
  "cache": "miss",
  "credits": {
    "charged": 30,
    "balance": 9857
  },
  "data": {
    "domain": "semrush.com",
    "keywords_total": 2834,
    "keywords_considered": 200,
    "total_competitors": 1743,
    "competitors": [
      {
        "domain": "reddit.com",
        "shared_keywords": 106,
        "keywords_total": 114768,
        "avg_rank": 3.9,
        "target_avg_rank": 1,
        "overlap": 0.53,
        "shared_volume": 370910,
        "sample_keywords": [
          {
            "keyword": "google search console",
            "volume": 368000
          },
          "…"
        ]
      },
      "…"
    ],
    "coverage": {
      "from": "2026-05-20",
      "observations": 200
    }
  }
}

Ranked by overlap — shared keywords, weighted by how often you actually co-occur. Expect a giant or two at the top: reddit.com intersects everyone, and pruning them is your first edit. Built from the domain's most-recent archived keywords — keywords_considered tells you the sample.

step 2

Prune the giants; keep the 3-5 with real overlap.

$ localrun locally
jq -c '[.data.competitors[] | select(.overlap >= 0.3) | select(.domain as $d | ["reddit.com", "wikipedia.org", "quora.com"] | index($d) | not)]' competitors.json

The blocklist is editorial — you know which platforms aren't really competing with you. What survives is a candidate cohort — the 0.3 floor and the blocklist are editorial defaults, so adjust both for your market.

step 3

Size up each survivor.

→ requestcurl
curl https://api.seofetch.com/v1/domains/overview \
  -H "Authorization: Bearer $SEOFETCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domain": "ahrefs.com", "location": 2840, "language": "en"}'
← response200
{
  "id": "doma_hlstjm46fad2alfsznf2imkz",
  "request_id": "req_hctzlupizfftbpd5fumx7zxs6m",
  "object": "domain_overview",
  "created_at": "2026-08-09T10:06:21Z",
  "elapsed_ms": 180,
  "cache": "miss",
  "credits": {
    "charged": 10,
    "balance": 9857
  },
  "data": {
    "organic": {
      "keywords_count": 101,
      "traffic_estimate": 70.17,
      "traffic_value": 704.38,
      "positions": {
        "top_3": 0,
        "top_10": 3,
        "top_20": 11,
        "top_100": 101
      }
    },
    "paid": {
      "keywords_count": 0,
      "traffic_estimate": 0,
      "traffic_value": 0,
      "positions": {
        "top_3": 0,
        "top_10": 0,
        "top_20": 0,
        "top_100": 0
      }
    }
  }
}

Keyword count, traffic estimate, position spread — 10 credits per competitor. Small overlap plus a big footprint reads aspirational, not head-to-head.

step 4

Turn the closest one into a keyword list.

→ requestcurl
curl https://api.seofetch.com/v1/domains/gap \
  -H "Authorization: Bearer $SEOFETCH_KEY" \
  -H "Content-Type: application/json" \
  -d '{"domains": ["semrush.com", "ahrefs.com"], "mode": "gap", "engine": "google", "location": 2840, "language": "en", "device": "desktop", "limit": 100, "offset": 0}'
← response200
{
  "id": "doma_al45dzebjfr4rjz4hz44htt2",
  "request_id": "req_qendynpyyzavfjvq6gzn4r7dzy",
  "object": "domain_gap",
  "created_at": "2026-08-09T10:06:21Z",
  "elapsed_ms": 180,
  "cache": "miss",
  "credits": {
    "charged": 15,
    "balance": 9857
  },
  "data": {
    "mode": "gap",
    "domains": [
      "semrush.com",
      "ahrefs.com"
    ],
    "items": [
      {
        "keyword": "search engine optimization",
        "volume": 18100,
        "ranks": [
          null,
          {
            "rank": 27,
            "url": "https://ahrefs.com/blog/what-is-seo/",
            "date": "2026-06-28"
          }
        ]
      },
      "…"
    ],
    "totals": [
      2834,
      1315
    ],
    "matched_count": 409,
    "coverage": {
      "from": "2026-06-28",
      "observations": 409
    },
    "limit": 100,
    "offset": 0
  }
}

mode "gap" is keywords they rank for that you don't — which is exactly where the content-gaps walkthrough picks up.

script

Run the whole thing.

Each language below is the full pipeline — swap in your domain, run it after exporting SEOFETCH_KEY: the survivors land in cohort.csv, best footprint first, with a gap pull against the closest one already queued up in gap.json.

→ runcompetitors.sh
#!/usr/bin/env bash
# competitors.sh -- who ranks where you rank -> pruned cohort -> gap vs the closest one.
set -euo pipefail
: "${SEOFETCH_KEY:?export SEOFETCH_KEY first}"
DOMAIN="${TARGET_DOMAIN:-yourdomain.com}"
api() { curl -sS --fail-with-body "https://api.seofetch.com$1" \
  -H "Authorization: Bearer $SEOFETCH_KEY" \
  -H "Content-Type: application/json" \
  -d "$2"; }

# 1. who ranks where you rank
api /v1/domains/competitors \
  "$(jq -cn --arg d "$DOMAIN" '{domain: $d, engine: "google", location: 2840, language: "en", device: "desktop", limit: 25}')" \
  > competitors.json

# 2. prune giants, keep real overlap (0.3 floor -- policy knob)
jq -c '[.data.competitors[] | select(.overlap >= 0.3) | select(.domain as $d | ["reddit.com","wikipedia.org","quora.com"] | index($d) | not)]' \
  competitors.json > cohort.json
[ "$(jq length cohort.json)" -gt 0 ] || { echo "nothing survives the prune -- lower the floor or widen the blocklist"; exit 0; }

# 3. size up the top 3 survivors by overlap -- best-matched cohort first
SURVIVORS=3   # size up more: +10cr each
top_survivors=$(jq -c "sort_by(-.overlap) | .[0:${SURVIVORS}]" cohort.json)
: > cohort.csv
echo "$top_survivors" | jq -r '.[].domain' | while IFS= read -r d; do
  api /v1/domains/overview \
    "$(jq -cn --arg d "$d" '{domain: $d, location: 2840, language: "en"}')" > "overview-$d.json"
  jq -r --arg d "$d" \
    '[$d, (.data.organic.keywords_count // 0), (.data.organic.traffic_estimate // 0)] | @csv' \
    "overview-$d.json" >> cohort.csv
done
sort -t, -k3 -rn cohort.csv -o cohort.csv
echo "cohort.csv: domain, organic keywords, traffic estimate -- best first (footprint, not overlap)"

# 4. turn the highest-overlap survivor into a keyword list
top=$(echo "$top_survivors" | jq -r 'sort_by(-.overlap) | .[0].domain')
api /v1/domains/gap \
  "$(jq -cn --arg a "$DOMAIN" --arg b "$top" '{domains: [$a, $b], mode: "gap", engine: "google", location: 2840, language: "en", device: "desktop", limit: 100, offset: 0}')" \
  > gap.json
echo "gap.json: $(jq '.data.matched_count' gap.json) keywords $top ranks for that $DOMAIN doesn't"

Turned your cohort into a shortlist? The content-gaps walkthrough turns your closest competitor into a keyword list you can act on.