<?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: Daniel Kolbassen</title>
    <description>The latest articles on DEV Community by Daniel Kolbassen (@dan_kolb).</description>
    <link>https://dev.to/dan_kolb</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3915449%2F1895b7c5-ef77-4168-8c1e-0b7194be8be4.jpg</url>
      <title>DEV Community: Daniel Kolbassen</title>
      <link>https://dev.to/dan_kolb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dan_kolb"/>
    <language>en</language>
    <item>
      <title>Find Jobs on Twitter: How I Built a Hiring Scraper</title>
      <dc:creator>Daniel Kolbassen</dc:creator>
      <pubDate>Wed, 06 May 2026 08:11:31 +0000</pubDate>
      <link>https://dev.to/dan_kolb/find-jobs-on-twitter-how-i-built-a-hiring-scraper-37fb</link>
      <guid>https://dev.to/dan_kolb/find-jobs-on-twitter-how-i-built-a-hiring-scraper-37fb</guid>
      <description>&lt;h2&gt;
  
  
  How to Find Jobs on Twitter When LinkedIn Fails You
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaway:&lt;/strong&gt; The fastest way to find jobs on Twitter is to search for hiring keywords ("we are hiring", "open role", "looking for a [role]") combined with your niche, then run that search on a schedule and filter out noise. A small custom scraper hitting Twitter's search results every 30 minutes will surface real openings days before they hit LinkedIn or Wellfound, and most of the postings that matter in data, Web3, and AI never reach those boards at all.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A friend of mine, a junior data engineer, got laid off in early 2024. He spent three weeks on LinkedIn, Wellfound, and AngelList, applied to roughly 80 roles, and got back four auto-rejections and silence. I'd been telling him for months that the actually-good roles in our world were closing inside Twitter threads before they were ever posted, but you can't really "browse" Twitter for jobs. There's no filter for "people who are hiring right now." So one Saturday I sat down and built him one.&lt;/p&gt;

&lt;p&gt;Six weeks later he had an offer from a Series B crypto data infrastructure company. The scraper had surfaced about 140 hiring threads in his niche, he'd messaged 11 of them, and one converted. Total infrastructure cost: $49, paid to &lt;a href="https://api.sorsa.io/" rel="noopener noreferrer"&gt;Sorsa API&lt;/a&gt; for the Starter plan. This article is the build log: the keyword set, the code, the filtering rules, and the structural reasons Twitter ended up working when the job boards didn't.&lt;/p&gt;

&lt;p&gt;A quick note before we dig in. I've been writing about Twitter/X data pipelines for years. I'm Daniel Kolbassen, a data engineer and API consultant in Austin. I've worked with Twitter's API since the v1.1 days and have helped 40+ companies migrate off the official API since the 2023 pricing reset. Sorsa API, which I'll use throughout this article, is also our product, so factor that in when reading the cost comparisons. The technique itself works with any source of Twitter data; the API choice is a cost decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why Twitter is a real hiring channel for data, Web3, and AI&lt;/li&gt;
&lt;li&gt;Why the official X API wasn't an option&lt;/li&gt;
&lt;li&gt;How the scraper works (architecture overview)&lt;/li&gt;
&lt;li&gt;Building the scraper step by step&lt;/li&gt;
&lt;li&gt;What it cost&lt;/li&gt;
&lt;li&gt;What actually happened&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;li&gt;How to try this yourself&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a id="why-twitter"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is Twitter the Real Hiring Channel for Data, Web3, and AI?
&lt;/h2&gt;

&lt;p&gt;A surprising share of senior and mid-level roles in data, Web3, and AI infrastructure are filled before the job description ever lands on LinkedIn. The hiring manager posts a "we're hiring a data engineer, DM if interested" thread, the replies fill up within hours, and the role closes from inside that thread.&lt;/p&gt;

&lt;p&gt;Three structural reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Speed beats process.&lt;/strong&gt; Small teams hiring engineer #4 don't want a six-week funnel. They want someone who already follows the right people, gets the product, and can start in two weeks. A tweet does that filtering for free.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust beats credentials.&lt;/strong&gt; In Web3 and AI infra, "who you follow and who follows you back" is a stronger signal than a resume. Hiring inside the network is an explicit strategy. The 2024 &lt;a href="https://survey.stackoverflow.co/2024/" rel="noopener noreferrer"&gt;Stack Overflow Developer Survey&lt;/a&gt; showed referrals and personal networks remained the dominant route into developer roles, and Twitter is where a lot of those networks actually meet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Job boards have an attention tax.&lt;/strong&gt; A Wellfound posting attracts hundreds of applicants, most low-signal. Twitter is the opposite default: high-signal, no spam filter needed because the cost of replying publicly is real.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The catch is that Twitter has no built-in job board view. The information is there, but it's spread across millions of tweets per day. That's where a scraper earns its keep.&lt;/p&gt;

