Skip to content

Channel Discovery

Channel Discovery grows the shared Creator universe (ADR 0019) so that tracked Brands’ sponsor videos are observed. It nominates Channel candidates from four sources, qualifies them, and admits the highest-scoring ones through the same admission path as a person adding a Channel. It is implemented in packages/api/src/channel-discovery/, runs as TypeScript jobs on Cloudflare Queues driven by the server’s scheduled trigger, and is off by default. This page describes implemented behavior; it does not claim Discovery is enabled in production.

A candidate is keyed by its canonical UC… Channel ID, so every handle, URL, and search result for the same Channel converges on one record. Each sighting records its source, the Brand (and query, for Brand search) or Creator that produced it, its rank, an evidence reference, and when it was first and last seen. Repeating a sighting increments its count; it never creates a duplicate.

Source Evidence reference
meta_creator Approved YouTube social link decision, Creator name-search match, or Meta Page profile link
brand_search Video (or Channel) in a Brand’s youtube-search results, with query and rank
sponsor_graph A Mention: its Channel’s featured/related Channels, or Channels in the same Brand’s results
user, staff A Channel a Workspace member or Mosaic staff admitted (staff = MOSAIC_STAFF_USAGE_WORKSPACE_IDS)
seed A Channel from the operator seed

Candidates are pending, qualified, rejected (with a reason), or admitted. A candidate already in the roster is admitted with admitted_by = roster; one Discovery admitted has admitted_by = discovery.

Discovery stores no Creator names or Channel titles. A Creator name search records the Creator’s ID, not the query text, and a name match only nominates a Channel for collection. It never links that Channel to the Creator (Creator identities still merge only by staff decision).

  1. Meta partnership Creators. For every Creator with explicit Meta partnership evidence, the current approved youtube social link is resolved to its Channel through the logged-out public page and RSS feed (no Data API quota). YouTube URLs in the Creator’s confirmed Meta Page profile URL or metadata are resolved the same way. A Creator with no approved link gets one type: channels search for its display name; only results whose name equals the display name after normalization (lowercase letters and digits) are sighted, at most two.
  2. Brand search. Each active catalog Brand is searched for its name, staff product terms, and each confirmed company domain and exact campaign hostname, in that order, at most eight queries per Brand, once per refresh period (seven days by default). Staff add product terms with POST /control-plane/channel-discovery/query-terms ({ "brandId": "…", "term": "Skylight calendar" }); terms never become Matchers. Video and live results with an owning Channel are sighted by rank; Shorts lockups without an owner are counted but not sighted.
  3. Sponsor graph. For each visible Mention in the last 30 days, every Channel already sighted in that Brand’s search results gets a sponsor_graph sighting, and the Mention Channel’s channel graph (Home featured-channel and collaboration shelves, and the Channels tab where YouTube still exposes it) is read once per refresh period and sighted. Mentions come from the current canonical Mentions and from ADR 0019 youtube_mention rows.
  4. Existing admissions. Channels admitted by users, staff, or the seed are recorded as sightings and marked admitted, so Discovery never re-admits them and its metrics cover the whole roster.

Sightings that reveal a suppressed or erased Channel, video, or Creator, or a governed candidate, are dropped (ADR 0018). A reference that resolves only to governed subjects completes as governed and its work keeps no copy of the handle or URL.

An ADR 0018 erasure scrubs Discovery in the same transaction as the rest of Mosaic:

  • Candidates. The candidate of an erased Channel, and any candidate sighted through an erased Creator’s evidence (their approved link, Meta Page profile, or name search), becomes a terminal rejected/governed tombstone. Subscriber count, upload recency, kids status, profile source, admission score, and admission time are cleared; counts, priority, and times stay. A tombstone is never re-sighted, re-qualified, admitted, or marked as admitted from the roster. A Channel found only by an exact match on the erased Creator’s name is tombstoned too: a name match cannot prove the Channel is someone else’s, and the cost is a few Channels of coverage.
  • Sightings. Sightings of those candidates, sightings attributed to the erased Creator, and sightings whose evidence is an erased video, a hidden Mention, or scrubbed work keep their source, Brand, rank, counts, and times; their query becomes null and their evidence reference [erased].
  • Work. Reference resolution and name searches for the erased Creator, the erased Channel’s channel graph, and references that resolved to or literally name the erased Channel keep only { "kind", "erased": true }. Unfinished rows complete as subject_erased. Work keys hold only identifiers, periods, Brand query text, and a SHA-256 digest of a Page-profile reference.

A sighting write, an admission, and an erasure are serialized by an advisory lock that erasure takes exclusively before writing any receipt. A concurrent sighting is either scrubbed or refused. The admission service re-reads the candidate under a row lock inside its transaction, so an erasure that lands between a reservation and the admission refuses it as subject_governed; the candidate stays a tombstone and its reservation is released. The erasure also makes each affected run’s Apify storage due for deletion again (see below).

Qualification reads up to 50 candidates at a time, highest priority first, with one official channels.list call (snippet,statistics,status,topicDetails, one Data API unit) and each Channel’s public RSS feed (no quota). In order, a candidate is rejected as:

Reason Rule
governed The Channel or its Creator is suppressed or erased. Terminal.
unavailable channels.list did not return the Channel.
made_for_kids status.madeForKids is true.
music Title ends in - Topic or VEVO, or every official topic is a music topic.
brand_owned Title or handle is a catalog Brand’s name or confirmed-hostname label, or starts with the Brand’s name while linking to its confirmed hostname.
no_recent_upload No public upload in the preceding 12 months. Re-checked after 90 days.

