<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harsh Maur</title>
    <description>The latest articles on DEV Community by Harsh Maur (@harshmaur).</description>
    <link>https://dev.to/harshmaur</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F988089%2F3148a155-ea7d-48ec-abe0-4da70e9fa33f.jpeg</url>
      <title>DEV Community: Harsh Maur</title>
      <link>https://dev.to/harshmaur</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/harshmaur"/>
    <language>en</language>
    <item>
      <title>YouTube Transcripts for RAG - Ask a Whole Channel, Get Answers With Timestamps</title>
      <dc:creator>Harsh Maur</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:56:07 +0000</pubDate>
      <link>https://dev.to/harshmaur/youtube-transcripts-for-rag-ask-a-whole-channel-get-answers-with-timestamps-4hi1</link>
      <guid>https://dev.to/harshmaur/youtube-transcripts-for-rag-ask-a-whole-channel-get-answers-with-timestamps-4hi1</guid>
      <description>&lt;p&gt;I wanted to ask Andrej Karpathy's YouTube channel questions and get back the exact moment in the video where he answers them. Not a summary. A link that starts the video at the right second.&lt;/p&gt;

&lt;p&gt;His three most recent videos together are almost 10 hours long. This post shows how I turned them into a searchable index in about 60 lines of Python: 15,285 caption segments, 598 one-minute chunks, and a search that answers "what is flash attention" with &lt;code&gt;&amp;amp;t=7241s&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Full disclosure: I built the transcript scraper used here and sell it on the Apify Store. Everything below is a real run from 20 October 2026, with the real output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why YouTube transcripts are awkward to get in bulk
&lt;/h2&gt;

&lt;p&gt;For one video, you can open the transcript panel and copy it. For a whole channel, the usual paths are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The YouTube Data API. It lists videos, but it only downloads captions for videos you own.&lt;/li&gt;
&lt;li&gt;An open-source transcript library to fetch captions in your own code.&lt;/li&gt;
&lt;li&gt;A hosted scraper that takes a channel, playlist or list of videos and returns the transcripts as JSON.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I use option 3, &lt;a href="https://apify.com/harshmaur/youtube-transcript-scraper?fpr=tl88um" rel="noopener noreferrer"&gt;YouTube Transcript Scraper&lt;/a&gt; on Apify, because it returns transcripts ready for indexing. You need no YouTube account, no Google API key and no cookies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A free Apify account. The free plan includes $5 of credit each month.&lt;/li&gt;
&lt;li&gt;Your Apify API token (Console → Settings → API &amp;amp; Integrations).&lt;/li&gt;
&lt;li&gt;Python 3 with &lt;code&gt;requests&lt;/code&gt;. Nothing else: the search below is plain Python, so you can run it before you pick a vector database.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Get every transcript from the channel
&lt;/h2&gt;

&lt;p&gt;The input takes a channel handle. &lt;code&gt;maxVideos&lt;/code&gt; limits how many of its newest videos you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channelUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"@AndrejKarpathy"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxVideos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transcriptFormat"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"both"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;transcriptFormat: "both"&lt;/code&gt; returns the timestamped segments and the joined plain text. For RAG you want the segments, because they carry the start time of each line. You can also pass &lt;code&gt;videoUrls&lt;/code&gt;, &lt;code&gt;playlistUrls&lt;/code&gt;, a &lt;code&gt;dateFrom&lt;/code&gt; and &lt;code&gt;dateTo&lt;/code&gt; window, and a &lt;code&gt;language&lt;/code&gt; code. If a video has no captions in that language, YouTube translates them, and &lt;code&gt;wasTranslated&lt;/code&gt; tells you so.&lt;/p&gt;

&lt;p&gt;Here is one real row from my run, with the long fields cut:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"videoId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"l8pRSuU81PU"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.youtube.com/watch?v=l8pRSuU81PU"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Let's reproduce GPT-2 (124M)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channelName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Andrej Karpathy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publishedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-06-09T16:31:35-07:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"durationSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;14486&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"viewCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1212773&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"availableLanguages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isAutoGenerated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"wasTranslated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transcript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.52&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.68&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hi everyone so today we are going to be"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"duration"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"continuing our Zero to Hero series and"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"transcriptText"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hi everyone so today we are going to be continuing our Zero to Hero series and ..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one video has 6,049 segments and 223,000 characters of text. Each row also includes the &lt;code&gt;description&lt;/code&gt; (with the chapter list, when the creator wrote one), &lt;code&gt;likeCount&lt;/code&gt;, &lt;code&gt;commentCount&lt;/code&gt;, &lt;code&gt;keywords&lt;/code&gt; and the thumbnail.&lt;/p&gt;

&lt;p&gt;Notice &lt;code&gt;isAutoGenerated: true&lt;/code&gt;. All three videos use YouTube's automatic captions, so the text has no punctuation and has some misheard words ("chpd" for ChatGPT). Embeddings handle that well. A keyword search like the one below is more sensitive to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Chunk by time, not by tokens
&lt;/h2&gt;

&lt;p&gt;Most RAG tutorials split text every N tokens. For video, split by time instead. Each chunk then keeps the start time of its first segment, and that start time becomes the citation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transcript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;60 seconds is about 150 to 200 spoken words, which is a good size for both embeddings and an LLM context window. A link to the chunk is &lt;code&gt;video["url"] + f"&amp;amp;t={start}s"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Search the chunks
&lt;/h2&gt;

&lt;p&gt;Here is the full script. It runs the scraper, chunks every video, and ranks the chunks with BM25, the classic keyword ranking that Lucene and Elasticsearch use by default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;

&lt;span class="n"&gt;ACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;harshmaur~youtube-transcript-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;videos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.apify.com/v2/acts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACTOR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/run-sync-get-dataset-items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channelUrls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@AndrejKarpathy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxVideos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transcriptFormat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;both&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;videos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;videos&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transcript&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;seg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;

&lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;videos&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; videos, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;transcript&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; segments, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; chunks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tokenize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[a-z0-9]+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;avg_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;bm25&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;tokenize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;term&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;idf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;tf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;term&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;idf&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;k1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;avg_len&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ranked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bm25&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Q: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranked&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; @ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;t=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
    &lt;span class="nf"&gt;ask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with your questions as arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;... python3 ask_channel.py &lt;span class="s2"&gt;"when should I use a thinking model"&lt;/span&gt; &lt;span class="s2"&gt;"why are LLMs bad at spelling"&lt;/span&gt; &lt;span class="s2"&gt;"how does gradient accumulation work"&lt;/span&gt; &lt;span class="s2"&gt;"what is flash attention"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The real output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;3 videos, 15285 segments, 598 chunks

Q: when should I use a thinking model
- Deep Dive into LLMs like ChatGPT @ 2:39:55  https://www.youtube.com/watch?v=7xTGNNLPyMI&amp;amp;t=9595s
  "on many math uh and kind of code problems that have verifiable Solutions so there's like an answer three Etc now these thinking models you can access in for exa..."
- How I use LLMs @ 0:30:24  https://www.youtube.com/watch?v=EWvNQjAaOHw&amp;amp;t=1824s
  "you now when you go to Gro for example when I start a new conversation with grock um when you put the question here like hello you should put something importan..."
- Deep Dive into LLMs like ChatGPT @ 3:27:03  https://www.youtube.com/watch?v=7xTGNNLPyMI&amp;amp;t=12423s
  "labeling instructions at open a so that's what we're getting back now I do think that the uh things change a little bit when you actually go and reach for one o..."

Q: why are LLMs bad at spelling
- Deep Dive into LLMs like ChatGPT @ 1:47:56  https://www.youtube.com/watch?v=7xTGNNLPyMI&amp;amp;t=6476s
  "was Data labeler and I was creating one of these one of these would be uh a really terrible answer for the assistant and the other would be okay and so I'd like..."
- Deep Dive into LLMs like ChatGPT @ 2:03:32  https://www.youtube.com/watch?v=7xTGNNLPyMI&amp;amp;t=7412s
  "third character and the actual truth is u2s uqs uh which looks correct to me so um again an example of spelling related tasks not working very well a very famou..."
- Deep Dive into LLMs like ChatGPT @ 2:00:37  https://www.youtube.com/watch?v=7xTGNNLPyMI&amp;amp;t=7237s
  "the model to copy paste those token IDs and um kind of unpack them into Dots here and so it creates this string and then it calls python routine. count and then..."

Q: how does gradient accumulation work
- Let's reproduce GPT-2 (124M) @ 2:45:50  https://www.youtube.com/watch?v=l8pRSuU81PU&amp;amp;t=9950s
  "processed per step is B * T * gradient accumulation so long story short here we have the optimization it looks uh reasonable right we're starting at a good spot..."
- Let's reproduce GPT-2 (124M) @ 2:43:54  https://www.youtube.com/watch?v=l8pRSuU81PU&amp;amp;t=9834s
  "well uh so where's the loss after that cross entropy and by default the reduction uh here in Cross entropy is also I don't know why they don't show it but it's ..."
- Let's reproduce GPT-2 (124M) @ 2:40:59  https://www.youtube.com/watch?v=l8pRSuU81PU&amp;amp;t=9659s
  "loss objective and this is the average the one over four because there are four independent examples here and then we have the four examples and their mean squa..."

