YouTube Channel Lookalike Finder
Pricing
from $8.50 / 1,000 similar channel delivereds
YouTube Channel Lookalike Finder
Find the similar/lookalike channels YouTube itself shows on any channel's About page: channelId, name, subscriber count. No API key, no login.
Pricing
from $8.50 / 1,000 similar channel delivereds
Rating
0.0
(0)
Developer
Tim Zinin
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
YouTube Channel Lookalike Finder: Reading the Similar-Channels Widget on Any Channel's About Page
This Actor returns the exact similar/lookalike channels YouTube itself recommends on a channel's
About page — channel ID, name and subscriber count — for competitive research and lookalike
audience building. It reads exactly one surface: the "similar channels" shelf YouTube's own front
end renders on https://www.youtube.com/@handle/about, the same shelf a human visitor scrolls past
on that page. It does not read search, does not read the YouTube Data API, does not read comments
or transcripts, and does not guess at a channel YouTube did not itself choose to show.
The billable outcome is a delivered similar channel: one channel from that widget, with its
channelId and name, distinct from the channel you requested. Everything else — a channel whose
widget is genuinely empty, a handle that does not resolve, a blocked or malformed input, or a run
that hit its own spending cap partway through — is free, with a named reason on every row.
Read this before you build anything on top of this Actor's output. The number of similar
channels YouTube's widget returns for the same channel is not fixed and is not something this
Actor — or any actor — controls. Measured directly, on the identical channel, across independent
runs: 12, then 7, then 6 similar channels for @mkbhd (see Evidence and boundaries for the
full measurement table, including four consecutive reproductions run for this page). A workflow
built on "this channel always returns N similar channels" will break. A workflow built on "this
channel returns some number of similar channels YouTube chose today, each one a real row with a
real channelId" will not.
What you get
- The channels YouTube itself recommends, not a guess. This Actor reads the literal
gridChannelRendererobjects inside the About page's own "similar channels" shelf — the same data structure that renders the widget a human sees. It never infers similarity from category, keyword overlap, or any heuristic of its own; every row is a channel YouTube's own recommendation system chose to display next to the requested channel, on the day the run happened. channelId, name, subscriber count and the widget's own position for each similar channel.widgetPositionis the 1-based order the shelf rendered the channel in at the moment of the run — useful as a same-run ranking signal, not guaranteed stable between runs (see Evidence and boundaries).- The requested channel is never returned as its own lookalike. The extraction step is handed
the requested channel's own confirmed
channelIdand structurally excludes it from the similar list — see the inversion trap in Evidence and boundaries for why this specific guarantee needed its own regression test, not just a code comment. - A resolved identity for the channel you asked for, on every outcome.
requestedChannelIdandrequestedCanonicalChannelUrlare populated whenever the requested channel itself resolved, whether or not it had any similar channels to show — so a buyer can always tell "this channel exists and I checked it, it just had nothing similar" apart from "this channel could not be resolved at all." - Two closed negative cases, not one. A syntactically invalid or genuinely deleted handle
returns
HTTP 404at resolution time. A syntactically valid but non-existentUC…channel ID returnsHTTP 200with the literal text "This channel does not exist" in the page body — a clean success status hiding a real failure. This Actor checks both, live, on every run; both are demonstrated with real output below, not asserted. - A structural fix to a real extraction trap, not a convention.
subscriberCountTextoccurs on an About page in two different JSON shapes, one belonging to the widget this Actor sells and one belonging to the requested channel itself — reading either shape from the wrong place returns a confidently wrong number atHTTP 200. This Actor's extraction code physically cannot read the wrong one; see Evidence and boundaries for the exact page structure and the regression test that proves it. - No external links, no email, no comments, no transcripts. This Actor deliberately returns none of that — see Sources and rights and the Boundaries subsection for why, and where that data actually lives if you need it.
- No API key, no login, no captcha, no account. Every request in this Actor is an
unauthenticated
GETwith a consent cookie and an English/US locale query string — verified live, repeatedly, while building this page. - A honest empty case, not a silent zero. A channel whose widget genuinely renders nothing
(confirmed live on
@GoogleDeveloperswhile building this page — see Real happy, partial and failure output) returns one clearly labelledstatus:"empty"row, free, with the requested channel's own identity still resolved and reported.
Who uses it
- Agencies and marketers building lookalike audiences for influencer marketing. A common brief is "find channels similar to this creator/brand for outreach or a media buy" — this Actor answers exactly that question using YouTube's own recommendation judgment, not a third-party similarity score.
- Competitive researchers benchmarking a channel against its self-declared peer set. YouTube's own widget is, in effect, YouTube telling you who it considers this channel's neighbors — a signal a competitor's own marketing team cannot see or control.
- Developers and workflow builders wiring channel-discovery into a pipeline. A flat,
machine-checkable row per similar channel —
status,similarChannelId,action— is built to be branched on directly in n8n, an agent tool call, or a script, without parsing YouTube's own HTML. - Proven, live market demand, not a guess. The Apify Store's own API (queried 2026-08-17)
shows a directly competing Actor of the same name,
dataovercoffee/youtube-channel-lookalike-finder, with 71 users in the last 30 days at $0.12 per delivered item — a single, primaryapify-default-dataset-itemcharge event, no add-on. (A separate $0.28 charge event visible in the same author's portfolio belongs to a different Actor entirely, a business-email scraper — not this niche, and not this Actor's pricing comparison.) A second, identically-named competitor,fetch_cat's build, holds $0.005 start + $0.00115/item and just 1 user in the same 30 days — the cheap end of this specific niche is not where the demand is, unlike some adjacent YouTube data niches where the cheap tier wins.