&lt;p&gt;&lt;a id="why-not-official"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Didn't Use the Official X API
&lt;/h2&gt;

&lt;p&gt;I considered the official X API for about ten minutes. As of 2026, the X Developer Platform's pay-per-use model means search at scale gets expensive fast, and historical search is gated behind enterprise tiers that don't make sense for a personal job-hunting tool. We covered the full pricing breakdown in &lt;a href="https://api.sorsa.io/blog/twitter-api-pricing-2026" rel="noopener noreferrer"&gt;our 2026 X API pricing analysis&lt;/a&gt;, but the short version: paying $200+ a month to look for a job is absurd.&lt;/p&gt;

&lt;p&gt;Sorsa API hit the right tradeoff. The Starter plan is $49 a month for 10,000 requests, all endpoints included, with a flat 20 requests per second rate limit. No OAuth, no app approval, no waitlist. I bought a key on Saturday morning, had the first search query running by lunch.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: Sorsa API is our product. The choice of provider isn't the point of this article; if you prefer a different Twitter data source, the architecture below works on anything that exposes a search endpoint.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a id="architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Scraper Works (60-Second Architecture Tour)
&lt;/h2&gt;

&lt;p&gt;The whole system is four moving parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A keyword catalog.&lt;/strong&gt; Hiring phrases ("we are hiring", "open role") cross-multiplied with niche terms ("data engineer", "dbt", "snowflake"). Roughly 30-40 distinct queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A search loop.&lt;/strong&gt; Every 30 minutes, hit the &lt;a href="https://docs.sorsa.io/api-reference/search/search-tweets" rel="noopener noreferrer"&gt;Sorsa search endpoint&lt;/a&gt; once per query, ordered by latest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A filter pass.&lt;/strong&gt; Drop retweets, accounts with no followers, obvious shitposts, and anything we've already seen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A notification.&lt;/strong&gt; Push the survivors to a Slack channel (or Telegram, or just print to a terminal). The tweet URL goes in the message so you can read context and reply directly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. No vector database, no LLM ranking, no front-end. The whole thing is one Python file and a SQLite database. I'll show the code below.&lt;/p&gt;

&lt;p&gt;&lt;a id="build"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Scraper, Step by Step
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Get an API Key
&lt;/h3&gt;

&lt;p&gt;Sign up at &lt;a href="https://api.sorsa.io/" rel="noopener noreferrer"&gt;api.sorsa.io&lt;/a&gt;, pick the Starter plan, copy your key from the &lt;a href="https://api.sorsa.io/overview/keys" rel="noopener noreferrer"&gt;dashboard&lt;/a&gt;. Authentication is a single header: &lt;code&gt;ApiKey: your_key_here&lt;/code&gt;. The full &lt;a href="https://docs.sorsa.io/authentication" rel="noopener noreferrer"&gt;authentication doc&lt;/a&gt; is one page. This part takes about two minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Define Your Hiring Keyword Set
&lt;/h3&gt;

&lt;p&gt;This is where most people screw up. If you only search "we are hiring", you'll drown in startup announcements that aren't relevant. The fix is to cross-multiply hiring phrases with niche-specific terms.&lt;/p&gt;

&lt;p&gt;Here's the keyword config my friend used. Replace the &lt;code&gt;NICHE_TERMS&lt;/code&gt; with whatever describes the job you actually want.&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="c1"&gt;# config.py
&lt;/span&gt;
&lt;span class="n"&gt;HIRING_PHRASES&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;we are hiring&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;we&lt;/span&gt;&lt;span class="se"&gt;\'&lt;/span&gt;&lt;span class="s"&gt;re hiring&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;now hiring&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;looking for a&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;open role&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;open position&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;join our team&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;hiring a&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="c1"&gt;# Edit these for your niche.
&lt;/span&gt;&lt;span class="n"&gt;NICHE_TERMS&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;data 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;dbt&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;snowflake&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;airflow&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;etl pipeline&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;analytics engineer&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="c1"&gt;# Optional: minimum signal threshold
&lt;/span&gt;&lt;span class="n"&gt;MIN_AUTHOR_FOLLOWERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The combinatorics give you about 50 unique queries. At one request per query every 30 minutes, that's 2,400 requests per day, well inside the 10,000-per-month Starter quota if you tighten the schedule a bit (we ran every two hours instead).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Hit the Search Endpoint
&lt;/h3&gt;