Q: what is flash attention
- Let's reproduce GPT-2 (124M) @ 2:00:41  https://www.youtube.com/watch?v=l8pRSuU81PU&amp;amp;t=7241s
  "attention will come here and we will take out these four lines and Flash attention implements these four lines really really quickly and how does it do that wel..."
- Let's reproduce GPT-2 (124M) @ 2:04:40  https://www.youtube.com/watch?v=l8pRSuU81PU&amp;amp;t=7480s
  "single fused kernel flash attention and that's what we are about to use so great example I think of being aware of um memory hierarchy the fact that flops don't..."
- Let's reproduce GPT-2 (124M) @ 2:02:40  https://www.youtube.com/watch?v=l8pRSuU81PU&amp;amp;t=7360s
  "we're getting a t BYT Matrix of attention which is a Million numbers even for a single head at a single batch index at like so so basically this is a ton of mem..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The run took 35 seconds and returned all three transcripts.&lt;/p&gt;

&lt;p&gt;What worked and what did not:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Specific technical terms work very well.&lt;/strong&gt; All three "flash attention" hits are inside the chapter "flash attention, 96ms" (2:00:18 in the GPT-2 video). The three "gradient accumulation" hits are all inside the "gradient accumulation" chapter, which starts at 2:34:09.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spelling question hit the right place.&lt;/strong&gt; The chapter "tokenization revisited: models struggle with spelling" starts at 2:01:11. Two of the three hits overlap it: the chunk at 2:00:37 runs into it, and the one at 2:03:32 is in the middle of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Even the loosely worded question found real answers.&lt;/strong&gt; The top hit, 2:39:55 in "Deep Dive", is where he says to use thinking models for prompts that need advanced reasoning, and GPT-4o for simpler questions. The second hit is the last minute of the chapter "Thinking models and when to use them" (0:22:54 in "How I use LLMs"). BM25 still only matches words, so a question phrased differently from what he said can miss. That is the point where you swap BM25 for embeddings: embed each chunk's text once, embed the question, and rank by cosine similarity. The chunks and the &lt;code&gt;&amp;amp;t=&lt;/code&gt; links stay the same.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you have the top chunks, give their text to an LLM with the instruction "answer only from these excerpts and cite the links", and you have a channel Q&amp;amp;A bot with clickable sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips for a real index
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the chapter list.&lt;/strong&gt; When the &lt;code&gt;description&lt;/code&gt; contains lines like &lt;code&gt;00:22:54 Thinking models and when to use them&lt;/code&gt;, parse them and store the chapter title with each chunk. It is a strong extra signal for search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overlap the chunks.&lt;/strong&gt; Start a new chunk every 30 seconds with a 60-second window, so an answer that crosses a border is not split in two.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep a stable key.&lt;/strong&gt; Use &lt;code&gt;videoId&lt;/code&gt; + &lt;code&gt;start&lt;/code&gt; as the chunk ID, so you can run the scraper again on a schedule and add only new videos. &lt;code&gt;dateFrom&lt;/code&gt; limits each run to recent uploads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check &lt;code&gt;isAutoGenerated&lt;/code&gt;.&lt;/strong&gt; If a creator uploaded their own captions, the text has punctuation and correct names, and keyword search works better.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Use it from an AI agent
&lt;/h2&gt;

&lt;p&gt;If you work in Claude, Cursor or another MCP client, you can give the agent the scraper as a tool and let it fetch transcripts itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"youtube-transcripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.apify.com/?tools=harshmaur/youtube-transcript-scraper"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time your client starts the server, a browser window opens to connect your Apify account.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs
&lt;/h2&gt;

&lt;p&gt;$5 per 1,000 transcripts on the free plan, and less on paid plans. You pay only for transcripts that come back. A private, deleted or caption-less video returns a row with &lt;code&gt;status: "failed"&lt;/code&gt;, and that row is free. There is no start fee.&lt;/p&gt;

&lt;p&gt;My three-video run cost 1.4 cents. Indexing a channel with 200 videos costs about $1, once. For more than about 20 videos, do not use the run-and-wait endpoint, which waits at most 5 minutes. Start the run with &lt;code&gt;POST /v2/acts/{ACTOR}/runs&lt;/code&gt;, wait for it to finish, then read &lt;code&gt;/v2/datasets/{defaultDatasetId}/items&lt;/code&gt;. After that, a weekly run with &lt;code&gt;dateFrom&lt;/code&gt; costs a few cents.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I download all transcripts from a YouTube channel at once?&lt;/strong&gt;&lt;br&gt;
Yes. Put the channel handle in &lt;code&gt;channelUrls&lt;/code&gt; and set &lt;code&gt;maxVideos&lt;/code&gt; to the number of videos you want. Playlists work the same way with &lt;code&gt;playlistUrls&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a YouTube API key?&lt;/strong&gt;&lt;br&gt;
No. You need only an Apify API token, which comes free with an Apify account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when a video has no transcript?&lt;/strong&gt;&lt;br&gt;
You get a row with &lt;code&gt;status: "failed"&lt;/code&gt; and no charge. The row has only &lt;code&gt;videoId&lt;/code&gt;, &lt;code&gt;url&lt;/code&gt;, &lt;code&gt;errorCode&lt;/code&gt; and &lt;code&gt;error&lt;/code&gt;, with no transcript fields, so filter on &lt;code&gt;status == "success"&lt;/code&gt; as the script does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I get transcripts in another language?&lt;/strong&gt;&lt;br&gt;
Set &lt;code&gt;language&lt;/code&gt; to a code like &lt;code&gt;es&lt;/code&gt; or &lt;code&gt;de&lt;/code&gt;. If the video has captions in that language, you get them. If not, you get a machine translation and &lt;code&gt;wasTranslated&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt;. If no translation is available, you get the original captions, and &lt;code&gt;language&lt;/code&gt; shows which language you got.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I get SRT or VTT files?&lt;/strong&gt;&lt;br&gt;
Yes. Set &lt;code&gt;transcriptFormat&lt;/code&gt; to &lt;code&gt;srt&lt;/code&gt; or &lt;code&gt;vtt&lt;/code&gt;, and each row has an &lt;code&gt;srt&lt;/code&gt; or &lt;code&gt;vtt&lt;/code&gt; field that holds the subtitle file as text. If you already have an SRT or VTT file from somewhere else and only want the words, this &lt;a href="https://www.webscrapinghq.com/tools/srt-vtt-to-text" rel="noopener noreferrer"&gt;free SRT and VTT to text converter&lt;/a&gt; removes the timestamps and the repeated lines that YouTube's automatic captions often have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apify.com/harshmaur/youtube-transcript-scraper?fpr=tl88um" rel="noopener noreferrer"&gt;YouTube Transcript Scraper on Apify&lt;/a&gt;. Paste a channel handle into the input form, press Start, and copy the script above to search what comes back.&lt;/p&gt;

&lt;p&gt;If you build a channel Q&amp;amp;A bot with it, tell me in the comments what you indexed and which retrieval worked best for you.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Apify links in this post include my referral code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rag</category>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
    </item>
    <item>
      <title>How I Find Winning Facebook Ads (Days Running Is Not Enough)</title>
      <dc:creator>Harsh Maur</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:55:31 +0000</pubDate>
      <link>https://dev.to/harshmaur/how-i-find-winning-facebook-ads-days-running-is-not-enough-26ln</link>
      <guid>https://dev.to/harshmaur/how-i-find-winning-facebook-ads-days-running-is-not-enough-26ln</guid>
      <description>&lt;p&gt;The usual advice for finding winning Facebook ads is simple: open the Meta Ad Library, find ads that have run for a long time, and copy what they do. An advertiser does not keep paying for an ad that loses money.&lt;/p&gt;

&lt;p&gt;I tested that advice on 60 active US ads for "magnesium glycinate" on 20 October 2026. The median ad had already run for &lt;strong&gt;129 days&lt;/strong&gt;. 52 of the 60 were past the usual 30-day "winner" mark. When almost every ad is a "winner", the rule tells you nothing.&lt;/p&gt;

&lt;p&gt;What did work: looking for the &lt;strong&gt;same hook launched more than once&lt;/strong&gt;. When a brand keeps putting the same opening line into new ads, that hook is working for them. This post shows how to pull the ads, rank them, and find those repeated hooks with a short Python script.&lt;/p&gt;

&lt;p&gt;I built the scraper used here and sell it on the Apify Store, so keep that in mind. All numbers below come from real runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "days running" breaks in a busy niche
&lt;/h2&gt;

&lt;p&gt;The Ad Library shows ads that are active now. In a mature niche, most active ads are old, because the new ones that failed were already switched off. That is survivorship: the losers left the list before you looked. So:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a new or small niche, "running 30+ days" is a useful filter.&lt;/li&gt;
&lt;li&gt;In a busy niche like supplements, compare each ad against the niche median instead. Here the top quarter of ads had run 168 days or more.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Ad Library does not tell you
&lt;/h2&gt;

&lt;p&gt;For normal commercial ads in the US, Meta shows no spend, no impressions and no reach. In my data, &lt;code&gt;spend&lt;/code&gt;, &lt;code&gt;impressions_lower&lt;/code&gt; and &lt;code&gt;reach_lower&lt;/code&gt; were &lt;code&gt;null&lt;/code&gt; for every ad. Meta shows spend and impressions only for political and issue ads. Ads shown in the EU and UK also show reach.&lt;/p&gt;

