# YouTube Competitor Channel Monitor (`coregent/youtube-competitor-channel-monitor`) Actor

Monitor competitor YouTube channels and get only what changed: new videos, Shorts and live streams, breakout videos versus each channel's own baseline, view and subscriber surges, upload cadence and content-mix shifts, plus a cross-competitor ranking. No login, no API key needed.

- **URL**: https://apify.com/coregent/youtube-competitor-channel-monitor.md
- **Developed by:** [Delowar Munna](https://apify.com/coregent) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$3.00 / 1,000 channel checks

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 Competitor Channel Monitor

![YouTube Competitor Channel Monitor](https://raw.githubusercontent.com/coregentdevspace/youtube-competitor-channel-monitor-assets/main/thumbnail-youtube-competitor-channel-monitor.png)

**Track your competitors' YouTube channels and get only what changed:** new videos, Shorts and live streams, videos breaking out against the channel's own normal performance, view and subscriber surges, upload-cadence changes, and Shorts/long-form mix shifts. Every run also ranks the competitors against each other.

Schedule it daily and connect it to Slack, email, Google Sheets, Make, Zapier or n8n to get a competitor briefing without opening YouTube. You don't need a login, cookies or a YouTube API key.

### ✨ What it answers

- What did my competitors publish since the last run?
- Which competitor has a video doing far better than their usual content?
- Whose channel views are suddenly accelerating?
- Did a competitor start posting more often, or go quiet?
- Is a competitor shifting toward Shorts?
- Which competitor has the strongest recent median views, and who uploads most often?
- Which title keywords keep coming up, and which are new?

### 🆚 How is this different?

| | Answers | This actor |
|---|---|---|
| **YouTube Channel Scraper** | "What content and stats exist now?" | Remembers what it has seen and reports only the **changes** since the last run |
| **YouTube Channel Growth Tracker** | "How has this channel grown over months?" | Tracks growth **lightly** (since the last run, plus a 7-day trend). It focuses on **competitor activity and how each new video performs** |
| **Other "new videos only" monitors** | New uploads, subscriber and view deltas | Also compares each video with the channel's own videos at the **same age**, detects breakouts, tracks cadence and content mix, and ranks competitors |

### 🚀 Quick start

1. Add the competitor channels: `@handle`, a channel URL, a channel ID or any of their video URLs.
2. Give the monitor a name, such as `tech-competitors`.
3. Run it once. The first run saves a **baseline** for each channel and does not report old videos as new.
4. Schedule it (daily is ideal). Each later run reports only what changed.

```json
{
    "channels": ["@competitor1", "@competitor2"],
    "monitorKey": "my-competitors"
}
```

With the default input, a run checks **2 channels** (`@mkbhd`, `@veritasium`) and reads up to 30 recent videos and 30 recent Shorts from each. It returns one channel summary row per channel, plus an event row for each change found.

### 🧭 Modes

| Mode | What it does |
|---|---|
| `MONITOR` (default) | Reports only what changed since the last run. A channel seen for the first time gets a baseline (`BASELINE_CREATED`) instead. |
| `BASELINE` | Saves or resets the state without reporting changes. Use it to restart a monitor from a clean point. |
| `COMPARE` | Returns a full current summary and ranking for every channel. **It does not change the saved state**, so the next `MONITOR` run still reports everything new. |

### 📦 What you get

#### Event rows (`rowType: youtube_competitor_event`)

| Event | When |
|---|---|
| `NEW_VIDEO` / `NEW_SHORT` / `NEW_LIVE_STREAM` / `NEW_PREMIERE` | A video ID the monitor has not seen before |
| `VIDEO_METRICS_UPDATED` | A recent video's views, likes or comments grew past your thresholds |
| `VIDEO_BREAKOUT` | A recent video reaches the breakout tier against the channel's own baseline |
| `VIDEO_UNDERPERFORMING` | Opt-in. A video at least a day old is well below the channel's views at the same age |
| `CHANNEL_VIEW_SURGE` | Total channel views per day jumped against the previous 7-day rate |
| `CHANNEL_GROWTH_SURGE` | Subscribers per day jumped, counted only when the change is larger than YouTube's rounding |
| `UPLOAD_FREQUENCY_INCREASED` / `UPLOAD_FREQUENCY_DECREASED` | Uploads per week changed by your threshold |
| `SHORTS_SHARE_INCREASED` / `CONTENT_MIX_CHANGED` | The Shorts / long-form / live mix shifted |
| `HANDLE_CHANGED` | The channel changed its @handle. History is kept, because channels are identified by channel ID |
| `CHANNEL_DORMANT` | No uploads for your dormancy period (reported once) |

Each event includes `confidence` (HIGH / MEDIUM / LOW), the `channel`, the `video` (where relevant), metric `metrics` deltas, `performance`, a `change` block, `signals`, `alertTriggered` and `alertReasons`.

##### Output preview: Competitor events

![Competitor events table view](https://raw.githubusercontent.com/coregentdevspace/youtube-competitor-channel-monitor-assets/main/youtube-competitor-channel-monitor-output-competitor-events-table-view.png)

This is a first run of two channels with *Report recent videos on the first run* turned on, so every upload of the last 45 days appears once as `NEW_VIDEO` or `NEW_SHORT` with `confidence: LOW`. On later runs, only uploads that appear since the previous run are reported, with HIGH or MEDIUM confidence.

Example event from that run (`tags` and `description` shortened):

```json
{
    "rowType": "youtube_competitor_event",
    "monitorKey": "readme-screenshots-2026-10-27",
    "eventId": "444229275a43601f7bcf",
    "eventType": "NEW_VIDEO",
    "confidence": "LOW",
    "detectedAt": "2026-10-26T13:08:20.621Z",
    "channel": {
        "channelId": "UCHnyfMqiRRG1u-2MsSQLbXA",
        "handle": "@veritasium",
        "title": "Veritasium",
        "channelUrl": "https://www.youtube.com/@veritasium",
        "subscriberCount": 21400000,
        "subscriberCountText": "21.4M",
        "subscriberCountPrecision": "ROUNDED",
        "totalViews": 4695846306,
        "videoCount": 543,
        "avatarUrl": "https://yt3.ggpht.com/7vCbvtCqtjQ3YLgsJt7Y952MQV1sBvhllSCSxHP8_sVZdcPCBrITfhkN2RdyCuwPnsByq-1GoA=s800-c-k-c0x00ffffff-no-rj"
    },
    "video": {
        "videoId": "JsBZOcqZerk",
        "videoUrl": "https://www.youtube.com/watch?v=JsBZOcqZerk",
        "title": "The Insane Real Engineering of the Nazi Enigma Machine",
        "publishedAt": "2026-09-21T17:49:57Z",
        "contentType": "VIDEO",
        "isShort": false,
        "isLive": false,
        "isUpcoming": false,
        "durationSeconds": 2862,
        "views": 20951141,
        "likes": 149164,
        "comments": 7437,
        "metricsExact": true,
        "ageHours": 835.3,
        "channelId": "UCHnyfMqiRRG1u-2MsSQLbXA",
        "tags": ["veritasium", "science", "physics", "engineering", "enigma"],
        "category": "27",
        "language": "en",
        "hashtags": [],
        "description": "How did Bletchley Park's codebreakers crack the \"unbreakable\" enigma? …",
        "thumbnailUrl": "https://i.ytimg.com/vi/JsBZOcqZerk/maxresdefault.jpg"
    },
    "metrics": null,
    "performance": {
        "ageBucket": "30d+",
        "viewsPerHourSincePublish": 25082,
        "performanceIndex": 3.11,
        "performanceTier": "BREAKOUT",
        "performanceBasis": "AGE_BUCKET",
        "baselineMedianViewsAtAge": 6747104,
        "baselineMedianViews": 6747104,
        "baselineSampleSize": 10
    },
    "change": null,
    "signals": ["ABOVE_CHANNEL_BASELINE", "HIGH_VIEW_VELOCITY", "HIGH_LIKE_VELOCITY", "HIGH_COMMENT_VELOCITY"],
    "firstSeenAt": "2026-10-26T13:08:20.621Z",
    "newToMonitor": true,
    "alertTriggered": true,
    "alertReasons": ["NEW_VIDEO", "ABOVE_CHANNEL_BASELINE", "HIGH_VIEW_VELOCITY", "HIGH_LIKE_VELOCITY", "HIGH_COMMENT_VELOCITY"],
    "status": "SUCCESS"
}
```

`category` is YouTube's numeric category ID (`27` is Education). `performanceIndex: 3.11` means 3.11× the median views of Veritasium's previous long-form videos at the same age.

#### Channel summary rows (`eventType: CHANNEL_CHECKED`)

There is one row per channel on every run, so a run always has output, even when nothing changed. Each row contains:

- **status**: `BASELINE_CREATED`, `SUCCESS`, `NO_CHANGES` or an error status (see below)
- **channel**: ID, handle, title, subscribers (with precision), total views, video count, country, topics
- **summary**: new videos/Shorts/lives this run, breakout videos in the window, median and average recent views (overall and by type), uploads per week, subscriber and view deltas, views per day, Shorts share, days since last upload, top 3 recent videos by `performanceIndex`
- **cadence**: uploads in the last 7 and 30 days, uploads per week, the previous 30 days (when that history is known), days since the last upload, and upload consistency
- **contentMix**: counts and shares of long-form, Shorts and live in the lookback window
- **growth**: subscriber / total-view / video-count deltas since the last run, views per day, and the prior 7-day rates
- **titleKeywords**: `topTitleKeywords`, `newTitleKeywords`, `risingTitleKeywords`
- **competitorRank**: the channel's rank by `RECENT_PERFORMANCE`, `VIEW_GROWTH`, `UPLOAD_ACTIVITY` and `BREAKOUT_COUNT`. `VIEW_GROWTH` needs a previous run, so it is missing on the first run, as is the `growth` block
- **coverage**: which content types were read, how many items, how many had exact metrics, subscriber precision, and lookback coverage

Example summary row from the same first run (`channel.description`, `channel.keywords` and `topRecentVideos` shortened):

```json
{
    "rowType": "youtube_competitor_channel_summary",
    "monitorKey": "readme-screenshots-2026-10-27",
    "eventType": "CHANNEL_CHECKED",
    "status": "BASELINE_CREATED",
    "mode": "MONITOR",
    "checkedAt": "2026-10-26T13:08:20.621Z",
    "previousSuccessfulCheckAt": null,
    "channelRef": "@veritasium",
    "channel": {
        "channelId": "UCHnyfMqiRRG1u-2MsSQLbXA",
        "handle": "@veritasium",
        "channelUrl": "https://www.youtube.com/@veritasium",
        "title": "Veritasium",
        "description": "An element of truth - videos about science, education, and anything else we find interesting.",
        "country": "US",
        "isVerified": null,
        "subscriberCount": 21400000,
        "subscriberCountText": "21.4M",
        "subscriberCountPrecision": "ROUNDED",
        "subscriberCountPrecisionUnit": 100000,
        "totalViews": 4695846306,
        "videoCount": 543,
        "keywords": "\"science videos\" science \"science education\" …",
        "topicCategories": ["Knowledge"]
    },
    "summary": {
        "newVideos": 4,
        "newShorts": 6,
        "newLives": 0,
        "totalNewContent": 10,
        "metricUpdates": 0,
        "breakoutEventsThisRun": 0,
        "breakoutVideos": 1,
        "medianRecentViews": 1604357,
        "avgRecentViews": 5118214,
        "medianRecentViewsByType": { "VIDEO": 9178180, "SHORT": 1401154, "LIVE": null },
        "uploadFrequency": 1.63,
        "subscriberDelta": null,
        "totalViewsDelta": null,
        "viewsPerDay": null,
        "shortsShare": 0.6,
        "daysSinceLastUpload": 2.8,
        "topRecentVideos": [
            { "videoId": "JsBZOcqZerk", "title": "The Insane Real Engineering of the Nazi Enigma Machine", "contentType": "VIDEO", "views": 20951141, "performanceIndex": 3.11, "performanceTier": "BREAKOUT" },
            { "videoId": "j9p3Sn8ROL4", "title": "What is edge dislocation?", "contentType": "SHORT", "views": 3978867, "performanceIndex": 2.2, "performanceTier": "STRONG" }
        ]
    },
    "cadence": {
        "uploadsLast7Days": 2,
        "uploadsLast30Days": 7,
        "uploadsPerWeek": 1.63,
        "uploadsPrevious30Days": 6,
        "previousUploadsPerWeek": 1.4,
        "daysSinceLastUpload": 2.8,
        "lastUploadAt": "2026-10-23T18:46:44.000Z",
        "uploadGapVariation": 0.52
    },
    "contentMix": {
        "videosCountRecent": 4,
        "shortsCountRecent": 6,
        "liveCountRecent": 0,
        "totalCountRecent": 10,
        "shortsShareRecent": 0.6,
        "longFormShareRecent": 0.4,
        "liveShareRecent": 0,
        "dominantType": "SHORT"
    },
    "growth": null,
    "titleKeywords": {
        "topTitleKeywords": [
            { "keyword": "engineering", "count": 2 },
            { "keyword": "insane", "count": 2 },
            { "keyword": "something", "count": 2 }
        ],
        "newTitleKeywords": [],
        "risingTitleKeywords": []
    },
    "coverage": {
        "channelFetchStatus": "OK",
        "channelSource": "youtube_data_api",
        "listingSource": "youtube_data_api",
        "detailSource": "youtube_data_api",
        "recentVideosRequested": 60,
        "recentVideosReturned": 60,
        "activeVideosRefreshed": 0,
        "videoDetailsMissing": 0,
        "contentTypesFetched": ["VIDEOS", "SHORTS"],
        "contentTypesFailed": [],
        "exactMetricCoverage": 1,
        "subscriberPrecision": "ROUNDED",
        "totalViewsAvailable": true,
        "lookbackCoverage": "FULL",
        "historyCoveredFrom": "2026-05-10T13:00:20Z"
    },
    "eventsEmitted": 10,
    "competitorRank": { "RECENT_PERFORMANCE": 2, "UPLOAD_ACTIVITY": 2, "BREAKOUT_COUNT": 1 },
    "error": null
}
```

#### Run summary (`RUN_SUMMARY` in the key-value store)

The run summary has totals across all channels (new content, metric updates, breakouts, surges, cadence changes, dormant channels, failed channels), plus a **cross-competitor comparison**: `fastestGrowingByViews`, `highestMedianRecentViews`, `mostActiveUploader`, `mostBreakoutVideos`, `highestShortsShare` and `mostConsistentUploader`.

### ❓ FAQ

**What happens on the first run?**
Each channel's current stats and recent videos are saved as a baseline (`BASELINE_CREATED`), and its summary row is returned. Old videos are not reported as new. To list them once anyway, turn on *Report recent videos on the first run*.

**What counts as a new video?**
A video ID the monitor has not seen before for that monitor name. `confidence` shows how sure the timing is:

- **HIGH**: published after the previous successful check
- **MEDIUM**: the publish time is unclear, or the video went public shortly before the previous check
- **LOW**: an older video seen for the first time (for example after you raise the item count). These are saved but not reported while *Only report genuinely new uploads* is on

**Are Shorts and live streams supported?**
Yes. Choose any of Videos, Shorts and Live. Upcoming premieres and scheduled streams are reported once, when they first appear, with `isUpcoming: true`.

**What is `performanceIndex`?**
A video's views divided by the median views of the channel's previous videos of the same kind (Shorts, long-form or live). For example, 2.4 means 2.4× the channel's normal. Shorts are never compared with long-form videos.

**How does age-normalized performance work?**
A 3-hour-old video should not be compared with 30-day-old ones. On every run, the monitor records each video's views at its current age bucket: 0–6 h, 6–24 h, 1–3 d, 3–7 d, 7–30 d or 30 d+. A new video is compared with the channel's previous videos **at the same age** (`performanceBasis: AGE_BUCKET`). Until enough of those samples exist, it falls back to the median views of comparable videos at least as old (`RECENT_MEDIAN`). That fallback can understate a young video, but it never overstates it. Age-matched baselines build up as you keep running the monitor.

**How are breakout videos detected?**
A recent video gets a tier from its `performanceIndex`:

- **Strong**: 1.5× or more
- **Breakout**: 2.5× or more
- **Exceptional**: 5× or more

Signals such as `HIGH_VIEW_VELOCITY`, `HIGH_LIKE_VELOCITY` and `HIGH_COMMENT_VELOCITY` (at least 2× the comparable rate at that age) are attached. `VIDEO_BREAKOUT` is reported when a video reaches your chosen tier, and again only if it climbs to a higher tier. Videos that were already hits when the baseline was taken are not reported as new breakouts.

**Are subscriber counts exact?**
No. YouTube rounds public subscriber counts to three significant figures, so 21,412,345 shows as 21.4M. Each channel carries `subscriberCountPrecision` (`EXACT`, `ROUNDED` or `HIDDEN`) and the rounding step. Subscriber changes smaller than that step are never reported as growth. View counts are the stronger short-term signal.

**Which channel metrics are monitored?**
Subscribers (with precision), total channel views, video count, views per day since the last run, and the prior 7-day rate.

**How long are video metrics refreshed?**
For *Refresh videos for* days after publishing (default 7). Videos that reached the breakout tier keep being refreshed for up to 30 days. Older videos stop being refreshed, which keeps runs fast.

**Can I monitor upload cadence? Can I track Shorts share?**
Yes. Every summary row has uploads per week, the last 7 and 30 days, days since the last upload, and the Shorts / long-form / live shares. Changes larger than your thresholds become `UPLOAD_FREQUENCY_*`, `SHORTS_SHARE_INCREASED` and `CONTENT_MIX_CHANGED` events.

**Are unchanged videos emitted?**
No. A video produces a row only when something about it is worth reporting. Channel summary rows can be turned off if you want events only.

**Does it require a YouTube API key or login?**
No. You don't supply any credentials. The actor reads public YouTube data only.

**How often should I schedule it?**
Daily suits most competitor tracking. Every 6–12 hours suits fast-moving niches, and 2–3 times a week suits channels that rarely post. Hourly is rarely useful. Always use the **same monitor name** on scheduled runs.

**Can I trigger Slack, email or webhooks?**
Yes. Every event has `alertTriggered` and `alertReasons`. You choose which event types count as alerts. Add an Apify webhook on *Run succeeded*, or use the Slack, email, Google Sheets, Make, Zapier or n8n integrations, and filter rows on `alertTriggered = true`.

**What happens if a run is partial?**
One failed channel never fails the run. It gets a summary row with an error status such as `CHANNEL_NOT_FOUND`, `CHANNEL_TERMINATED`, `VIDEOS_UNAVAILABLE`, `PARTIAL_VIDEOS`, `RATE_LIMITED`, `PROVIDER_ERROR`, `STATE_READ_ERROR` or `STATE_WRITE_ERROR`. A failed check never overwrites saved state. A video missing from a listing is never treated as deleted. A partial check does not move the "new since" reference point, so nothing is missed or re-announced on the next run.

**Can I monitor the same channel in two projects?**
Yes. State is saved per monitor name (`monitorKey`) and channel ID, so `client-a` and `client-b` keep separate histories.

**How is pricing calculated?**
You pay per **successful channel check**. One channel checked in one run is one charge, whether or not anything changed. Scanning a channel costs the same work either way. Event rows, summary rows, and failed or skipped channels are not charged. If your run has a spending limit, the actor checks only as many channels as the limit covers and marks the rest `SKIPPED_SPENDING_LIMIT`. See the **Pricing** tab for current rates. This README deliberately doesn't quote figures, because they can change.

**Can I call it via API or MCP?**
Yes, like any Apify actor. Use the Apify API, the JavaScript or Python clients, or an MCP server with Apify tools:

```bash
curl -X POST "https://api.apify.com/v2/acts/coregent~youtube-competitor-channel-monitor/run-sync-get-dataset-items?token=<YOUR_APIFY_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"channels":["@competitor1","@competitor2"],"monitorKey":"tech-competitors"}'
```

### 🧪 Example inputs

**First-run snapshot** (the input behind the examples above: 2 channels, 22 event rows and 2 summary rows)

```json
{
    "channels": ["@mkbhd", "@veritasium"],
    "monitorKey": "readme-screenshots-2026-10-27",
    "mode": "MONITOR",
    "contentTypes": ["VIDEOS", "SHORTS"],
    "lookbackDays": 45,
    "maxRecentVideosPerChannel": 30,
    "emitBaselineContent": true,
    "includeRecentPerformance": true,
    "includeContentMix": true,
    "includeTitleKeywordAnalysis": true,
    "includeThumbnailUrls": true,
    "includeChannelSummaryRows": true
}
```

`emitBaselineContent` only lists videos on a monitor name's **first** run. Run it again with the same `monitorKey` and you get the 2 summary rows only, until something changes.

**Daily competitor monitor**

```json
{
    "channels": ["@competitor1", "@competitor2", "@competitor3"],
    "monitorKey": "tech-competitors",
    "mode": "MONITOR",
    "contentTypes": ["VIDEOS", "SHORTS"],
    "lookbackDays": 30,
    "maxRecentVideosPerChannel": 30,
    "onlyNewContent": true,
    "trackVideoMetricChanges": true,
    "videoMetricTrackingDays": 7
}
```

**Breakout video watch**

```json
{
    "channels": ["@competitor1", "@competitor2"],
    "monitorKey": "breakout-watch",
    "trackVideoMetricChanges": true,
    "videoMetricTrackingDays": 7,
    "minimumViewGrowthPct": 25,
    "performanceBaselineVideos": 20,
    "breakoutMinTier": "STRONG"
}
```

**Shorts strategy monitor**

```json
{
    "channels": ["@competitor1", "@competitor2", "@competitor3"],
    "monitorKey": "shorts-strategy",
    "contentTypes": ["SHORTS"],
    "includeContentMix": true,
    "includeRecentPerformance": true
}
```

**Competitor comparison**

```json
{
    "channels": ["@competitor1", "@competitor2", "@competitor3"],
    "monitorKey": "monthly-benchmark",
    "mode": "COMPARE",
    "includeChannelGrowth": true,
    "includeRecentPerformance": true,
    "includeContentMix": true
}
```

### 💾 Where state is kept

State is kept in a named key-value store in your Apify account, `youtube-competitor-monitor-state`, with one record per monitor name and channel. If *Save history dataset* is on, each run's channel observations and events are also added to the named dataset `youtube-competitor-monitor-history`, which builds a long-term log. Deleting the state store resets every monitor.

Avoid running two monitors with the same name at the same time. Each run reads the state when it starts and writes it when it finishes.

#### 🚦 Proxy policy

Most runs need no proxy. A proxy is only used if the actor falls back to reading public YouTube pages. If you use one, choose **Apify Datacenter** proxy or **no proxy**. Both work reliably for reading YouTube channel and video pages at this actor's request rate.

**Apify Residential proxy is not supported.** The actor fails at startup if `apifyProxyGroups` includes `RESIDENTIAL`. In pay-per-event actors, residential bandwidth is billed to the developer, not the run user, so one bandwidth-heavy run could cost more than the run earns.

If you need residential routing, add your own residential provider in the proxy editor's **Custom proxy URLs** field. That traffic goes through your provider, not Apify:

```
http://user:pass@proxy.iproyal.com:12321
http://user:pass@proxy.brightdata.com:22225
http://user:pass@proxy.oxylabs.io:7777
```

### ⚖️ Notes and limits

- This is an unofficial product and is not affiliated with YouTube or Google. It uses public YouTube data only.
- Subscriber counts are rounded by YouTube. Video metrics change continuously, and each run captures them at one moment.
- Breakout tiers, `performanceIndex` and signals are calculated by this actor. They are not YouTube metrics.
- Monitoring starts from the first baseline, and comparisons use a sample of recent public videos.
- A video doing well alongside channel growth does not prove that the video caused the growth.
- The actor does **not** provide YouTube Analytics data such as watch time, audience retention, CTR, impressions, revenue or where subscribers came from.
- If YouTube's data API is unavailable, the actor falls back to reading public pages. Total channel views and comment counts are then unavailable (`null`). Each summary row's `coverage` shows which source was used.

# Actor input Schema

## `channels` (type: `array`):

The YouTube channels to monitor, up to 500 per run.

Each entry can be:

- a handle: `@mkbhd`
- a channel URL: `https://www.youtube.com/@veritasium` or `https://www.youtube.com/channel/UC...`
- a channel ID: `UCBJycsmduvYEL83R_U4JriQ`
- a video URL (resolved to the channel that uploaded it)

The same channel listed twice is checked once.

## `monitorKey` (type: `string`):

Name for this monitor's saved state, such as `tech-competitors` or `client-a`. Use the same name on every scheduled run so the monitor remembers what it has already seen. Different names keep separate histories, even for the same channel.

## `mode` (type: `string`):

- **Monitor (recommended)**: reports only what changed since the last run. On a channel's first run it saves a baseline instead and does not report old videos as new.
- **Baseline**: saves or resets the current state without reporting changes.
- **Compare**: returns a current summary and ranking for every channel. It does not change the saved state, so the next Monitor run still reports everything new.

## `contentTypes` (type: `array`):

The kinds of content to watch. Long-form **Videos** include premieres.

## `lookbackDays` (type: `integer`):

The recent period used for new-content reports, content mix, median recent views and title keywords. Videos published before this window are never reported as new. Common values are 7, 14, 30 and 90.

## `maxRecentVideosPerChannel` (type: `integer`):

How many of the newest items to read for each content type on each channel. For example, 30 means up to 30 videos and 30 Shorts. Raise it for channels that post many times a day.

## `onlyNewContent` (type: `boolean`):

When on, an older video the monitor sees for the first time (for example after you raise the item count) is saved but not reported as new. Turn it off to report those too. They are marked `newToMonitor: true` with LOW confidence.

## `emitBaselineContent` (type: `boolean`):

When on, a channel's first run also reports the videos already in the lookback window, marked `newToMonitor: true`. When off, the first run only saves the baseline.

## `trackVideoMetricChanges` (type: `boolean`):

Keep refreshing recent videos and report `VIDEO_METRICS_UPDATED` when views, likes or comments grow past the thresholds below.

## `videoMetricTrackingDays` (type: `integer`):

How many days after publishing a video keeps being refreshed. Breakout videos are refreshed for up to 30 days. Common values are 1, 3, 7, 14 and 30.

## `minimumViewGrowthPct` (type: `integer`):

The percentage growth in views since the last report that counts as an update. It works together with the minimum view increase below.

## `minimumViewDelta` (type: `integer`):

The number of extra views since the last report that counts as an update. Also the smallest total-view gain for a channel view surge.

## `minimumLikeGrowthPct` (type: `integer`):

The percentage growth in likes since the last report that counts as an update.

## `minimumCommentDelta` (type: `integer`):

The number of extra comments since the last report that counts as an update.

## `includeRecentPerformance` (type: `boolean`):

Compare each video with the channel's own recent videos at a similar age (`performanceIndex`) and detect breakouts.

## `performanceBaselineVideos` (type: `integer`):

How many of the channel's previous videos of the same kind (Shorts, long-form or live) make up the baseline. Common values are 5, 10, 20 and 30.

## `breakoutMinTier` (type: `string`):

The lowest tier that triggers `VIDEO_BREAKOUT`.

- **Strong**: 1.5× the channel baseline or more
- **Breakout**: 2.5× or more
- **Exceptional**: 5× or more

A video is reported again only if it moves up a tier.

## `minimumBreakoutViews` (type: `integer`):

Ignore breakouts on videos with fewer views than this, so a small channel's 300-view video is not flagged.

## `includeUnderperforming` (type: `boolean`):

Report `VIDEO_UNDERPERFORMING` when a video at least a day old has 0.4× or less of the channel's views at the same age. Off by default to avoid negative noise.

## `includeChannelGrowth` (type: `boolean`):

Track subscriber, total-view and video-count changes since the last run, and report view or subscriber surges.

## `channelSurgeMultiplier` (type: `number`):

A channel is surging when its views (or subscribers) per day since the last run are at least this many times its median daily rate over the previous 7 days.

## `cadenceChangePct` (type: `integer`):

How much uploads per week must change before `UPLOAD_FREQUENCY_INCREASED` or `UPLOAD_FREQUENCY_DECREASED` is reported. The last 30 days are compared with the 30 days before, when that history is known.

## `dormantAfterDays` (type: `integer`):

Report `CHANNEL_DORMANT` once when a channel has not uploaded in this many days.

## `includeContentMix` (type: `boolean`):

Track the share of Shorts, long-form and live in the lookback window, and report `SHORTS_SHARE_INCREASED` or `CONTENT_MIX_CHANGED`. This needs at least two content types.

## `includeTitleKeywordAnalysis` (type: `boolean`):

Find recurring, new and rising words and phrases in each channel's recent titles. Simple word counting is used, not AI.

## `includeChannelSummaryRows` (type: `boolean`):

Add one `CHANNEL_CHECKED` row per channel with its status, cadence, content mix, growth, keywords and competitor rank. These rows mean every run has output, even when nothing changed.

## `includeThumbnailUrls` (type: `boolean`):

Include video thumbnail, channel avatar and channel banner URLs in the output.

## `alertEventTypes` (type: `array`):

Events that get `alertTriggered: true`. Filter on that field in a webhook, Slack, Make, Zapier or n8n integration.

## `saveHistory` (type: `boolean`):

Also append each run's channel observations and events to the named dataset `youtube-competitor-monitor-history` in your account. This builds a long-term log across runs.

## `maxConcurrency` (type: `integer`):

How many channels are checked at the same time.

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

Used only when the actor falls back to reading public YouTube pages. Most runs need no proxy. Apify Residential proxy is not supported. Use Apify Datacenter, or your own provider through Custom proxy URLs.

## `debug` (type: `boolean`):

Log more detail about each request.

## Actor input object example

```json
{
  "channels": [
    "@mkbhd",
    "@veritasium"
  ],
  "monitorKey": "my-competitors",
  "mode": "MONITOR",
  "contentTypes": [
    "VIDEOS",
    "SHORTS"
  ],
  "lookbackDays": 30,
  "maxRecentVideosPerChannel": 30,
  "onlyNewContent": true,
  "emitBaselineContent": false,
  "trackVideoMetricChanges": true,
  "videoMetricTrackingDays": 7,
  "minimumViewGrowthPct": 20,
  "minimumViewDelta": 1000,
  "minimumLikeGrowthPct": 20,
  "minimumCommentDelta": 50,
  "includeRecentPerformance": true,
  "performanceBaselineVideos": 10,
  "breakoutMinTier": "BREAKOUT",
  "minimumBreakoutViews": 1000,
  "includeUnderperforming": false,
  "includeChannelGrowth": true,
  "channelSurgeMultiplier": 2,
  "cadenceChangePct": 50,
  "dormantAfterDays": 30,
  "includeContentMix": true,
  "includeTitleKeywordAnalysis": true,
  "includeChannelSummaryRows": true,
  "includeThumbnailUrls": true,
  "alertEventTypes": [
    "NEW_VIDEO",
    "NEW_SHORT",
    "NEW_LIVE_STREAM",
    "NEW_PREMIERE",
    "VIDEO_BREAKOUT",
    "CHANNEL_VIEW_SURGE",
    "CHANNEL_GROWTH_SURGE",
    "UPLOAD_FREQUENCY_INCREASED",
    "CHANNEL_DORMANT"
  ],
  "saveHistory": true,
  "maxConcurrency": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "debug": false
}
```

# Actor output Schema

## `events` (type: `string`):

No description

## `channelSummaries` (type: `string`):

No description

## `runSummary` (type: `string`):

No description

# 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 = {
    "channels": [
        "@mkbhd",
        "@veritasium"
    ],
    "monitorKey": "my-competitors",
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("coregent/youtube-competitor-channel-monitor").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 = {
    "channels": [
        "@mkbhd",
        "@veritasium",
    ],
    "monitorKey": "my-competitors",
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("coregent/youtube-competitor-channel-monitor").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 '{
  "channels": [
    "@mkbhd",
    "@veritasium"
  ],
  "monitorKey": "my-competitors",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call coregent/youtube-competitor-channel-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,coregent/youtube-competitor-channel-monitor"
        }
    }
}
```

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/ElvwOZXlRjY2Vvva6/builds/BwMPIm8ejtMQHyorh/openapi.json
