---
name: dataforseo-keyword-gap
description: "Monthly: keywords where two competitors both rank in the top 10 and you are missing, checked against Search Console to show which already have a page. Use when the user asks for this, or when it runs on a schedule."
---

# Keyword gap vs competitors: DataForSEO Labs and Search Console

Monthly: keywords where two competitors both rank in the top 10 and you are missing, checked against Search Console to show which already have a page.

A keyword gap report from any SEO suite ends as a long list of things to write. Half of it is wrong: Google already shows one of your pages for many of those keywords, just on page three, where the suite does not count it. Writing a new page there splits your signals; improving the existing one is faster.

Once a month the agent asks DataForSEO Labs which keywords two of your competitors both rank for in the top 10, removes the ones you already rank for, and then looks each remaining keyword up in your own Search Console data. Content and SEO leads get two lists instead of one: keywords where a page of yours already shows and needs work, and keywords where you have nothing yet.

## Two ways to get the data

Use the API or MCP route when you have an MCP server, a CLI or an API token for the app. Otherwise, if you can drive a browser the user is signed in to (Claude in Chrome, a Playwright or Chrome DevTools MCP server), use the browser route. Say which one you used.

## Route A: API or MCP server

### What you need

- DataForSEO API login and password (or the DataForSEO MCP server). A run makes 2 to 4 DataForSEO Labs requests, roughly a cent each plus a fraction of a cent per row returned; try the requests on `https://sandbox.dataforseo.com/v3/` first.
- Read access to the Search Console property, and two competitor domains that really compete for your search traffic, not just your product.

If something is missing, say what and stop; do not invent numbers.

### Steps

1. **Find what both competitors rank for** (DataForSEO): Call `POST /v3/dataforseo_labs/google/domain_intersection/live` with `target1` and `target2` set to the two competitor domains (no `https://` or `www.`), `intersections: true`, `item_types: ["organic"]`, your `location_code` and `language_code`, `limit: 1000`, and `filters: [["first_domain_serp_element.rank_group", "<=", 10], "and", ["second_domain_serp_element.rank_group", "<=", 10]]`. Keep `keyword_data.keyword`, `keyword_data.keyword_info.search_volume` and each side's `rank_group` and `url`.
2. **Get what you already rank for** (DataForSEO): Call `POST /v3/dataforseo_labs/google/ranked_keywords/live` with `target` set to your domain, the same location and language, `item_types: ["organic"]`, `filters: [["ranked_serp_element.serp_item.rank_group", "<=", 20]]` and `limit: 1000`. If `total_count` is above 1,000, page with `offset`, at most 3 pages.
3. **Take out what you already have**: Normalize keywords on both lists (lowercase, trim, collapse spaces) and keep the competitors' keywords that are not in your ranked list: that anti-join is the gap. Drop keywords with a search volume under 50.
4. **Check each gap keyword in Search Console** (Google Search Console): Call `searchanalytics.query` for the last 3 months with `dimensions: ["query", "page"]`, `rowLimit: 25000` and the country that matches your location. Join on the normalized query: a match with average position above 20 is a page to improve (note the page with most impressions); a match at 20 or better means Labs missed you, so drop it and count it; no match means a page to write.
5. **Post both lists**: Sort each list by search volume. Show the counts (gap, to improve, to write, dropped), the top keywords of each list with both competitors' positions, and the page of yours that should take the first one.

## Route B: the browser

### What you need

- Signed in to the DataForSEO dashboard with a funded balance (Playground requests are charged like API calls), and to Google with access to the Search Console property.

If something is missing, say what and stop; do not invent numbers.

### Steps

Start at https://app.dataforseo.com/. The user is signed in already; never type passwords or codes, and ask if a sign-in page appears.

1. **Run the domain intersection**: In the DataForSEO dashboard open API Playground, choose DataForSEO Labs > Google > Domain Intersection, enter the two competitor domains as target 1 and target 2, set your location and language, leave intersections on, set the limit to `1000` and click Request. Keep keywords where both `rank_group` values are 10 or better, with search volume.
2. **Run your ranked keywords**: In the same Playground choose Ranked Keywords, enter your domain with the same location and language, limit `1000`, and click Request. Remove every keyword you rank 20 or better for from the first list; what is left is the gap.
3. **Look each gap keyword up in Search Console**: Open `https://search.google.com/search-console/performance/search-analytics`, set Date to `Last 3 months` and your country, turn on Average position, and export the Queries table with Export > Download CSV. Match the gap keywords against the export in lowercase.
4. **Sort into improve, write and drop**: Past position 20 in Search Console: a page to improve (click the query, then the Pages tab, to see which page). At 20 or better: drop it. Not in the export: a page to write. Sort both lists by search volume.