&lt;p&gt;Sorsa's &lt;code&gt;/search-tweets&lt;/code&gt; endpoint accepts the same operator syntax as Twitter's advanced search, so you can layer in filters directly in the query string. The full operator list is in the &lt;a href="https://docs.sorsa.io/search-operators" rel="noopener noreferrer"&gt;search operators reference&lt;/a&gt;; the ones we care about are exact-phrase quotes, &lt;code&gt;min_faves:&lt;/code&gt;, and &lt;code&gt;lang:en&lt;/code&gt;.&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="c1"&gt;# scraper.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API_KEY&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;SORSA_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;BASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.sorsa.io/v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_tweets&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latest&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;Hit Sorsa /search-tweets and return the tweet list.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;response&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BASE_URL&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/search-tweets&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;ApiKey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;API_KEY&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;query&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&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;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&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;response&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="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;tweets&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;def&lt;/span&gt; &lt;span class="nf"&gt;build_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hiring_phrase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;niche_term&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&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;hiring_phrase&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;niche_term&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; lang:en min_faves:1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;min_faves:1&lt;/code&gt; filter alone removes about a third of the spam. It costs nothing because Sorsa charges per request, not per result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Filter the Noise
&lt;/h3&gt;

&lt;p&gt;Even with operators, you'll get garbage: scam recruiters, MLM pitches, people quoting other hiring tweets ironically. A short filter function handles 95% of it.&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="c1"&gt;# filter.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;SPAM_PATTERNS&lt;/span&gt; &lt;span class="o"&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="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;dm me to earn&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="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;(?:🔥|🚀|💰){4,}&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="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;https?://\S*\.(?:tk|ml|ga|cf)\b&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="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_real_hiring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tweet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_followers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&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;tweet&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;retweeted_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&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;tweet&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;full_text&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="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SPAM_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tweet&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;user&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;if&lt;/span&gt; &lt;span class="n"&gt;user&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;followers_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;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;min_followers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tweet&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;is_reply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;tweet&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;reply_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;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two notes on the rules. First, the followers threshold is the single highest-signal filter; bots and scam accounts almost always have fewer than 50 followers. Second, dropping replies that have no further replies removes "me too, hire me" comments without losing genuine hiring threads (those almost always accumulate replies fast).&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Deduplicate
&lt;/h3&gt;