&lt;p&gt;So run time and repetition are the best public signals of what is working. Neither is proof, but together they are much stronger than either alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Pull the ads
&lt;/h2&gt;

&lt;p&gt;Build your search on &lt;a href="https://www.facebook.com/ads/library" rel="noopener noreferrer"&gt;facebook.com/ads/library&lt;/a&gt;: choose a country, "All ads", and type a keyword. Copy the URL from the address bar. (Or build it with a &lt;a href="https://www.webscrapinghq.com/tools/facebook-ad-library-search-url-builder" rel="noopener noreferrer"&gt;free Ad Library URL builder&lt;/a&gt;, which also sets filters like media type and date.) Then run &lt;a href="https://apify.com/harshmaur/facebook-ads-library-scraper?fpr=tl88um" rel="noopener noreferrer"&gt;Facebook Ads Library Scraper&lt;/a&gt; with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"urls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.facebook.com/ads/library/?active_status=active&amp;amp;ad_type=all&amp;amp;country=US&amp;amp;q=magnesium%20glycinate&amp;amp;search_type=keyword_unordered&amp;amp;media_type=all"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Facebook matches keywords loosely. In my run, 50 of the 60 ads mentioned magnesium somewhere in the ad (text, headline or cards). The other 10 were for other products, from general vitamin brands to pharmacy deals at Walgreens and CVS. If you need exact matches, filter on the ad text after the run. For a clean list of one competitor's ads, use their Page instead.&lt;/p&gt;

&lt;p&gt;You can paste a brand's Facebook Page URL to get all ads from that one advertiser. You need no Facebook login and no Meta API token.&lt;/p&gt;

&lt;p&gt;The run took 34 seconds. Each ad comes back as one flat row. These are the fields I used, from a real ad:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"page_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allison Arnold - Migraine Coach"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"start_date_iso"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-07T07:00:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_active"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ad_duration_days"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;135&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_long_running"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"headline"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"I Charge $189 for a Migraine IV Drip. My Patients Keep Asking About a $29 Roll-On I Keep at the Front Desk."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cta_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Learn more"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"display_format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IMAGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"link_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://florusrelief.com/blogs/news/every-migraine-tool-you-have-works-downstream-of-the-source-here-is-what-works-at-it"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"publisher_platform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"FACEBOOK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INSTAGRAM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AUDIENCE_NETWORK"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MESSENGER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"THREADS"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ad_library_url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.facebook.com/ads/library/?id=987150940600917"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"spend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ad_duration_days&lt;/code&gt; is the number of days between the start date and today (or the end date, for ads that stopped). &lt;code&gt;is_long_running&lt;/code&gt; is &lt;code&gt;true&lt;/code&gt; from 30 days on. The ad text, headline and media links sit at the top level, so the rows drop straight into a spreadsheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Rank by days live and find repeated hooks
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="n"&gt;ACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;harshmaur~facebook-ads-library-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SEARCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.facebook.com/ads/library/?active_status=active&amp;amp;ad_type=all&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;country=US&amp;amp;q=magnesium%20glycinate&amp;amp;search_type=keyword_unordered&amp;amp;media_type=all&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;ads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.apify.com/v2/acts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACTOR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/run-sync-get-dataset-items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;urls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;SEARCH&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Catalog ads show a template placeholder (product.name in double braces), so their copy says nothing.
&lt;/span&gt;&lt;span class="n"&gt;PLACEHOLDER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;ads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ads&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;PLACEHOLDER&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad_duration_days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ads&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ads, median &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; days live, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
      &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;is_long_running&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; flagged long-running&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Group the same opening line from the same page: one hook, many launches.
&lt;/span&gt;&lt;span class="n"&gt;hooks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;opening&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\s+&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad_text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;opening&lt;/span&gt;&lt;span class="p"&gt;)].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad_duration_days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="nf"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opening&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hooks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;kv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;runs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ad_duration_days&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ad(s), days live &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;runs&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;opening&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;display_format&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;link_url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Drop catalog ads first.&lt;/strong&gt; 8 of my 60 ads were catalog (dynamic product) ads. Their &lt;code&gt;headline&lt;/code&gt; is literally the placeholder &lt;code&gt;product.name&lt;/code&gt; in double curly braces, because Meta fills it in per viewer. They run for months, but their copy teaches you nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group by page and opening line.&lt;/strong&gt; The first 60 characters of the ad text are the hook. The same page reusing the same hook in a new ad is the signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The real output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;52 ads, median 121 days live, 45 flagged long-running

Restelle: 4 ad(s), days live [210, 69, 52, 39]
  "Tired of restless nights? 😴 Introducing our NEW Sleep Patche..."
  IMAGE -&amp;gt; https://myrestelle.com/pages/lp-sleep-2

Allison Arnold - Migraine Coach: 2 ad(s), days live [213, 151]
  "I tracked my migraines for 2,190 days. It took me six years ..."
  IMAGE -&amp;gt; https://florusrelief.com/blogs/news/every-migraine-tool-you-have-works-downstream-of-the-source-here-is-what-works-at-it

We Heart Nutrition: 2 ad(s), days live [211, 187]
  "Why our magnesium glycinate isn't like the others... Researc..."
  IMAGE -&amp;gt; https://www.weheartnutrition.com/

Top 5 Choices: 2 ad(s), days live [155, 117]
  "We've tested them all you don't have to...."
  VIDEO -&amp;gt; https://top5-choices.com/magnesiumreview

Best Supplement Finds: 2 ad(s), days live [152, 133]
  "We tested 47 magnesium supplements for brain fog, cortisol, ..."
  VIDEO -&amp;gt; https://goldsealsupplements.com/pages/magnesium
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The script printed one more group. I cut it here to keep the list short.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What the winners have in common
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Restelle ran one hook in four ads.&lt;/strong&gt; The first ad has run 210 days. Then they added the same opening line in new ads at 69, 52 and 39 days. A brand does that when one message keeps converting and they want more creative variety around it. That is the strongest "winner" signal in the whole dataset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The landing page is often not the store.&lt;/strong&gt; Look at the links. "Allison Arnold - Migraine Coach" sends people to a blog post on florusrelief.com. "Top 5 Choices" and "Best Supplement Finds" send people to "we tested them all" review pages. These are advertorials: a story or a comparison page that sells before the product page. Several of the longest-running ads in this niche use this pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Several hooks are first-person stories with exact numbers.&lt;/strong&gt; "I tracked my migraines for 2,190 days." "We tested 47 magnesium supplements." "I charge $189 for a migraine IV drip." None of the long-running hooks open with the product name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Video and image both win.&lt;/strong&gt; Of the 60 ads, 27 were video and 18 were image. The repeated hooks above are split between the two, so format alone was not the deciding factor here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn it into a weekly swipe file
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Save the input as a task in Apify Console and schedule it weekly.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;ad_archive_id&lt;/code&gt; from each run. An ad that shows up week after week, and a hook that gets new ad IDs, are the ones to study.&lt;/li&gt;
&lt;li&gt;To follow one competitor, paste their Page URL instead of a keyword search.&lt;/li&gt;
&lt;li&gt;For EU and UK ads, turn on &lt;code&gt;scrapeAdDetails&lt;/code&gt; to also get reach, age and gender breakdowns and target countries.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;$0.50 per 1,000 ads, with no start fee. My 60-ad run cost 3 cents. A weekly run of 500 ads is about $1 a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I see how much a competitor spends on Facebook ads?&lt;/strong&gt;&lt;br&gt;
Not for normal commercial ads in the US. Meta publishes spend only for political and issue ads. Ads shown in the EU and UK come with reach numbers, which are the closest public proxy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What counts as a long-running Facebook ad?&lt;/strong&gt;&lt;br&gt;
There is no official definition. 30 days is a common rule of thumb, and it is what &lt;code&gt;is_long_running&lt;/code&gt; uses. In a busy niche, compare against the niche median instead, as shown above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Does a long-running ad always mean it is profitable?&lt;/strong&gt;&lt;br&gt;
No. Some brands leave ads on by mistake, and catalog ads run for months by design. Repetition (the same hook in several new ads) plus run time is a much stronger signal than run time alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a Facebook account or Meta API access?&lt;/strong&gt;&lt;br&gt;
No. The scraper reads the public Ad Library, the same one anyone can open without logging in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apify.com/harshmaur/facebook-ads-library-scraper?fpr=tl88um" rel="noopener noreferrer"&gt;Facebook Ads Library Scraper on Apify&lt;/a&gt;. Paste your niche's Ad Library URL, set &lt;code&gt;count&lt;/code&gt; to 60, and sort the results by &lt;code&gt;ad_duration_days&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Want an exact-phrase search, a date window or one platform only? Build the URL with this &lt;a href="https://www.webscrapinghq.com/tools/facebook-ad-library-search-url-builder" rel="noopener noreferrer"&gt;free Facebook Ad Library search URL builder&lt;/a&gt;, open it to check the ads, then paste it into &lt;code&gt;urls&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;What signal do you use besides run time? Tell me in the comments. I would like to add it to the script.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The links to my Actor include my Apify referral code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>webscraping</category>
      <category>python</category>
      <category>automation</category>
    </item>
    <item>
      <title>Clockworks TikTok Scraper Alternative: Same Input JSON, 6x Cheaper</title>
      <dc:creator>Harsh Maur</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:54:54 +0000</pubDate>
      <link>https://dev.to/harshmaur/clockworks-tiktok-scraper-alternative-same-input-json-6x-cheaper-4j7c</link>
      <guid>https://dev.to/harshmaur/clockworks-tiktok-scraper-alternative-same-input-json-6x-cheaper-4j7c</guid>
      <description>&lt;p&gt;I pasted one TikTok scraping input into two Apify Actors on 20 October 2026. Same JSON, same account, under a minute apart. One run cost &lt;strong&gt;$0.071&lt;/strong&gt;. The other cost &lt;strong&gt;$0.011&lt;/strong&gt;, and its date filter also worked on the hashtag, which the first run's did not.&lt;/p&gt;

