# YouTube Shorts Competitor & Viral Trend Radar (`nexascout/youtube-shorts-trend-radar`) Actor

Track YouTube Shorts from competitor channels and search queries. Collect public views and likes, save history, and measure view growth between runs. Identify rising and accelerating videos with transparent metrics. No YouTube API key required.

- **URL**: https://apify.com/nexascout/youtube-shorts-trend-radar.md
- **Developed by:** [NexaScout](https://apify.com/nexascout) (community)
- **Stats:** 2 total users, 1 monthly users, 66.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 shorts observations

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 Shorts Competitor & Viral Trend Radar

Monitor public Shorts from competitor channels and search queries. Save observations, measure view growth between runs, and rank videos whose measured view velocity is increasing.

This Actor makes its own HTTP requests to YouTube. It does not call other Actors, require a YouTube API key, download video/audio, or use a paid data API. Optional proxy traffic has its own cost.

### Quick start

Paste this into Input → JSON:

```json
{
  "channelUrls": ["https://www.youtube.com/@NASA/shorts"],
  "searchQueries": [],
  "maxAgeDays": 0,
  "maxShorts": 5,
  "maxDetailRequests": 15,
  "saveHistory": true,
  "historyProfile": "nasa-demo"
}
```

Publication-age filtering is off by default (`maxAgeDays: 0`). This includes videos whose publication date is unavailable; it does not mean those videos are recent. Set a positive value only when you want strict date filtering: unknown dates will then be excluded. Existing saved inputs/tasks may retain 90 and must be changed explicitly.

Run with **512 MB memory** and **300 seconds timeout**. The collection budget defaults to 180 seconds, reserving time for storage writes. Empty channel and query lists use NASA as a public demo source.

The first successful run gives a baseline with current views, metadata and lifetime-average pace where available. Keep the same sources, country and history profile, then run again at least **60 minutes** later to measure growth. A third suitably spaced observation is needed for acceleration. A daily task can provide longer-window measurements; this package does not create schedules automatically.

For niche discovery, use `examples/niche-search.json`. A run can combine channels and queries. The `country` setting localizes YouTube discovery; it is **not** a creator-country or audience-country filter.

### What is returned

Each dataset row contains one observed Short. Repeated runs may return the same video with a new observation; this is deliberate monitoring behavior.

| Field | Meaning |
|---|---|
| `videoId`, `url` | Video identifier and canonical Shorts link |
| `title`, `description`, `hashtags` | Public text metadata |
| `channel`, `channelId`, `channelUrl` | Public channel identity |
| `views` | Exact numeric public counter required for a usable observation |
| `likes`, `comments` | Public counters when available; otherwise `null` |
| `commentsDisplayEstimate` | Rounded comment label, when available; excluded from engagement calculation |
| `subscribers`, `subscribersApproximate` | Displayed subscriber count and rounding flag |
| `publishedAt`, `publishedAtPrecision` | Publication time/date reported by YouTube |
| `ageHours` | Time since reported publication; date-only sources are less precise |
| `averageViewsPerHourSincePublished` | Total views ÷ age in hours; requires timestamp precision and age ≥ 1 hour |
| `averageViewsPerDaySincePublished` | Lifetime hourly average × 24; **not** measured views gained in the last day |
| `measuredViewsPerHour` | Difference in exact views ÷ elapsed observation hours |
| `viewsDelta`, `comparisonHours`, `baselineAt` | Evidence for the measured velocity |
| `priorMeasuredViewsPerHour`, `velocityRatio` | Previous interval velocity and current/previous ratio |
| `likeRatePct` | 100 × likes / views, when available |
| `engagementRatePct` | 100 × (likes + exact comments) / views; null if either counter is unavailable |
| `viralScore` | 0–100 ranking heuristic; null until velocity is measurable |
| `hookSignals`, `hookAnalysisBasis` | English phrase/punctuation rules applied to the **title only** |
| `firstSeenAt`, `observedAt`, `discoveredIn` | Collection provenance |
| `shortsEvidence`, `metricWarnings` | Shorts classification evidence and missing metrics |
| `metadataSource`, `viewsSource`, `playerStatus` | Which metadata path supplied the row and whether the player was available |

Shares, retention, watch time, engaged views, audience geography and the spoken/visual opening of the video are not collected. A hook flag is not proof that viewers liked the opening. No score is a probability of becoming viral or evidence of future saturation.

### State definitions

| State | Interpretation |
|---|---|
| `BASELINE` | First observation in a new history profile |
| `NEW` | Video first observed after this profile was established; it may have been published earlier |
| `INSUFFICIENT_INTERVAL` | Existing video, but no saved observation sufficiently far apart |
| `STALE_BASELINE` | Previous observations exceed `maxComparisonHours` |
| `STABLE` | Measurable velocity below the configured rising threshold |
| `RISING` | Measured velocity ≥ `risingViewsPerHour` (default 100) |
| `ACCELERATING` | Three suitably spaced observations; current velocity ≥ rising threshold and current/previous velocity ≥ `accelerationFactor` (default 1.5) |
| `DECLINING` | Previous velocity ≥ rising threshold and current/previous velocity ≤ 1 / accelerationFactor; total views can still be increasing |
| `COUNTER_CORRECTION` | Public views fell versus the last saved count; the comparison series restarts without a negative growth signal |

If previous velocity is zero, the ratio is undefined and remains null. Intervals need not be equal: each rate is divided by its own elapsed hours. Intervals must fall between `minIntervalMinutes` and `maxComparisonHours`. These are interval averages and do not establish the exact moment a spike started.

### Transparent score

`heuristic-v1` is:

```
min(70, 14 × log10(1 + measuredViewsPerHour))
+ min(20, 2 × likeRatePct)
+ min(10, 10 × max(0, velocityRatio - 1))
```

Unavailable likes or acceleration contribute no points; the original metric remains null. The score is intentionally absent on a baseline run. Missing metadata can lower a score, so inspect `metricWarnings` when comparing videos. The score favors absolute measured pace and has not been statistically validated as a prediction model.

### Discovery and limits

- Channels are read from their Shorts tab. Searches use YouTube's Shorts filter and Shorts result renderers. Video length alone is not used to call an ordinary video a Short.
- Metadata comes from the public watch-page JSON. If the player is unavailable, the Actor can read exact counters and metadata independently exposed in the same anonymous public page. It validates the video identity and reads only the main video column. If both paths are unavailable, it returns an explicit error.
- Default discovery: up to 30 candidates per source, at most 2 pages per source. Up to 10 channels and 10 queries.
- Previous history candidates and newly discovered candidates are interleaved so the Actor can recheck videos that disappear from a search sample.
- `maxShorts` limits output rows, while `maxDetailRequests` caps attempted detail requests including filtered/failed videos. `minViews` and `maxAgeDays` filter rows after details are obtained.
- Rows are written progressively. Once `maxShorts` is reached, collection stops. The report/dashboard ranks **the collected output sample**, not all candidate videos or all of YouTube.
- Discovery is a bounded, localized sample and can vary between runs. Search-result totals and sampled growth do not prove market-wide or niche-wide growth.

YouTube can block datacenter traffic or change page structures. Configure `proxyConfiguration` if needed; success on a development machine does not guarantee success on an Apify cloud IP. The Actor does not sign in or request protected playback. A player denial alone does not mean all publicly returned text metadata is unavailable. No missing values are reconstructed from rounded labels. A verified Short with unavailable comments still produces a useful row with `comments: null`.

### Persistent history

History is kept in a named key-value store. Its name is derived from the Actor ID, `historyProfile`, normalized source lists and country. It is isolated from the default run storage and from other source profiles. Keep these inputs unchanged for comparisons; output filters and thresholds can change without resetting the profile.

The store keeps immutable run snapshots and reads/merges the newest five. Old snapshots are pruned to 60; each compact state retains up to 5,000 videos seen in the last 30 days and up to 100 spaced observations per video. Older history is not a permanent archive. For predictable comparisons, avoid overlapping runs of the same profile; a burst of more than five overlapping runs is outside the supported concurrency envelope.

Snapshots retain counters and provenance, not complete video descriptions. Successfully observed videos filtered out by age/views can be remembered without producing a paid dataset row. Failed video requests do not delete a baseline. No deletion/takedown is inferred from a missing search result. An output-write error prevents this run from saving history because the dataset outcome is uncertain.

### Outputs and failure reporting

- **Dataset:** progressively saved rows; export using Apify's standard JSON/CSV/Excel options.
- **OUTPUT:** status, coverage, source results, errors, request/byte counters, history status, state counts, appliedFilters and filterCounts explaining exclusions.
- **REPORT:** summary plus collected rows sorted by score, then lifetime-average pace.
- **DASHBOARD:** self-contained HTML table linking to observed Shorts.

`OUTPUT.status` can be `SUCCEEDED`, `PARTIAL` or `FAILED`. The Apify run itself may succeed with `PARTIAL` output if useful rows were collected before an error or limit. Always check `OUTPUT`, including `historySaved`. A valid source with all videos excluded by filters can succeed with zero rows. A run with no detectable Shorts or no verifiable observations after errors fails explicitly.

### Costs and monetization

The package uses native `apify-default-dataset-item` billing if configured by the owner. It checks the remaining event budget before collection and each write, and does not manually charge the same row again. An optional `apify-actor-start` event is handled by the platform. The package does not configure prices or publish the Actor.

Every emitted observation is a result, including baseline/stable observations. A monitor can consume network/compute while filters exclude all results. Keep a small startup fee or platform-usage billing under consideration only after cloud cost tests. Residential proxy transfer can dominate compute costs: full public pages are substantially larger than output rows. `OUTPUT.http.bytes` records response-body bytes, not the final billable proxy total. No profit margin is claimed from local testing.

Before publication, run two small cloud tests with the same profile, verify history survives under Limited permissions, inspect actual usage/transfer cost, and verify the chosen default input produces a nonempty dataset. For measured velocity, space runs at least one hour apart. Real acceleration requires a third such run. Do not advertise a guarantee of detecting Shorts before saturation.

### Develop and deploy

Node.js 22 or newer is required. `npm ci`, `npm test`, `npm run check`, then `apify validate-schema`. `bash deploy.sh` performs these checks and runs `apify push`. It targets `youtube-shorts-trend-radar` in your logged-in Apify account; it does not publish to Store or alter other Actors.

The Dockerfile uses `apify/actor-node:22`. Runtime dependencies are pinned and accompanied by a lockfile. No Python, FFmpeg, browser or third-party Actor is required.

Optional public-network tests:

```
npm run test:live
node test/sdk-live.mjs
node test/public-live.mjs
```

The SDK test uses temporary local storage and two actual public collections; it never starts a cloud Actor. It verifies baseline reuse without inventing elapsed time. Unit tests cover spaced growth and acceleration using controlled timestamps.

### References

- [Apify JavaScript SDK: Actor and storage](https://docs.apify.com/sdk/js/reference/class/Actor)
- [Apify pay-per-event documentation](https://docs.apify.com/actors/publishing/monetize/pay-per-event)
- Version 0.1.1 was verified in Apify with three results and persistent history when age filtering was disabled. Version 0.1.2 makes that the default; see `CHANGELOG.md`. The new package still requires deployment.
- Live public-page structure was inspected on 2026-09-21. Test evidence is included in `examples/live-verification.json` and `TEST_REPORT.md`.

Independent tool; not affiliated with YouTube or Google.

# Changelog

This Actor's version history is a separate document: https://apify.com/nexascout/youtube-shorts-trend-radar/changelog.md

# Actor input Schema

## `channelUrls` (type: `array`):

Public YouTube channel URLs or @handles. With no channels and no queries, the NASA Shorts channel is used as a demo.

## `searchQueries` (type: `array`):

Up to 10 queries. Searches YouTube Shorts shelves; results are a bounded sample.

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

Two-letter country code. This localizes YouTube results; it does not identify audience or creator country.

## `maxAgeDays` (type: `integer`):

Default 0 disables age filtering, so Shorts with unknown publication dates remain visible. A positive value strictly excludes older videos AND videos whose date is unavailable.

## `minViews` (type: `integer`):

Minimum exact public view counter. Filtered observations can still be saved to history.

## `maxShorts` (type: `integer`):

Maximum paid dataset rows. Collection stops once this limit is reached.

## `maxCandidatesPerSource` (type: `integer`):

Maximum Shorts discovered per channel or query. Does not promise full coverage.

## `maxPagesPerSource` (type: `integer`):

Bounded pagination. Continuations depend on YouTube availability.

## `maxDetailRequests` (type: `integer`):

Hard cap, including failed and filtered detail requests. Larger values increase usage.

## `maxRunSeconds` (type: `integer`):

Stops collection early enough to write output and history. Platform timeout should be at least 300 seconds.

## `minIntervalMinutes` (type: `integer`):

Do not compute view velocity from observations closer than this interval.

## `maxComparisonHours` (type: `integer`):

Older baselines are marked stale. Keep this greater than the minimum interval.

## `risingViewsPerHour` (type: `integer`):

Measured velocity threshold for RISING. This is configurable, not a universal viral threshold.

## `accelerationFactor` (type: `number`):

ACCELERATING requires at least this ratio of successive measured velocities and a current velocity above the rising threshold.

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

Save only verified observations. Reuse the profile and sources for comparisons.

## `historyProfile` (type: `string`):

Separate experiments with separate names. Source lists and country also form part of the history identity.

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

Direct requests by default. Configure your proxy if YouTube blocks cloud traffic; proxy usage may add cost.

## Actor input object example

```json
{
  "channelUrls": [],
  "searchQueries": [],
  "country": "US",
  "maxAgeDays": 0,
  "minViews": 0,
  "maxShorts": 25,
  "maxCandidatesPerSource": 30,
  "maxPagesPerSource": 2,
  "maxDetailRequests": 80,
  "maxRunSeconds": 180,
  "minIntervalMinutes": 60,
  "maxComparisonHours": 72,
  "risingViewsPerHour": 100,
  "accelerationFactor": 1.5,
  "saveHistory": true,
  "historyProfile": "default",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `shorts` (type: `string`):

No description

## `dashboard` (type: `string`):

No description

## `report` (type: `string`):

No description

## `summary` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("nexascout/youtube-shorts-trend-radar").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("nexascout/youtube-shorts-trend-radar").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 '{}' |
apify call nexascout/youtube-shorts-trend-radar --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nexascout/youtube-shorts-trend-radar"
        }
    }
}
```

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/mpbOhdxwg50EARNme/builds/xWNbEQJH2Y9NXFes0/openapi.json
