# YouTube Opportunity Finder (`apt_marble/youtube-opportunity-finder`) Actor

Find YouTube video ideas with high audience demand and low creator competition — before they saturate. Scores every topic for demand vs. competition, spots gaps in your competitor's channels, and delivers ready-to-use titles, thumbnails, and hooks (AI) straight to Notion.

- **URL**: https://apify.com/apt\_marble/youtube-opportunity-finder.md
- **Developed by:** [Hamza](https://apify.com/apt_marble) (community)
- **Categories:** SEO tools, Social media, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $100.00 / 1,000 opportunity discovereds

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## YouTube Opportunity Finder

**Stop guessing what to make next.** Most YouTube tools hand you a spreadsheet of views, likes, and subscribers. This one hands you a decision:

> ### What videos should I create *right now* that have high audience demand but low creator competition?

YouTube Opportunity Finder continuously analyzes your niche, scores every topic for **demand vs. competition**, and surfaces the **underserved opportunities** — each with ready-to-use **titles, thumbnail concepts, and hooks** — delivered straight to your **Notion** workspace.

It doesn't show you trending videos. **It tells you what to make next.**

---

### 👤 Who it's for

- **Creators** deciding their next video instead of copying saturated topics
- **Faceless / niche channel operators** scaling output across many ideas
- **Agencies & strategists** doing content research at scale
- Anyone who wants to publish *before* a topic gets crowded

---

### 🎯 What you get

| Output | The question it answers |
|---|---|
| **Content Opportunities** | "Make this video — strong demand, manageable competition." |
| **Emerging Topics** | "Demand here is accelerating — get in early." |
| **Viral Opportunities** | "Rare window: extreme growth, almost no competition." |
| **Competitor Gaps** | "A channel you follow hasn't covered this — you should." |
| **Daily & Weekly Reports** | "Here's where the whole niche is moving." |

Every opportunity comes with three transparent scores and a complete creative package.

---

### 📊 How it scores

Each topic is rated on a simple, transparent 0–100 model:

- **Demand Score** — audience interest: total views, view velocity, engagement, freshness, and search presence.
- **Competition Score** — how hard it is to rank: number of creators, recent uploads, channel sizes, and search saturation.
- **Opportunity Score** — the headline metric: **High Demand + High Growth + Low Competition + High Engagement.**

The higher the Opportunity Score, the better the topic to publish next.

---

### 🎬 Built-in AI title studio

For every opportunity, you get a complete, ready-to-shoot package:

- **10 video titles** — varied angles (how-to, listicle, contrarian, personal story, comparison…)
- **5 thumbnail concepts** — visual, text overlay, and emotion
- **5 hook ideas** — spoken opening lines for the first 5 seconds

No extra setup — the AI ideas are generated automatically and written into Notion alongside each opportunity.

---

### 🕵️ Content Gap engine (analyze competitors)

Add any competitor channels (URLs, `@handles`, or IDs) and the actor finds the high-value topics in your niche that **they haven't covered yet**:

> **@MrBeast is missing:** *AI Agents · Short-form storytelling · Interactive challenges*

It's the fastest way to find the white space your competitors left open.

---

### 🧠 Opportunity categories

Every opportunity is tagged so you know *why* it's a chance:

- **Emerging Topic** — demand is growing fast; an early-mover window.
- **Underserved Niche** — high demand, low supply, right now.
- **Rising Search Query** — search interest is climbing.
- **Competitor Gap** — a popular topic a tracked competitor skipped.

---

### 💡 Example

**Input**

```json
{
  "seedKeywords": ["World Cup 2026", "FIFA", "Champions League"],
  "competitorChannels": ["@FIFA"],
  "country": "us",
  "language": "en"
}
````

**Output (a sample opportunity)**

| Topic | Demand | Competition | Opportunity |
|---|---|---|---|
| World Cup 2026 Host Cities | 90 | 29 | 92 |
| Champions League Dark Horses | 86 | 34 | 89 |
| World Cup 2026 Qualified Teams | 82 | 25 | 88 |

> **🎯 World Cup 2026 Host Cities** — Opportunity 92 · *Emerging Topic*
>
> **Suggested titles**
>
> - All 16 World Cup 2026 Host Cities, Ranked
> - The World Cup 2026 City Nobody Is Talking About
> - How To Plan Your World Cup 2026 Trip (City-by-City Guide)
>
> **Thumbnail concepts** · **Hook ideas** … (included for every opportunity)

***

### ⚙️ How it works

1. **Discover** — expands each niche into many candidate topics (autocomplete + related searches) and tracks the videos behind them.
2. **Measure** — scores demand and competition for every topic, and tracks how each one grows over time.
3. **Surface** — flags the opportunities, emerging topics, viral windows, and competitor gaps worth your time.
4. **Deliver** — writes everything — with titles, thumbnails, and hooks — into your Notion workspace.

This is a **continuous platform, not a one-shot scraper.** Opportunities and competitor gaps appear from the **first run**; the growth-based signals (Emerging, Viral) sharpen as history accumulates.

***

### 🚀 Getting started

1. **Enter your niches** in `seedKeywords` (e.g. `["AI tools", "air fryer recipes"]`).
2. *(Optional)* **Add competitor channels** to unlock the Content Gap engine.
3. *(Optional)* **Connect Notion** + paste a parent page — your reports are organized into sub-pages automatically. Leave it empty to send results to a dataset instead.
4. **Run it on a daily schedule** (Schedules → Create → cron `0 8 * * *`). Each daily run builds on the last, so trends and reports accumulate.

> 💡 **Run daily, not back-to-back.** Growth and Emerging-Topic signals are measured *between* runs, so a daily cadence gives the best results.

***

### 📥 Key input fields

| Field | Description |
|---|---|
| `seedKeywords` | **Required.** The niches to mine for opportunities. |
| `competitorChannels` | Channel URLs / `@handles` / IDs to analyze for content gaps. |
| `notionConnector` + `notionParentPage` | Connect your Notion workspace and choose where reports are written. |
| `opportunityScoreMin` | Minimum score (0–100) for a topic to qualify. Lower = more (looser) opportunities. |
| `discoveryDateFilter` | How recent the videos to analyze should be (today / week / month / year). |
| `country` / `language` | Localize results (e.g. `us` / `en`). |

The defaults are tuned to work out of the box — most users only need to set `seedKeywords`.

***

### 📤 Output

- **Notion** (if connected): organized pages under *Content Opportunities, Emerging Topics, Viral Opportunities, Competitor Gaps, Daily Reports, Weekly Reports* — each opportunity a page with scores, titles, thumbnails, and hooks.
- **Dataset**: every opportunity as a structured row (topic, demand/competition/opportunity scores, category, suggested titles/thumbnails/hooks) — ready to export or pipe into other tools.

***

### 💳 Pricing — you pay for discoveries, not scraping

This actor uses **pay-per-event** pricing: you're only charged when it delivers something **actionable** — never for runs, compute, or empty results.

***

### ❓ FAQ

**Do I need my own API keys?** No. Just enter your niches (and optionally connect your Notion). Everything else is handled for you.

**My first run found few opportunities — is it broken?** No. Mainstream, saturated niches (e.g. "iPhone 18") correctly score as high-competition. Try a more specific niche, add competitor channels, or lower `opportunityScoreMin`. Growth-based signals also need a few daily runs to warm up.

**Why daily?** Demand and competition shift over days, not minutes. A daily schedule produces the cleanest growth signals and keeps your Notion fresh.

**Will it spam my Notion?** No — a cooldown prevents re-posting the same opportunity, and reports are paced (daily/weekly).

***

**Find the videos worth making — before everyone else does.**

# Actor input Schema

## `seedKeywords` (type: `array`):

The niches to mine for opportunities. Each tick expands every keyword into many candidate search queries (autocomplete + related searches), tracks the videos found, and scores demand vs competition to surface what you should publish next.

## `competitorChannels` (type: `array`):

Optional. Channel URLs, @handles, or channel IDs (e.g. https://youtube.com/@MrBeast, @mkbhd, UCX6OQ3DkcsbYNE6H8uQQuVA). For each, the actor finds high-value topics in your niche that the channel has NOT covered — "you are missing these opportunities".

## `notionConnector` (type: `string`):

Connect your Notion workspace. Content opportunities, emerging topics, viral opportunities, competitor gaps and daily/weekly reports are written here automatically. Leave empty to run in dry-run mode (reports go to a dataset instead of Notion).

## `notionParentPage` (type: `string`):

The page under which the opportunity workspace (Content Opportunities, Emerging Topics, Viral Opportunities, Competitor Gaps, Daily/Weekly Reports) is built. Required when a Notion connector is set.

## `opportunityScoreMin` (type: `integer`):

A topic must reach this opportunity score (0–100) to bill as a discovered opportunity. Lower = more (lower-confidence) opportunities.

## `demandScoreMin` (type: `integer`):

A topic must also reach this demand score (0–100) before it can bill as an opportunity, so low-demand low-competition noise is filtered out.

## `emergingGrowthMin` (type: `integer`):

Minimum growth score (0–100, demand vs the topic's baseline) for an Emerging Topic event. Needs ≥3 ticks of history.

## `viralOpportunityScoreMin` (type: `integer`):

Minimum opportunity score (0–100) for the premium Viral Opportunity event (also requires Low competition and strong growth).

## `competitorGapScoreMin` (type: `integer`):

A missing topic must reach this opportunity score (0–100) to bill as a Competitor Gap.

## `topicMinVideos` (type: `integer`):

How many related videos must cluster together to form a scoreable topic.

## `opportunityCooldownHours` (type: `integer`):

How long before the same opportunity can bill again (unless its score escalates).

## `emergingTopicCooldownHours` (type: `integer`):

How long before the same emerging topic can bill again (unless its growth escalates).

## `competitorGapCooldownHours` (type: `integer`):

How long before the same channel+topic gap can bill again (unless its value escalates).

## `viralOpportunityCooldownHours` (type: `integer`):

How long before the same viral opportunity can bill again (unless its score escalates).

## `expandTopics` (type: `boolean`):

Use YouTube autocomplete + related searches to expand each seed niche into many candidate search queries (the topic-discovery engine). Turn off to search only the exact seed keywords.

## `maxCandidateTopicsPerSeed` (type: `integer`):

How many autocomplete-expanded queries to actually search per seed niche each tick. Higher = broader discovery, more data requests.

## `maxVideosPerKeyword` (type: `integer`):

How many videos to add to the tracked set per search query each tick.

## `enableContentGap` (type: `boolean`):

Analyze the competitor channels above for missing high-value topics. No effect if no competitor channels are provided.

## `enableTitleGen` (type: `boolean`):

For each opportunity, generate 10 titles, 5 thumbnail concepts and 5 hook ideas. Uses an AI model when the OF\_LLM\_API\_KEY environment variable is set (e.g. Kimi/Moonshot), otherwise falls back to deterministic templates.

## `maxChannelVideos` (type: `integer`):

How many recent uploads to read per competitor channel when computing its covered topics.

## `discoverySort` (type: `string`):

How to pick videos to track. 'View count' (recommended) tracks videos with real view momentum so demand is measurable; 'Upload date' tracks the newest (often near-zero-view) uploads.

## `discoveryDateFilter` (type: `string`):

Only discover videos uploaded within this window. 'This month' is a good default for opportunity finding (recent supply without being too sparse).

## `runMode` (type: `string`):

tick = a normal scheduled run. backfill = discover + snapshot only (seed history). dryRun = force local reporter. reporterTest = probe the Notion workspace.

## `reportHourUtc` (type: `integer`):

The UTC hour at which daily and weekly reports are published. Match this to your daily schedule's hour.

## `weeklyReportDow` (type: `integer`):

Day of week the weekly report is published (0 = Sunday … 6 = Saturday).

## `country` (type: `string`):

Two-letter country code for region-specific results.

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

Two-letter language code for results.

## `resetState` (type: `boolean`):

Turn ON for ONE run to wipe all stored data (tracked videos, history, baselines, cached Notion pages) and rebuild from scratch — useful after you change niches or your Notion page. ⚠️ Turn it OFF again after that run, or every run resets and growth signals never accumulate.

## `discoveryPagesPerKeyword` (type: `integer`):

Operator-only (hidden; set via JSON). How many search-result pages to page through per query during discovery.

## `maxTrackedVideos` (type: `integer`):

Operator-only (hidden; set via JSON). The tracked set is capped here; lowest-value videos are evicted when exceeded.

## `snapshotConcurrency` (type: `integer`):

Operator-only (hidden; set via JSON). How many tracked videos to snapshot in parallel each tick.

## `snapshotProxyFallback` (type: `boolean`):

Internal: retries zeroed view counts through the premium proxy for accuracy. Hidden from the input form; always on.

## `maxEventsPerTick` (type: `integer`):

Operator-only (hidden; set via JSON). Safety cap on how many billable events a single run can emit. The highest-value events are kept; the rest can re-emit on a later tick. Set 0 for unlimited.

## `forceDaily` (type: `boolean`):

Operator-only (hidden from the form; set via JSON input). Fires a daily report on this run regardless of the schedule. If left on it bills a daily report every run.

## `forceWeekly` (type: `boolean`):

Operator-only (hidden from the form; set via JSON input). Fires a weekly report on this run regardless of the schedule. If left on it bills a weekly report every run.

## Actor input object example

```json
{
  "seedKeywords": [
    "world cup 2026",
    "fifa 2026",
    "champions league"
  ],
  "competitorChannels": [],
  "opportunityScoreMin": 55,
  "demandScoreMin": 30,
  "emergingGrowthMin": 45,
  "viralOpportunityScoreMin": 85,
  "competitorGapScoreMin": 50,
  "topicMinVideos": 3,
  "opportunityCooldownHours": 72,
  "emergingTopicCooldownHours": 48,
  "competitorGapCooldownHours": 168,
  "viralOpportunityCooldownHours": 48,
  "expandTopics": true,
  "maxCandidateTopicsPerSeed": 15,
  "maxVideosPerKeyword": 15,
  "enableContentGap": true,
  "enableTitleGen": true,
  "maxChannelVideos": 40,
  "discoverySort": "viewCount",
  "discoveryDateFilter": "month",
  "runMode": "tick",
  "reportHourUtc": 8,
  "weeklyReportDow": 1,
  "country": "us",
  "language": "en",
  "resetState": false,
  "discoveryPagesPerKeyword": 1,
  "maxTrackedVideos": 4000,
  "snapshotConcurrency": 4,
  "snapshotProxyFallback": true,
  "maxEventsPerTick": 100,
  "forceDaily": false,
  "forceWeekly": false
}
```

# Actor output Schema

## `opportunities` (type: `string`):

The feed of billable + non-billable opportunity events produced this run.

# 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 = {
    "seedKeywords": [
        "world cup 2026",
        "fifa 2026",
        "champions league"
    ],
    "competitorChannels": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("apt_marble/youtube-opportunity-finder").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 = {
    "seedKeywords": [
        "world cup 2026",
        "fifa 2026",
        "champions league",
    ],
    "competitorChannels": [],
}

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

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

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

```

## CLI example

```bash
echo '{
  "seedKeywords": [
    "world cup 2026",
    "fifa 2026",
    "champions league"
  ],
  "competitorChannels": []
}' |
apify call apt_marble/youtube-opportunity-finder --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=apt_marble/youtube-opportunity-finder",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "YouTube Opportunity Finder",
        "description": "Find YouTube video ideas with high audience demand and low creator competition — before they saturate. Scores every topic for demand vs. competition, spots gaps in your competitor's channels, and delivers ready-to-use titles, thumbnails, and hooks (AI) straight to Notion.",
        "version": "0.0",
        "x-build-id": "es60cDGjyRV76vQOd"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/apt_marble~youtube-opportunity-finder/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-apt_marble-youtube-opportunity-finder",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/apt_marble~youtube-opportunity-finder/runs": {
            "post": {
                "operationId": "runs-sync-apt_marble-youtube-opportunity-finder",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/apt_marble~youtube-opportunity-finder/run-sync": {
            "post": {
                "operationId": "run-sync-apt_marble-youtube-opportunity-finder",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "seedKeywords"
                ],
                "properties": {
                    "seedKeywords": {
                        "title": "Niches / seed keywords",
                        "minItems": 1,
                        "type": "array",
                        "description": "The niches to mine for opportunities. Each tick expands every keyword into many candidate search queries (autocomplete + related searches), tracks the videos found, and scores demand vs competition to surface what you should publish next.",
                        "default": [
                            "world cup 2026",
                            "fifa 2026",
                            "champions league"
                        ],
                        "items": {
                            "type": "string"
                        }
                    },
                    "competitorChannels": {
                        "title": "Competitor channels (Content Gap engine)",
                        "type": "array",
                        "description": "Optional. Channel URLs, @handles, or channel IDs (e.g. https://youtube.com/@MrBeast, @mkbhd, UCX6OQ3DkcsbYNE6H8uQQuVA). For each, the actor finds high-value topics in your niche that the channel has NOT covered — \"you are missing these opportunities\".",
                        "default": [],
                        "items": {
                            "type": "string"
                        }
                    },
                    "notionConnector": {
                        "title": "Notion connector",
                        "type": "string",
                        "description": "Connect your Notion workspace. Content opportunities, emerging topics, viral opportunities, competitor gaps and daily/weekly reports are written here automatically. Leave empty to run in dry-run mode (reports go to a dataset instead of Notion)."
                    },
                    "notionParentPage": {
                        "title": "Notion parent page (ID or URL)",
                        "type": "string",
                        "description": "The page under which the opportunity workspace (Content Opportunities, Emerging Topics, Viral Opportunities, Competitor Gaps, Daily/Weekly Reports) is built. Required when a Notion connector is set."
                    },
                    "opportunityScoreMin": {
                        "title": "Opportunity: min score",
                        "minimum": 0,
                        "maximum": 100,
                        "type": "integer",
                        "description": "A topic must reach this opportunity score (0–100) to bill as a discovered opportunity. Lower = more (lower-confidence) opportunities.",
                        "default": 55
                    },
                    "demandScoreMin": {
                        "title": "Opportunity: min demand score",
                        "minimum": 0,
                        "maximum": 100,
                        "type": "integer",
                        "description": "A topic must also reach this demand score (0–100) before it can bill as an opportunity, so low-demand low-competition noise is filtered out.",
                        "default": 30
                    },
                    "emergingGrowthMin": {
                        "title": "Emerging topic: min growth score",
                        "minimum": 0,
                        "maximum": 100,
                        "type": "integer",
                        "description": "Minimum growth score (0–100, demand vs the topic's baseline) for an Emerging Topic event. Needs ≥3 ticks of history.",
                        "default": 45
                    },
                    "viralOpportunityScoreMin": {
                        "title": "Viral opportunity: min score",
                        "minimum": 0,
                        "maximum": 100,
                        "type": "integer",
                        "description": "Minimum opportunity score (0–100) for the premium Viral Opportunity event (also requires Low competition and strong growth).",
                        "default": 85
                    },
                    "competitorGapScoreMin": {
                        "title": "Competitor gap: min value",
                        "minimum": 0,
                        "maximum": 100,
                        "type": "integer",
                        "description": "A missing topic must reach this opportunity score (0–100) to bill as a Competitor Gap.",
                        "default": 50
                    },
                    "topicMinVideos": {
                        "title": "Topic: min videos",
                        "minimum": 2,
                        "type": "integer",
                        "description": "How many related videos must cluster together to form a scoreable topic.",
                        "default": 3
                    },
                    "opportunityCooldownHours": {
                        "title": "Opportunity cooldown (hours)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "How long before the same opportunity can bill again (unless its score escalates).",
                        "default": 72
                    },
                    "emergingTopicCooldownHours": {
                        "title": "Emerging-topic cooldown (hours)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "How long before the same emerging topic can bill again (unless its growth escalates).",
                        "default": 48
                    },
                    "competitorGapCooldownHours": {
                        "title": "Competitor-gap cooldown (hours)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "How long before the same channel+topic gap can bill again (unless its value escalates).",
                        "default": 168
                    },
                    "viralOpportunityCooldownHours": {
                        "title": "Viral-opportunity cooldown (hours)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "How long before the same viral opportunity can bill again (unless its score escalates).",
                        "default": 48
                    },
                    "expandTopics": {
                        "title": "Expand niches into candidate topics",
                        "type": "boolean",
                        "description": "Use YouTube autocomplete + related searches to expand each seed niche into many candidate search queries (the topic-discovery engine). Turn off to search only the exact seed keywords.",
                        "default": true
                    },
                    "maxCandidateTopicsPerSeed": {
                        "title": "Max candidate topics per niche",
                        "minimum": 0,
                        "maximum": 200,
                        "type": "integer",
                        "description": "How many autocomplete-expanded queries to actually search per seed niche each tick. Higher = broader discovery, more data requests.",
                        "default": 15
                    },
                    "maxVideosPerKeyword": {
                        "title": "Max videos discovered per query",
                        "minimum": 1,
                        "maximum": 200,
                        "type": "integer",
                        "description": "How many videos to add to the tracked set per search query each tick.",
                        "default": 15
                    },
                    "enableContentGap": {
                        "title": "Enable Content Gap engine",
                        "type": "boolean",
                        "description": "Analyze the competitor channels above for missing high-value topics. No effect if no competitor channels are provided.",
                        "default": true
                    },
                    "enableTitleGen": {
                        "title": "Enable AI title / thumbnail / hook generation",
                        "type": "boolean",
                        "description": "For each opportunity, generate 10 titles, 5 thumbnail concepts and 5 hook ideas. Uses an AI model when the OF_LLM_API_KEY environment variable is set (e.g. Kimi/Moonshot), otherwise falls back to deterministic templates.",
                        "default": true
                    },
                    "maxChannelVideos": {
                        "title": "Competitor: videos analyzed per channel",
                        "minimum": 5,
                        "maximum": 200,
                        "type": "integer",
                        "description": "How many recent uploads to read per competitor channel when computing its covered topics.",
                        "default": 40
                    },
                    "discoverySort": {
                        "title": "Discover by",
                        "enum": [
                            "viewCount",
                            "relevance",
                            "uploadDate",
                            "rating"
                        ],
                        "type": "string",
                        "description": "How to pick videos to track. 'View count' (recommended) tracks videos with real view momentum so demand is measurable; 'Upload date' tracks the newest (often near-zero-view) uploads.",
                        "default": "viewCount"
                    },
                    "discoveryDateFilter": {
                        "title": "Discover within",
                        "enum": [
                            "today",
                            "week",
                            "month",
                            "year",
                            ""
                        ],
                        "type": "string",
                        "description": "Only discover videos uploaded within this window. 'This month' is a good default for opportunity finding (recent supply without being too sparse).",
                        "default": "month"
                    },
                    "runMode": {
                        "title": "Run mode",
                        "enum": [
                            "tick",
                            "backfill",
                            "dryRun",
                            "reporterTest"
                        ],
                        "type": "string",
                        "description": "tick = a normal scheduled run. backfill = discover + snapshot only (seed history). dryRun = force local reporter. reporterTest = probe the Notion workspace.",
                        "default": "tick"
                    },
                    "reportHourUtc": {
                        "title": "Daily/weekly report hour (UTC)",
                        "minimum": 0,
                        "maximum": 23,
                        "type": "integer",
                        "description": "The UTC hour at which daily and weekly reports are published. Match this to your daily schedule's hour.",
                        "default": 8
                    },
                    "weeklyReportDow": {
                        "title": "Weekly report day of week (0=Sun)",
                        "minimum": 0,
                        "maximum": 6,
                        "type": "integer",
                        "description": "Day of week the weekly report is published (0 = Sunday … 6 = Saturday).",
                        "default": 1
                    },
                    "country": {
                        "title": "Country (gl)",
                        "type": "string",
                        "description": "Two-letter country code for region-specific results.",
                        "default": "us"
                    },
                    "language": {
                        "title": "Language (hl)",
                        "type": "string",
                        "description": "Two-letter language code for results.",
                        "default": "en"
                    },
                    "resetState": {
                        "title": "Start fresh (reset stored data)",
                        "type": "boolean",
                        "description": "Turn ON for ONE run to wipe all stored data (tracked videos, history, baselines, cached Notion pages) and rebuild from scratch — useful after you change niches or your Notion page. ⚠️ Turn it OFF again after that run, or every run resets and growth signals never accumulate.",
                        "default": false
                    },
                    "discoveryPagesPerKeyword": {
                        "title": "Discovery pages per query",
                        "minimum": 1,
                        "maximum": 10,
                        "type": "integer",
                        "description": "Operator-only (hidden; set via JSON). How many search-result pages to page through per query during discovery.",
                        "default": 1
                    },
                    "maxTrackedVideos": {
                        "title": "Max tracked videos",
                        "minimum": 10,
                        "maximum": 100000,
                        "type": "integer",
                        "description": "Operator-only (hidden; set via JSON). The tracked set is capped here; lowest-value videos are evicted when exceeded.",
                        "default": 4000
                    },
                    "snapshotConcurrency": {
                        "title": "Snapshot concurrency",
                        "minimum": 1,
                        "maximum": 10,
                        "type": "integer",
                        "description": "Operator-only (hidden; set via JSON). How many tracked videos to snapshot in parallel each tick.",
                        "default": 4
                    },
                    "snapshotProxyFallback": {
                        "title": "Proxy fallback for view counts",
                        "type": "boolean",
                        "description": "Internal: retries zeroed view counts through the premium proxy for accuracy. Hidden from the input form; always on.",
                        "default": true
                    },
                    "maxEventsPerTick": {
                        "title": "Max billable events per tick",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Operator-only (hidden; set via JSON). Safety cap on how many billable events a single run can emit. The highest-value events are kept; the rest can re-emit on a later tick. Set 0 for unlimited.",
                        "default": 100
                    },
                    "forceDaily": {
                        "title": "Force a daily report now (operator)",
                        "type": "boolean",
                        "description": "Operator-only (hidden from the form; set via JSON input). Fires a daily report on this run regardless of the schedule. If left on it bills a daily report every run.",
                        "default": false
                    },
                    "forceWeekly": {
                        "title": "Force a weekly report now (operator)",
                        "type": "boolean",
                        "description": "Operator-only (hidden from the form; set via JSON input). Fires a weekly report on this run regardless of the schedule. If left on it bills a weekly report every run.",
                        "default": false
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