Note: Each Playground request is billed like the API, so run each one once and save the JSON. Search Console exports cap at 1,000 rows; gap keywords past position 20 are often beyond that on large sites, where the API route is the reliable one.

## The report

- **The gap**: how many keywords both competitors rank top 10 for while you are not in the top 20.
- **Pages to improve**: gap keywords where Search Console already shows your page past position 20, with the page.
- **Pages to write**: gap keywords with no impressions at all.
- **Dropped**: keywords Labs missed that you do rank for.
- The page to improve first.

## Post it to Briefkin

When `BRIEFKIN_TOKEN` is set, or `~/.config/briefkin/env` exists (the installer from https://briefkin.dev/docs writes it), also send the result to the user's Briefkin board as one card. Keep the `item_key` the same on every run so the card updates in place, use a new `event_id` per run, and put today's real numbers in place of the example ones. If the request fails, say so once and carry on.

```bash
[ -n "${BRIEFKIN_TOKEN:-}" ] || . ~/.config/briefkin/env
curl -sS -X POST "${BRIEFKIN_URL:-https://briefkin.dev}/v1/events" \
  -H "authorization: Bearer $BRIEFKIN_TOKEN" \
  -H "content-type: application/json" \
  -d @- <<'JSON'
{
  "v": 1,
  "event_id": "dataforseo-keyword-gap/<date of this run, e.g. 2026-10-16>",
  "item_key": "dataforseo-keyword-gap",
  "project": "example.com",
  "source": "Keyword gap vs competitors: DataForSEO Labs and Search Console skill",
  "kind": "update",
  "title": "31 of 140 gap keywords already have a page of yours",
  "summary": "**rival.com** and **ledgerly.io** both rank top 10 for **140 keywords** you don't. **31** of them already show your pages in Search Console at position 20+, so you have a page to improve, not one to write.",
  "widget": [
    {
      "type": "Badges",
      "props": {
        "items": [
          {
            "label": "140 in the gap",
            "tone": "info"
          },
          {
            "label": "31 to improve",
            "tone": "ok"
          },
          {
            "label": "109 to write",
            "tone": "warn"
          },
          {
            "label": "6 dropped: you rank",
            "tone": "neutral"
          }
        ]
      }
    },
    {
      "type": "Table",
      "props": {
        "columns": [
          "Keyword",
          "Searches/mo",
          "rival.com",
          "ledgerly.io",
          "Your page (GSC)",
          "Your pos"
        ],
        "align": [
          "left",
          "right",
          "right",
          "right",
          "left",
          "right"
        ],
        "rows": [
          [
            "invoice factoring",
            6600,
            4,
            7,
            "/blog/cash-flow-tips",
            34.2
          ],
          [
            "net 30 payment terms",
            4400,
            2,
            5,
            "/blog/payment-terms",
            27.8
          ],
          [
            "proforma invoice",
            3600,
            6,
            3,
            "/templates/proforma",
            22.5
          ],
          [
            "late payment fee",
            2900,
            8,
            9,
            "none",
            "to write"
          ],
          [
            "invoice vs receipt",
            2400,
            3,
            4,
            "none",
            "to write"
          ],
          [
            "credit note template",
            1900,
            5,
            10,
            "/templates",
            41
          ]
        ]
      }
    },
    {
      "type": "Callout",
      "props": {
        "text": "Start with /blog/payment-terms: it already shows for \"net 30 payment terms\" (4,400 searches) at 27.8, and both rivals answer it with a dedicated page.",
        "tone": "info"
      }
    }
  ]
}
JSON
```

Widgets come from the Briefkin catalog (https://briefkin.dev/v1/catalog). Without a token, just give the user the report.

## Run it on a schedule

Suggested: 1st of the month at 10:00 (`0 10 1 * *`). From cron, with the agent signed in on that machine:

```bash
0 10 1 * * claude -p "Run the dataforseo-keyword-gap skill"
# Codex: codex exec, Gemini CLI: gemini -p, Copilot CLI: copilot -p, OpenCode: opencode run
```

Or use the agent's own scheduler (Claude Code routines, Codex automations, Claude or ChatGPT scheduled tasks): https://briefkin.dev/skills/dataforseo-keyword-gap#schedule

Source: https://briefkin.dev/skills/dataforseo-keyword-gap