&lt;p&gt;The first Actor is &lt;a href="https://apify.com/clockworks/tiktok-scraper" rel="noopener noreferrer"&gt;clockworks/tiktok-scraper&lt;/a&gt;, the most used TikTok scraper on Apify with about 29,000 monthly users. The second is &lt;a href="https://apify.com/harshmaur/tiktok-scraper?fpr=tl88um" rel="noopener noreferrer"&gt;TikTok Scraper&lt;/a&gt;. I built the second one, so read this with that in mind. Input fields match clockworks', so moving over takes one or two lines of code.&lt;/p&gt;

&lt;p&gt;This post shows the exact test, the price math at 1,000 videos, which fields differ, and what the cheaper one does not do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test input
&lt;/h2&gt;

&lt;p&gt;This is a normal "watch a hashtag and a brand account" input in clockworks' format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hashtags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"booktok"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"profiles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"duolingo"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resultsPerPage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"oldestPostDateUnified"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-06"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shouldDownloadVideos"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shouldDownloadCovers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shouldDownloadSubtitles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"shouldDownloadSlideshowImages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"proxyCountryCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"None"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten videos from #booktok, ten from @duolingo, only videos posted since 6 October. I ran it on both Actors from the same Apify account on the Starter plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;clockworks/tiktok-scraper&lt;/th&gt;
&lt;th&gt;harshmaur/tiktok-scraper&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Videos returned&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Extra rows&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;1 profile row for @duolingo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;#booktok videos inside the date window&lt;/td&gt;
&lt;td&gt;0 of 10 (all from February to April 2023)&lt;/td&gt;
&lt;td&gt;10 of 10 (all from 9 to 19 October 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;@duolingo videos inside the date window&lt;/td&gt;
&lt;td&gt;10 of 10&lt;/td&gt;
&lt;td&gt;10 of 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run time&lt;/td&gt;
&lt;td&gt;18 s&lt;/td&gt;
&lt;td&gt;31 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost on the Starter plan&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.071&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$0.011&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things stand out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The price.&lt;/strong&gt; Clockworks charged 20 results at $0.003, plus 10 "date filter" add-on events at $0.001, plus a $0.001 start fee. Mine charged 20 videos at $0.00035, plus one profile row at $0.004.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The date filter on the hashtag.&lt;/strong&gt; Clockworks applied the date filter to the profile only (that is where the 10 add-on charges came from). The #booktok videos it returned were up to three years and eight months old. Check this in your own runs if you watch hashtags with a date filter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Speed.&lt;/strong&gt; Clockworks was faster: 18 seconds against 31. For a scheduled job this rarely matters. For a chat agent waiting on a reply, it might.&lt;/p&gt;

&lt;h2&gt;
  
  
  The price at 1,000 videos
&lt;/h2&gt;

&lt;p&gt;These are the per-event prices from each Actor's pricing table on 20 October 2026. Clockworks has tiered prices that fall on bigger Apify plans. Mine is one flat price on every plan.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;1,000 videos&lt;/th&gt;
&lt;th&gt;clockworks&lt;/th&gt;
&lt;th&gt;harshmaur&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free plan, no filters&lt;/td&gt;
&lt;td&gt;$3.70&lt;/td&gt;
&lt;td&gt;$0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free plan, with a date filter&lt;/td&gt;
&lt;td&gt;$5.00&lt;/td&gt;
&lt;td&gt;$0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Starter plan, with a date filter&lt;/td&gt;
&lt;td&gt;$4.00&lt;/td&gt;
&lt;td&gt;$0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Business plan, with a date filter&lt;/td&gt;
&lt;td&gt;$2.30&lt;/td&gt;
&lt;td&gt;$0.35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1,000 comments, free plan&lt;/td&gt;
&lt;td&gt;$1.25&lt;/td&gt;
&lt;td&gt;$0.20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The gap closes on clockworks' top tiers. On clockworks' Diamond tier, plain results are $0.50 per 1,000.&lt;/p&gt;

&lt;p&gt;Where mine costs &lt;strong&gt;more&lt;/strong&gt;: video downloads are $10 per 1,000 against clockworks' $1.30 or less, and each scraped profile adds a profile row at $4 per 1,000 profiles. If you mainly download MP4s, or scrape thousands of profiles for their bio only, do the math for your own job before you switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving over: the two lines you change
&lt;/h2&gt;

&lt;p&gt;If you call the Actor from code, you change the Actor ID and keep your input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# ACTOR = "clockworks~tiktok-scraper"
&lt;/span&gt;&lt;span class="n"&gt;ACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;harshmaur~tiktok-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;run_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hashtags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;booktok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;profiles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duolingo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resultsPerPage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;oldestPostDateUnified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-10-06&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shouldDownloadVideos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shouldDownloadCovers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.apify.com/v2/acts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACTOR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/run-sync-get-dataset-items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;videos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dataType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;createTimeISO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;playCount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; plays&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorMeta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webVideoUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The real output of that script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;2026-10-13      4,703 plays laurinelexis https://www.tiktok.com/@laurinelexis/video/7695950683073187086
2026-10-19     59,400 plays probablyoffreading https://www.tiktok.com/@probablyoffreading/video/7698425693255798030
2026-10-11    370,200 plays caroline.montague https://www.tiktok.com/@caroline.montague/video/7695446883426110742
2026-10-19     85,100 plays duolingo https://www.tiktok.com/@duolingo/video/7698408739077901598
2026-10-12    665,400 plays duolingo https://www.tiktok.com/@duolingo/video/7695843389211708702
2026-10-13  2,300,000 plays duolingo https://www.tiktok.com/@duolingo/video/7696211706317409550
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Besides the Actor ID, the &lt;code&gt;dataType&lt;/code&gt; filter is the only other change most scripts need. Clockworks puts only videos in the main dataset. Mine puts videos, comments, followers and profiles in one dataset and marks each row with &lt;code&gt;dataType&lt;/code&gt;. The &lt;code&gt;.get("dataType", "video")&lt;/code&gt; default keeps the same script working on both Actors.&lt;/p&gt;

&lt;p&gt;In the Apify Console it is even simpler: open your clockworks task, copy the input JSON, paste it into the new Actor's JSON tab, and start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which output fields differ
&lt;/h2&gt;

&lt;p&gt;I compared every video row from both runs, including nested keys. 76 fields have the same name in both. Video fields such as &lt;code&gt;createTimeISO&lt;/code&gt;, &lt;code&gt;webVideoUrl&lt;/code&gt;, &lt;code&gt;authorMeta.fans&lt;/code&gt; and the counts held the same values, apart from a few likes, comments and saves of drift, because the runs were seconds apart and people kept interacting. Some author fields differ: mine leaves &lt;code&gt;authorMeta.bioLink&lt;/code&gt; and &lt;code&gt;authorMeta.createTime&lt;/code&gt; empty and reports &lt;code&gt;authorMeta.friends&lt;/code&gt; as 0, and in &lt;code&gt;text&lt;/code&gt; it replaces line breaks with spaces.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Only in clockworks' output:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;locationMeta.city&lt;/code&gt;, &lt;code&gt;locationMeta.address&lt;/code&gt; and similar place details on videos tagged with a location.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;authorMeta.commerceUserInfo&lt;/code&gt;, the business-account category.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Only in mine:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;dataType&lt;/code&gt;, as above.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;isAiGenerated&lt;/code&gt;, &lt;code&gt;locationCreated&lt;/code&gt;, &lt;code&gt;duetEnabled&lt;/code&gt;, &lt;code&gt;stitchEnabled&lt;/code&gt;, &lt;code&gt;suggestedWords&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;authorMeta.region&lt;/code&gt; and &lt;code&gt;authorMeta.secUid&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;videoMeta.transcript&lt;/code&gt;, filled when you ask for TikTok's own subtitles.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;aiAnalysis&lt;/code&gt;, filled when you turn on the paid AI analysis option.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your spreadsheet uses place details or the business category, mine leaves those columns empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input fields that do not carry over
&lt;/h2&gt;

&lt;p&gt;Almost every clockworks field works as it is. These do not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;scrapeRelatedSearchWords&lt;/code&gt; and &lt;code&gt;scrapeAdditionalAuthorMeta&lt;/code&gt; have no effect.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;videoKvStoreIdOrName&lt;/code&gt; is ignored. Downloads always go to the run's own key-value store, and the links are in &lt;code&gt;mediaUrls&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;profileScrapeSections: ["stories"]&lt;/code&gt; returns no stories, because TikTok shows stories only to logged-in users.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Relative dates work in both: &lt;code&gt;"30 days"&lt;/code&gt;, &lt;code&gt;"2 weeks"&lt;/code&gt;, or just &lt;code&gt;"30"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to stay on clockworks
&lt;/h2&gt;

&lt;p&gt;To be fair to a very good Actor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are on a Platinum or Diamond Apify plan, where their prices come close to mine.&lt;/li&gt;
&lt;li&gt;You download a lot of videos.&lt;/li&gt;
&lt;li&gt;You need place details or the business category on each video.&lt;/li&gt;
&lt;li&gt;You want the Actor with a long track record: clockworks has over 140 million runs. Mine launched on 12 October 2026.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For everyone else, watching hashtags, brands or creators on a free or Starter plan, the same input now costs a sixth to a fourteenth as much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it with your own input
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="https://apify.com/harshmaur/tiktok-scraper?fpr=tl88um" rel="noopener noreferrer"&gt;TikTok Scraper on Apify&lt;/a&gt;, switch the input to JSON, and paste the input from your last clockworks run. Set &lt;code&gt;resultsPerPage&lt;/code&gt; to 5 for a first test. For one hashtag and one profile, that costs less than a cent.&lt;/p&gt;

&lt;p&gt;To check one video before you scrape a whole account, I use this &lt;a href="https://www.webscrapinghq.com/tools/tiktok-engagement-rate-calculator" rel="noopener noreferrer"&gt;free TikTok engagement rate calculator&lt;/a&gt;. Enter the &lt;code&gt;playCount&lt;/code&gt;, &lt;code&gt;diggCount&lt;/code&gt;, &lt;code&gt;commentCount&lt;/code&gt;, &lt;code&gt;shareCount&lt;/code&gt; and &lt;code&gt;authorMeta.fans&lt;/code&gt; values from a row, and it gives the engagement rate by views and by followers.&lt;/p&gt;

&lt;p&gt;If a field you rely on is missing or named differently, leave a comment with the field name. I would rather add it than have you write a mapping layer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The links to my Actor include my Apify referral code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>tiktok</category>
      <category>webscraping</category>
      <category>python</category>
      <category>apify</category>
    </item>
    <item>
      <title>Scrape LinkedIn Jobs Without Login or API Key (Half Were Recruiters)</title>
      <dc:creator>Harsh Maur</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:54:42 +0000</pubDate>
      <link>https://dev.to/harshmaur/scrape-linkedin-jobs-without-login-or-api-key-half-were-recruiters-3iej</link>
      <guid>https://dev.to/harshmaur/scrape-linkedin-jobs-without-login-or-api-key-half-were-recruiters-3iej</guid>
      <description>&lt;p&gt;This morning I pulled every "founding engineer" job posted in the US in the last 24 hours, straight into Python. No LinkedIn account, no cookies, no API key from LinkedIn. The run took 10 seconds and cost about 1.5 cents.&lt;/p&gt;

&lt;p&gt;The more useful part came after that: half of those "founding engineer" listings were not posted by a startup at all. They came from recruiting agencies. If you are job hunting, building a job board, or looking for companies that are hiring, that difference matters. So this post also shows the 15-line filter I use to split them.&lt;/p&gt;

&lt;p&gt;Full disclosure: I built the scraper used here and sell it on the Apify Store. Everything below is a real run from 20 October 2026, with the real output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why there is no official LinkedIn jobs API for you
&lt;/h2&gt;

&lt;p&gt;LinkedIn has a Job Posting API, but it is for partners who post jobs (ATS vendors, job boards with a contract). It does not let you search or read other companies' listings. If you want job data, your options are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy and paste from linkedin.com/jobs by hand.&lt;/li&gt;
&lt;li&gt;Write your own scraper and maintain it every time LinkedIn changes its pages.&lt;/li&gt;
&lt;li&gt;Use a hosted scraper that reads LinkedIn's public job pages, the ones anyone sees without logging in.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I went with option 3 and built &lt;a href="https://apify.com/harshmaur/linkedin-jobs-scraper?fpr=tl88um" rel="noopener noreferrer"&gt;LinkedIn Jobs Scraper&lt;/a&gt; on Apify. It never logs in, so there is no account to get banned and no cookie to refresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A free Apify account. The free plan includes $5 of credit each month.&lt;/li&gt;
&lt;li&gt;Your Apify API token (Console → Settings → API &amp;amp; Integrations).&lt;/li&gt;
&lt;li&gt;Python 3 with &lt;code&gt;requests&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is all. You do not need anything from LinkedIn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Describe the search
&lt;/h2&gt;

&lt;p&gt;The input is plain JSON. These are the fields I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"founding engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"United States"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"datePosted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scrapeCompany"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;datePosted: "1"&lt;/code&gt; means the past 24 hours. Use &lt;code&gt;"7"&lt;/code&gt; for a week or &lt;code&gt;"30"&lt;/code&gt; for a month.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxItems&lt;/code&gt; caps the number of jobs, and you pay per job, so keep it low while you test.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scrapeCompany&lt;/code&gt; adds the employer's employee count, website and headquarters to each row. It costs nothing extra.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you already built a search on linkedin.com/jobs, you can paste its URL instead. LinkedIn's own filter menu only offers 24 hours, a week or a month. I use this &lt;a href="https://www.webscrapinghq.com/tools/linkedin-job-search-url-builder" rel="noopener noreferrer"&gt;free LinkedIn job search URL builder&lt;/a&gt; when I want "posted in the last 2 hours" or another custom window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"startUrls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.linkedin.com/jobs/search/?keywords=founding%20engineer&amp;amp;location=United%20States&amp;amp;f_TPR=r86400"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Run it from Python and get the jobs back
&lt;/h2&gt;

&lt;p&gt;Apify has a "run and wait" endpoint that starts the scraper and returns the dataset in the same HTTP call. It is fine for a few hundred jobs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;ACTOR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;harshmaur~linkedin-jobs-scraper&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APIFY_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;run_input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keywords&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;founding engineer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;location&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;United States&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datePosted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# past 24 hours
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maxItems&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scrapeCompany&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# adds employee count, website, HQ
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.apify.com/v2/acts/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ACTOR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/run-sync-get-dataset-items&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each item in &lt;code&gt;jobs&lt;/code&gt; is one job. Here is a real one from my run, trimmed (the description fields are long):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Founding Fullstack AI Engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"company"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AustinWorks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"companyUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.linkedin.com/company/austinworksrecruitment"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"San Francisco Bay Area"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postedTimeAgo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"9 hours ago"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"employmentType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Full-time"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seniorityLevel"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Entry level"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"applicants"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$150,000.00/yr - $210,000.00/yr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;150000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryMax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;210000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryCurrency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"salaryPeriod"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YEAR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jobPosterName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Charlie Austin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jobPosterTitle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Connecting top talent across Software + FDE + AI"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jobPosterProfileUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.linkedin.com/in/charlie-austin-a4a87a170"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://www.linkedin.com/jobs/view/4478636704"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row also has &lt;code&gt;descriptionText&lt;/code&gt;, &lt;code&gt;descriptionMarkdown&lt;/code&gt; and &lt;code&gt;descriptionHtml&lt;/code&gt;, plus &lt;code&gt;industries&lt;/code&gt;, &lt;code&gt;jobFunction&lt;/code&gt;, &lt;code&gt;companyWebsite&lt;/code&gt;, &lt;code&gt;companyEmployeesCount&lt;/code&gt; and &lt;code&gt;companyHeadquarters&lt;/code&gt;. When LinkedIn does not show a value, the field is &lt;code&gt;null&lt;/code&gt; instead of missing, so your code does not need &lt;code&gt;try/except&lt;/code&gt; around every key.&lt;/p&gt;

&lt;p&gt;Look at &lt;code&gt;companyUrl&lt;/code&gt; in that example: &lt;code&gt;austinworksrecruitment&lt;/code&gt;. That "founding engineer" job is a recruiter's listing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Filter out recruiting agencies
&lt;/h2&gt;

&lt;p&gt;My first idea was to filter on &lt;code&gt;industries == "Staffing and Recruiting"&lt;/code&gt;. On real data, that caught only 2 of the agencies. Half of the agencies list "Software Development" or "Artificial Intelligence" as their industry, just like their clients.&lt;/p&gt;

&lt;p&gt;What works better is to check the company name, its LinkedIn slug, its website and its industry together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;AGENCY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recruit|staffing|talent|\bsearch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;I&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_agency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;companyUrl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;companyWebsite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;industries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AGENCY&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;direct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;jobs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_agency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; jobs, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; from agencies, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;direct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; posted by the company&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;direct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;salaryMin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-$&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;salaryMax&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;salaryMin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no pay listed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; @ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;company&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;companyEmployeesCount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; staff), &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pay&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;applicants&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;?&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; applicants&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;j&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;\b&lt;/code&gt; before &lt;code&gt;search&lt;/code&gt; matters. Without it, "Research Services" counts as an agency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real output
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;15 jobs, 8 from agencies, 7 posted by the company
- Founding Aerospace Engineer @ Aether (1 staff), no pay listed, ? applicants
- Founding Engineer @ Bearcroft (4 staff), no pay listed, 61 applicants
- Founding Backend Engineer @ ALL KNOWN SERVICES (9 staff), no pay listed, 111 applicants
- Founding Full-Stack Engineer (up to $220k) @ Dex (40 staff), no pay listed, ? applicants
- Founding AI Engineer @ Convoy Health (4 staff), no pay listed, 62 applicants
- Founding Engineer @ Arcafeed (Acq. by Whop) (4 staff), no pay listed, 52 applicants
- Founding Product Engineer @ WhyHireWrong? (1 staff), no pay listed, ? applicants
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(I removed the job URLs from this listing to keep it short. The script prints them.)&lt;/p&gt;