&lt;p&gt;You'll see the same tweet on multiple queries. SQLite is overkill but takes 10 lines and never fails.&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="c1"&gt;# storage.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;open_db&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jobs.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        CREATE TABLE IF NOT EXISTS seen (
            id TEXT PRIMARY KEY,
            seen_at TEXT NOT NULL
        )
    &lt;/span&gt;&lt;span class="sh"&gt;"""&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;db&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tweet_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT 1 FROM seen WHERE id = ?&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="n"&gt;tweet_id&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;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mark_seen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tweet_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT OR IGNORE INTO seen VALUES (?, ?)&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="n"&gt;tweet_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen_at&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Schedule It and Pipe to Slack
&lt;/h3&gt;

&lt;p&gt;The main loop ties it together. Run it under cron, systemd, or just a &lt;code&gt;while True: time.sleep(7200)&lt;/code&gt; if you don't care about elegance.&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="c1"&gt;# main.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HIRING_PHRASES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NICHE_TERMS&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;scraper&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;search_tweets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;build_query&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;is_real_hiring&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;open_db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mark_seen&lt;/span&gt;

&lt;span class="n"&gt;SLACK_WEBHOOK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://hooks.slack.com/services/...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# optional
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tweet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tweet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&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;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&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;*@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&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;tweet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;user&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;followers_count&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; followers)&lt;/span&gt;&lt;span class="se"&gt;\n&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;tweet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;full_text&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="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&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="s"&gt;https://x.com/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/status/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tweet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# requests.post(SLACK_WEBHOOK, json={"text": msg})
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_once&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;open_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&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;phrase&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;HIRING_PHRASES&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;niche&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;NICHE_TERMS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phrase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;niche&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;tweets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;search_tweets&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;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;skip &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&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;continue&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tweets&lt;/span&gt;&lt;span class="p"&gt;:&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_real_hiring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;is_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="nf"&gt;mark_seen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&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="c1"&gt;# be polite, stay under 20 req/s
&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run_once&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole system. About 120 lines total. If you've followed our &lt;a href="https://api.sorsa.io/blog/twitter-api-python" rel="noopener noreferrer"&gt;Twitter API in Python guide&lt;/a&gt;, the patterns will look familiar; this is the same plumbing applied to a hiring use case.&lt;/p&gt;

&lt;p&gt;&lt;a id="cost"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Cost
&lt;/h2&gt;

&lt;p&gt;Six weeks of running the scraper every two hours, across 50 queries, used roughly 8,400 requests. That fit comfortably inside the Starter plan's 10,000-request monthly allowance. Total spend: $49 for the first month, $49 for the second, then he canceled. So $98 for the entire job hunt.&lt;/p&gt;

&lt;p&gt;For context, &lt;a href="https://www.bls.gov/ooh/business-and-financial/human-resources-managers.htm" rel="noopener noreferrer"&gt;tech recruiters in the US typically charge 20-25% of first-year salary&lt;/a&gt; as a placement fee, which on a $130K data engineer salary is $26K-$32K. The math is comically lopsided. Even if the scraper had only marginally helped (it did more than that), the ROI would have been absurd.&lt;/p&gt;

&lt;p&gt;A quick rate-limit note: Sorsa's flat 20 requests per second is plenty for this workload, since the loop sends about one request per second at most. If you parallelized aggressively across many keyword sets, the &lt;a href="https://docs.sorsa.io/rate-limits" rel="noopener noreferrer"&gt;rate limits doc&lt;/a&gt; explains how to request a higher cap, but you almost certainly won't need it for a personal scraper.&lt;/p&gt;

&lt;p&gt;&lt;a id="results"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Happened
&lt;/h2&gt;

&lt;p&gt;I want to be careful with this section, because "I built a tool and got rich" stories are usually lies. Here's the unembellished version.&lt;/p&gt;

&lt;p&gt;In six weeks of running, the scraper surfaced 142 unique tweets that survived the filter. About 30 of those were genuine hiring posts in his niche; the rest were tangentially related (companies hiring "data" roles that turned out to be marketing analytics, recruitment agency posts, etc.). My friend cold-replied to 11 threads. He got responses from 6, made it to interviews with 3, and took an offer from a Series B crypto data infrastructure company eight weeks after we started.&lt;/p&gt;

&lt;p&gt;Two things were notable. First, the offer came from a posting he never would have seen otherwise: the founder tweeted it, got 14 replies in two hours, and never posted it on a job board. Second, the median time between a hiring tweet appearing and the role being effectively closed (founder stopped responding to new replies) was about 36 hours. Speed mattered. The scraper hitting every two hours meant he was usually in the first 10 replies, not the 80th.&lt;/p&gt;

&lt;p&gt;The technique generalizes. Swap the niche keywords for "solidity engineer" or "ML researcher" or "rust developer" and the same approach surfaces roles in those worlds. The bottleneck isn't the tool; it's the keyword discipline.&lt;/p&gt;

&lt;p&gt;&lt;a id="faq"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Can I find jobs on Twitter without using an API?
&lt;/h3&gt;

&lt;p&gt;Yes, but it doesn't scale. Twitter's built-in search lets you query "we are hiring data engineer" manually, and you can pin a search column in TweetDeck or use the advanced search interface. The problem is that you need to be looking at the screen at the right moment. A scraper running every 30 minutes catches tweets you'd miss while sleeping or in a meeting, and the dedupe layer means you don't re-read the same tweet 40 times.&lt;/p&gt;

&lt;h3&gt;
  
  
  What hashtags are best for finding jobs on Twitter?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;#hiring&lt;/code&gt;, &lt;code&gt;#nowhiring&lt;/code&gt;, &lt;code&gt;#jobopening&lt;/code&gt;, and niche-specific tags like &lt;code&gt;#web3jobs&lt;/code&gt;, &lt;code&gt;#dataengineering&lt;/code&gt;, or &lt;code&gt;#mljobs&lt;/code&gt; work, but hashtags alone are noisier than phrase searches. Most genuine hiring tweets in technical fields don't use hashtags at all; they're written in natural language. The "we are hiring" phrase search outperforms hashtag searches by roughly 3-to-1 in our experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the X API free?
&lt;/h3&gt;

&lt;p&gt;No. As of 2026, the official X API has no permanent free tier for new developer access. The cheapest paid tier still runs into significant monthly costs once you do meaningful search volume. We laid out the current pricing in our &lt;a href="https://api.sorsa.io/blog/twitter-api-alternative" rel="noopener noreferrer"&gt;Twitter API alternative comparison&lt;/a&gt;. For a job-hunting tool, the official pricing model doesn't make sense; this is exactly the use case third-party APIs are built for.&lt;/p&gt;

&lt;h3&gt;
  
  
  What companies hire through Twitter?
&lt;/h3&gt;

&lt;p&gt;Mostly small to mid-size technical companies in areas where the founder is technical and active on the platform: Web3 protocols, AI infrastructure, dev tools, data infrastructure, devrel-heavy SaaS. Larger companies (Google, Meta, etc.) post on LinkedIn first because they have HR processes that demand it. Twitter hiring skews toward seed-to-Series-B startups, which is also where the most interesting roles tend to be in 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I avoid scams when replying to hiring tweets on Twitter?
&lt;/h3&gt;

&lt;p&gt;Three checks before replying: (1) does the company exist with a website older than three months, (2) is the person tweeting actually associated with the company (check their pinned tweet and bio), (3) does the role description name a real tech stack, not just buzzwords. Scam "hiring" posts almost always fail check 2; real founders link their own company in their bio. The followers filter in the scraper above already kills most scam accounts before you ever see them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I scrape Twitter without an API at all?
&lt;/h3&gt;

&lt;p&gt;Technically yes; in practice, no. Headless-browser scrapers against the X.com web interface get rate-limited or blocked within hours, and the legal terrain is murky. We've covered the &lt;a href="https://api.sorsa.io/blog/how-to-scrape-twitter" rel="noopener noreferrer"&gt;tradeoffs in detail&lt;/a&gt;, but the short answer is that paying $49 a month for a stable API is worth it compared to babysitting a fragile scraper that breaks every time X ships a UI change.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long should I run a job scraper before deciding it works?
&lt;/h3&gt;

&lt;p&gt;Three to four weeks. The first week is mostly tuning: you'll discover that some keyword combinations return pure noise and others surface gold, and you'll iterate the filter rules. By week two the signal-to-noise ratio stabilizes. By week four you'll have a sense of how often roles in your niche actually get tweeted. If after a month you're seeing fewer than five real hiring posts a week in your niche, the niche probably doesn't hire on Twitter and you should shift channels.&lt;/p&gt;

&lt;p&gt;&lt;a id="cta"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Try This Yourself
&lt;/h2&gt;

&lt;p&gt;If you want to run this scraper, the four things you need are: a &lt;a href="https://api.sorsa.io/overview/keys" rel="noopener noreferrer"&gt;Sorsa API key&lt;/a&gt;, Python 3.10+, a niche keyword list that actually describes the role you want, and the patience to let it run for a few weeks. The full code above is copy-paste ready; the only edit is the &lt;code&gt;NICHE_TERMS&lt;/code&gt; list and your API key.&lt;/p&gt;

&lt;p&gt;A few starting points if you're new to the API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://docs.sorsa.io/quickstart" rel="noopener noreferrer"&gt;quickstart guide&lt;/a&gt; walks through your first request in under five minutes.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://api.sorsa.io/playground" rel="noopener noreferrer"&gt;API playground&lt;/a&gt; lets you test search queries in the browser without writing code, useful for tuning your keyword set before you build anything.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://docs.sorsa.io/search-operators" rel="noopener noreferrer"&gt;search operators reference&lt;/a&gt; is the cheat sheet for getting cleaner results out of &lt;code&gt;/search-tweets&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This isn't a magic system. It's a small piece of plumbing that does what manual searching can't: catch hiring tweets the moment they appear, filter the obvious garbage, and dedupe across multiple keyword angles. Whether you find a job depends on the quality of your replies, your portfolio, your timing. But finding the right tweets to reply to in the first place is solvable, and at $49 a month it's solved cheaply.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Daniel Kolbassen is a data engineer and API infrastructure consultant in Austin, TX, with 12+ years of experience building data pipelines around social media platforms. He has worked with the Twitter/X API since the v1.1 era and has helped over 40 companies restructure their data infrastructure after the 2023 pricing overhaul.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Last updated: May 6, 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>career</category>
      <category>showdev</category>
      <category>webscraping</category>
    </item>
  </channel>
</rss>
