# YouTube Keyword Research - Autocomplete & Competition Score (`mibedk/youtube-keyword-opportunity-scorer`) Actor

Expands seed topics into YouTube autocomplete keywords and scores each for demand, competition and opportunity via the official YouTube Data API v3: median views of top results, ranking channel size, small-channel share and upload age.

- **URL**: https://apify.com/mibedk/youtube-keyword-opportunity-scorer.md
- **Developed by:** [Mikkel Bech-Hansen](https://apify.com/mibedk) (community)
- **Categories:** Videos, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 keyword scoreds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## YouTube Keyword Research - Autocomplete & Competition Score

Give this actor a few seed topics. It expands each one against YouTube's own search
autocomplete — the suggestions that drop down when you type in the YouTube search box — and
deduplicates everything into a keyword list. Then, for as many of those keywords as you
allow, it looks at the top YouTube search results through the official YouTube Data API v3
and works out how hard each keyword actually is to rank for: the median view count of the
top results, how big the channels that hold those slots are, what share of them are small
channels, and how old the ranking videos are. Each keyword comes back with those raw numbers
plus a demand score, a competition score, and a composite opportunity score from 0 to 100.

The keyword list works with no configuration and no API key. The scores need a free YouTube
Data API v3 key of your own, which you paste into the `apiKey` input.

### What a first run does

Press **Start** with the defaults and you get roughly 200 deduplicated autocomplete keywords
for the seeds `ai tools` and `home gym`, with each keyword's autocomplete rank, how many
expansion queries surfaced it, and a direct YouTube search link. No key, no signup, no proxy.

Add a YouTube Data API v3 key and the same run also scores the 20 most prominent keywords
(configurable) with the full competition profile.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `seeds` | array of strings | `["ai tools", "home gym"]` | 1–25 seed topics. Lowercased and deduplicated. |
| `apiKey` | string (secret) | *empty* | Your own YouTube Data API v3 key. Empty = keyword list only, no scores. |
| `expansionLevel` | `basic` / `standard` / `deep` | `standard` | Autocomplete queries per seed: 1 / 14 / 40. |
| `regionCode` | string | `US` | Two-letter ISO 3166-1 code. Sets autocomplete `gl` and the API's `regionCode`. |
| `language` | string | `en` | Two-letter ISO 639-1 code. Sets autocomplete `hl` and the API's `relevanceLanguage`. |
| `maxScoredKeywords` | integer | `20` | 1–500. How many keywords get the full profile. ~102 API quota units each. |
| `maxKeywords` | integer | `200` | 1–5000. Hard cap on total records saved, scored and unscored combined. |
| `includeUnscoredKeywords` | boolean | `true` | Off = only fully scored keywords are saved. |
| `topResultsPerKeyword` | integer | `25` | 5–50. How many top results each competition profile is computed over. No extra quota cost. |
| `smallChannelMaxSubscribers` | integer | `100000` | 100–100,000,000. Below this, a ranking channel counts as "small". |
| `proxyConfiguration` | object | `{ "useApifyProxy": false }` | Off by default. Turn on datacenter proxy if autocomplete starts rate-limiting you. |

#### How many keywords do you get?

Autocomplete returns roughly 10–14 suggestions per query, with heavy overlap between
modifier queries. Measured on live runs at the time of writing:

| Seeds | Level | Queries sent | Unique keywords found |
|---|---|---|---|
| 2 (`ai tools`, `home gym`) | `standard` | 28 | 286 |
| 1 (`ai video editing`) | `deep` | 40 | 417 |

### Output

One record per keyword. Scored keywords come first, highest `opportunityScore` at the top, then
the unscored ones — ordered by autocomplete prominence within each seed, taken round-robin across
your seeds so one noisy seed cannot crowd out the others.

A scored record:

```json
{
  "keyword": "ai tools for video editing",
  "seed": "ai tools",
  "regionCode": "US",
  "language": "en",
  "searchUrl": "https://www.youtube.com/results?search_query=ai%20tools%20for%20video%20editing",
  "suggestRank": 2,
  "suggestHits": 2,
  "suggestProminence": 0.67,
  "discoveredFrom": "ai tools",
  "scored": true,
  "demandScore": 80.6,
  "competitionScore": 48.7,
  "opportunityScore": 65.9,
  "opportunityLabel": "good",
  "analyzedResults": 25,
  "estimatedResultCount": 1000000,
  "medianTopViews": 128000,
  "medianTopSubscribers": 47500,
  "smallChannelShare": 0.68,
  "smallChannelCount": 17,
  "channelsWithKnownSubscribers": 25,
  "medianUploadAgeDays": 412,
  "medianEngagementRate": 0.0341,
  "topVideoTitle": "I Tested 12 AI Video Editors So You Don't Have To",
  "topVideoUrl": "https://www.youtube.com/watch?v=XXXXXXXXXXX",
  "topChannelTitle": "Example Creator",
  "topChannelSubscribers": 214000,
  "scoreNote": null,
  "scrapedAt": "2026-08-21T23:32:29.198Z"
}
```

An unscored record — this is exactly what a default, keyless run produces:

```json
{
  "keyword": "ai tools for video editing",
  "seed": "ai tools",
  "regionCode": "US",
  "language": "en",
  "searchUrl": "https://www.youtube.com/results?search_query=ai%20tools%20for%20video%20editing",
  "suggestRank": 2,
  "suggestHits": 2,
  "suggestProminence": 0.67,
  "discoveredFrom": "ai tools",
  "scrapedAt": "2026-08-21T23:32:29.198Z",
  "scored": false,
  "opportunityScore": null,
  "opportunityLabel": null,
  "demandScore": null,
  "competitionScore": null,
  "medianTopViews": null,
  "medianTopSubscribers": null,
  "smallChannelShare": null,
  "medianUploadAgeDays": null,
  "topVideoTitle": null,
  "topVideoUrl": null,
  "scoreNote": "Not scored: no 'apiKey' was supplied, so only the autocomplete keyword itself was collected."
}
```

#### Field reference

| Field | Meaning |
|---|---|
| `keyword` | The autocomplete suggestion, verbatim. |
| `seed` | Which of your seeds it came from. |
| `searchUrl` | YouTube search page for this keyword — the fastest way to sanity-check a score. |
| `suggestRank` | Best position the keyword held in an autocomplete list (1 = first). |
| `suggestHits` | How many of your expansion queries surfaced it. Repeats mean broader demand. |
| `suggestProminence` | 0–1 blend of `suggestRank` and `suggestHits`. |
| `discoveredFrom` | The expansion query that first surfaced it. |
| `scored` | Whether the competition profile was computed. |
| `analyzedResults` | How many top results the profile is based on. |
| `estimatedResultCount` | YouTube's own estimate of matching videos. Rough — see limitations. |
| `medianTopViews` | Median view count across the analysed top results. The demand signal. |
| `medianTopSubscribers` | Median subscriber count of the channels holding those slots. |
| `smallChannelShare` | 0–1 share of ranking channels under `smallChannelMaxSubscribers`. |
| `smallChannelCount` | Count behind that share. |
| `channelsWithKnownSubscribers` | Denominator — channels that hide their subscriber count are excluded. |
| `medianUploadAgeDays` | Median age of the ranking videos. Old = a stale page you can displace. |
| `medianEngagementRate` | Median `(likes + comments) / views` of the ranking videos. |
| `topVideoTitle` / `topVideoUrl` / `topChannelTitle` / `topChannelSubscribers` | The current #1 result. |
| `demandScore` / `competitionScore` / `opportunityScore` | 0–100. See below. |
| `opportunityLabel` | `excellent` ≥ 70, `good` ≥ 60, `fair` ≥ 45, otherwise `poor`. |
| `scoreNote` | Why a keyword was not scored, when it wasn't. |

#### How the scores are calculated

Deliberately simple and fully reproducible from the raw fields, so you can re-weight it
yourself if you disagree with the weights.

```
suggestProminence = clamp01( 0.6 × clamp01( (20 − (suggestRank − 1)) / 20 )
                           + 0.4 × clamp01( (suggestHits − 1) / 4 ) )

demandScore      = 100 × clamp01( 0.75 × log10(1 + medianTopViews) / 6
                                + 0.25 × suggestProminence )

competitionScore = 100 × clamp01( 0.40 × log10(1 + medianTopSubscribers) / 6
                                + 0.40 × (1 − smallChannelShare)
                                + 0.20 × clamp01(1 − medianUploadAgeDays / 540) )

opportunityScore = clamp01( (demandScore − competitionScore + 100) / 200 ) × 100
```

Demand and competition are built from **disjoint** signals on purpose — demand from how much
audience the topic pulls, competition from how entrenched the incumbents are — so they do not
cancel out. `opportunityScore` of 50 is neutral: demand and competition are balanced. Above 60
means real audience with beatable incumbents.

The score is a judgement call, not ground truth. Every input to it ships in the same record,
so if you think upload age matters more than subscriber size for your niche, rescore from the
raw columns.

### Cost

Pay-per-event: **$0.01 per scored keyword**, **$0.001 per unscored keyword** returned.

| Run | Result | Cost |
|---|---|---|
| Defaults, no API key | 200 keywords, no scores | **$0.20** |
| Defaults, with API key | 20 scored + 180 unscored | **$0.38** |
| With API key, `includeUnscoredKeywords: false` | 20 scored only | **$0.20** |
| `deep`, 1 seed, 90 scored, unscored off | 90 scored only | **$0.90** |

Set a maximum charge on the run and the actor stops cleanly inside it — it checks the
remaining budget before every charge, and before spending your YouTube API quota, so it never
scores keywords it cannot deliver.

The YouTube Data API quota is **your** cost, not a charge from this actor: scoring one keyword
uses about 102 units of the 10,000 units/day a fresh Google Cloud project gets for free —
roughly **90–95 scored keywords per day per key**. The autocomplete half uses no quota at all.

### Limitations — please read before your first run

- **Scores need your own API key.** There is no way around this: YouTube Data API quota is
  per-project and the actor cannot supply one for you. Without a key you get the keyword list
  and nothing else. Getting a key is free and takes about two minutes
  (console.cloud.google.com → Create credentials → API key → enable "YouTube Data API v3").
- **~90 scored keywords per day, per key.** `search.list` costs 100 quota units per keyword and
  the default free quota is 10,000/day. `maxScoredKeywords` accepts up to 500, but anything
  above ~90 will exhaust a default-quota key partway through. When that happens the run keeps
  everything it scored before the wall and marks the rest with a `scoreNote` — it degrades, it
  does not throw the run away.
- **Autocomplete is not search volume.** YouTube does not publish per-keyword search volume,
  and neither does this actor, and neither does anyone else honestly. `suggestRank` and
  `suggestHits` are ordering signals from YouTube's own suggestion ranking; `medianTopViews` is
  the closest thing to a demand measurement available, and that is what `demandScore` mostly
  uses. If a tool quotes you an exact YouTube "search volume" number, it is modelled, not measured.
- **`estimatedResultCount` is unreliable.** It is YouTube's own `totalResults` estimate, which
  is frequently a round number like 1,000,000 regardless of the query. It is included for
  completeness; do not build decisions on it. Use `medianTopViews` and `smallChannelShare`.
- **The autocomplete endpoint is undocumented.** It is the same public endpoint the YouTube
  search box uses, it needs no key and no login, and it has been stable for years — but Google
  can change or throttle it without notice. The actor retries with backoff and fails loudly
  rather than returning a silently empty dataset. If your IP gets rate-limited, switch on
  Apify Proxy in `proxyConfiguration`.
- **Results are country- and language-specific.** `regionCode`/`language` materially change
  both the suggestions and the ranking sets. A keyword that looks open in `US` may be
  saturated in `GB`. Run each market separately.
- **Channels can hide subscriber counts.** Those channels are excluded from
  `medianTopSubscribers` and `smallChannelShare`; `channelsWithKnownSubscribers` tells you how
  many were actually counted. If every channel on a keyword hides its count, the competition
  score falls back to a neutral assumption for that component.
- **A snapshot, not a trend.** One run is one day's ranking set. Opportunity moves. Schedule
  the run weekly against the same seeds if you want to see a keyword opening up.
- **It does not touch private data.** No comments, no user profiles, no logged-in data — only
  public search suggestions and public video/channel statistics.

### Legal

Not affiliated with, endorsed by, or sponsored by YouTube or Google LLC. "YouTube" is a
trademark of Google LLC.

This actor gets its data two ways, with different terms attached to each.

**Search autocomplete** comes from `clients1.google.com/complete/search`, the public,
undocumented endpoint behind YouTube's search box. No key, no login and no access control is
involved, and the suggestions are what any visitor sees when typing. Even so: Google's and
YouTube's Terms of Service very likely restrict automated or programmatic access to their
services regardless of whether the data is public, and using this actor may not comply with
those terms. "Publicly visible" and "permitted by the Terms of Service" are different
questions. You are responsible for your own compliance and for what you do with the data.

**Competition metrics** come from the official YouTube Data API v3, called with **your** API
key under **your** Google Cloud project. That means you — not the operator of this actor — are
the developer bound by the [YouTube API Services Terms of
Service](https://developers.google.com/youtube/terms/api-services-terms-of-service) and the
[YouTube API Services Developer
Policies](https://developers.google.com/youtube/terms/developer-policies). Things worth
knowing before you store the output:

- Stored YouTube API data generally must be refreshed or deleted within 30 days. Treat this
  actor's dataset as a snapshot with an expiry date, not a permanent archive.
- The policies require attribution to YouTube where API data is displayed, and prohibit using
  API data to build a substitute for, or replicate the experience of, YouTube.
- Aggregating API data into public metrics or reselling it can require separate permission
  from Google. Read the policies before you publish or resell anything derived from this
  output.

Read those terms yourself and use the data accordingly. Nothing here is legal advice.

# Actor input Schema

## `seeds` (type: `array`):

One or more seed topics to expand into YouTube autocomplete keywords. Each seed is expanded into many autocomplete queries according to 'Expansion level'. Plain words work best — type what a viewer would type into the YouTube search box.

## `apiKey` (type: `string`):

Your own YouTube Data API v3 key. LEAVE EMPTY to get the deduplicated autocomplete keyword list only (no API key, no signup, works immediately). Provide a key to also get the competition profile and opportunity score for each keyword. Free key: console.cloud.google.com/apis/credentials -> Create credentials -> API key, then enable 'YouTube Data API v3' in the API Library for that project. Scoring one keyword costs ~102 units of your 10,000/day free quota, so keep 'Max scored keywords' at or below 90.

## `expansionLevel` (type: `string`):

How hard to expand each seed against YouTube autocomplete. 'basic' = the seed alone (1 query per seed). 'standard' = the seed plus 10 suffix modifiers (how, what, why, best, vs, for, without, tutorial, review, free) and 3 prefix modifiers (14 queries per seed). 'deep' = standard plus alphabet soup, seed + a..z (40 queries per seed).

## `regionCode` (type: `string`):

Two-letter ISO 3166-1 country code. Controls which country's autocomplete suggestions are returned (the 'gl' parameter) and which country's search results are used for scoring (YouTube Data API 'regionCode'). Autocomplete differs a lot by country.

## `language` (type: `string`):

Two-letter ISO 639-1 language code. Controls the language of autocomplete suggestions (the 'hl' parameter) and biases scoring search results toward that language ('relevanceLanguage').

## `maxScoredKeywords` (type: `integer`):

How many keywords get the full competition profile and opportunity score. Only used when an API key is supplied. Each scored keyword costs about 102 units of your daily YouTube API quota (10,000/day by default), so 90 is the practical ceiling for one day on a fresh key. Keywords are scored in order of autocomplete prominence, spread evenly across your seeds.

## `maxKeywords` (type: `integer`):

Hard cap on the total number of keyword records saved to the dataset, scored and unscored combined. Scored keywords are always saved first.

## `includeUnscoredKeywords` (type: `boolean`):

Save the remaining autocomplete keywords that were discovered but not scored (because they fell outside 'Max scored keywords', or because no API key was given). Turn this off to get only fully scored keywords.

## `topResultsPerKeyword` (type: `integer`):

How many of the top YouTube search results are profiled per scored keyword. All competition metrics (median views, median subscribers, small-channel share, median upload age) are computed over this set. Higher is more stable but does not cost extra API quota.

## `smallChannelMaxSubscribers` (type: `integer`):

A ranking channel with fewer subscribers than this counts as a 'small channel'. The share of top results held by small channels is the strongest single signal that a keyword is winnable, and it feeds the competition score.

## `proxyConfiguration` (type: `object`):

Optional. Off by default so the actor is cheap to try. Turn on Apify Proxy (datacenter is enough) if YouTube's autocomplete endpoint starts rate-limiting your runs, or to pull suggestions as if from another country.

## Actor input object example

```json
{
  "seeds": [
    "ai tools",
    "home gym"
  ],
  "expansionLevel": "standard",
  "regionCode": "US",
  "language": "en",
  "maxScoredKeywords": 20,
  "maxKeywords": 200,
  "includeUnscoredKeywords": true,
  "topResultsPerKeyword": 25,
  "smallChannelMaxSubscribers": 100000,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `scoredKeywords` (type: `string`):

Every discovered autocomplete keyword with its demand, competition and opportunity scores.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "seeds": [
        "ai tools",
        "home gym"
    ],
    "regionCode": "US",
    "language": "en"
};

// Run the Actor and wait for it to finish
const run = await client.actor("mibedk/youtube-keyword-opportunity-scorer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "seeds": [
        "ai tools",
        "home gym",
    ],
    "regionCode": "US",
    "language": "en",
}

# Run the Actor and wait for it to finish
run = client.actor("mibedk/youtube-keyword-opportunity-scorer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "seeds": [
    "ai tools",
    "home gym"
  ],
  "regionCode": "US",
  "language": "en"
}' |
apify call mibedk/youtube-keyword-opportunity-scorer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,mibedk/youtube-keyword-opportunity-scorer"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/G2vcV9ctSa2lfYtwa/builds/4xmz1FNl37vqGaQWw/openapi.json