madeForKids comes only from the Data API; without a Data API profile source, qualification waits rather than guessing. Priority (before qualification) is 12 × Brands (max 5) + 6 × sources (max 4) + rank weight (Σ 3/rank, max 12) + 20 for an approved Creator link + 10 for any Meta Creator source + 15 for a channel-graph sighting (8 for a co-search sighting). The admission score adds recent activity (0–10: uploads in the last 90 days, saturating at 12) and audience (0–10: 2.5 × (log10(subscribers) − 3), so 1K scores 0, 100K 5, and 10M 10). Scoring version: channel-discovery-score.v1.

Every 15 minutes an admission job admits the best qualified candidates with an admission score of at least 30 and at least 1,000 public subscribers (hidden counts are allowed), until the daily admission budget is used. Each admission calls the existing admission service, which re-checks the execution policy, governance, uniqueness, and 12-month recency, creates the Channel with a discovery contributor (no Workspace or member), forms the silent Channel-addition baseline, and queues the first collection. A Channel governed after qualification is refused.

Budget (per UTC day) Default Environment variable
Discovery admissions 25 MOSAIC_CHANNEL_DISCOVERY_DAILY_ADMISSIONS
Apify spend $1.00 MOSAIC_CHANNEL_DISCOVERY_DAILY_APIFY_USD
YouTube Data API units 300 MOSAIC_CHANNEL_DISCOVERY_DAILY_DATA_API_UNITS

User and staff admissions do not count against the Discovery budget. An Actor run is charged at its maxTotalChargeUsd cap ($0.02) when it starts and at Apify’s reported usageTotalUsd once it finishes (observed 0.2.3 costs: about $0.0002 per search without extras and $0.0017 per channel graph). A run held back only by other runs’ unsettled reservations retries in five minutes; a run that settled spend alone would push past the day’s Apify budget waits for the next UTC day. An admission reserves two Data API units (the admission check) and a qualification batch one. The Data API units are also metered in the shared quota ledger and pause with it.

MOSAIC_CHANNEL_DISCOVERY_MODE is off (default: nothing is planned), observe (source and qualify, never admit), or admit. An invalid value is off. Actor-backed sources need APIFY_TOKEN; without it only Creator links, profile links, co-search, and existing admissions run.

Discovery calls private Apify Actors through a pinned run client modeled on the Meta collector: it resolves the reviewed build ID to its build number, starts with a timeout, memory, and charge cap, verifies that the run belongs to that Actor and build, polls, and reads exactly one dataset item. It never selects latest. An unset pin fails closed.

Actor Reviewed pin (brianyip/youtube-actors 0.2.3)
brianatpaceline/youtube-search thHLqRaE7gjTv9L47, build AuAtciodNwbzvDSXY; override with MOSAIC_CHANNEL_DISCOVERY_SEARCH_BUILD_ID
brianatpaceline/youtube-channel-details ImFUBTmcGFPcFeyKN, build tESQnG7LsqaDKn5sv; override with MOSAIC_CHANNEL_DISCOVERY_GRAPH_ACTOR_ID/…_BUILD_ID

Search requests { query, type?, includeExtras: false, maxPages: 1, region: "US" } and reads the ScrapeCreators-shaped envelope; a row whose enrichmentStatus is failed (for example BLOCKED) is a coverage gap and is not sighted. A SUCCEEDED run with a PARTIAL status still yields its complete listing. The channel graph requests { channelId, includeChannelGraph: true } and reads channelGraph.channels[] (channelId, source of home_featured_channels, home_collaborations, or channels_tab); a surface reporting failed is a gap, never an empty graph. A new Actor build is a reviewed pin change.

Durable state lives in Postgres; there is no trigger logic. Once per 15-minute slot, the scheduled trigger plans at most 40 new work rows with idempotent keys (for example one search per Brand, query, and refresh period); every tick syncs existing admissions and relays due row IDs to the collection queue as channel_discovery_work messages. A consumer starts a row with a compare-and-set and a ten-minute lease, and finishes, fails, or defers it back to the outbox. Every message is acknowledged; the row owns retries. Expired leases are re-sent by the next relay. An Actor start writes a dispatch marker first, so a lost start response ends the row as outcome_unknown instead of paying for a second run. Admission reservations older than an hour are reconciled against the roster.

Apify run storage. Apify keeps each run’s default dataset (the output) and key-value store (including its INPUT, which can carry a Creator’s name or handle). Once a run’s results are durably recorded and its work row completes, succeeded or failed, Discovery deletes both through DELETE /v2/actor-runs/{runId}/dataset and /key-value-store, addressed by the run ID recorded on the row. A 404 counts as already deleted. The result is recorded on the row (provider_storage_state pending, deleted, or abandoned, with attempts, last outcome, and deletion time; migration 0099). Deletion is best effort and never blocks ingestion: a failure stays pending and each relay tick sweeps due rows (up to 20) an hour later, also while Discovery is off. After eight failed attempts the row is abandoned; an erasure that reaches the row makes it pending again.

GET /control-plane/channel-discovery/metrics (Mosaic staff browser sessions only; API keys are refused) returns candidates by state and by source × state, rejections by reason, Discovery admissions per day and spend per day for 14 days, cost per admitted Channel over the trailing seven days (Apify USD and Data API units), and the work backlog by kind and state. Governed Channels are excluded from every count. This route is not an OpenAPI operation or MCP tool.