YouTube Channel Lookalike Finder avatar

YouTube Channel Lookalike Finder

Pricing

from $8.50 / 1,000 similar channel delivereds

Go to Apify Store
YouTube Channel Lookalike Finder

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

Tim Zinin

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

Share

YouTube Channel Lookalike Finder: Reading the Similar-Channels Widget on Any Channel's About Page

How this Actor turns your input into a decision 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 gridChannelRenderer objects 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. widgetPosition is 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 channelId and 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. requestedChannelId and requestedCanonicalChannelUrl are 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 404 at resolution time. A syntactically valid but non-existent UC… channel ID returns HTTP 200 with 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. subscriberCountText occurs 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 at HTTP 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 GET with 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 @GoogleDevelopers while building this page — see Real happy, partial and failure output) returns one clearly labelled status:"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, primary apify-default-dataset-item charge 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.

Where this Actor sits in your workflow

How to run

  1. Provide 1–50 channel values in channels. Each entry can be a bare @handle, a full https://www.youtube.com/... URL (/@handle, /channel/UC…, /c/Name, /user/Name, with or without a trailing /about, /videos, /featured or /playlists tab segment), or a bare UC… channel ID. Handle matching is case-insensitive.
  2. Optionally set maxConcurrency (default 5, 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.
  3. 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 a SUCCEEDED run with a non-empty dataset — see Real happy, partial and failure output for the exact rows it produced.
  4. 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").
  5. Do not build automation on an assumed row count per channel. Treat status as the signal that matters, not the row count — see Limits and Evidence and boundaries for the measured range.
  6. Route similarChannelId values 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 full youtube.com channel URL, or a bare UC… channel ID. A value that is a URL but does not resolve to youtube.com, www.youtube.com or m.youtube.com is rejected as status:"error" before any network request is made — demonstrated live below with https://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, default 5, min 1, max 20). 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

FieldMeaningImportant boundary
requestedChannelThe raw input value exactly as the buyer supplied itNever normalized in this field — compare against requestedChannelId/requestedCanonicalChannelUrl for the resolved identity
requestedChannelIdThe requested channel's own confirmed UC… IDnull only when the requested channel itself failed to resolve (status:"error"); populated on both "ok" and "empty" outcomes
requestedCanonicalChannelUrlThe requested channel's own canonical URL, read as-is from the pageReported exactly as YouTube's own page carries it — including its own http:// scheme, observed live, not normalized to https://
similarChannelIdThe similar channel's UC… IDnull on every summary row ("empty"/"error"); never equal to requestedChannelId by construction — see the inversion trap in Evidence and boundaries
similarChannelNameThe similar channel's display name, as YouTube's widget shows itUntrusted third-party text, never interpreted or executed — see Evidence and boundaries, threat model
similarSubscriberCountTextThe similar channel's subscriber count, as YouTube's own rounded UI textnull 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)
widgetPosition1-based position of this channel within the widget shelf, this runNot guaranteed stable between runs — the widget's own composition and order can change
sourceFixed to "about-page-similar-channels-widget"Names the one page and one widget this Actor reads — no other surface
statusThe 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)
confidenceFixed to "display-text" on every "ok" row, null otherwiseStates plainly that the subscriber count is rounded display text, not a measured exact number
partialtrue when a delivered similar channel's subscriber-count text is missingDoes not mean unbilled — see Pricing; a partial:true row is still status:"ok" and still charged
actionA 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
errorHuman-readable failure reason, or null on "ok"/"empty" rowsDistinguishes the two closed negative cases above by wording — "HTTP 404" vs. "confirmed via page body text"
checkedAtISO-8601 timestamp of this specific requestShared 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:

MeasurementSimilar channels foundWhen / how
Project record #112 unique channelIds2026-08-17, first live Gate B measurement (HUB10_GATE_EVIDENCE.md)
Project record #27 unique channelIds2026-08-17, independent control measurement, same channel, same day
This page, run 1–46 — identical across all four2026-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 own gridChannelRenderer entries. This is the data this Actor sells.
  • One string-form occurrence — a bare "subscriberCountText":"21.1M subscribers" — sits inside aboutChannelViewModel, 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"
) rather than parsed partially and reported as if it were complete.

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 shapestatusBillable?actionWhat 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 for adjacent workflows in social platforms and regional directories.

ActorWhat it does
YouTube Channel IntelPair 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 ListPair 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 ScraperPair 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 ScraperPair 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 ScraperPair 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.

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, @NASA and @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.
  • widgetPosition is 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.