&lt;p&gt;Three things surprised me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every salary in the sample came from an agency.&lt;/strong&gt; Five of the 15 jobs had a parsed salary, and all five were recruiter listings. None of the direct startup posts filled in LinkedIn's pay field. Dex put "up to $220k" in the job title instead, so &lt;code&gt;salary&lt;/code&gt; is &lt;code&gt;null&lt;/code&gt; for that row. If pay matters to you, also search &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;descriptionText&lt;/code&gt; for a dollar sign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The filter is not perfect.&lt;/strong&gt; ALL KNOWN SERVICES passed as "direct", but the person who posted it describes themselves as helping people land W2 contract roles. Adding &lt;code&gt;jobPosterTitle&lt;/code&gt; to the text you check catches more agencies, with some false positives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Applicant counts are mostly low.&lt;/strong&gt; Most direct posts had under 65 applicants within a day. A &lt;code&gt;null&lt;/code&gt; count (printed as &lt;code&gt;?&lt;/code&gt;) usually means LinkedIn showed "Be among the first 25 applicants". LinkedIn shows at most about 200, so &lt;code&gt;200&lt;/code&gt; means "200 or more".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two runs a few minutes apart did not return exactly the same 15 jobs. LinkedIn reorders its results, so for a daily feed, de-duplicate on &lt;code&gt;jobId&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this costs
&lt;/h2&gt;