How to run
- Provide 1–50 channel values in
channels. Each entry can be a bare@handle, a fullhttps://www.youtube.com/...URL (/@handle,/channel/UC…,/c/Name,/user/Name, with or without a trailing/about,/videos,/featuredor/playliststab segment), or a bareUC…channel ID. Handle matching is case-insensitive. - Optionally set
maxConcurrency(default5, range 1–20) — how many requested channels this run processes in parallel. This controls throughput only; it has no effect on how many similar channels any one channel's widget returns. - Start the run from the Console, the Apify API, a scheduled Task, or an MCP client. The prefilled
input,
["@mkbhd"], needs no buyer secret and is confirmed live, while building this page, to produce aSUCCEEDEDrun with a non-empty dataset — see Real happy, partial and failure output for the exact rows it produced. - Read the Dataset for the result: one row per similar channel found (
status:"ok"), or one summary row per requested channel when its widget was genuinely empty (status:"empty") or the channel itself could not be resolved (status:"error"). - Do not build automation on an assumed row count per channel. Treat
statusas the signal that matters, not the row count — see Limits and Evidence and boundaries for the measured range. - Route
similarChannelIdvalues into whatever downstream tool needs them — an ad platform's custom-audience seed list, a CRM competitor-tracking record, or a second Actor run (this Actor deliberately does not chain into other Actors itself; see Integration recipes).
Pricing
$0.010 per delivered similar channel, plus $0.005 Actor start. The primary pricing noun on this page, in the Dataset schema and in the Store listing is the same throughout: the delivered similar channel — one row, naming one channel distinct from the one you requested, that YouTube's own widget actually returned this run.
Every other row is free: a status:"empty" row (the requested channel resolved but its widget had
nothing to show), a status:"error" row (the channel could not be resolved, or the input was
rejected before any request was made), and the one explanatory row this Actor writes if your run's
own spending cap is reached partway through.
One deliberate nuance, stated plainly because it is easy to assume the opposite: a delivered
similar channel is billed as status:"ok" even when its subscriber count is missing.
partial:true on a delivered row means "this channel's similarSubscriberCountText field is
null," not "this row is free." The billing boundary is whether YouTube's widget named a real,
distinct channel — not whether every field on that channel could be filled in. This is different
from a "partial match, held back and unbilled" pattern you may have seen on other Actors in this
line, and it is intentional: identifying that YouTube considers a specific channel similar is the
product being sold here, and that identification does not become less true just because the
subscriber-count text happened to be absent from the widget's own markup for that one entry.
This Actor's own external data cost is close to zero — one HTML GET per requested channel,
measured at roughly 2.5–2.6 MB, no proxy, no LLM call — so the price reflects the extraction and
identity-verification work described in Evidence and boundaries, not a resold bandwidth bill. On
Apify's own billing record these are metered as two platform events — apify-actor-start for the run
fee, and result-found for each delivered similar channel.
Before doing any paid work, this Actor reads its own live pricing configuration on the platform and
refuses to proceed if it looks broken — e.g. if the delivered-similar-channel event is not actually
billable, or if ordinary dataset writes would themselves carry an unexpected charge. In the worst
case it fails the run having written and charged for nothing at all, rather than silently doing
free paid-tier work or silently double-charging. This check runs against Apify's own live charging
API and only executes meaningfully once this Actor is deployed on the platform — it could not be
triggered from local development while building this page, so it is described here as a verified
property of the code (lib/billing.js, read in full while writing this page), not as something
this page's own local test runs observed happening.
Input contract
channels(array of strings, required, 1–50 items). Each value is a@handle, a fullyoutube.comchannel URL, or a bareUC…channel ID. A value that is a URL but does not resolve toyoutube.com,www.youtube.comorm.youtube.comis rejected asstatus:"error"before any network request is made — demonstrated live below withhttps://evil.example/@fake. A bare value containing a path separator,..,://or whitespace is rejected the same way, for the same reason: nothing URL-shaped is ever substituted into a trusted request template without being checked first.maxConcurrency(integer, default5, min1, max20). How many requested channels are fetched in parallel in this run. Raising it does not raise or lower how many similar channels any individual channel's widget returns — that number is entirely up to YouTube, on the day of the run.
Both examples below are real inputs, run against the live Actor while building this page — not constructed from the schema alone.
{"channels": ["@mkbhd"],"maxConcurrency": 1}
This exact input produced 6 delivered similar-channel rows in four separate, independent runs while building this page — see Real happy, partial and failure output for the full first row, and Evidence and boundaries for why "4 out of 4 identical" does not contradict the "12, then 7, then 6" measurement this page leads with.
{"channels": ["@mkbhd", "@NASA", "@ycombinator"],"maxConcurrency": 3}
This exact input, run once while building this page, produced 21 total rows: 6 for @mkbhd,
12 for @NASA, and 3 for @ycombinator — three different channels processed in the same run,
each independently returning a different count, demonstrated in a single live call rather than
three separate ones.
Real happy, partial and failure output
Every row shares one flat, 14-field schema regardless of outcome, matching
.actor/dataset_schema.json exactly. All four rows below are copied verbatim from real dataset
output produced by apify run --purge against this Actor's own code while building this page —
none were constructed by hand. Only the field order is unchanged from the actual run; nothing has
been reworded.
Happy path — a delivered similar channel, @mkbhd
{"requestedChannel": "@mkbhd","requestedChannelId": "UCBJycsmduvYEL83R_U4JriQ","requestedCanonicalChannelUrl": "http://www.youtube.com/@mkbhd","similarChannelId": "UCG7J20LhUeLl6y_Emi7OJrA","similarChannelName": "The Studio","similarSubscriberCountText": "1.13M subscribers","widgetPosition": 1,"source": "about-page-similar-channels-widget","status": "ok","confidence": "display-text","partial": false,"action": "ingest","error": null,"checkedAt": "2026-08-17T20:17:28.175Z"}
requestedCanonicalChannelUrl carries a scheme-first http:// (not https://) here because that
is literally what YouTube's own canonicalChannelUrl field on the About page contains for this
channel — reported exactly as read, not normalized. confidence:"display-text" is fixed on every
delivered row: similarSubscriberCountText is YouTube's own rounded UI text ("1.13M subscribers"),
never a computed exact number.
Happy path — a channel with a much larger widget, @NASA
This same run also demonstrates that the count genuinely differs by channel, in the same session,
using the same code: @NASA's widget returned 12 similar channels the same minute @ycombinator
returned 3 and @mkbhd returned 6 (see the combined-input example above). First row:
{"requestedChannel": "@NASA","requestedChannelId": "UCLA_DiR1FfKNvjuUpBHmylQ","requestedCanonicalChannelUrl": "http://www.youtube.com/@NASA","similarChannelId": "UC9SM7V7J1pAhPabOUST01fw","similarChannelName": "Learn With NASA","similarSubscriberCountText": "60.3K subscribers","widgetPosition": 1,"source": "about-page-similar-channels-widget","status": "ok","confidence": "display-text","partial": false,"action": "ingest","error": null,"checkedAt": "2026-08-17T20:18:39.344Z"}
All 12 rows for @NASA in that run carried a subscriber count (from 19K to 1.69M), and every one
of the other channels tested for this page — @mkbhd, @ycombinator, @NASA — did too. No row
with partial:true (a delivered similar channel with a missing subscriber count) was observed live
while building this page; that shape exists in the code and the Dataset schema, but its live
frequency is honestly unknown from this Actor's own testing — see Limits.
Quiet/free path — a genuinely empty widget, @GoogleDevelopers
This closes an open question the Actor's own build spec flagged before this Actor was written: does
an unpopular or non-creator channel's widget render as genuinely empty, or does it fail? Confirmed
live while building this page — @GoogleDevelopers resolved cleanly and returned zero similar
channels:
{"requestedChannel": "@GoogleDevelopers","requestedChannelId": "UC_x5XG1OV2P6uZZ5FSM9Ttw","requestedCanonicalChannelUrl": "http://www.youtube.com/@GoogleDevelopers","similarChannelId": null,"similarChannelName": null,"similarSubscriberCountText": null,"widgetPosition": null,"source": "about-page-similar-channels-widget","status": "empty","confidence": null,"partial": false,"action": "skip-not-found","error": null,"checkedAt": "2026-08-17T20:18:09.261Z"}
Note action:"skip-not-found" here even though the channel was found — see the field
dictionary note on action below; status, not action, is the field that tells empty apart from
error.
Failure path 1 — a handle that does not resolve at all (HTTP 404)
{"requestedChannel": "@this-handle-should-not-exist-zzz123","requestedChannelId": null,"requestedCanonicalChannelUrl": null,"similarChannelId": null,"similarChannelName": null,"similarSubscriberCountText": null,"widgetPosition": null,"source": "about-page-similar-channels-widget","status": "error","confidence": null,"partial": false,"action": "skip-not-found","error": "channel not found (HTTP 404)","checkedAt": "2026-08-17T20:18:38.749Z"}
Failure path 2 — a syntactically valid but non-existent channel ID (HTTP 200, body text)
A different negative case, deliberately distinct from the one above: a well-formed 24-character
UC… ID that does not belong to any real channel returns a clean HTTP 200 with the literal text
"This channel does not exist" inside the page body — a status-code check alone would have called
this a success.
{"requestedChannel": "UC0000000000000000000000","requestedChannelId": null,"requestedCanonicalChannelUrl": null,"similarChannelId": null,"similarChannelName": null,"similarSubscriberCountText": null,"widgetPosition": null,"source": "about-page-similar-channels-widget","status": "error","confidence": null,"partial": false,"action": "skip-not-found","error": "channel does not exist (confirmed via page body text)","checkedAt": "2026-08-17T20:21:26.617Z"}
Failure path 3 — a blocked host disguised as a channel value (SSRF guard)
{"requestedChannel": "https://evil.example/@fake","requestedChannelId": null,"requestedCanonicalChannelUrl": null,"similarChannelId": null,"similarChannelName": null,"similarSubscriberCountText": null,"widgetPosition": null,"source": "about-page-similar-channels-widget","status": "error","confidence": null,"partial": false,"action": "skip-not-found","error": "blocked host (not a youtube.com URL): evil.example","checkedAt": "2026-08-17T20:18:38.759Z"}
No request was ever sent to evil.example — the host check runs before any fetch call, on the
input value alone.
Field dictionary
| Field | Meaning | Important boundary |
|---|---|---|
requestedChannel | The raw input value exactly as the buyer supplied it | Never normalized in this field — compare against requestedChannelId/requestedCanonicalChannelUrl for the resolved identity |
requestedChannelId | The requested channel's own confirmed UC… ID | null only when the requested channel itself failed to resolve (status:"error"); populated on both "ok" and "empty" outcomes |
requestedCanonicalChannelUrl | The requested channel's own canonical URL, read as-is from the page | Reported exactly as YouTube's own page carries it — including its own http:// scheme, observed live, not normalized to https:// |
similarChannelId | The similar channel's UC… ID | null on every summary row ("empty"/"error"); never equal to requestedChannelId by construction — see the inversion trap in Evidence and boundaries |
similarChannelName | The similar channel's display name, as YouTube's widget shows it | Untrusted third-party text, never interpreted or executed — see Evidence and boundaries, threat model |
similarSubscriberCountText | The similar channel's subscriber count, as YouTube's own rounded UI text | null when the widget entry itself carries no subscriber-count text — not observed live while building this page, but a real, nullable code path (partial reflects it) |
widgetPosition | 1-based position of this channel within the widget shelf, this run | Not guaranteed stable between runs — the widget's own composition and order can change |
source | Fixed to "about-page-similar-channels-widget" | Names the one page and one widget this Actor reads — no other surface |
status | The outcome for this row | "ok" (a delivered similar channel), "empty" (requested channel resolved, widget had nothing), or "error" (requested channel did not resolve, or input was rejected) |
confidence | Fixed to "display-text" on every "ok" row, null otherwise | States plainly that the subscriber count is rounded display text, not a measured exact number |
partial | true when a delivered similar channel's subscriber-count text is missing | Does not mean unbilled — see Pricing; a partial:true row is still status:"ok" and still charged |
action | A routing hint: "ingest", "skip-no-subcount", or "skip-not-found" | "skip-not-found" is used for both a channel that failed to resolve and a channel that resolved with a genuinely empty widget — read status, not action, to tell those two apart |
error | Human-readable failure reason, or null on "ok"/"empty" rows | Distinguishes the two closed negative cases above by wording — "HTTP 404" vs. "confirmed via page body text" |
checkedAt | ISO-8601 timestamp of this specific request | Shared across all rows produced for one requested channel in one run |
Evidence and boundaries
The central number this Actor's buyers must understand: the similar-channels widget's size is
not fixed, and this Actor does not control it. This is the single most likely way a buyer
misuses this Actor's output. Three independent, dated measurements of the identical channel,
@mkbhd, are on record:
| Measurement | Similar channels found | When / how |
|---|---|---|
| Project record #1 | 12 unique channelIds | 2026-08-17, first live Gate B measurement (HUB10_GATE_EVIDENCE.md) |
| Project record #2 | 7 unique channelIds | 2026-08-17, independent control measurement, same channel, same day |
| This page, run 1–4 | 6 — identical across all four | 2026-08-18, four consecutive apify run --purge cycles, back to back, while writing this page |
The last row is worth reading carefully rather than as a contradiction of the first two: four
back-to-back local runs, minutes apart, from the same machine, returned the exact same 6 channels
in the exact same order every time — The Studio, Auto Focus, WVFRM Podcast, Waveform Clips,
20syl, Madeon, in that order, with identical subscriber-count text each time. That short-run
stability is a real, observed property; it is not evidence that the count is fixed. It sits
alongside two independently-run measurements, on a different day, that returned 12 and then 7 for
the identical channel. The honest reading is: within one short session, the widget can look stable;
across sessions and days, it visibly is not. Neither number is "the real one."
The same session that reproduced @mkbhd at a stable 6 also measured three other channels once
each, and got three different answers: @NASA → 12, @ycombinator → 3, @GoogleDevelopers → 0
(a genuinely empty widget). That range — zero to twelve, on ordinary, well-known channels, measured
in the same few minutes with the same code — is the honest shape of this data source. This Actor
reports the widget's actual count for that run, without padding, capping, or normalizing it to look
more consistent than it is.
The extraction trap this Actor exists to get right. subscriberCountText occurs on a channel's
About page seven times, in two different JSON shapes, and getting this backwards is the
single most damaging bug this kind of Actor could ship:
- Six object-form occurrences —
{"accessibility":…, "simpleText":"1.13M subscribers"}— belong to the "similar channels" widget's owngridChannelRendererentries. This is the data this Actor sells. - One string-form occurrence — a bare
"subscriberCountText":"21.1M subscribers"— sits insideaboutChannelViewModel, and belongs to the requested channel itself, not to any similar channel.
A parser that walks the whole page and returns the first (or any) match of the key name by
position, rather than by which structure it lives inside, will confidently return the requested
channel's own subscriber count as if it were a similar channel's — a wrong answer delivered at a
clean HTTP 200, worse than an empty result, because nothing about the response signals a problem.
This Actor's extraction function (extractSimilarChannels in lib/channel.js) reads only
gridChannelRenderer objects and never touches aboutChannelViewModel at all — the leak is
structurally impossible, not merely avoided by convention. This is exactly what acceptance golden
#2 in this Actor's own acceptance.json checks: every similar-channel row's similarChannelId
must differ from the requested channel's own ID, and no similar-channel row's subscriber count may
equal the requested channel's own ("21.1M subscribers" for @mkbhd). Every live row produced while
writing this page satisfies both checks.
Each similar channel's channelId, name and subscriber count are read from one single JSON
object, never assembled from three separate page scans. Every gridChannelRenderer entry already
carries its channelId, title and subscriber text together, as one object; extractSimilarChannels
pulls the triple from that one object, in one pass. Collecting all channelIds in one scan and all
subscriber counts in a separate scan — the more obvious first design — risks pairing the wrong
count with the wrong channel if the widget re-orders between the two passes. This Actor never does
that.
A second, independent trap on the same page: identifying the requested channel's own row is not
a matter of page position either. aboutChannelViewModel — the block that carries the requested
channel's own channelId, canonical URL and subscriber count — occurs on the page a variable
number of times: measured as few as one occurrence and as many as two, on different live
measurements. Picking "the second occurrence" (a plausible, and this Actor's own earlier design)
silently breaks the moment a page only carries one. The fix actually shipped is identity-based, not
position-based: every occurrence is checked against the buyer's own requested channel — by exact
channelId match for a UC…-id request, or by extracting the @handle segment from the
occurrence's own canonicalChannelUrl/displayCanonicalChannelUrl for a handle request — and the
last occurrence that confirms a match is used. An occurrence that provably names a different
channel is never eligible, even as the page's only occurrence. An occurrence with no comparable
field for the input kind (a legacy /c/<name> request against a block with no vanity-name field to
compare) is accepted only when it is the page's sole candidate; the moment a second, undetermined
candidate exists, this Actor refuses rather than guesses. This logic is covered by 16 offline unit
tests in lib/test/channel.test.js (run while writing this page: 16/16 pass), including a
constructed identity-mismatch case a live page can never legitimately produce — the one scenario a
live network run structurally cannot exercise on its own.
Consent and locale are mandatory on every request, not an optimization. Every fetch to YouTube
carries ?hl=en&gl=US in the query string and Cookie: CONSENT=YES+1 in the request header.
Without both, a request from certain IP ranges renders in a local, non-English page shape, and an
English-token-dependent parser returns a silent empty result on a clean HTTP 200 — a more
dangerous failure than a network error, because it is indistinguishable from a genuinely empty
widget. This Actor sends both on every single request, unconditionally.
The SSRF and DNS-rebinding guard, and why the blocked-host example above is not decorative.
Every channel value that looks like a URL is checked against an explicit host allowlist
(youtube.com, www.youtube.com, m.youtube.com) before any request is attempted — demonstrated
live above with https://evil.example/@fake. Beyond the hostname check, the resolved IP address of
every request is itself checked against the private/loopback/link-local/CGNAT/metadata address
ranges (both IPv4 and IPv6, including the IPv4-mapped and NAT64 IPv6 forms), and the connection is
then pinned to exactly the verified addresses via a custom DNS resolver — so an attacker who
controls DNS cannot race a second resolution back to a private target after the check passes.
Redirects are followed manually, capped at 5 hops, with the full host-and-address check re-run at
every hop — a redirect cannot be used to smuggle a request past the guard either. None of this is
inferred from documentation; it is read directly from lib/fetch.js while writing this page.
Byte and time bounds, and what happens when they are hit. Every fetch is capped at 20 seconds
and 4.5 MB — comfortably above the largest About page measured for this hub (roughly 2.5–2.6 MB) —
and a response that hits the byte cap is explicitly marked truncated and reported as an honest
status:"error" row (
"response truncated at the byte cap on /about — page may be incomplete/ corrupt"Billing is atomic and mutex-protected, by design, against a known class of race. A charge and
its corresponding dataset write happen together, inside a single serialized critical section
(lib/billing.js's withLock), so two concurrent workers processing different requested channels
cannot both read "budget remains" and both proceed past the buyer's own spending cap. This exact
class of bug — a plain counter read outside the lock, allowing an over-delivery race under
concurrency — is documented in this codebase's own history as something Codex review previously
reproduced live on a sibling Actor line before this pattern was standardized. This Actor was built
with the already-fixed version from the start, not patched into it afterward.
Independent review before this page was written. This Actor's identity-confirmation fix — the
aboutChannelViewModel occurrence-count drift described above — went through its own dedicated fix
cycle after live measurement showed the original "always pick the second occurrence" design was
unsafe; the replacement logic is what ships today, backed by the 16-test suite named above. The
billing guard is the same previously-reviewed lib/billing.js component, reused unmodified from an
already-shipped sibling line, per this codebase's own build standard: "a copy that looks equivalent
is how the line drifts and re-introduces a fixed bug."
Boundary with sibling Actors in this hub. youtube-channel-intel reads the requested
channel's own data from the same page — including its external links and business email — and is
the correct tool if that is what you need; this Actor deliberately never returns those fields for
either the requested or the similar channels, to avoid selling the same field twice under two
different Actors. youtube-channel-videos-list and youtube-playlist-scraper read a completely
different part of a channel (its video/upload list), not the About page's similar-channels widget,
and do not overlap with this Actor's data at all.
Decision routing
| Row shape | status | Billable? | action | What it means |
|---|---|---|---|---|
| Delivered similar channel, subscriber count present | "ok" | Yes | "ingest" | The core billable outcome |
| Delivered similar channel, subscriber count missing | "ok" | Yes | "skip-no-subcount" | Still billed — partial:true describes a missing field, not a discount; see Pricing |
| Requested channel resolved, widget genuinely empty | "empty" | No | "skip-not-found" | A real, honest zero — not distinguishable from an error by action alone, only by status |
Requested channel did not resolve (HTTP 404, or blocked/invalid input) | "error" | No | "skip-not-found" | error field names the specific reason — see Field dictionary |
Requested channel ID is well-formed but does not exist (HTTP 200, body text) | "error" | No | "skip-not-found" | The second, distinct negative case — see Evidence and boundaries |
| Run's own spending cap reached partway through | "error" | No | "skip-not-found" | One free explanatory row is written naming exactly this; already-billed rows from earlier in the same run are unaffected |
A workflow only needs two rules to route this Actor's output correctly: treat status:"ok" as the
one billed, deliverable outcome, and never assume a fixed number of "ok" rows per requested
channel — see Evidence and boundaries for the measured range.
Commercial playbooks
Lookalike audience seeding for influencer marketing
A media buyer or influencer-marketing team runs this Actor against a shortlist of creator/brand
channels they already know convert, and feeds every delivered similarChannelId — plus, when
present, similarSubscriberCountText for a rough size filter — into an ad platform's custom or
lookalike audience tool, or directly into an outreach spreadsheet for influencer partnerships,
without manually clicking through YouTube's own About-page widget for each channel by hand.
Competitive peer-set benchmarking
A competitive analyst runs this Actor against a competitor's own channel to see who YouTube itself considers that competitor's neighbors — a signal the competitor cannot see, curate, or suppress, since it comes from YouTube's own recommendation system, not from the competitor's declared positioning.
Bulk channel-discovery seed for a larger research pipeline
A researcher or agency runs this Actor against a batch of up to 50 known "hub" channels in a
vertical, collects every similarChannelId returned, and de-duplicates the result into a candidate
list for a second-stage tool (e.g. this hub's own youtube-channel-intel) to enrich each candidate
with links, email and subscriber detail — chaining two purpose-built Actors rather than asking one
Actor to do both jobs and charge for both.
Integration recipes
Scheduled competitive watch
An Apify Task runs this Actor weekly or monthly against a fixed list of competitor channels,
reading the Dataset for status:"ok" rows and diffing similarChannelId sets between runs in your
own storage to see which lookalike channels are new, gone, or persistent — this Actor itself keeps
no memory between runs; the diff is the caller's responsibility.
Agent/MCP pattern
An agent supplies one or more channel values and reads status before doing anything else with a
row: "ok" rows carry a usable similarChannelId; "empty"/"error" rows never do. It should
keep consuming rows for a given requestedChannel until that channel's batch ends, whatever size
it turns out to be.
Data-warehouse append pattern
Dataset rows are appended keyed by (requestedChannelId, similarChannelId) when both are present —
that pair is stable and meaningful even though the total count per requested channel is not. A
status:"empty"/"error" row carries no similarChannelId; key those rows by
(requestedChannel, checkedAt) instead, so a "this channel had nothing" observation is not lost
or silently merged with a genuine channel-not-found error.
Operating guide
Why this Actor never exposes a "top N similar channels" input. There is no server-side ranking
or truncation this Actor could apply on top of YouTube's own widget order that would not
misrepresent what YouTube actually showed. widgetPosition reports the order observed this run;
it is intentionally not exposed as a filterable/cappable input, because doing so would imply a
promise about ranking stability this data source does not support.
Reconciling a run against your invoice. This run's key-value store record OUTPUT carries a
one-time run summary written at the end of the run: how many channels were requested, how many rows
were delivered, how many of those were charged and how many were free, how many failed, how many
came back incomplete, whether re-running the same input is safe, and whether the run ended in a
fatal error. Because the number of similar channels varies between runs, this roll-up is the fastest
way to see what a given run actually cost you. The Dataset stays the complete row-by-row record;
OUTPUT is the summary, not a replacement for it.
Reading a status:"empty" row correctly. An empty widget is a genuine, confirmed-live outcome
(see @GoogleDevelopers above), not a bug — it happens for channels YouTube's own recommendation
system has not populated a similar-channels shelf for. See Limits for what this page's testing
could and couldn't establish about which channels this affects.
Choosing maxConcurrency. The default of 5 is a conservative starting point, not a measured
ceiling — this Actor has not been separately load-tested for a maximum sustainable concurrency
against YouTube's own rate limits. If you are running the full 50-channel input ceiling regularly,
consider a lower value first and watch for status:"error" rows with a fetch-timeout or non-404
HTTP error before raising it.
Interpreting repeated runs against the same channel. Short successive runs can return an
identical widget composition; runs separated by a day or more have measured genuinely different
ones for the same channel (see Evidence and boundaries). Schedule any change-detection in days, not
minutes, and diff the delivered similarChannelId sets in your own storage — see Integration
recipes.
Related tools
Related tools for adjacent workflows in social platforms and regional directories.
| Actor | What it does |
|---|---|
| YouTube Channel Intel | Pair it in the social platforms and regional directories workflow: Public intel card for any YouTube channel: subscribers, total views, video count, join date, country, full... |
| YouTube Channel Videos List | Pair it in the social platforms and regional directories workflow: List up to 100 most recent uploads for any YouTube channel: videoId, title, view count, and publish age... |
| YouTube Playlist Scraper | Pair it in the social platforms and regional directories workflow: Scrape any YouTube playlist by id or URL — including other people's public playlists, not just your own... |
| Substack Publication Scraper | Pair it in the social platforms and regional directories workflow: Read recent posts from any Substack publication's own domain — title, date, full HTML body, comment count... |
| Bluesky Profile Scraper | Pair it in the social platforms and regional directories workflow: Batch-fetch public Bluesky profiles by handle or DID via the official keyless AppView API — display name,... |
FAQ
Will the same channel always return the same number of similar channels?
No — see Evidence and boundaries. The same channel, @mkbhd, measured across independent sessions
returned 12, then 7, then a stable 6 across four consecutive runs in the session that produced this
page.
Does a missing subscriber count mean the row is free?
No. partial:true on a status:"ok" row means the subscriber-count text is missing for that one
channel — the row is still delivered and still billed. See Pricing for why this is intentional.
Can the requested channel appear in its own similar-channels list?
No, by construction — the extraction step is given the requested channel's own confirmed
channelId and structurally excludes it, and this Actor's own acceptance suite specifically checks
that no delivered row's similarChannelId or subscriber count matches the requested channel's own.
See Evidence and boundaries for the exact page-structure trap this closes.
What happens if a channel's widget is genuinely empty?
One free status:"empty" row, with the requested channel's own identity (requestedChannelId,
requestedCanonicalChannelUrl) still correctly resolved and reported — demonstrated live above
with @GoogleDevelopers. This is not an error.
Does this Actor return external links or the channel's business email?
No, for either the requested channel or any similar channel. That data belongs to the sibling
Actor youtube-channel-intel in this hub, deliberately, so the two Actors do not sell the same
field twice.
Does this Actor read comments, transcripts, or search results?
No. This Actor reads exactly one page shape — a channel's About page — and nothing else. See
Sources and rights for the exact robots.txt paths this Actor never requests.
Do I need a YouTube API key or a login?
No. Every request this Actor makes is an unauthenticated GET with a consent cookie and a locale
query string — no key, no session, no account, confirmed by every live run made while building
this page.
How is the subscriber count reported — is it exact?
No. similarSubscriberCountText is YouTube's own rounded display text (e.g. "1.13M subscribers"),
exactly as the widget shows a human visitor — never a computed exact integer. confidence is fixed
to "display-text" on every delivered row specifically to keep this visible on every row, not just
in this page's prose.
What is the difference between the two "channel not found" cases?
Two genuinely different situations, both demonstrated live above: a handle or ID that plainly does
not resolve returns HTTP 404 (error: "channel not found (HTTP 404)"); a syntactically valid
UC… ID with no real channel behind it returns a clean HTTP 200 with "This channel does not
exist" in the body (error: "channel does not exist (confirmed via page body text)"). Both are
free, status:"error" rows — the wording of error is the only thing that tells them apart.
Sources and rights
Source family: YouTube public channel and playlist pages. This Actor reads the channel page's own recommendations shelf, anonymously, with no login, no API key and no browser session.
The only source. https://www.youtube.com/@handle/about (and the equivalent /channel/UC…,
/c/Name, /user/Name forms) — the same public, unauthenticated page a human visitor sees in a
browser, including its "similar channels" widget. No YouTube Data API key, no OAuth, no session.
robots.txt. www.youtube.com/robots.txt disallows /api/, /youtubei/, /results and
/comment. The About-page path this Actor reads is not disallowed. This Actor never requests any
of the disallowed paths for any reason — it has no code path that could.
No warranty implied. This Actor reports whatever YouTube's own front end rendered for a given channel at the moment of the request. It cannot guarantee YouTube's continued page shape, the widget's continued existence, or that any specific channel will have a populated widget on any given day — see Evidence and boundaries for the measured range this claim is based on.
What this Actor does not read or redistribute. No video content, no comments, no transcripts, no
search results, no data from any authenticated or paid YouTube surface. Channel names returned in
similarChannelName are the channel owners' own public display names, shown by YouTube itself in
the widget — the same text a human visitor already sees on that page.
Limits
- The similar-channels widget's size is not fixed and is entirely YouTube's decision — measured live between 0 and 12 similar channels across ordinary, well-known channels in one short session, and 12/7/6 across independent measurements of the identical channel over time. See Evidence and boundaries.
partial:true(a missing subscriber count on a delivered row) was not observed live during this page's own testing across@mkbhd,@NASAand@ycombinator— every delivered row carried a subscriber count in every test run made while writing this page. The code path and Dataset field exist and are nullable by design; how often it actually fires in the wild is honestly unmeasured by this Actor's own testing.- A genuinely empty widget (
status:"empty") was observed for exactly one channel tested,@GoogleDevelopers. This confirms the shape exists and behaves as an honest empty result rather than a crash or a silent error, but this page's own testing did not establish which category of channel (small, new, non-personal, or something else) reliably produces it. widgetPositionis a same-run observation, not a stable ranking. It records the order the shelf rendered in for that one request; nothing in this Actor or in YouTube's own page contract promises the same channel keeps the same position, or appears at all, on a later run.- No cross-run memory. This Actor keeps no state between runs — it does not diff, deduplicate, or track "new since last time" for you. A scheduled watch needs its own storage on the caller's side to detect change; see Integration recipes.
maxConcurrency's upper bound (20) has not been separately load-tested against YouTube's own rate limits by this Actor's own build process; the default of 5 is a conservative starting point, not a measured safe ceiling. See Operating guide.- No external links, business email, video list, or transcript data — out of scope by design, not by omission; see Evidence and boundaries and Sources and rights for where that data actually lives in this hub.
- English/US locale only. Every request is sent with
hl=en&gl=US; this Actor does not support reading a channel's About page in another rendered locale.
Support boundary
Actor support covers deterministic input validation (the host/path checks demonstrated live above),
the extraction behavior described in Evidence and boundaries, the Dataset schema, and the billing
split between a delivered similar channel and every free outcome. It does not cover, and cannot
promise: how many similar channels YouTube's widget will show for any given channel on any given
day (see Evidence and boundaries — this is YouTube's decision, not this Actor's); why YouTube's own
recommendation system chose the specific channels it did; recovery if YouTube changes this page's
underlying structure in a way that breaks extraction (this Actor's own build history already shows
one such structural drift, in aboutChannelViewModel's occurrence count, closed by the
identity-based fix described in Evidence and boundaries — a future drift of a different kind is
possible and would need its own fix); and it does not provide marketing, audience-targeting, or
competitive-strategy advice on top of the raw channel identities this Actor returns. When reporting
a problem, include the Actor run ID, the exact channels input used, the affected
requestedChannel value, its status/error, and the approximate checkedAt time.