&lt;p&gt;You pay per job in the dataset. There is no start fee and no subscription. On Apify's free plan it is $1.00 per 1,000 jobs, and $0.40 to $0.47 per 1,000 on paid plans. Recruiter fields and company details are included in that price.&lt;/p&gt;

&lt;p&gt;My 15-job test cost about 1.5 cents on the free plan. A daily run of 50 new jobs is roughly $0.70 a month on the Starter plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get more than 1,000 jobs
&lt;/h2&gt;

&lt;p&gt;LinkedIn stops every search near 1,000 results. To go past that, turn on &lt;code&gt;splitByLocation&lt;/code&gt;. The scraper then runs your search in each large city of the country and removes duplicates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"keywords"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"software engineer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"splitByLocation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"splitCountry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"us"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Make it a daily alert
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In Apify Console, save the input above as a task.&lt;/li&gt;
&lt;li&gt;Add a schedule that runs it every morning.&lt;/li&gt;
&lt;li&gt;Pick Slack, Notion or Google Sheets in the scraper's &lt;strong&gt;Send results to an app&lt;/strong&gt; option, or call the run-and-wait endpoint from your own cron job and keep the filter step.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With &lt;code&gt;datePosted: "1"&lt;/code&gt; and a daily schedule, you see each new job once.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I scrape LinkedIn jobs without logging in?&lt;/strong&gt;&lt;br&gt;
Yes. Job listings are public. Anyone can open linkedin.com/jobs in a private window and read them. The scraper reads those same public pages and never asks for your LinkedIn account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do I need a LinkedIn API key?&lt;/strong&gt;&lt;br&gt;
No. You need only an Apify API token, which comes free with an Apify account.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can I get the recruiter's name for a job?&lt;/strong&gt;&lt;br&gt;
When the listing names the person who posted it, you get &lt;code&gt;jobPosterName&lt;/code&gt;, &lt;code&gt;jobPosterTitle&lt;/code&gt; and &lt;code&gt;jobPosterProfileUrl&lt;/code&gt;. In my run, 7 of 15 jobs named someone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do LinkedIn's remote and experience filters still work?&lt;/strong&gt;&lt;br&gt;
LinkedIn moved to an AI job search in 2026 and dropped several classic filters. Date posted, company and Easy Apply still filter exactly. Results for remote, job type and experience level can be approximate. Filter on &lt;code&gt;seniorityLevel&lt;/code&gt; or &lt;code&gt;employmentType&lt;/code&gt; afterwards if you need an exact match.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is it legal?&lt;/strong&gt;&lt;br&gt;
The scraper collects only public job listings that anyone can see without logging in. It does not touch profiles or anything behind a login. Check the rules for your own use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://apify.com/harshmaur/linkedin-jobs-scraper?fpr=tl88um" rel="noopener noreferrer"&gt;LinkedIn Jobs Scraper on Apify&lt;/a&gt;. Paste the JSON from step 1 into the input form, press Start, and you have a spreadsheet of jobs in about 10 seconds.&lt;/p&gt;

&lt;p&gt;Want to test a search before you scrape it? Build the URL with the &lt;a href="https://www.webscrapinghq.com/tools/linkedin-job-search-url-builder" rel="noopener noreferrer"&gt;LinkedIn job search URL builder&lt;/a&gt;, open it in a private window to check the results, then paste it into &lt;code&gt;startUrls&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you build something with it, or the agency filter misses a pattern you see often, tell me in the comments.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Apify links in this post include my referral code.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>career</category>
      <category>automation</category>
    </item>
    <item>
      <title>How to build a Reddit lead generation monitor with Apify MCP connectors</title>
      <dc:creator>Harsh Maur</dc:creator>
      <pubDate>Tue, 01 Sep 2026 06:09:32 +0000</pubDate>
      <link>https://dev.to/apify/how-to-build-a-reddit-lead-generation-monitor-with-apify-mcp-connectors-12j</link>
      <guid>https://dev.to/apify/how-to-build-a-reddit-lead-generation-monitor-with-apify-mcp-connectors-12j</guid>
      <description>&lt;p&gt;People do not always announce buying intent on a landing page. They ask for recommendations in a subreddit, complain about a workflow, or look for an alternative to a tool they already use.&lt;/p&gt;

&lt;p&gt;I saw that pattern while building and testing my &lt;a href="https://apify.com/harshmaur/reddit-scraper" rel="noopener noreferrer"&gt;Reddit Scraper&lt;/a&gt; Actor. I kept repeating the same keywords and communities across runs. These were no longer one-off research jobs; they were monitors. The missing piece was delivery. A useful post could sit in an Apify dataset until someone remembered to check it.&lt;/p&gt;

&lt;p&gt;I added optional Model Context Protocol (MCP) delivery so the Actor can send completed results to another service. This Reddit lead generation workflow uses Slack's official MCP server. For this guide, I configured the server, discovered its live tool schema, ran a one-result test, and verified the message in a private Slack channel. The first attempt failed for a non-obvious Slack setting, which is included below because it is exactly the kind of integration detail that polished examples tend to hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apify schedule -&amp;gt; Reddit Scraper -&amp;gt; structured Reddit posts and comments
  -&amp;gt; Apify MCP connector -&amp;gt; Slack MCP server -&amp;gt; private lead-alert channel -&amp;gt; human review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Actor searches recent Reddit discussions, stores the complete results in its dataset, and sends selected posts through an Apify MCP connector. A Slack alert can contain the post title, subreddit, score, comment count, link, and selected comments.&lt;/p&gt;

&lt;p&gt;A person still decides whether to respond. This workflow surfaces relevant public conversations; it does not automate unsolicited Reddit replies.&lt;/p&gt;

&lt;p&gt;Two Apify features have similar names but opposite directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An Apify MCP connector lets an Actor call an external MCP server.&lt;/strong&gt; That is what sends the alerts in this tutorial.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Apify MCP server lets an external AI client call Apify Actors and tools.&lt;/strong&gt; It is useful for Claude, ChatGPT, or Cursor, but it is not the delivery path here.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;You will need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an Apify account;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://apify.com/harshmaur/reddit-scraper" rel="noopener noreferrer"&gt;Reddit Scraper&lt;/a&gt;;&lt;/li&gt;
&lt;li&gt;permission to create a Slack app and a private destination channel;&lt;/li&gt;
&lt;li&gt;permission to create Apify tasks and schedules; and&lt;/li&gt;
&lt;li&gt;a small list of brand, competitor, and problem-related searches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Slack exposes its official Streamable HTTP MCP endpoint at &lt;code&gt;https://mcp.slack.com/mcp&lt;/code&gt;. It requires a registered internal or Marketplace Slack app with a confidential OAuth client. Slack does not support Dynamic Client Registration for this endpoint, so the one-click OAuth option is not enough; use Apify's &lt;strong&gt;Own OAuth client&lt;/strong&gt; flow. Some Console versions label the same option &lt;strong&gt;Your own OAuth client&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with high-intent Reddit searches
&lt;/h2&gt;

&lt;p&gt;A broad category keyword creates noise. A phrase describing a decision or painful workflow creates better alerts.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Broad query&lt;/th&gt;
&lt;th&gt;Higher-intent query&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;scheduling software&lt;/td&gt;
&lt;td&gt;alternative to calendly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;appointment tools&lt;/td&gt;
&lt;td&gt;keep missing client calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;booking app&lt;/td&gt;
&lt;td&gt;double booked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRM&lt;/td&gt;
&lt;td&gt;looking for a simple CRM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A useful starter set contains your brand and common misspellings, two or three competitor names, a few problem phrases, and buying-intent language such as looking for, alternative to, or would pay for.&lt;/p&gt;

&lt;p&gt;Reddit Scraper has four controls that matter for monitoring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;searchSort: "new"&lt;/code&gt; orders results chronologically;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;searchTime: "day"&lt;/code&gt; limits the search window;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;withinCommunity&lt;/code&gt; limits a run to one subreddit; and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;maxPostsCount&lt;/code&gt; caps the posts collected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sorting by new does not remove old results, so use a time window as well.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqoc9ha11karbjteukf1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foqoc9ha11karbjteukf1.png" alt="Reddit Scraper configured to search for a recent high-intent phrase" width="800" height="815"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1: Search for a problem or buying-intent phrase, sort by New, and use a recent time window.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Start with 10 to 20 posts. It is easier to improve five weak alerts than to clean up a Slack channel after sending hundreds.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Configure Slack for Reddit lead generation alerts
&lt;/h2&gt;

&lt;p&gt;Create the private destination first: in Slack, select &lt;strong&gt;Add channels -&amp;gt; Create a new channel&lt;/strong&gt;, give it a clear test name, and make it private. The Slack user who authorizes the app must be a member of that channel and able to post there.&lt;/p&gt;

&lt;p&gt;Next open &lt;a href="https://api.slack.com/apps" rel="noopener noreferrer"&gt;Slack app management&lt;/a&gt;, select &lt;strong&gt;Create New App -&amp;gt; From scratch&lt;/strong&gt;, name the app, and choose the receiving workspace. In the new app, open &lt;strong&gt;OAuth &amp;amp; Permissions&lt;/strong&gt; and add this redirect URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://console.apify.com/oauth/mcp-proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;chat:write&lt;/code&gt; as a &lt;strong&gt;User Token Scope&lt;/strong&gt;. Then open the app's &lt;strong&gt;Agents&lt;/strong&gt; page and enable &lt;strong&gt;Slack Model Context Protocol (MCP) Server&lt;/strong&gt;. This toggle is separate from the app's OAuth scopes and is easy to miss. Open &lt;strong&gt;Basic Information -&amp;gt; App Credentials&lt;/strong&gt; to find the client ID and client secret required by Apify.&lt;/p&gt;

&lt;p&gt;In Apify Console, open &lt;strong&gt;Settings -&amp;gt; API &amp;amp; Integrations -&amp;gt; MCP connectors&lt;/strong&gt;, select &lt;strong&gt;Add connector&lt;/strong&gt;, and enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Slack lead alerts&lt;/span&gt;
&lt;span class="na"&gt;Server URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://mcp.slack.com/mcp&lt;/span&gt;
&lt;span class="na"&gt;Authentication&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Own OAuth client (or Your own OAuth client)&lt;/span&gt;
&lt;span class="na"&gt;Authorization URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://slack.com/oauth/v2_user/authorize&lt;/span&gt;
&lt;span class="na"&gt;Token URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://slack.com/api/oauth.v2.user.access&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Supply the Slack app's client ID and client secret in Apify's secure fields. They should never appear in Actor input, datasets, source code, screenshots, or logs.&lt;/p&gt;

&lt;p&gt;Review the Slack consent page instead of approving it mechanically. The workflow needs &lt;code&gt;chat:write&lt;/code&gt;, not workspace-wide search, file, user, and channel-management permissions. In my August 2026 test, Apify generated the authorization popup with Slack's much broader advertised scope set. This was observed Apify behavior, not a Slack-supported scope picker. I completed the least-privilege flow as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In the same OAuth popup, focus the address bar without closing or duplicating the window.&lt;/li&gt;
&lt;li&gt;Change only the &lt;code&gt;scope&lt;/code&gt; query parameter to &lt;code&gt;chat:write&lt;/code&gt; and remove the empty &lt;code&gt;user_scope&lt;/code&gt; parameter.&lt;/li&gt;
&lt;li&gt;Preserve &lt;code&gt;client_id&lt;/code&gt;, &lt;code&gt;redirect_uri&lt;/code&gt;, &lt;code&gt;state&lt;/code&gt;, &lt;code&gt;code_challenge&lt;/code&gt;, &lt;code&gt;code_challenge_method&lt;/code&gt;, and &lt;code&gt;response_type&lt;/code&gt; exactly as Apify generated them.&lt;/li&gt;
&lt;li&gt;Reload that edited URL in the same popup and verify that the consent screen requests only permission to send messages as the authorizing user.&lt;/li&gt;
&lt;li&gt;Approve the request and return to Apify, where the connector should show the discovered tools.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That URL edit is a workaround for the current connector flow. Do not alter, share, or reuse the OAuth state or Proof Key for Code Exchange (PKCE) values. If your administrator does not approve this workaround, stop rather than granting the broader scopes and use an administrator-managed least-privilege integration instead. Re-check Apify's connector UI before following this step because native scope selection may replace the workaround.&lt;/p&gt;

&lt;p&gt;After successful authorization, the live server exposed three tools with this scope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;slack_send_message&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;slack_schedule_message&lt;/code&gt;; and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;slack_send_message_draft&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;slack_send_message&lt;/code&gt; input schema required exactly two values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"channel_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optional fields included &lt;code&gt;thread_ts&lt;/code&gt;, &lt;code&gt;reply_broadcast&lt;/code&gt;, &lt;code&gt;draft_id&lt;/code&gt;, and &lt;code&gt;unfurl_app_links&lt;/code&gt;. I disabled &lt;strong&gt;Allow all&lt;/strong&gt; in the connector and permitted only &lt;code&gt;slack_send_message&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsp2qavabd1omyntq3ir1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsp2qavabd1omyntq3ir1.png" alt="Slack connector restricted to the slack_send_message tool" width="800" height="920"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2: OAuth scope and connector tool allowlists are separate controls. This connector can call only the immediate-send operation.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Apify stores the Slack credential server-side. The Actor receives a connector ID and authenticates to Apify's MCP proxy with its run token; it never receives the Slack OAuth token.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Configure Reddit Scraper delivery
&lt;/h2&gt;

&lt;p&gt;Open Reddit Scraper and expand &lt;strong&gt;Send to your apps (MCP integration, optional)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Copy the Slack channel ID before filling &lt;code&gt;mcpTarget&lt;/code&gt;: open the destination channel, select its name to open channel details, and copy the &lt;strong&gt;Channel ID&lt;/strong&gt; shown on the &lt;strong&gt;About&lt;/strong&gt; tab. You can also copy the channel link and use the segment beginning with C. Do not use the human-readable #channel-name in &lt;code&gt;mcpTarget&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feoqoxohtetrgfe4yhxb9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feoqoxohtetrgfe4yhxb9.png" alt="Reddit Scraper configured for recent results and an eligible Slack MCP connector" width="800" height="661"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3: The final configuration uses New, Last day, the authorized Slack connector, and one message per post.&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it controls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcpConnector&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Managed connector selected in Apify Console&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcpMode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;One message per post (&lt;code&gt;perPost&lt;/code&gt;) or one digest (&lt;code&gt;summary&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcpTarget&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Destination expected by the tool; for Slack, a channel ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcpComments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Ignore comments, bundle them, or send separate follow-ups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcpCommentsPerPost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Highest-scoring comments included per post&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcpMessage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deterministic &lt;code&gt;{{field}}&lt;/code&gt; template&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcpTool&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exact tool-name override&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcpArguments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Extra rendered arguments merged over inferred values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mcpMaxItems&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Maximum posts dispatched in &lt;code&gt;perPost&lt;/code&gt; mode&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For Slack lead alerts, use &lt;code&gt;perPost&lt;/code&gt;. Each opportunity becomes a message that can be discussed or assigned independently.&lt;/p&gt;

&lt;p&gt;A useful template is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;**&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;**
&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;communityName&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt; | score &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;score&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt; | &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;commentsCount&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt; comments
&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;postUrl&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Templates are deterministic substitutions, not AI prompts. Missing values render as empty strings, and nested dataset paths are supported.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fme6ar8lc42wxrbq5jk9g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fme6ar8lc42wxrbq5jk9g.png" alt="MCP message, comment, tool override, and dispatch cap settings" width="800" height="641"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 4: Control formatting, comment delivery, the exact MCP tool, and the dispatch cap.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is the schema-validated smoke-test input used for the final screenshots. Replace the two placeholders with values from your account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchTerms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"AI"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchPosts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchComments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchCommunities"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchSort"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"day"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"crawlCommentsPerPost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxPostsCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpConnector"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPLACE_WITH_APIFY_CONNECTOR_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"perPost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpTarget"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REPLACE_WITH_SLACK_CHANNEL_ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpComments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ignore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpTool"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"slack_send_message"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpMessage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**{{title}}**&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;{{communityName}} | score {{score}} | {{commentsCount}} comments&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;{{postUrl}}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpMaxItems"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The broad, harmless AI query makes a one-result transport test more reliable than a narrow lead phrase. If it returns no post for day, widen &lt;code&gt;searchTime&lt;/code&gt; to &lt;code&gt;week&lt;/code&gt; and rerun before changing any connector settings. Once delivery works, replace AI with the high-intent phrases from step 1, optionally set &lt;code&gt;withinCommunity&lt;/code&gt;, and raise both caps gradually.&lt;/p&gt;

&lt;p&gt;The live Slack schema uses &lt;code&gt;channel_id&lt;/code&gt; and &lt;code&gt;message&lt;/code&gt;. Reddit Scraper puts the &lt;code&gt;mcpTarget&lt;/code&gt; value and the rendered message into the matching fields of the tool. You do not write a provider-specific request body.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Test the Reddit lead generation workflow
&lt;/h2&gt;

&lt;p&gt;Run the Actor manually before creating a task or schedule. Use &lt;code&gt;maxPostsCount: 1&lt;/code&gt; and &lt;code&gt;mcpMaxItems: 1&lt;/code&gt; for the first end-to-end test.&lt;/p&gt;

&lt;p&gt;My first run scraped one post, but the delivery to Slack failed with a connection error. The error message did not show the real cause.&lt;/p&gt;

&lt;p&gt;The real cause was a Slack setting: the Slack app was not enabled for Slack MCP server access. After I enabled &lt;strong&gt;Agents -&amp;gt; Slack Model Context Protocol (MCP) Server&lt;/strong&gt;, I recreated and reauthorized the Apify connector so it could rediscover tools. No change to the Actor input was needed.&lt;/p&gt;

&lt;p&gt;The second run discovered the three Slack tools, called &lt;code&gt;slack_send_message&lt;/code&gt;, and finished successfully. After restricting the connector to that one allowed tool, I repeated the one-item run; it dispatched one call and produced a second message in the private channel.&lt;/p&gt;

&lt;p&gt;The first message showed the title in italics, not bold. The official Slack MCP tool uses standard Markdown, so write &lt;code&gt;**{{title}}**&lt;/code&gt;, not &lt;code&gt;*{{title}}*&lt;/code&gt;. The Actor default now uses the bold form, and the next test showed a bold title.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F51wqplzqiz2rd0ux45x8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F51wqplzqiz2rd0ux45x8.png" alt="Successful Apify run log showing one Slack MCP dispatch" width="800" height="109"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 5: The test extracted one post, dispatched one MCP call through slack_send_message, and preserved a successful crawl result.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The private destination channel then contained the rendered Reddit alert and source link:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhqg28pe1960kpg3s5rq8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhqg28pe1960kpg3s5rq8.png" alt="Reddit lead alert received in a private Slack channel" width="800" height="46"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 6: The external result confirms that connector authorization, schema mapping, and the Slack write all worked.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Check a manual run in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify the expected Reddit rows in the dataset.&lt;/li&gt;
&lt;li&gt;Confirm deliverable rows have &lt;code&gt;dataType: "post"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Inspect the log for the selected MCP tool and dispatched-call count.&lt;/li&gt;
&lt;li&gt;Open Slack and verify the channel, formatting, and links.&lt;/li&gt;
&lt;li&gt;Compare the number of delivered posts with Slack messages.&lt;/li&gt;
&lt;li&gt;Tighten searches before raising the caps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you set &lt;code&gt;mcpTool&lt;/code&gt;, use the exact tool name from the connector. A wrong name does not fall back to automatic selection, and nothing is sent.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Turn the working input into a monitor
&lt;/h2&gt;

&lt;p&gt;Once the manual run is clean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Return to the Actor's &lt;strong&gt;Input&lt;/strong&gt; tab, open &lt;strong&gt;Actions&lt;/strong&gt;, and choose &lt;strong&gt;Save as a new task&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Name the task, save it, and open &lt;strong&gt;Schedules -&amp;gt; Create schedule&lt;/strong&gt; in Apify Console.&lt;/li&gt;
&lt;li&gt;Choose the saved task as the action, set an hourly or two-hour interval, and save the schedule.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Run now&lt;/strong&gt; once and confirm the new run, dataset row, dispatch log, and Slack message before leaving the schedule enabled.&lt;/li&gt;
&lt;li&gt;Review alert quality for several days and remove noisy searches before increasing volume.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reddit Scraper does not currently keep a cross-run set of posts delivered through MCP. Overlapping search windows can therefore repeat a post. Reduce duplicates with a shorter &lt;code&gt;searchTime&lt;/code&gt;, fewer overlapping queries, a slower schedule, or downstream deduplication on the Reddit post id.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Reddit Scraper fills in Slack's fields
&lt;/h2&gt;

&lt;p&gt;After the crawl, Reddit Scraper builds one message for each post, up to &lt;code&gt;mcpMaxItems&lt;/code&gt;. In &lt;code&gt;bundle&lt;/code&gt; mode, it adds the top comments to the same message. It then reads the tools of your connector and sends the messages one at a time.&lt;/p&gt;

&lt;p&gt;For the Slack tool, the message goes to &lt;code&gt;message&lt;/code&gt; and the channel ID goes to &lt;code&gt;channel_id&lt;/code&gt;. Other MCP servers use other field names, such as &lt;code&gt;text&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;rows&lt;/code&gt;, or &lt;code&gt;properties&lt;/code&gt;. If a tool needs a field that the Actor does not fill, add it in &lt;code&gt;mcpArguments&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reddit lead generation failure modes
&lt;/h2&gt;

&lt;p&gt;Scraping finishes before MCP delivery. A connector failure normally leaves the collected dataset intact, but partial delivery is possible.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Failure&lt;/th&gt;
&lt;th&gt;What you see&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Slack MCP Server toggle is disabled&lt;/td&gt;
&lt;td&gt;The connection fails; enable the toggle, then reauthorize the connector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The connector is not available to the run&lt;/td&gt;
&lt;td&gt;Nothing is sent, and the run log shows a delivery error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;mcpTool&lt;/code&gt; names a tool that does not exist&lt;/td&gt;
&lt;td&gt;Nothing is sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The connector has no tool that can send a message&lt;/td&gt;
&lt;td&gt;Nothing is sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A tool call fails&lt;/td&gt;
&lt;td&gt;Delivery stops at that message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Connector is empty&lt;/td&gt;
&lt;td&gt;The Actor only scrapes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The run finds no posts&lt;/td&gt;
&lt;td&gt;Nothing is sent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Actor does not retry a failed message. It sends messages one at a time, and one failure stops the rest. Earlier Slack messages can already exist when the run log shows a delivery error. Check Slack before you run again, so you do not get duplicates.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mcpMaxItems&lt;/code&gt; caps posts, not total calls. In separate-comment mode, one post can generate one post message plus several comment messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security, Reddit policies, and human review
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Give the Slack app only the scopes required by the permitted MCP tool.&lt;/li&gt;
&lt;li&gt;Disable &lt;strong&gt;Allow all&lt;/strong&gt; and allow only &lt;code&gt;slack_send_message&lt;/code&gt; for this connector.&lt;/li&gt;
&lt;li&gt;Send test alerts to a designated private channel.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;mcpMaxItems&lt;/code&gt; low until the query quality is proven.&lt;/li&gt;
&lt;li&gt;Prefer a managed connector over placing a bearer token in Actor input.&lt;/li&gt;
&lt;li&gt;Never put OAuth credentials in datasets, templates, screenshots, or logs.&lt;/li&gt;
&lt;li&gt;Treat public Reddit content as context, not consent for automated outreach.&lt;/li&gt;
&lt;li&gt;Keep a person responsible for deciding whether and how to respond.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use this workflow in accordance with Reddit's &lt;a href="https://www.redditinc.com/policies/user-agreement" rel="noopener noreferrer"&gt;User Agreement&lt;/a&gt;, &lt;a href="https://support.reddithelp.com/hc/en-us/articles/26410290525844-Public-Content-Policy" rel="noopener noreferrer"&gt;Public Content Policy&lt;/a&gt;, privacy requirements, and applicable law. Do not use it to profile sensitive traits, harass users, or automate spam.&lt;/p&gt;

&lt;p&gt;Slack also warns about mixing data across several MCP servers. Review every endpoint that can receive workspace context, and avoid sending workspace data through an MCP service you do not trust.&lt;/p&gt;

&lt;p&gt;After a temporary proof-of-concept, pause or delete the Apify schedule and task, remove the MCP connector from Apify, revoke the Slack app's OAuth grant, and archive the test channel. Delete the Slack app only if no other connector or workflow uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would improve next
&lt;/h2&gt;

&lt;p&gt;The useful first version is working: scheduled discovery, deterministic formatting, live tool discovery, least-privilege connector controls, comments, and safety caps.&lt;/p&gt;

&lt;p&gt;The next improvements are no repeat alerts across runs, retries for short MCP errors, a count of delivered messages, and templates for each destination. The main lesson, however, is that query quality matters more than scrape volume. Start with three phrases indicating a real problem or decision, send a tiny sample to Slack, review it with a human, and improve the searches before scheduling more runs.&lt;/p&gt;

&lt;p&gt;That turns Reddit Scraper from a dataset generator into a monitor a team can actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apify.com/harshmaur/reddit-scraper" rel="noopener noreferrer"&gt;Reddit Scraper on Apify&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.apify.com/platform/integrations/mcp-connectors" rel="noopener noreferrer"&gt;Apify MCP connectors overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.apify.com/integrations/mcp-connectors/use-in-actors" rel="noopener noreferrer"&gt;Build Actors with MCP connectors&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.apify.com/platform/schedules" rel="noopener noreferrer"&gt;Apify schedules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.slack.dev/ai/slack-mcp-server/" rel="noopener noreferrer"&gt;Slack MCP server overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>mcp</category>
      <category>webscraping</category>
    </item>
  </channel>
</rss>
