<?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: APITube News API</title>
    <description>The latest articles on DEV Community by APITube News API (@apitube).</description>
    <link>https://dev.to/apitube</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%2F3726865%2F6d155502-fac9-4d75-9fa5-cd3c752fca54.jpg</url>
      <title>DEV Community: APITube News API</title>
      <link>https://dev.to/apitube</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apitube"/>
    <language>en</language>
    <item>
      <title>One news bot for Bluesky and X, and why X gets only five posts a day</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Sun, 27 Sep 2026 05:54:28 +0000</pubDate>
      <link>https://dev.to/apitube/one-news-bot-for-bluesky-and-x-and-why-x-gets-only-five-posts-a-day-30gc</link>
      <guid>https://dev.to/apitube/one-news-bot-for-bluesky-and-x-and-why-x-gets-only-five-posts-a-day-30gc</guid>
      <description>&lt;p&gt;X charges $0.200 for every post that contains a URL. Bluesky charges nothing. That one line from the &lt;a href="https://docs.x.com/x-api/getting-started/pricing" rel="noopener noreferrer"&gt;X pricing page&lt;/a&gt; decides the shape of a twitter news bot in Python in 2026: it posts every story to Bluesky and only the best few to X, and "best" has to be a number the script can check. We built that bot for one industry feed, measured the feed for a day, and ran the script on a 30-minute GitHub Actions schedule. One day of Renewable Energy news came to 270 articles; after the filters the bot actually wants, 5 of them were worth $0.200 each.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A news bot&lt;/strong&gt;, as built here, is a script that pulls fresh articles from a news API, drops the ones it has already posted, and publishes the rest to Bluesky and X on a schedule with no server of its own. This post is for developers and marketers who want one that runs unattended: one Python file, one workflow file, one state file. Everything below ran on 2026-09-17 with Python 3.12, atproto 0.0.72, tweepy 4.17.0 and Pillow 12.3.0. The feed came from the APITube news API; the numbers come from the CSVs published next to this post.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;X: $0.015 per post, $0.200 per post with a URL, no free tier. Bluesky: $0.&lt;/li&gt;
&lt;li&gt;One day of one industry: 270 articles → 252 unique headlines → 35 with a share image → 5 from sources ranked 6 or higher.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_duplicate=0&lt;/code&gt; and href dedupe removed 0 of the 270; normalising titles removed 18.&lt;/li&gt;
&lt;li&gt;Of 29 share images fetched, 5 were over Bluesky's 1,000,000-byte blob limit and 6 were smaller than 1200×630 despite the filter.&lt;/li&gt;
&lt;li&gt;Posting all 34 stories a day to X costs $204 a month; posting the top 5 costs $30.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the bot does
&lt;/h2&gt;

&lt;p&gt;Every 30 minutes a GitHub Actions job runs &lt;code&gt;bot.py&lt;/code&gt;, and each run does five things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Asks the news API for the last 24 hours of one industry, share-image articles only.&lt;/li&gt;
&lt;li&gt;Drops anything whose normalised title or URL is already in &lt;code&gt;posted.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Posts up to three new stories to Bluesky, each with a link card and a thumbnail under 1,000,000 bytes.&lt;/li&gt;
&lt;li&gt;Sends the ones whose source ranks 6 or higher to X as well.&lt;/li&gt;
&lt;li&gt;Writes what it posted back to &lt;code&gt;posted.json&lt;/code&gt;, which the workflow commits so the next run remembers.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The one rule that matters: &lt;strong&gt;a story goes to X only if its source has an OpenPageRank of 6 or more&lt;/strong&gt; (&lt;code&gt;X_MIN_OPR = 6&lt;/code&gt;). Everything else goes to Bluesky only. That threshold is the difference between a $204 and a $30 monthly bill, and it comes from the feed's own numbers, not from taste.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Pick a feed and measure it before you post it
&lt;/h2&gt;

&lt;p&gt;The bot filters on &lt;code&gt;industry.id&lt;/code&gt;. Industry ids come from &lt;code&gt;GET /v1/suggest/industries?prefix=renew&lt;/code&gt;, which returned &lt;code&gt;367 Renewable Energy&lt;/code&gt; among others. We pulled every English article tagged with it for 16 September 2026 UTC and ran the filters the bot would run.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filter stage (Sep 16, &lt;code&gt;industry.id=367&lt;/code&gt;, &lt;code&gt;language.code=en&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Articles&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw&lt;/td&gt;
&lt;td&gt;270&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unique by &lt;code&gt;href&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;270&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unique by normalised title&lt;/td&gt;
&lt;td&gt;252&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;has_social_share_image=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ unique title&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ &lt;code&gt;source.rankings.opr &amp;gt;= 5&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ &lt;code&gt;source.rankings.opr &amp;gt;= 6&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fbx83kntl12t1xn5dvquh.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%2Fbx83kntl12t1xn5dvquh.png" alt="270 Renewable Energy articles in a day become 252 unique headlines, 35 with a share image and 5 from sources ranked 6 or higher" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One day, 16 September 2026 UTC, &lt;code&gt;industry.id=367&lt;/code&gt;, English only; each stage is a filter the bot applies, counted from the same 270-row pull (&lt;code&gt;data.csv&lt;/code&gt;).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Two things in that table surprised us. First, the deduplication most bots rely on did nothing: all 270 hrefs were distinct and &lt;code&gt;is_duplicate&lt;/code&gt; was &lt;code&gt;false&lt;/code&gt; on all 270. Syndicated stories arrive with different URLs on different domains. Lowercasing the title and stripping punctuation found 18 copies, including one story ("Egypt's wind power push…") published six times. Title normalisation is the dedupe that matters; the flag and the URL are not.&lt;/p&gt;

&lt;p&gt;Second, the image filter is doing most of the work. &lt;code&gt;has_social_share_image=1&lt;/code&gt; is meant to keep articles with an image of at least 1200×630, the size a Bluesky link card or an X card renders well, and it cut 270 to 35. The other candidates we counted the same day: &lt;code&gt;is_high_quality=1&lt;/code&gt; 53, &lt;code&gt;is_twitter_card_ready=1&lt;/code&gt; 52, &lt;code&gt;has_hq_images=1&lt;/code&gt; 41, &lt;code&gt;has_image=1&lt;/code&gt; 147. A bot posting 147 stories a day is spam; 35 is a feed.&lt;/p&gt;

&lt;p&gt;Arrivals are lumpy. Over the raw 270 the busiest 30-minute slot had 21 articles and the median slot 4, which is why the script caps posts per run rather than posting whatever arrived.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. The script, part one: fetch, dedupe, thumbnails
&lt;/h2&gt;

&lt;p&gt;The whole bot is one file. This first half talks to the news API and prepares the image; no social network is involved yet.&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Post fresh industry news to Bluesky and X. Runs once, exits. Cron does the looping.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&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;re&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="kn"&gt;import&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;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;

&lt;span class="n"&gt;API&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.apitube.io/v1/news/everything&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;STATE_FILE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;posted.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;INDUSTRY_ID&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="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;INDUSTRY_ID&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;367&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;# 367 = Renewable Energy
&lt;/span&gt;&lt;span class="n"&gt;MAX_PER_RUN&lt;/span&gt; &lt;span class="o"&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;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="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;MAX_PER_RUN&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;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;X_MIN_OPR&lt;/span&gt; &lt;span class="o"&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;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="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;X_MIN_OPR&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;6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;          &lt;span class="c1"&gt;# X costs money per post; only the best go there
&lt;/span&gt;&lt;span class="n"&gt;DRY_RUN&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="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;DRY_RUN&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;1&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;fetch_recent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&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;dt&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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hours&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;API&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;X-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;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;APITUBE_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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&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;industry.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;INDUSTRY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;language.code&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;en&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;published_at.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;since&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%dT%H:%M:%SZ&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;has_social_share_image&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_duplicate&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sort.by&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;published_at&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;sort.order&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;desc&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;per_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fl&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;id,href,title,published_at,image,media,source.domain,source.rankings.opr&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;r&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;r&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                      &lt;span class="c1"&gt;# oldest first, so posts come out in order
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;title_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&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;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;[^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="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&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="nf"&gt;split&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;image_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&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;article&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;image&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;article&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&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="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;m&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;article&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;media&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="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&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;type&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;image&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_thumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Bluesky rejects blobs over 1,000,000 bytes; shrink anything bigger.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;data&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&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;20&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;User-Agent&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;industry-news-bot/0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;
    &lt;span class="k"&gt;if&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;data&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;cap&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;data&lt;/span&gt;
    &lt;span class="n"&gt;im&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;convert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RGB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;thumbnail&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JPEG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;85&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;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fl=&lt;/code&gt; trims each article to eight fields; without it every result carries the full body, entities and media list. &lt;code&gt;image_url()&lt;/code&gt; exists because 6 of the 35 articles that passed the share-image filter had no &lt;code&gt;image&lt;/code&gt; field at all; the picture was in &lt;code&gt;media[]&lt;/code&gt; with &lt;code&gt;type: "image"&lt;/code&gt;. &lt;code&gt;fetch_thumb()&lt;/code&gt; exists because of the other 29: median size 293,840 bytes, largest 2,717,236, and 5 of them over the 1,000,000-byte limit Bluesky enforces on uploaded blobs. Pillow's &lt;code&gt;thumbnail((1200, 1200))&lt;/code&gt; plus JPEG quality 85 brought those five down to between 129,764 and 250,145 bytes. Fetching an image took a median of 717 ms and a maximum of 2,544 ms, which is where most of a run's wall time goes.&lt;/p&gt;

&lt;p&gt;One number we cannot fix in code: 6 of the 29 images were smaller than 1200×630 (650×475 and 1000×667 among them) even though the filter promises that size. The card still renders; it just renders smaller. If exact dimensions matter to you, check &lt;code&gt;im.size&lt;/code&gt; after opening and skip the card.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. The script, part two: Bluesky and X
&lt;/h2&gt;

&lt;p&gt;Bluesky wants a rich-text post plus an "external" embed for the link card. &lt;code&gt;TextBuilder&lt;/code&gt; writes the title, then the domain as a clickable facet, so the visible text stays short and the 300-grapheme limit is never close: the longest title in the sample was 118 characters, the median 77. X wants plain text; a URL counts as 23 characters regardless of length, so every title in the sample fits in 280 as well.&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;bluesky_client&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;atproto&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&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;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;BSKY_HANDLE&lt;/span&gt;&lt;span class="sh"&gt;"&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;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;BSKY_APP_PASSWORD&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;client&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;post_bluesky&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thumb&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;atproto&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;client_utils&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;models&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;client_utils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TextBuilder&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&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="o"&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="nf"&gt;link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;source&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;domain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upload_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thumb&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thumb&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppBskyEmbedExternal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;External&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;article&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="n"&gt;description&lt;/span&gt;&lt;span class="o"&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;thumb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_post&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;embed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AppBskyEmbedExternal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;external&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;langs&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;en&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;x_client&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tweepy&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tweepy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;consumer_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;X_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;consumer_secret&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;X_API_SECRET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;access_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;X_ACCESS_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;access_token_secret&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;X_ACCESS_SECRET&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;post_x&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_tweet&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="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;article&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;article&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&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="c1"&gt;# a URL counts as 23 chars on X
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bluesky credentials are a handle and an app password, created under Settings → App Passwords; never use the account password. X credentials are the four OAuth 1.0a strings from a developer app with read-and-write permission, and the app has to be attached to a project with purchased credits, because there is no free tier to fall back on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4. The main loop, and what a dry run prints
&lt;/h2&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;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STATE_FILE&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;os&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="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STATE_FILE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;titles&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;hrefs&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;seen_titles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seen_hrefs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;titles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hrefs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;fresh&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="nf"&gt;fetch_recent&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;title_key&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;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&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;seen_titles&lt;/span&gt; &lt;span class="ow"&gt;and&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;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&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;seen_hrefs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keys&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nf"&gt;set&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;fresh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                      &lt;span class="c1"&gt;# same story from two domains -&amp;gt; post once
&lt;/span&gt;        &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;title_key&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;title&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;k&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;keys&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&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;batch&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="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;MAX_PER_RUN&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;fetched &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;fresh&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; unseen, posting &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;batch&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="n"&gt;bsky&lt;/span&gt; &lt;span class="o"&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;DRY_RUN&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;bluesky_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&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;DRY_RUN&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;x_client&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;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;opr&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="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;source&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="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;rankings&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="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;opr&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="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;image_url&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="n"&gt;thumb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_thumb&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&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;url&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="n"&gt;to_x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;opr&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;X_MIN_OPR&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;dry&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;DRY_RUN&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;post&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] bsky&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; + x&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;to_x&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | opr &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;opr&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | thumb &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;thumb&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;thumb&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; B | &lt;/span&gt;&lt;span class="si"&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;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="mi"&gt;70&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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;DRY_RUN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;post_bluesky&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bsky&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="n"&gt;thumb&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;to_x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;post_x&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&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="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;titles&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;title_key&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;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
        &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hrefs&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;titles&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hrefs&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;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;titles&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="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;:],&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hrefs&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="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;STATE_FILE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;indent&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;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="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three consecutive runs with &lt;code&gt;DRY_RUN=1&lt;/code&gt;, about 04:59 UTC:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetched 33 unseen, posting 3
[dry] bsky | opr 4 | thumb 601276 B | Chongqing, Spain's Extremadura Deepen Ties in EVs and Green Energy
[dry] bsky | opr 3 | thumb 55318 B | Egypt's Wind Turbine Manufacturing Push: A New Blueprint for Africa's
[dry] bsky | opr 4 | thumb 152741 B | Heathrow expansion incompatible with climate targets, advises UK gover
fetched 30 unseen, posting 3
...
fetched 27 unseen, posting 3
[dry] bsky | opr 5 | thumb 956337 B | Elcogen and Shanghai HyEnergy advance collaboration on commercial depl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runs took 5.4 s, 3.0 s and 3.1 s, and &lt;code&gt;posted.json&lt;/code&gt; ended with 9 titles. The "unseen" count drops by three each run because the state file works; none of the nine reached the &lt;code&gt;opr 6&lt;/code&gt; bar, so none would have gone to X. What ran live here is the fetch, dedupe and thumbnail path. The Bluesky and X calls were exercised against the SDK signatures (&lt;code&gt;login&lt;/code&gt; + &lt;code&gt;send_post&lt;/code&gt;, &lt;code&gt;create_tweet&lt;/code&gt;) but not against real accounts from this run; do your first real run with &lt;code&gt;MAX_PER_RUN=1&lt;/code&gt; and look at the result before letting the cron loose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5. Every 30 minutes on GitHub Actions
&lt;/h2&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;industry-news-bot&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*/30&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;post&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.12"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install requests pillow atproto tweepy&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python bot.py&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;APITUBE_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.APITUBE_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;BSKY_HANDLE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.BSKY_HANDLE }}&lt;/span&gt;
          &lt;span class="na"&gt;BSKY_APP_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.BSKY_APP_PASSWORD }}&lt;/span&gt;
          &lt;span class="na"&gt;X_API_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.X_API_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;X_API_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.X_API_SECRET }}&lt;/span&gt;
          &lt;span class="na"&gt;X_ACCESS_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.X_ACCESS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;X_ACCESS_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.X_ACCESS_SECRET }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;git config user.name "news-bot"&lt;/span&gt;
          &lt;span class="s"&gt;git config user.email "news-bot@users.noreply.github.com"&lt;/span&gt;
          &lt;span class="s"&gt;git add posted.json&lt;/span&gt;
          &lt;span class="s"&gt;git commit -m "state: $(date -u +%FT%TZ)" || exit 0&lt;/span&gt;
          &lt;span class="s"&gt;git push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last step is the part most schedule-a-bot posts skip. A GitHub Actions runner is a fresh machine every time, so the state has to live somewhere; committing &lt;code&gt;posted.json&lt;/code&gt; back to the repository is the zero-infrastructure answer, and &lt;code&gt;permissions: contents: write&lt;/code&gt; is what lets the default token push. &lt;code&gt;|| exit 0&lt;/code&gt; keeps a run with nothing new from failing on an empty commit.&lt;/p&gt;

&lt;p&gt;Does 30 minutes fit the feed? We replayed 16 September through the same cap: with the 34 post-filter stories arriving at their real timestamps, 23 of the 48 runs would have posted nothing, no run would have hit the 3-post cap, the busiest slot had 2 arrivals, and nothing was left in the backlog at midnight. If your industry is busier, raise &lt;code&gt;MAX_PER_RUN&lt;/code&gt; before you shorten the interval. &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule" rel="noopener noreferrer"&gt;GitHub documents&lt;/a&gt; that scheduled runs can be delayed under load and that the cron floor is 5 minutes, so a late run is normal and the 24-hour fetch window plus the state file means a late run posts what a missed one would have.&lt;/p&gt;

&lt;h2&gt;
  
  
  What X costs at this volume
&lt;/h2&gt;

&lt;p&gt;The X pricing page lists "Post: Create" at $0.015 per request and "with URLs" at $0.200 per request, pay-per-use, no subscription and no free tier. A news bot posts URLs. Multiply by the feed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sent to X per day&lt;/th&gt;
&lt;th&gt;Per day&lt;/th&gt;
&lt;th&gt;Per month (30 days)&lt;/th&gt;
&lt;th&gt;Bluesky&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;All 34&lt;/td&gt;
&lt;td&gt;$6.80&lt;/td&gt;
&lt;td&gt;$204&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sources with &lt;code&gt;opr &amp;gt;= 5&lt;/code&gt; (23)&lt;/td&gt;
&lt;td&gt;$4.60&lt;/td&gt;
&lt;td&gt;$138&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sources with &lt;code&gt;opr &amp;gt;= 6&lt;/code&gt; (5)&lt;/td&gt;
&lt;td&gt;$1.00&lt;/td&gt;
&lt;td&gt;$30&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All 34, no URL in the post&lt;/td&gt;
&lt;td&gt;$0.51&lt;/td&gt;
&lt;td&gt;$15.30&lt;/td&gt;
&lt;td&gt;$0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fmcheph9799mdzh6k2jjc.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%2Fmcheph9799mdzh6k2jjc.png" alt="Posting all 34 daily stories to X costs $204 a month, the 23 from sources ranked 5+ cost $138, the 5 ranked 6+ cost $30; Bluesky is $0 in every row" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Daily counts from the Sep 16 funnel × $0.200 per URL post × 30 days; Bluesky posting is free, so its bar is zero in every row.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That table is the whole argument for &lt;code&gt;X_MIN_OPR&lt;/code&gt;. Unlike Bluesky, which bills nothing per post, X bills $0.200 for every post with a link, which means the filter in front of X is a budget line and the filter in front of Bluesky is only a taste question. The feed already carries a source rank on every article, so the script spends $0.200 only where a well-ranked domain is behind the headline and puts the rest on the network where the same post is free. Dropping the URL to hit the $0.015 rate is the cheaper-looking row, but a news post without a link is a headline nobody can act on. If you would rather pick by topic than by rank, the same gate works on anything in the article: &lt;code&gt;is_breaking&lt;/code&gt;, a sentiment score, an entity name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;A news bot in 2026 is a filter with a budget attached. The script above is the filter: one industry, share images only, titles deduped, three posts per run, every run remembered. The budget is the one line from the X pricing page, and the feed's own source rank is what turns it into $30 a month instead of $204 while Bluesky gets all 34 stories for nothing. Copy &lt;code&gt;bot.py&lt;/code&gt; and &lt;code&gt;bot.yml&lt;/code&gt;, set the seven secrets, run once with &lt;code&gt;MAX_PER_RUN=1&lt;/code&gt;, then let the cron take over.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Is the X (Twitter) API free for bots?
&lt;/h3&gt;

&lt;p&gt;The X API is not free for bots, because as of the current pricing page it is pay-per-use with credits bought in the developer console and the earlier Free, Basic and Pro tiers are gone. A bot that posts links pays $0.200 per post. Reading is billed separately.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much does it cost to post a link with the X API?
&lt;/h3&gt;

&lt;p&gt;The cost to post a link with the X API is $0.200 per request, against $0.015 for a post without a URL, because the X API pricing page prices "Post: Create" and "with URLs" as two different rates. For this feed that is $204 a month for all 34 daily stories, or $30 a month for the 5 from sources ranked 6 or higher.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can you make a news bot on Bluesky?
&lt;/h3&gt;

&lt;p&gt;A news bot on Bluesky is possible and free, because posting through the AT Protocol costs nothing and needs only a handle plus an app password. Log in through the &lt;code&gt;atproto&lt;/code&gt; package, build the text with &lt;code&gt;TextBuilder&lt;/code&gt; so the link is a facet, upload the thumbnail with &lt;code&gt;upload_blob&lt;/code&gt; (under 1,000,000 bytes), and call &lt;code&gt;send_post&lt;/code&gt; with an &lt;code&gt;AppBskyEmbedExternal&lt;/code&gt; embed for the link card.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I schedule a Python bot for free?
&lt;/h3&gt;

&lt;p&gt;The free way to schedule a Python bot is a GitHub Actions workflow with &lt;code&gt;on: schedule: cron: "*/30 * * * *"&lt;/code&gt;, because it runs the script on a fresh runner every 30 minutes at no cost for a public repository and within the free minutes for a private one. Keep state by committing a small JSON file back to the repository at the end of each run; runs can be delayed under load, so keep the fetch window wider than the interval.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Tweepy still working in 2026?
&lt;/h3&gt;

&lt;p&gt;Tweepy still works in 2026, because tweepy 4.17.0 wraps the X API v2 and &lt;code&gt;tweepy.Client(...).create_tweet(text=...)&lt;/code&gt; posts as before. What changed is the bill behind it: every call goes through the pay-per-use X API, so Tweepy is free and the request it makes is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;X API pricing: &lt;a href="https://docs.x.com/x-api/getting-started/pricing" rel="noopener noreferrer"&gt;https://docs.x.com/x-api/getting-started/pricing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;atproto Python SDK: &lt;a href="https://atproto.blue" rel="noopener noreferrer"&gt;https://atproto.blue&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Tweepy documentation: &lt;a href="https://docs.tweepy.org" rel="noopener noreferrer"&gt;https://docs.tweepy.org&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;APITube &lt;code&gt;/v1/news/everything&lt;/code&gt; reference (industry, image and duplicate filters): &lt;a href="https://docs.apitube.io/platform/news-api/everything" rel="noopener noreferrer"&gt;https://docs.apitube.io/platform/news-api/everything&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Actions scheduled events: &lt;a href="https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule" rel="noopener noreferrer"&gt;https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#schedule&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Scripts and CSVs behind every number here: &lt;code&gt;bot.py&lt;/code&gt;, &lt;code&gt;bot.yml&lt;/code&gt;, &lt;code&gt;pull_day.py&lt;/code&gt;, &lt;code&gt;data.csv&lt;/code&gt; (the 270 articles), &lt;code&gt;funnel.csv&lt;/code&gt;, &lt;code&gt;images.csv&lt;/code&gt;, &lt;code&gt;slots.csv&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;APITube is the news API used above — free tier at apitube.io.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: we work on APITube. The feed measurements were taken with an APITube key against the public API on 2026-09-17; X prices are quoted from the X pricing page on the same day and may change.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>automation</category>
      <category>programming</category>
    </item>
    <item>
      <title>Recall alerts for the stuff you own, by text message</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Thu, 24 Sep 2026 06:12:37 +0000</pubDate>
      <link>https://dev.to/apitube/recall-alerts-for-the-stuff-you-own-by-text-message-4bnp</link>
      <guid>https://dev.to/apitube/recall-alerts-for-the-stuff-you-own-by-text-message-4bnp</guid>
      <description>&lt;p&gt;Over 31 days, a list of 26 brands from one home produced 38 text messages, 21 of them about a distinct product recall, for about $2 in Twilio fees. That is the whole result. The rest of this post is the 80-line script that did it and what the log looked like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The recall tracker is a Python script that runs once a day, asks a news API for headlines with the word "recall" and any name from a text file, and sends each new headline to your phone through Twilio.&lt;/strong&gt; No app, no account with a recall service, no six agency mailing lists. If you can run a Python file from a terminal, you can run this.&lt;/p&gt;

&lt;p&gt;One honest note before the numbers. We wrote the script this month. Instead of waiting a month to publish, we replayed the same daily query over August 17 to September 16, 2026 with a &lt;code&gt;--dry-run&lt;/code&gt; flag that prints texts instead of sending them. Every number below comes from that replay, and the CSV it wrote is next to this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;26 names on the list, 47 API hits in 31 days, 38 texts after dedup and a name check.&lt;/li&gt;
&lt;li&gt;21 distinct recalls, 10 repeat headlines about a recall already texted, 6 stories about a recall, 1 wrong match.&lt;/li&gt;
&lt;li&gt;17 of the 26 names never came up. Walmart alone was 17 of 38 texts.&lt;/li&gt;
&lt;li&gt;66 SMS segments cost $0.55 at Twilio plus carrier fees, plus $1.15 for the number.&lt;/li&gt;
&lt;li&gt;A per-brand cooldown does not help: one day of cooldown removes 13 texts and 3 real recalls.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why not just subscribe to recalls.gov
&lt;/h2&gt;

&lt;p&gt;Because the official lists send everything. Recalls.gov forwards notices from six US agencies with no way to say "only Toyota and Whirlpool". The U.S. Consumer Product Safety Commission (CPSC) email covers CPSC products only, so a car recall from the National Highway Traffic Safety Administration (NHTSA) or a frozen berry recall from the Food and Drug Administration (FDA) never arrives. Consumer Reports has a car recall tracker that texts you, but it is vehicles by VIN and nothing else.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Covers&lt;/th&gt;
&lt;th&gt;Filter by brand&lt;/th&gt;
&lt;th&gt;Texts you&lt;/th&gt;
&lt;th&gt;API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cpsc.gov email / RSS&lt;/td&gt;
&lt;td&gt;CPSC products only&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;recalls.gov email&lt;/td&gt;
&lt;td&gt;Six agencies&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Consumer Reports car tracker&lt;/td&gt;
&lt;td&gt;NHTSA vehicles&lt;/td&gt;
&lt;td&gt;By VIN&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;News API + this script&lt;/td&gt;
&lt;td&gt;Anything a newsroom reported&lt;/td&gt;
&lt;td&gt;Yes, your list&lt;/td&gt;
&lt;td&gt;Yes, via Twilio&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Unlike the recalls.gov email, which sends every recall from six agencies, the script sends only headlines that name something on your list, which means 38 texts a month for 26 names instead of hundreds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Write down what you own
&lt;/h2&gt;

&lt;p&gt;One name per line in &lt;code&gt;my_stuff.txt&lt;/code&gt;. We used a plausible home: appliances, two cars, kid gear, the stores we shop at, a few food brands.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Samsung
LG
Whirlpool
Bosch
KitchenAid
Instant Pot
Dyson
Philips
Toyota
Honda
Ford
Subaru
Fisher-Price
Graco
IKEA
Peloton
Walmart
Costco
Trader Joe's
Kroger
Target
Great Value
Kirkland
Nestle
Tyson
Blue Buffalo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store names matter more than you would guess. Food recalls are usually reported as "sold at Walmart, Target and Kroger", not by the packer's name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Find the filter that actually works
&lt;/h2&gt;

&lt;p&gt;We used APITube, our News API Intelligence (disclosure at the bottom). Articles come back already tagged with entities, categories and events, which is why the obvious filters were the first thing we tried. They did not do what we hoped, so here is what each one returned, measured on the same day:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filter&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;event.type=recall&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Accepted silently, returns the unfiltered feed. First hit was a story about tariffs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;event.name=recall&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;400 ER0228&lt;/code&gt;: "entity event name 'recall' not found".&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;category.id=medtop:20000207&lt;/code&gt; (IPTC "product recall")&lt;/td&gt;
&lt;td&gt;18 of the latest 197 English articles had "recall" in the headline.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;query=recall AND (Walmart OR Ford OR …)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;47 hits in 31 days, 45 of them with a listed name in the headline.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the working route is the boolean &lt;code&gt;query&lt;/code&gt; parameter: a bare word searches headlines, and &lt;code&gt;AND&lt;/code&gt;/&lt;code&gt;OR&lt;/code&gt; combine them. One request covers the whole list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-G&lt;/span&gt; &lt;span class="s2"&gt;"https://api.apitube.io/v1/news/everything"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: YOUR_API_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s1"&gt;'query=recall AND (Walmart OR Ford OR Toyota OR "Great Value")'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"language.code=en"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"published_at.start=2026-09-16"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"sort.by=published_at"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"sort.order=asc"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"per_page=200"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each result is a large object. The script uses five fields of 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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3083205852&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;"Fuel Tank Issue Sparks Ford F-150 Recall"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"href"&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://lite987.com/ixp/39/p/ford-recall-new-york-state-drivers/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published_at"&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-09-16T13:21:38.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;"source"&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;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lite987.com"&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;Names with a space, apostrophe or hyphen go in quotes (&lt;code&gt;"Great Value"&lt;/code&gt;, &lt;code&gt;"Trader Joe's"&lt;/code&gt;, &lt;code&gt;"Fisher-Price"&lt;/code&gt;). The script does that for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. The script
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;recall_alerts.py&lt;/code&gt; next to &lt;code&gt;my_stuff.txt&lt;/code&gt;. It needs &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;twilio&lt;/code&gt; (&lt;code&gt;pip install requests twilio&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&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;pathlib&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dt&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&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.apitube.io/v1/news/everything&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;HERE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;
&lt;span class="n"&gt;NAMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&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;n&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;HERE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_stuff.txt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&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;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
&lt;span class="n"&gt;LOG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HERE&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alerts.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;DRY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--dry-run&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&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;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="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="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flag&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="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&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;name&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;re&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="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="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;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;


&lt;span class="n"&gt;QUERY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;recall AND (&lt;/span&gt;&lt;span class="sh"&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; OR &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;term&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&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;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;NAMES&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;)&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;fetch&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;end&lt;/span&gt;&lt;span class="o"&gt;=&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;params&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;language.code&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;en&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;published_at.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;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;sort.by&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;published_at&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;sort.order&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;asc&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;per_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&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&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at.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;end&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;r&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;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;API&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;X-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;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;APITUBE_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;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&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;60&lt;/span&gt;&lt;span class="p"&gt;)&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;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&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;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&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;has_next_pages&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;params&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&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="mi"&gt;1&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;title_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&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;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;[^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="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;title&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="nf"&gt;strip&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;names_in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;NAMES&lt;/span&gt; &lt;span class="k"&gt;if&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;search&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;(?&amp;lt;![A-Za-z])&lt;/span&gt;&lt;span class="sh"&gt;"&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;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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-Za-z])&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&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;send_sms&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;if&lt;/span&gt; &lt;span class="n"&gt;DRY&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;text&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="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dry-run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;twilio.rest&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&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;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;TWILIO_ACCOUNT_SID&lt;/span&gt;&lt;span class="sh"&gt;"&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;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;TWILIO_AUTH_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;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="o"&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;from_&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;TWILIO_FROM&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;to&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;MY_PHONE&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;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;


&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&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;LOG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;row&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_key&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;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--since&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;[&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&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;rows&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&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;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timedelta&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%dT%H:%M:%SZ&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;until&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--until&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&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;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&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;article_id&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;names&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;title_key&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;title&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;url&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;source&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;sms_chars&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;sid&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;until&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="nf"&gt;title_key&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;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;names_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;title&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;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;names&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;text&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;RECALL (&lt;/span&gt;&lt;span class="si"&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;names&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;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;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;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;href&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="n"&gt;sid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;send_sms&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;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&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;published_at&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;id&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;key&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;title&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;href&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;source&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;domain&lt;/span&gt;&lt;span class="sh"&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;text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each part does, in plain words:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;QUERY&lt;/code&gt; builds the &lt;code&gt;recall AND (…)&lt;/code&gt; string from your file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch()&lt;/code&gt; asks the API for everything since the last run, 200 at a time, and follows &lt;code&gt;has_next_pages&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;title_key()&lt;/code&gt; turns a headline into lowercase letters and digits. Syndicated copies (the same F-150 headline ran on three radio station sites) collapse into one key, so you get one text, not three.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;names_in()&lt;/code&gt; checks the headline again for your names, case-sensitive and whole-word. The API matches loosely; this second check decides which names go into the text.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;send_sms()&lt;/code&gt; prints in &lt;code&gt;--dry-run&lt;/code&gt; mode and calls Twilio otherwise. Four environment variables: &lt;code&gt;TWILIO_ACCOUNT_SID&lt;/code&gt;, &lt;code&gt;TWILIO_AUTH_TOKEN&lt;/code&gt;, &lt;code&gt;TWILIO_FROM&lt;/code&gt; (your Twilio number) and &lt;code&gt;MY_PHONE&lt;/code&gt;. A trial account can only text numbers you verified in the console, and it prefixes every message with a trial notice.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;alerts.csv&lt;/code&gt; is both the log and the memory. The last &lt;code&gt;published_at&lt;/code&gt; in it is where the next run starts.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 4. Run it, then let cron run it
&lt;/h2&gt;

&lt;p&gt;First run, dry, from a date you pick:&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="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;APITUBE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;...   &lt;span class="c"&gt;# free tier at apitube.io&lt;/span&gt;
python3 recall_alerts.py &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--since&lt;/span&gt; 2026-09-10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the printed texts look right, add the Twilio variables and a cron line. Once a day at 08:00 is enough. Recalls are not breaking news; the article is usually hours behind the official notice anyway.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;0 8 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/recall-alerts &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; python3 recall_alerts.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What 31 days looked like
&lt;/h2&gt;

&lt;p&gt;The replay ran &lt;code&gt;--since 2026-08-17 --until 2026-09-16T23:59:59Z&lt;/code&gt;. 47 API hits became 40 unique headlines, and 38 texts after the name check. We then read all 38 and labelled them by hand.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Label&lt;/th&gt;
&lt;th&gt;Texts&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Distinct recall&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;First headline about a recall you had not been told about&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repeat coverage&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Another outlet, another headline, same recall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;About a recall&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;A stock story, a sales story, a lawyer ad, a listicle, commentary, a Honda "check your recall status" notice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Doug Ford, the Ontario premier, being asked to recall the legislature&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fys4m3jxoslz16qgcplhx.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%2Fys4m3jxoslz16qgcplhx.png" alt="Walmart produced 17 of 38 texts, Ford 11, Toyota 6; 17 of the 26 names on the list never produced a text in 31 days" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Name&lt;/th&gt;
&lt;th&gt;Texts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Walmart&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ford&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Toyota&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kroger&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Costco&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Great Value&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Honda&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subaru&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;(A text can carry more than one name, so the column adds up past 38.) The other 17 names, from Samsung to Blue Buffalo, produced nothing in 31 days. The Walmart frozen-berry E. coli recall alone produced 8 texts over two weeks as it expanded state by state. The busiest day was September 4 with 5 texts; 24 of the 31 days had at least one.&lt;/p&gt;

&lt;p&gt;The name check dropped 2 of the 47 hits. One drop was right: a Tesla door-handle recall matched because the headline contained the verb "target". One was a loss: a Goal Zero power-station recall posted on a Costco fan site, where "Costco" appeared only in the body. Title-only matching has a price, and that was it for the month.&lt;/p&gt;

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

&lt;p&gt;A recall tracker for 26 names costs about $2 a month: 66 SMS segments in 31 days at Twilio's US long-code rate of $0.0083 per segment ($0.55), carrier pass-through fees of $0.0035–0.005 per segment ($0.23–0.33), and $1.15 a month for the number. $1.93 to $2.03 for the month. 28 texts needed two segments because the article URL pushes them past 160 characters; 10 fit in one.&lt;/p&gt;

&lt;p&gt;One detail that matters for the bill: the text starts with &lt;code&gt;RECALL (Walmart):&lt;/code&gt; and not &lt;code&gt;Recall — Walmart&lt;/code&gt;. An em dash is not in the GSM-7 character set, and a single one switches the whole message to 70-character segments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should you add a cooldown?
&lt;/h2&gt;

&lt;p&gt;The obvious fix for 8 berry texts is "don't text about the same name twice in N days". We replayed the log with that rule:&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%2Fj8g7xz0pmisyfes2db9w.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%2Fj8g7xz0pmisyfes2db9w.png" alt="No cooldown sends 38 texts and catches all 21 recalls; a one-day cooldown drops to 25 texts and 18 recalls; seven days drops to 14 texts and 10 recalls" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cooldown per name&lt;/th&gt;
&lt;th&gt;Texts&lt;/th&gt;
&lt;th&gt;Distinct recalls caught&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;td&gt;21 of 21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1 day&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;18 of 21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 days&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;15 of 21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 days&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;10 of 21&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A one-day cooldown removes 13 texts, and 3 of those were distinct recalls: a Ford Bronco airbag recall, a Toyota GR Supra recall in China and a UK roundup naming Ford and Toyota. Ford had five separate recalls in the month, and they land days apart. A per-name cooldown is the wrong lever for recall alerts, because it throws away real recalls faster than it removes repeats. Our recommendation: no cooldown. If 38 texts is too many, shorten the list. Taking Walmart off it leaves 28 texts; taking Walmart and Ford off leaves 18.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  How do I get notified about product recalls?
&lt;/h3&gt;

&lt;p&gt;The two ways to get notified about product recalls are the recalls.gov email, which sends every notice from six US agencies, and a script like the one above, which texts you only when a headline names something you own. The official email is complete but unfiltered; the script is filtered but only as good as the newsroom coverage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there a product recall API?
&lt;/h3&gt;

&lt;p&gt;There is no single product recall API for all products. The CPSC Recall API covers consumer products under its jurisdiction, not vehicles, food or drugs. For everything else, a news API with a headline search covers whatever gets reported: &lt;code&gt;query=recall AND (Brand OR Brand)&lt;/code&gt; returned 47 headlines in 31 days for 26 names.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is there an app for product recalls?
&lt;/h3&gt;

&lt;p&gt;We did not find an app that watches product recalls for a personal list of brands across all agencies. Consumer Reports covers vehicles and several retailers cover their own sales. This script is the app: a text file, a cron line and about $2 a month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Amazon notify you of recalls?
&lt;/h3&gt;

&lt;p&gt;Amazon notifies you of recalls only for items bought on Amazon, by email and a notice in Your Orders. It does not know about the Instant Pot you bought elsewhere, which is what the list in &lt;code&gt;my_stuff.txt&lt;/code&gt; is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Title-only matching misses recalls whose headline names the product and not the brand or store. The Goal Zero power-station recall was the one we lost this month: "Costco" was in the body, not the headline.&lt;/li&gt;
&lt;li&gt;Title dedup merges identical headlines, not different headlines about one recall. Expect a few texts per big recall.&lt;/li&gt;
&lt;li&gt;The news is hours behind the official notice. The text links to the article; check the lot number and dates there or on the agency site before you throw anything out.&lt;/li&gt;
&lt;li&gt;17 of 26 names were silent for a month. That is a normal month, not a broken script.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The script, &lt;code&gt;my_stuff.txt&lt;/code&gt; and the 38-row &lt;code&gt;data.csv&lt;/code&gt; from the replay are next to this post. Change the list and run it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.recalls.gov/" rel="noopener noreferrer"&gt;recalls.gov&lt;/a&gt; — the six-agency hub and its email subscription&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.cpsc.gov/Recalls/CPSC-Recalls-Application-Program-Interface-API-Information" rel="noopener noreferrer"&gt;CPSC Recall API&lt;/a&gt; — official API for CPSC-jurisdiction recalls&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.twilio.com/docs/messaging/quickstart/python" rel="noopener noreferrer"&gt;Twilio Messaging quickstart for Python&lt;/a&gt; — client setup and trial-account rules&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.apitube.io/platform/news-api/everything" rel="noopener noreferrer"&gt;APITube docs: /v1/news/everything&lt;/a&gt; — the &lt;code&gt;query&lt;/code&gt; parameter and its 31-day window&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disclosure: APITube is our product, a News API Intelligence, and the API used above. It has a free tier at &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;apitube.io&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>tutorial</category>
      <category>beginners</category>
      <category>automation</category>
    </item>
    <item>
      <title>A news site in Flask, deployed for free</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Mon, 21 Sep 2026 16:33:27 +0000</pubDate>
      <link>https://dev.to/apitube/a-news-site-in-flask-deployed-for-free-1mfh</link>
      <guid>https://dev.to/apitube/a-news-site-in-flask-deployed-for-free-1mfh</guid>
      <description>&lt;h1&gt;
  
  
  A news site in Flask, deployed for free
&lt;/h1&gt;

&lt;p&gt;The copy-paste Flask news app calls the news API inside the request handler. On our laptop that made the business page take &lt;strong&gt;10.6 seconds&lt;/strong&gt; to load, and every visitor cost one API call: &lt;strong&gt;30 page views, 30 calls&lt;/strong&gt;. With a five-minute cache and one background thread the same 30 views cost &lt;strong&gt;1 call&lt;/strong&gt;, and the page rendered in under a millisecond. This post builds that version and deploys it on a free tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Flask news app&lt;/strong&gt; is a small Python web server that fetches headlines from a news API, keeps them in memory for a few minutes, and renders them with a Jinja template. The one below is 110 lines, has category pages, search and translated world news, and runs on a free key and a free host.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Top headlines page: &lt;strong&gt;310 ms&lt;/strong&gt; cold, API median &lt;strong&gt;285 ms&lt;/strong&gt; over 30 calls.&lt;/li&gt;
&lt;li&gt;Category pages through &lt;code&gt;category.id&lt;/code&gt;: &lt;strong&gt;1.8 s&lt;/strong&gt; (health) to &lt;strong&gt;10.6 s&lt;/strong&gt; (business). Not something a visitor should wait for.&lt;/li&gt;
&lt;li&gt;30 views without a cache = &lt;strong&gt;30 API calls&lt;/strong&gt;; with a 300 s cache = &lt;strong&gt;1 call&lt;/strong&gt;, median &lt;strong&gt;&amp;lt;1 ms&lt;/strong&gt; per view.&lt;/li&gt;
&lt;li&gt;Free plan: &lt;strong&gt;10 requests/min&lt;/strong&gt;, &lt;strong&gt;10 articles/page&lt;/strong&gt;, results &lt;strong&gt;12 hours&lt;/strong&gt; behind. The cache keeps a site inside that.&lt;/li&gt;
&lt;li&gt;Render free tier: &lt;strong&gt;750 hours/month&lt;/strong&gt;, sleeps after &lt;strong&gt;15 min&lt;/strong&gt; idle, wakes in about &lt;strong&gt;1 minute&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is for Python beginners who have run &lt;code&gt;flask run&lt;/code&gt; once and want a project that survives being deployed. Everything below was measured on 16 September 2026 with a Basic-plan key, in-process with Flask's test client, on a laptop. The CSV is next to the charts.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What we're building&lt;/li&gt;
&lt;li&gt;The recipe everyone copies, and where it breaks&lt;/li&gt;
&lt;li&gt;The code, in four parts&lt;/li&gt;
&lt;li&gt;What the pages cost, measured&lt;/li&gt;
&lt;li&gt;What the cache saves&lt;/li&gt;
&lt;li&gt;Deploy it for free&lt;/li&gt;
&lt;li&gt;Lock the key&lt;/li&gt;
&lt;li&gt;FAQ&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;A news site with six pages and a search box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; top headlines for one country&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/c/business&lt;/code&gt;, &lt;code&gt;/c/tech&lt;/code&gt;, &lt;code&gt;/c/sport&lt;/code&gt;, &lt;code&gt;/c/health&lt;/code&gt;, &lt;code&gt;/c/politics&lt;/code&gt; category pages&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/world&lt;/code&gt; German, French and Spanish headlines shown with their English translation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/search?q=...&lt;/code&gt; headline search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Files: &lt;code&gt;app.py&lt;/code&gt; (110 lines), &lt;code&gt;templates/index.html&lt;/code&gt; (38 lines), &lt;code&gt;requirements.txt&lt;/code&gt;, &lt;code&gt;render.yaml&lt;/code&gt;. No database, no JavaScript, no CSS framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recipe everyone copies, and where it breaks
&lt;/h2&gt;

&lt;p&gt;Search "flask news app" and the top tutorials share one shape: one route, &lt;code&gt;requests.get(...)&lt;/code&gt; inside it, the API key pasted into the source, run it on &lt;code&gt;localhost:5000&lt;/code&gt;, done. Three things go wrong the moment it leaves the laptop.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The key is in the code.&lt;/strong&gt; One of the top-ranking tutorials prints its key in the finished example. Anyone who forks the repo gets your quota.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The API call is inside the handler.&lt;/strong&gt; Every visitor waits for the API, and every visitor spends one request. On a free plan with 10 requests per minute, the eleventh visitor in a minute gets an error page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The free tier of the API those tutorials use is localhost-only.&lt;/strong&gt; NewsAPI's developer plan returns &lt;a href="https://newsapi.org/pricing" rel="noopener noreferrer"&gt;HTTP 426&lt;/a&gt; from any deployed host, so "deploy" is a step those posts skip.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We keep the key in an environment variable, move the API call behind a cache, and pick an API whose free tier works from a server. Disclosure: we work on &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube&lt;/a&gt;, which is the API used below. Its free tier has real limits, listed near the end, and the caching pattern works with any news API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: the project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flask-news/
├── app.py
├── templates/index.html
├── requirements.txt
└── render.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# requirements.txt
flask==3.1.*
requests==2.32.*
gunicorn==23.*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;APITUBE_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_key_here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: app.py, in four parts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The fetch function and the cache
&lt;/h3&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&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;import&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;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;API&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.apitube.io/v1/news&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;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;APITUBE_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;COUNTRY&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="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;NEWS_COUNTRY&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;us&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;CACHE_TTL&lt;/span&gt; &lt;span class="o"&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;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="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;CACHE_TTL&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;300&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# seconds
&lt;/span&gt;&lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id,title,href,description,image,published_at,language,source.domain,translations.en.title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;CATEGORIES&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;business&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;medtop:04000000&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;tech&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;medtop:13000000&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;sport&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;medtop:15000000&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;health&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;medtop:07000000&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;politics&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;medtop:11000000&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;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;_cache&lt;/span&gt; &lt;span class="o"&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;fetch&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;_force&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&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="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="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&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;params&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;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_cache&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="n"&gt;key&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;hit&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;_force&lt;/span&gt; &lt;span class="ow"&gt;and&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;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;hit&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;CACHE_TTL&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;hit&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;r&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;get&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;API&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;path&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;params&lt;/span&gt;&lt;span class="o"&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;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&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;X-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;KEY&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;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&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;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="o"&gt;=&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;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;_cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&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;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;articles&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;articles&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fetch()&lt;/code&gt; is the only place that talks to the API. The cache is a dict keyed by endpoint plus sorted parameters, holding a timestamp and the article list. &lt;code&gt;fl=&lt;/code&gt; asks for nine fields instead of the full sixty-field article, which keeps the response small. The category IDs are IPTC media topics; the full list is in the &lt;a href="https://docs.apitube.io/platform/news-api/list-of-categories" rel="noopener noreferrer"&gt;category reference&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  One function that says what backs each page
&lt;/h3&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;page_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One place that says which API call backs which page.&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;slug&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top-headlines&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;language.code&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;en&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;source.country.code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;COUNTRY&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;slug&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;world&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top-headlines&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;language.code&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;de,fr,es&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;# Category filters are slow on the archive: pin them to the last day and to ranked sources.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;everything&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;category.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;CATEGORIES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at.start&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-1DAY&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;source.rank.opr.min&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;language.code&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;en&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;source.country.code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;COUNTRY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why category pages use &lt;code&gt;/everything&lt;/code&gt; with a one-day window instead of &lt;code&gt;/top-headlines&lt;/code&gt; is a measurement, covered below.&lt;/p&gt;

&lt;h3&gt;
  
  
  The routes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.route&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;home&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;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;page_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;top&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="nf"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;fetch&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Top headlines&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/c/&amp;lt;slug&amp;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;def&lt;/span&gt; &lt;span class="nf"&gt;category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&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;slug&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;CATEGORIES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&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;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;page_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&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;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;fetch&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;


&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/world&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;world&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;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;page_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;world&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="nf"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;fetch&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;World, translated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/search&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;search&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&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;q&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="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;articles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;everything&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;q&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;language.code&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;en&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;sort.by&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;published_at&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;q&lt;/span&gt; &lt;span class="k"&gt;else&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;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&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;Search: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;q&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;q&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;Search&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;h3&gt;
  
  
  The error page and the warm thread
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.errorhandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&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;api_down&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status_code&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Too many requests, try again in a minute.&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;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&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;The news feed is unavailable right now.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;render_template&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;articles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;warm_cache&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Refresh every page in the background so no visitor ever waits for the API.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&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;slug&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;top&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;world&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;CATEGORIES&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;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;page_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slug&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="nf"&gt;fetch&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;_force&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;pass&lt;/span&gt;  &lt;span class="c1"&gt;# keep the stale copy, try again next round
&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# spread calls out under the per-minute limit
&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="n"&gt;CACHE_TTL&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;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="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;WARM_CACHE&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="o"&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="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;warm_cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daemon&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;start&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;__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="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The error handler catches every &lt;code&gt;requests&lt;/code&gt; failure, including a 429 from the API, and renders the normal template with a message and a 503 status. We forced a 429 in the test run and got the page, not a stack trace. The warm thread fetches all seven pages once, sleeps &lt;code&gt;CACHE_TTL&lt;/code&gt;, and repeats. Visitors only ever read the dict.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: the template
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;{{ title }}&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.5&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;720px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#111&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;nav&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.8rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0254EC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;form&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#ddd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;120px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;object-fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;small&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#666&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;.orig&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#666&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-style&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;italic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Top&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
    {% for slug in ["business", "tech", "sport", "health", "politics"] %}&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/c/{{ slug }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ slug|title }}&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;{% endfor %}
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/world"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;World&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/search"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"q"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Search headlines"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{{ request.args.get('q', '') }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Go&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;{{ title }}&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  {% for a in articles %}
  &lt;span class="nt"&gt;&amp;lt;article&amp;gt;&lt;/span&gt;
    {% if a.image %}&lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"{{ a.image }}"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{% endif %}
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"{{ a.href }}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ a.translations.en.title or a.title }}&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      {% if a.translations.en.title %}&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"orig"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{{ a.title }}&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;{% endif %}
      &lt;span class="nt"&gt;&amp;lt;br&amp;gt;&amp;lt;small&amp;gt;&lt;/span&gt;{{ a.source.domain }} · {{ a.published_at[:16].replace("T", " ") }} · {{ a.language }}&lt;span class="nt"&gt;&amp;lt;/small&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/article&amp;gt;&lt;/span&gt;
  {% else %}
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Nothing here yet.&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  {% endfor %}
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one line worth noticing is &lt;code&gt;a.translations.en.title or a.title&lt;/code&gt;. Every non-English article comes with an English title and description already attached; English articles have &lt;code&gt;null&lt;/code&gt; there, so the &lt;code&gt;or&lt;/code&gt; falls back to the original. On the &lt;code&gt;/world&lt;/code&gt; page all &lt;strong&gt;20 of 20&lt;/strong&gt; articles had a translation. No translation service, no extra call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;flask run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;http://127.0.0.1:5000&lt;/code&gt;. The warm thread starts with the app, so by the time you click Business the page is already in memory. Set &lt;code&gt;CACHE_TTL=0&lt;/code&gt; if you want to see the uncached timings yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the pages cost, measured
&lt;/h2&gt;

&lt;p&gt;Two cold runs per page, cache off, one API call each. Time is the full Flask request through the test client, so it includes the API round trip and rendering.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page&lt;/th&gt;
&lt;th&gt;Run 1&lt;/th&gt;
&lt;th&gt;Run 2&lt;/th&gt;
&lt;th&gt;Articles&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/&lt;/code&gt; top headlines&lt;/td&gt;
&lt;td&gt;310 ms&lt;/td&gt;
&lt;td&gt;513 ms&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/c/business&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10,622 ms&lt;/td&gt;
&lt;td&gt;10,250 ms&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/c/tech&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2,909 ms&lt;/td&gt;
&lt;td&gt;2,506 ms&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/c/sport&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10,155 ms&lt;/td&gt;
&lt;td&gt;10,259 ms&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/c/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,775 ms&lt;/td&gt;
&lt;td&gt;1,797 ms&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/c/politics&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4,643 ms&lt;/td&gt;
&lt;td&gt;4,734 ms&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/world&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;389 ms&lt;/td&gt;
&lt;td&gt;575 ms&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/search?q=climate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;681 ms&lt;/td&gt;
&lt;td&gt;591 ms&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Ft3nznrxqu6zrwou1pdwx.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%2Ft3nznrxqu6zrwou1pdwx.png" alt="Top headlines, world and search pages render in 310 to 681 ms cold; category pages take 1.8 to 10.6 seconds, business and sport slowest at over 10 seconds on both runs" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The plain endpoints are fast. Category filters are not: a &lt;code&gt;category.id&lt;/code&gt; query walks a much larger slice of the index, and business and sport are the biggest categories. Our first version used &lt;code&gt;/top-headlines&lt;/code&gt; with &lt;code&gt;category.id&lt;/code&gt; and no date bound, and four of the five categories came back as a 502 after 30 seconds or a 500; tech alone answered, in 24.7 s. Unlike &lt;code&gt;/top-headlines&lt;/code&gt; with a bare &lt;code&gt;category.id&lt;/code&gt;, which failed on four of five categories, &lt;code&gt;/everything&lt;/code&gt; with &lt;code&gt;published_at.start=NOW-1DAY&lt;/code&gt; and &lt;code&gt;source.rank.opr.min=5&lt;/code&gt; answered every category under 11 s and gave the same timings on both runs, which means the date bound, not the endpoint, is what makes a category page usable. Ten seconds is still not a page load, which is the whole argument for the warm thread: the visitor reads a copy that was fetched in the background, and the 10 s happens where nobody is waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the cache saves
&lt;/h2&gt;

&lt;p&gt;Thirty requests to &lt;code&gt;/&lt;/code&gt;, twice: once with &lt;code&gt;CACHE_TTL=0&lt;/code&gt;, once with &lt;code&gt;CACHE_TTL=300&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;Page views&lt;/th&gt;
&lt;th&gt;API calls&lt;/th&gt;
&lt;th&gt;Median per view&lt;/th&gt;
&lt;th&gt;Slowest view&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cache off&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;285 ms&lt;/td&gt;
&lt;td&gt;461 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cache 300 s&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&amp;lt;1 ms&lt;/td&gt;
&lt;td&gt;263 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fm6vbcf09nyx6e9w5h1is.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%2Fm6vbcf09nyx6e9w5h1is.png" alt="Thirty page views cost 30 API calls without a cache and 1 with a 300-second cache; the median view drops from 285 ms to under 1 ms" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The math for a real day: without a cache, calls equal page views. Unlike the copy-paste recipe, where every visitor is one API call, the cached app spends at most &lt;strong&gt;12 calls an hour, 288 a day&lt;/strong&gt; per page whatever the traffic, which means visitors no longer decide the API bill. Seven warmed pages cost &lt;strong&gt;2,016 calls a day&lt;/strong&gt;. A Free key allows 10 calls a minute; the warm loop makes 7 calls per cycle with 2 s gaps, so it fits, with room for search queries on top.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you need which piece:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Under 10 visitors a minute, no category pages: the plain recipe works on a Free key. Add the cache anyway; it is 8 lines.&lt;/li&gt;
&lt;li&gt;Any category page: you need the cache and the warm thread, because 10 s is not a page load at any traffic level.&lt;/li&gt;
&lt;li&gt;More than 10 uncached visitors a minute on Free, or 50 on Basic: without a cache you hit the limit and serve the 503 page.&lt;/li&gt;
&lt;li&gt;More than one gunicorn worker: each worker has its own dict and its own warm thread, so calls multiply by worker count. Keep &lt;code&gt;-w 1&lt;/code&gt; on a small instance, or move the cache to Redis.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deploy it for free
&lt;/h2&gt;

&lt;p&gt;We picked &lt;a href="https://render.com/docs/free" rel="noopener noreferrer"&gt;Render&lt;/a&gt; because its free web service runs from a &lt;code&gt;render.yaml&lt;/code&gt; in the repo, needs no card to start, and takes environment variables from the dashboard. The numbers in this post were measured locally; the deploy steps below follow Render's documentation. What the free tier gives you, from Render's own docs: &lt;strong&gt;750 instance hours a month&lt;/strong&gt;, the service &lt;strong&gt;spins down after 15 minutes&lt;/strong&gt; without traffic, and waking it &lt;strong&gt;takes about one minute&lt;/strong&gt;. That minute is the price; for a personal news page it is fine, for anything with an audience it is not.&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="c1"&gt;# render.yaml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flask-news&lt;/span&gt;
    &lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python&lt;/span&gt;
    &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;free&lt;/span&gt;
    &lt;span class="na"&gt;buildCommand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
    &lt;span class="na"&gt;startCommand&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gunicorn -w 1 -b 0.0.0.0:$PORT app:app&lt;/span&gt;
    &lt;span class="na"&gt;envVars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;APITUBE_API_KEY&lt;/span&gt;
        &lt;span class="na"&gt;sync&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NEWS_COUNTRY&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Steps: push the four files to GitHub, in Render choose &lt;strong&gt;New → Blueprint&lt;/strong&gt;, pick the repo, and when it asks for &lt;code&gt;APITUBE_API_KEY&lt;/code&gt; paste the key (&lt;code&gt;sync: false&lt;/code&gt; means it is never written to the file). Build takes a couple of minutes; the URL is &lt;code&gt;https://flask-news.onrender.com&lt;/code&gt; or whatever name you chose. Every deploy restarts the process, which empties the cache, and the warm thread refills it in about 43 seconds, the sum of the seven cold fetches from run 1 (30.8 s) plus six 2 s gaps.&lt;/p&gt;

&lt;p&gt;Two alternatives we checked. &lt;a href="https://www.pythonanywhere.com/" rel="noopener noreferrer"&gt;PythonAnywhere&lt;/a&gt; has a free tier without a card, but its free web apps only allow outbound requests to an allowlist of hosts, so check whether your API's host is on it before choosing it. &lt;a href="https://fly.io/docs/about/pricing/" rel="noopener noreferrer"&gt;Fly.io&lt;/a&gt; no longer has a free tier: every organization needs a card on file, and the smallest machine is $2.02 a month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lock the key
&lt;/h2&gt;

&lt;p&gt;The key never appears in a file. Locally it lives in the shell, on Render in the dashboard. Two more things cost nothing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the APITube dashboard, restrict the key to your server's outbound IP, or to your site's domain via the referrer allowlist. A blocked request returns &lt;code&gt;403 ER0601&lt;/code&gt; or &lt;code&gt;ER0602&lt;/code&gt; and does not count against quota.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/v1/balance&lt;/code&gt; is free to call and returns the points left on the key. A one-line cron that reads it and emails you under a threshold is the whole budget alarm.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the Free plan gives you
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.apitube.io/platform/news-api/rate-limits" rel="noopener noreferrer"&gt;rate limits page&lt;/a&gt; is short and worth reading before you rely on it: &lt;strong&gt;10 requests a minute&lt;/strong&gt;, &lt;strong&gt;10 articles a page&lt;/strong&gt; (our &lt;code&gt;per_page=20&lt;/code&gt; is clamped to 10), &lt;strong&gt;5 pages&lt;/strong&gt; deep, and every result &lt;strong&gt;12 hours&lt;/strong&gt; behind publication. The delay applies everywhere, streams included. The Basic key we measured with has no delay, 50 requests a minute and 250 per page. So on Free the site works exactly as built, with yesterday's news and ten stories a page.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How do you make a news app in Flask?&lt;/strong&gt;&lt;br&gt;
A Flask news app needs one route per page, one function that calls the news API, and a Jinja template that loops over the articles. The version above adds a dict cache with a time-to-live and a background thread that refreshes it, so visitors never wait on the API. The complete app is 110 lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you deploy a Flask app for free?&lt;/strong&gt;&lt;br&gt;
A Flask app deploys for free on Render with a &lt;code&gt;render.yaml&lt;/code&gt; that sets &lt;code&gt;runtime: python&lt;/code&gt; and &lt;code&gt;startCommand: gunicorn -w 1 -b 0.0.0.0:$PORT app:app&lt;/code&gt;, connected as a Blueprint, with the API key set as an environment variable. Render's free tier gives 750 hours a month and sleeps after 15 idle minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which news API is free?&lt;/strong&gt;&lt;br&gt;
A free news API plan exists at most providers, with limits: NewsAPI's free plan works on localhost only, APITube's free plan works from a server but returns results 12 hours late, at 10 requests a minute and 10 articles a page. With responses cached for five minutes, those limits are workable for a small site.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How do you hide an API key in Flask?&lt;/strong&gt;&lt;br&gt;
An API key stays out of Flask code when it is read with &lt;code&gt;os.environ["APITUBE_API_KEY"]&lt;/code&gt;, exported in the shell locally, set in the host's dashboard in production, and never committed. Restricting the key by IP or domain on the API side makes a leaked key useless anywhere else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this leaves you
&lt;/h2&gt;

&lt;p&gt;A Flask news app that survives deployment is the recipe plus three things: the key in the environment, the API call behind a five-minute cache, and a thread that refills the cache so the 10-second category query happens off the request path. That turned 30 views into 1 API call and a 10.6 s page into a cached read, and it runs on a free key and a free host. The next step that changes the picture is a second gunicorn worker, at which point the dict becomes Redis.&lt;/p&gt;

&lt;p&gt;APITube is one of the APIs mentioned here, and it is the one the code targets. Free tier at &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;apitube.io&lt;/a&gt;; the free key is enough to run everything in this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Code and data for this post: &lt;code&gt;app.py&lt;/code&gt;, &lt;code&gt;templates/index.html&lt;/code&gt;, &lt;code&gt;render.yaml&lt;/code&gt;, &lt;code&gt;data.csv&lt;/code&gt;, &lt;code&gt;plot.py&lt;/code&gt; in the images folder next to this article&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://flask.palletsprojects.com/en/stable/quickstart/" rel="noopener noreferrer"&gt;Flask quickstart&lt;/a&gt;, &lt;a href="https://docs.gunicorn.org/en/stable/run.html" rel="noopener noreferrer"&gt;gunicorn docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://render.com/docs/free" rel="noopener noreferrer"&gt;Render free tier&lt;/a&gt;, &lt;a href="https://fly.io/docs/about/pricing/" rel="noopener noreferrer"&gt;Fly.io pricing&lt;/a&gt;, &lt;a href="https://www.pythonanywhere.com/pricing/" rel="noopener noreferrer"&gt;PythonAnywhere plans&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;APITube docs: &lt;a href="https://docs.apitube.io/platform/news-api/top-headlines" rel="noopener noreferrer"&gt;top headlines&lt;/a&gt;, &lt;a href="https://docs.apitube.io/platform/news-api/everything" rel="noopener noreferrer"&gt;search&lt;/a&gt;, &lt;a href="https://docs.apitube.io/platform/news-api/rate-limits" rel="noopener noreferrer"&gt;rate limits and plans&lt;/a&gt;, &lt;a href="https://docs.apitube.io/platform/news-api/list-of-categories" rel="noopener noreferrer"&gt;categories&lt;/a&gt;, &lt;a href="https://docs.apitube.io/platform/news-api/integrations/python/flask-framework" rel="noopener noreferrer"&gt;Flask integration&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://newsapi.org/pricing" rel="noopener noreferrer"&gt;NewsAPI pricing&lt;/a&gt;, for the localhost-only note&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>flask</category>
      <category>python</category>
      <category>tutorial</category>
      <category>api</category>
    </item>
    <item>
      <title>A news app without a backend, in one HTML file</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Thu, 17 Sep 2026 17:40:22 +0000</pubDate>
      <link>https://dev.to/apitube/a-news-app-without-a-backend-in-one-html-file-2foj</link>
      <guid>https://dev.to/apitube/a-news-app-without-a-backend-in-one-html-file-2foj</guid>
      <description>&lt;h1&gt;
  
  
  A news app without a backend, in one HTML file
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;A news app without a backend&lt;/strong&gt; is a single HTML file that calls a news API from the browser with &lt;code&gt;fetch&lt;/code&gt;, renders the JSON, and is served by a static host such as GitHub Pages. No server of yours runs anywhere. Ours is 97 lines. The first headline lands 858 ms after page load, and a repeat visit in the same tab makes zero network requests because the response is cached in &lt;code&gt;sessionStorage&lt;/code&gt; for 15 minutes.&lt;/p&gt;

&lt;p&gt;The part that most "news app with vanilla JS" tutorials skip: the API key ships to the browser, where anyone can read it from the network tab. So this tutorial spends as much time on the key as on the DOM. There are three ways to handle it, and the right one depends on one number: how many requests per day you can afford. On a free plan that number is 100.&lt;/p&gt;

&lt;p&gt;Everything here was measured on 16 September 2026 with curl and headless Chrome. The CSV files behind every number sit next to the article.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: APITube is our product. We use it for the requests below because it answers browser calls from any origin; the file structure works with any JSON news API.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Asking for 7 fields instead of the full article object shrank a 10-headline response from 238 KB to 9.5 KB and cut the median request from 1.21 s to 0.85 s (15 runs each).&lt;/li&gt;
&lt;li&gt;Sending the key as an &lt;code&gt;X-API-Key&lt;/code&gt; header fails in Chrome today: the preflight reply carries &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; twice. The key in the query string works.&lt;/li&gt;
&lt;li&gt;A 100-requests-per-day free plan is gone after 100 first visits if the browser calls the API directly.&lt;/li&gt;
&lt;li&gt;A 35-line Cloudflare Worker with a 15-minute cache caps upstream calls at 96 per day, whatever the traffic. Cached responses came back in 3 ms locally, uncached in 656 ms.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Contents:&lt;/strong&gt; Step 1: get a key you can afford to expose · Step 2: the request, and two things that break in the browser · Step 3: index.html · Step 4: deploy to GitHub Pages · Step 5: when the key must stay secret, a 35-line Worker · FAQ&lt;/p&gt;

&lt;h2&gt;
  
  
  Who this is for
&lt;/h2&gt;

&lt;p&gt;This is for frontend beginners who know what &lt;code&gt;fetch&lt;/code&gt; and &lt;code&gt;querySelector&lt;/code&gt; do and want a deployable project, not a codepen. No build step, no npm, no framework. You need a GitHub account and an API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: get a key you can afford to expose
&lt;/h2&gt;

&lt;p&gt;Sign up at &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;apitube.io&lt;/a&gt;, open the dashboard, and decide which of these three keys you put in the file.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key model&lt;/th&gt;
&lt;th&gt;When it fits&lt;/th&gt;
&lt;th&gt;What limits the damage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Test key (&lt;code&gt;api_test_…&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Demos, portfolio, learning&lt;/td&gt;
&lt;td&gt;Consumes no quota; article text comes back truncated; stricter rate limit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live key with referrer and IP rules&lt;/td&gt;
&lt;td&gt;Real headlines, traffic under 100 requests/day&lt;/td&gt;
&lt;td&gt;Dashboard rules restrict the key to your domain and to one endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live key behind a Worker proxy&lt;/td&gt;
&lt;td&gt;Real headlines, more than 100 requests/day&lt;/td&gt;
&lt;td&gt;Key never leaves the Worker; the cache absorbs the traffic (Step 5)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The dashboard has a Test mode toggle at the bottom of the sidebar. Keys created there start with &lt;code&gt;api_test_&lt;/code&gt; and return the real response shape, so the file below works unchanged. For a live key, set the referrer rule to your GitHub Pages origin and the endpoint scope to &lt;code&gt;/v1/news/everything&lt;/code&gt; before you commit anything. Referrer rules are not a lock: a browser sends the &lt;code&gt;Referer&lt;/code&gt; header, curl sends whatever it likes. They stop drive-by abuse, not a determined person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: the request, and two things that break in the browser
&lt;/h2&gt;

&lt;p&gt;The request that powers the page, first in curl:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="s2"&gt;"https://api.apitube.io/v1/news/everything?language.code=en&amp;amp;is_duplicate=0&amp;amp;per_page=10&amp;amp;sort.by=published_at&amp;amp;sort.order=desc&amp;amp;fl=title,href,published_at,image,description,source.domain,source.favicon&amp;amp;api_key=YOUR_API_KEY"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One item from the &lt;code&gt;results&lt;/code&gt; array, exactly as returned:&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;"The AlleyWatch Startup Daily Funding Report: 9/16/2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"href"&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://alleywatch.com/2026/09/the-alleywatch-startup-daily-funding-report-9-16-2026/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published_at"&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-09-16T16:52:55.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;"image"&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://alleywatch.com/wp-content/uploads/2019/11/nyc_tech_startup_funding_daily-report_9-16.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The latest venture capital, seed, pre-seed, and angel deals for NYC startups for 9/16/2026 featuring funding details for Stuut, Evvy, Footprint, Tare, and much more. This page will be updated throughout the. . ."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&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;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alleywatch.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"favicon"&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.google.com/s2/favicons?domain=https://alleywatch.com"&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 &lt;code&gt;fl&lt;/code&gt; parameter is the field list. Without it, each article arrives with body, body HTML, entities, sentiment, and 25 other keys the page never reads. We measured both variants 15 times each with curl:&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%2Fh5ea3p932ioc09ekawjf.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%2Fh5ea3p932ioc09ekawjf.png" alt="Ten headlines weigh 238 KB with the full article object and 9.5 KB with seven fields; the median request drops from 1.21 s to 0.85 s" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Variant&lt;/th&gt;
&lt;th&gt;Response size&lt;/th&gt;
&lt;th&gt;Median time&lt;/th&gt;
&lt;th&gt;p90 time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full article objects, 10 items&lt;/td&gt;
&lt;td&gt;238 KB&lt;/td&gt;
&lt;td&gt;1.21 s&lt;/td&gt;
&lt;td&gt;1.56 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;fl=&lt;/code&gt; with 7 fields, 10 items&lt;/td&gt;
&lt;td&gt;9.5 KB&lt;/td&gt;
&lt;td&gt;0.85 s&lt;/td&gt;
&lt;td&gt;1.23 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things bit us before the page rendered anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;is_duplicate=false&lt;/code&gt; is a 400.&lt;/strong&gt; The API wants &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;1&lt;/code&gt; and answers &lt;code&gt;ER0003 "Invalid is_duplicate value."&lt;/code&gt; otherwise. The page shows that code in its status line instead of a generic "something went wrong", because the error body is JSON with an &lt;code&gt;errors[0].code&lt;/code&gt; and &lt;code&gt;errors[0].message&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The key cannot go in a header.&lt;/strong&gt; A custom header such as &lt;code&gt;X-API-Key&lt;/code&gt; turns the request into a preflighted one, and the API's reply to the &lt;code&gt;OPTIONS&lt;/code&gt; preflight currently carries &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; twice. Chrome refuses that with "contains multiple values '*, *'" and the fetch never happens. A plain &lt;code&gt;GET&lt;/code&gt; with the key in the query string is a simple request, gets a single &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;, and works from any origin. That is what the file does. The same test with &lt;code&gt;Authorization: Bearer&lt;/code&gt; fails the same way. We reported it; check the response headers before assuming it is still true when you read this.&lt;/p&gt;

&lt;p&gt;One more limit: the API does not send &lt;code&gt;Access-Control-Expose-Headers&lt;/code&gt;, so the page cannot read &lt;code&gt;X-RateLimit-Remaining&lt;/code&gt;. If you want a counter, count your own requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: index.html
&lt;/h2&gt;

&lt;p&gt;Replace &lt;code&gt;YOUR_API_KEY&lt;/code&gt;, open the file, done. Ninety-seven lines including CSS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Headlines&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"icon"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"data:,"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;720px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#111&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fafafa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;header&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;flex-wrap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;h1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.4rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;input&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8px&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#bbb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;#status&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#666&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.9rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;grid-template-columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;96px&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="n"&gt;fr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-top&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="nb"&gt;solid&lt;/span&gt; &lt;span class="m"&gt;#e5e5e5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;96px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;72px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;object-fit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cover&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#eee&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="nt"&gt;h2&lt;/span&gt; &lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0254EC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;text-decoration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="nt"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;.9rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#444&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="nt"&gt;small&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#777&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;align-items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;center&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;article&lt;/span&gt; &lt;span class="nt"&gt;small&lt;/span&gt; &lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;14px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Headlines&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"q"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Search titles…"&lt;/span&gt; &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Search titles"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"go"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Search&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"refresh"&lt;/span&gt; &lt;span class="na"&gt;title=&lt;/span&gt;&lt;span class="s"&gt;"Refresh"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;↻&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Loading…&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="c1"&gt;// Option A: a key you accept being public (a test key, or a live key with&lt;/span&gt;
&lt;span class="c1"&gt;// referrer rules). Option B: leave API_KEY empty and point PROXY_URL at a&lt;/span&gt;
&lt;span class="c1"&gt;// Worker that holds the real key.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;YOUR_API_KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PROXY_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                 &lt;span class="c1"&gt;// e.g. "https://news-proxy.you.workers.dev"&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ENDPOINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;PROXY_URL&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.apitube.io/v1/news/everything&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CACHE_TTL_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&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;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// free plan: 100 requests/day&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title,href,published_at,image,description,source.domain,source.favicon&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadNews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;language.code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;is_duplicate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;per_page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sort.by&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;published_at&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sort.order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FIELDS&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// The key goes in the query string, not in an X-API-Key header: a custom&lt;/span&gt;
  &lt;span class="c1"&gt;// header forces a CORS preflight, and the API's preflight reply currently&lt;/span&gt;
  &lt;span class="c1"&gt;// sends Access-Control-Allow-Origin twice, which Chrome rejects.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api_key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;null&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;Date&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="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;CACHE_TTL_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&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="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}))).&lt;/span&gt;&lt;span class="nx"&gt;errors&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="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`HTTP &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&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="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;t&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&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="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;esc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;&amp;amp;&amp;lt;&amp;gt;"'&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;lt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;quot;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;#39;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ago&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iso&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nb"&gt;Date&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="o"&gt;-&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;iso&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; min ago`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&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="s2"&gt; h ago`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`
    &amp;lt;article&amp;gt;
      &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;img src="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;" alt="" loading="lazy" onerror="this.style.visibility='hidden'"&amp;gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
      &amp;lt;div&amp;gt;
        &amp;lt;h2&amp;gt;&amp;lt;a href="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;" target="_blank" rel="noopener"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/a&amp;gt;&amp;lt;/h2&amp;gt;
        &amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;
        &amp;lt;small&amp;gt;&amp;lt;img src="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;favicon&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;" alt=""&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; · &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;ago&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/small&amp;gt;
      &amp;lt;/div&amp;gt;
    &amp;lt;/article&amp;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="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; stories · updated &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toLocaleTimeString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Loading…&lt;/span&gt;&lt;span class="dl"&gt;"&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="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;loadNews&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;go&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;onkeydown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Enter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;refresh&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;onclick&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;q&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fkz4yafy9uu7ev758r0ra.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%2Fkz4yafy9uu7ev758r0ra.png" alt="The page in headless Chrome: ten headlines with thumbnails, source favicon and " width="799" height="524"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four decisions worth knowing about:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every value goes through &lt;code&gt;esc()&lt;/code&gt;.&lt;/strong&gt; Headlines are untrusted input. A title containing &lt;code&gt;&amp;lt;img onerror=…&amp;gt;&lt;/code&gt; would otherwise run in your page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cache key is the full URL.&lt;/strong&gt; Search for "tesla" and you get a separate 15-minute bucket, so a second search for the same word costs nothing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images fail silently.&lt;/strong&gt; In our run 8 of 10 stories carried an image and 5 of those 8 loaded. The rest were blocked by the publisher's &lt;code&gt;Cross-Origin-Resource-Policy&lt;/code&gt; header. &lt;code&gt;onerror&lt;/code&gt; hides the grey box instead of showing a broken icon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Errors are the API's words.&lt;/strong&gt; A 429 shows &lt;code&gt;429 ER0203: …&lt;/code&gt; in the status line, which tells you it is the rate limit and not your code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On the free plan the description is a 200-character preview, &lt;code&gt;per_page&lt;/code&gt; maxes out at 10, and articles arrive with a 12-hour delay. The "4 min ago" in the screenshot comes from a paid key; a free key shows "12 h ago" on everything. Say so in your UI or people will think the clock is broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: deploy to GitHub Pages
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Create a repository, add &lt;code&gt;index.html&lt;/code&gt;, push.&lt;/li&gt;
&lt;li&gt;Settings → Pages → Source: "Deploy from a branch", branch &lt;code&gt;main&lt;/code&gt;, folder &lt;code&gt;/ (root)&lt;/code&gt;. Save.&lt;/li&gt;
&lt;li&gt;Wait about a minute. The site is at &lt;code&gt;https://YOUR-USER.github.io/REPO/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you used a live key, add that origin to the key's referrer rule now.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is where most vanilla JS news app tutorials quietly die. They are written against an API that only allows browser calls from &lt;code&gt;localhost&lt;/code&gt;, the page works during the tutorial, and the first deploy returns a CORS error. Unlike NewsAPI.org's Developer plan, which enables CORS for &lt;code&gt;localhost&lt;/code&gt; only, APITube's free plan answers every origin with &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt;, which means the same file keeps working after you deploy it. Check the free tier before you pick an API:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider, free tier&lt;/th&gt;
&lt;th&gt;Browser calls from a deployed site&lt;/th&gt;
&lt;th&gt;Delay&lt;/th&gt;
&lt;th&gt;Requests/day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;NewsAPI.org Developer&lt;/td&gt;
&lt;td&gt;No: "CORS enabled for localhost" only&lt;/td&gt;
&lt;td&gt;24 h&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;APITube Free&lt;/td&gt;
&lt;td&gt;Yes: &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; on every GET (measured)&lt;/td&gt;
&lt;td&gt;12 h&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A second thing to know about GitHub Pages: it serves files as-is. There is no environment variable, no server-side include, nowhere to hide a string. That is why Step 1 exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: when the key must stay secret, a 35-line Worker
&lt;/h2&gt;

&lt;p&gt;The free plan allows 100 requests a day. In direct mode every visitor's first load is one request, so 100 visitors empty the quota and visitor 101 sees &lt;code&gt;429 ER0203&lt;/code&gt;. The way out is a proxy that holds the key and caches the answer. Cloudflare Workers is the usual choice because the free tier covers it and the Cache API needs no configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// worker.js — holds the real key; the page calls this URL instead of the API.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_ORIGINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://YOUR-USER.github.io&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:8000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALLOWED_PARAMS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;language.code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;is_duplicate&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;per_page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;page&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sort.by&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sort.order&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CACHE_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;900&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// seconds. 86400 / 900 = 96 upstream calls a day, whatever the traffic.&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_ORIGINS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;forbidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Access-Control-Allow-Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Vary&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Origin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OPTIONS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;method not allowed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;405&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cors&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.apitube.io/v1/news/everything&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;searchParams&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="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_PARAMS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;HIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;hit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MISS&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-API-Key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;APITUBE_API_KEY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Cache-Control&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`public, max-age=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CACHE_TTL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;waitUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;caches&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clone&lt;/span&gt;&lt;span class="p"&gt;()));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cors&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-Proxy-Cache&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put the key in a secret (&lt;code&gt;wrangler secret put APITUBE_API_KEY&lt;/code&gt;, or &lt;code&gt;.dev.vars&lt;/code&gt; locally), set &lt;code&gt;PROXY_URL&lt;/code&gt; in &lt;code&gt;index.html&lt;/code&gt; to the Worker URL and &lt;code&gt;API_KEY&lt;/code&gt; to an empty string. The Worker rebuilds the upstream URL from an allowlist of parameters, so a visitor cannot smuggle &lt;code&gt;api_key=&lt;/code&gt; or an unrelated endpoint through it. The &lt;code&gt;Cache-Control&lt;/code&gt; override matters: the API answers with &lt;code&gt;no-store&lt;/code&gt;, and the Cache API refuses to store that.&lt;/p&gt;

&lt;p&gt;We ran it with &lt;code&gt;wrangler dev&lt;/code&gt; on a laptop rather than deploying to a Cloudflare account, so the timings are local:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Median time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Allowed origin, first call&lt;/td&gt;
&lt;td&gt;200, &lt;code&gt;X-Proxy-Cache: MISS&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;656 ms (12 different searches)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Allowed origin, repeat&lt;/td&gt;
&lt;td&gt;200, &lt;code&gt;X-Proxy-Cache: HIT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;3 ms (12 runs)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown origin, or no &lt;code&gt;Origin&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;403&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;405&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;?api_key=stolen&lt;/code&gt; appended&lt;/td&gt;
&lt;td&gt;200, parameter dropped&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In headless Chrome the page showed its first headline 45 ms after load through the cached proxy, against 858 ms straight to the API.&lt;/p&gt;

&lt;p&gt;The budget works out like this. With a 900-second TTL the default feed costs at most 96 upstream requests a day. Every distinct search term is its own cache entry with its own 96, so a search box on a free plan still needs a ceiling: cache longer, or only allow searches from a fixed list. Below 100 visitors a day, direct mode with a referrer-restricted key is fine and you skip the Worker entirely.&lt;/p&gt;

&lt;p&gt;That is the whole decision. One HTML file is enough for the app; the number of requests a day you can afford decides whether the key sits in that file, in a referrer rule, or behind 35 lines of Worker. Copy &lt;code&gt;index.html&lt;/code&gt;, pick the row of the table in Step 1 that matches your traffic, and push.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;How do you hide an API key in JavaScript on a static site?&lt;/strong&gt;&lt;br&gt;
An API key in client-side JavaScript cannot be hidden, because anything the browser sends the visitor can read in the network tab. The two working options are a key that is safe to expose (a test key, or a live key restricted to your domain and one endpoint) and a proxy such as a Cloudflare Worker that holds the real key and answers the page instead of the API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a plain HTML file call a news API with &lt;code&gt;fetch&lt;/code&gt;?&lt;/strong&gt;&lt;br&gt;
A plain HTML file can call a news API with &lt;code&gt;fetch&lt;/code&gt; when the API sends &lt;code&gt;Access-Control-Allow-Origin&lt;/code&gt; for your origin and the request stays simple: &lt;code&gt;GET&lt;/code&gt;, no custom headers, the key in the query string. A custom auth header triggers a preflight, and for this API the preflight reply is currently malformed, so the header route fails in Chrome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Which news API works from the browser on a free plan?&lt;/strong&gt;&lt;br&gt;
The news API that works from a deployed browser page on a free plan is one that sends &lt;code&gt;Access-Control-Allow-Origin: *&lt;/code&gt; on every GET; APITube's free plan does, at 100 requests a day, 10 per minute, with a 12-hour delay on articles. NewsAPI.org's Developer plan restricts CORS to &lt;code&gt;localhost&lt;/code&gt;, so it fails once deployed.&lt;/p&gt;

&lt;p&gt;APITube is the API we used here; there is a free tier at &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;apitube.io&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.apitube.io/platform/news-api/authentication" rel="noopener noreferrer"&gt;APITube docs: authentication, test keys, referrer rules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.apitube.io/platform/news-api/rate-limits" rel="noopener noreferrer"&gt;APITube docs: rate limits and plan ceilings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.apitube.io/platform/news-api/everything" rel="noopener noreferrer"&gt;APITube docs: /v1/news/everything parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;MDN: CORS, simple requests and preflight&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.cloudflare.com/workers/runtime-apis/cache/" rel="noopener noreferrer"&gt;Cloudflare Workers: Cache API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site" rel="noopener noreferrer"&gt;GitHub Pages: publishing from a branch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://newsapi.org/pricing" rel="noopener noreferrer"&gt;NewsAPI.org pricing (Developer plan: CORS for localhost)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Measurements: &lt;code&gt;images/1-news-app-without-backend-one-html-file/data.csv&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A week of NVIDIA news is 5,718 articles. My filter kept 161</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Wed, 16 Sep 2026 04:55:08 +0000</pubDate>
      <link>https://dev.to/apitube/a-week-of-nvidia-news-is-5718-articles-my-filter-kept-161-3ppp</link>
      <guid>https://dev.to/apitube/a-week-of-nvidia-news-is-5718-articles-my-filter-kept-161-3ppp</guid>
      <description>&lt;p&gt;I wanted a small script that pings me when something real happens to NVIDIA — an earnings surprise, an export-control change, a leadership move. Not a feed. A handful of lines a day I can actually read.&lt;/p&gt;

&lt;p&gt;The first version was fifteen lines and it was wrong in a way I couldn't see from the output. So I pulled a full week of coverage three different ways and counted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A company news watcher is a filter chain, not a query.&lt;/strong&gt; Over seven days (2026-08-30 to 2026-09-05) there were &lt;strong&gt;5,718 articles mentioning NVIDIA&lt;/strong&gt; — about 817 a day. Five filtering stages got that to &lt;strong&gt;161 alerts, or 23 a day&lt;/strong&gt;. This post is the measurements behind each stage, and the finished script.&lt;/p&gt;

&lt;p&gt;This is for developers who want a company news feed they can trust, and analysts who need to know what their alerts are silently dropping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the week showed, in four lines:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Searching headlines for the company name finds &lt;strong&gt;29.8%&lt;/strong&gt; of the coverage that mentions it.&lt;/li&gt;
&lt;li&gt;Entity resolution alone isn't a fix — it missed &lt;strong&gt;180 articles&lt;/strong&gt; that had "nvidia" in the headline.&lt;/li&gt;
&lt;li&gt;Only &lt;strong&gt;31.6%&lt;/strong&gt; of the coverage is in English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12.5%&lt;/strong&gt; of what survives filtering is the same story republished under a near-identical headline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Measured 2026-09-06 against the APITube news API, over articles published 2026-08-30 to 2026-09-05. The pull script (&lt;code&gt;pull.py&lt;/code&gt;) and the resulting counts (&lt;code&gt;data.csv&lt;/code&gt;) sit alongside this post.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fifteen-line version
&lt;/h2&gt;

&lt;p&gt;Start with the obvious thing: search headlines for the company name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$APITUBE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.apitube.io/v1/news/everything?title=nvidia&amp;amp;per_page=2&amp;amp;published_at.start=2026-08-30&amp;amp;published_at.end=2026-09-05"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trimmed to the fields that matter, one article comes back like this:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3078309425&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;"Nvidia earnings highlight remarkable growth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published_at"&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-09-02T17:56:16.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;"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;"source"&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;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"proactiveadvisormagazine.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"news"&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;"entities"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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;"Nvidia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"frequency"&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;"sentiment"&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;"score"&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.46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"polarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"positive"&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;"metadata"&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;"aliases"&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;"NVIDIA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nVidia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NVDA"&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_public_entity"&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;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;"sentiment"&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;"overall"&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;"score"&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.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;"polarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"positive"&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;"is_duplicate"&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;"story"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3078309425&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;Two fields decide everything later: &lt;code&gt;entities[].frequency&lt;/code&gt; (how many times the company is actually named in the piece) and &lt;code&gt;entities[].id&lt;/code&gt; (the canonical company, independent of spelling).&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline query finds 30% of the coverage
&lt;/h2&gt;

&lt;p&gt;Here is the part that made me throw away version one. I ran the same week twice — once filtering on the headline, once on the canonical entity id.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Unique articles, 7 days&lt;/th&gt;
&lt;th&gt;Share of entity-matched coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title=nvidia&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,831&lt;/td&gt;
&lt;td&gt;29.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;entity.id=1280220&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5,538&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;union of both&lt;/td&gt;
&lt;td&gt;5,718&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The headline query misses &lt;strong&gt;3,890 articles&lt;/strong&gt; — a supplier's export licence, a hyperscaler's capex call, a competitor's benchmark. Those are the stories that move a position, and they rarely put "NVIDIA" in the headline.&lt;/p&gt;

&lt;p&gt;The obvious fix is to drop the headline query and use entity resolution alone. That is also wrong. &lt;strong&gt;180 articles had "nvidia" in the headline and no NVIDIA entity extracted at all&lt;/strong&gt; — 9.8% of the headline set. Named-entity recognition misses things too, especially on smaller and non-English outlets.&lt;/p&gt;

&lt;p&gt;Neither filter is a superset of the other, so the watcher queries both and unions the results. Unlike a single-strategy filter, the union costs one extra request per window, which means you stop choosing between the two failure modes and just pay for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two thirds of it isn't in English
&lt;/h2&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%2F2fq7zw8oxacwfh1z0wdk.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%2F2fq7zw8oxacwfh1z0wdk.png" alt="Only 31.6% of NVIDIA coverage is English; German 9.4%, Spanish 7.4%, Chinese 6.4%, French 5.7%" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Language of all 5,718 articles mentioning NVIDIA over the seven days, from the &lt;code&gt;language&lt;/code&gt; field on each article.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Of those 5,718 articles, &lt;strong&gt;1,806 were in English — 31.6%&lt;/strong&gt;. German was 9.4%, Spanish 7.4%, Chinese 6.4%, French 5.7%, Italian 4.9%, Japanese 4.2%, Portuguese 4.1%.&lt;/p&gt;

&lt;p&gt;If you never set a language filter, you don't get a broader watcher — you get a mostly-unreadable one, and your theme keywords (written in English) silently fail against it. If you do set &lt;code&gt;language=en&lt;/code&gt;, be honest that you just dropped two thirds of the world's coverage. I set it, because I can't read the rest. That's a choice, not a default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five stages, with the numbers
&lt;/h2&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%2Fznjw00pwrk6uir7ym26b.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%2Fznjw00pwrk6uir7ym26b.png" alt="Five filter stages cut 5,718 articles a week to 161: English 1,806, headline plus 2+ mentions 679, dedup 594, theme match 161" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Each stage applied in order to the same 7-day pull. Counts are articles, not alerts sent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each stage below is one rule with one threshold, applied in order.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Articles / 7 days&lt;/th&gt;
&lt;th&gt;Per day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Everything mentioning NVIDIA (union of both queries)&lt;/td&gt;
&lt;td&gt;5,718&lt;/td&gt;
&lt;td&gt;817&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;language == "en"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,806&lt;/td&gt;
&lt;td&gt;258&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;NVIDIA in the headline &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;frequency &amp;gt;= 2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;679&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Near-duplicates removed&lt;/td&gt;
&lt;td&gt;594&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Matched a watch theme&lt;/td&gt;
&lt;td&gt;161&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stage 3 is the one worth explaining. Only &lt;strong&gt;32.2% of entity-matched articles have NVIDIA in the headline&lt;/strong&gt;; the other two thirds mention it in passing — a monitor review, a fund's holdings list, an unrelated AI story name-checking the chip. Requiring the headline &lt;em&gt;and&lt;/em&gt; at least two mentions in the body is a cheap proxy for "this article is about NVIDIA", and it cut 258/day to 97/day.&lt;/p&gt;

&lt;p&gt;Stage 4 removed 12.5% of what survived stage 3. Those 924 source domains republish each other constantly — one acquisition story appeared eight times under near-identical headlines. Normalising the title to a sorted set of its long words catches these without any similarity library.&lt;/p&gt;

&lt;p&gt;Stage 5 splits the remainder into themes, which is also the alert budget: earnings 11.4/day, leadership 10.0/day, legal 2.6/day, export controls 2.1/day. Those sum to more than 23 because one article can match two themes — an earnings call that announces a CFO change lands in both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The watcher
&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;json&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;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;API&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.apitube.io/v1/news/everything&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;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;APITUBE_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;NVIDIA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1280220&lt;/span&gt;

&lt;span class="n"&gt;THEMES&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;earnings&lt;/span&gt;&lt;span class="sh"&gt;"&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;earnings|revenue|quarter|guidance|\beps\b|results|forecast&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;leadership&lt;/span&gt;&lt;span class="sh"&gt;"&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;\bceo\b|jensen huang|\bcfo\b|resign|steps down|appoint&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;export&lt;/span&gt;&lt;span class="sh"&gt;"&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;export|sanction|china|\bban\b|restrict|licen[cs]e|blackwell|tariff&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;legal&lt;/span&gt;&lt;span class="sh"&gt;"&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;lawsuit|sue[sd]?\b|antitrust|probe|investigat|settle|court&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;THEME_RE&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="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;THEMES&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;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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;url&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&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;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&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="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&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;X-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;KEY&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-Agent&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;nvidia-watch/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# urllib's default UA gets a 403
&lt;/span&gt;    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;60&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;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&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;meta&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="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;warnings&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARNING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# a typo'd filter is ignored, not rejected
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mentions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;article&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;entities&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;e&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="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;entity_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;e&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;frequency&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="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;words&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;[^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="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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&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="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&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;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="k"&gt;if&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;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&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;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&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;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&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;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity.id&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;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                        &lt;span class="c1"&gt;# stage 1: union
&lt;/span&gt;        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;q&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;published_at.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;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;published_at.end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;end&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&lt;/span&gt;&lt;span class="sh"&gt;"&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;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity.id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;q&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;data&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;results&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;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;rows&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;a&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;language&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;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                              &lt;span class="c1"&gt;# stage 2
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&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="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;title&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;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="c1"&gt;# stage 3a
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;mentions&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="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                 &lt;span class="c1"&gt;# stage 3b
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalise&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;title&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;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                &lt;span class="c1"&gt;# stage 4
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;blob&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&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;title&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="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;a&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;description&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="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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;THEME_RE&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;blob&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                                  &lt;span class="c1"&gt;# stage 5
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;out&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&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;has_next_pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&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="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NVIDIA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nvidia&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-08-30&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-09-05&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="se"&gt;\n&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;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; alerts over 7 days (&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;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&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;/day)&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="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="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&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;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;THEMES&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="k"&gt;if&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;search&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="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;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;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;a&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;description&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="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;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="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;theme&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&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;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;published_at&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;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;  &lt;/span&gt;&lt;span class="si"&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;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="mi"&gt;70&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;Standard library only. Running it just now returned 153 rather than the 161 in my archived pull — the index keeps ingesting, so a re-run of a past window is close but not byte-identical. Worth knowing before you write a test that asserts an exact count.&lt;/p&gt;

&lt;p&gt;Piping it into Telegram is four more lines and a bot token; the filtering is the part that was hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five things that cost me time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;There is no ticker filter.&lt;/strong&gt; &lt;code&gt;ticker=NVDA&lt;/code&gt; returns HTTP 200 and quietly ignores you. NVDA lives in &lt;code&gt;entities[].metadata.aliases&lt;/code&gt; alongside &lt;code&gt;is_public_entity: true&lt;/code&gt;, so resolve ticker → entity id once, store the map, and filter on the id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;organization.name&lt;/code&gt; is case-sensitive.&lt;/strong&gt; &lt;code&gt;organization.name=Nvidia&lt;/code&gt; works. &lt;code&gt;organization.name=NVIDIA&lt;/code&gt; returns HTTP 400, &lt;code&gt;ER0220&lt;/code&gt;, "entity organization name 'NVIDIA' not found." The all-caps spelling most people type is the one that fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unknown parameters are warnings, not errors.&lt;/strong&gt; &lt;code&gt;entity.name&lt;/code&gt; and &lt;code&gt;ticker&lt;/code&gt; both come back 200 with &lt;code&gt;meta.warnings[].code == "ER0368"&lt;/code&gt; and &lt;em&gt;unfiltered&lt;/em&gt; results. A typo doesn't crash your watcher, it floods it. Print &lt;code&gt;meta.warnings&lt;/code&gt; — that's the two lines in &lt;code&gt;fetch()&lt;/code&gt; above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headline search defaults to a 31-day window&lt;/strong&gt; (&lt;code&gt;ER0366&lt;/code&gt;) if you don't pass &lt;code&gt;published_at.start&lt;/code&gt; and &lt;code&gt;published_at.end&lt;/code&gt;. Your "last 24 hours" alert is quietly a month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python's default User-Agent gets a 403.&lt;/strong&gt; &lt;code&gt;urllib&lt;/code&gt; sends &lt;code&gt;Python-urllib/3.x&lt;/code&gt; and the edge rejects it. Set any real User-Agent and it works — this cost me twenty minutes of blaming my API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I get news alerts for a specific company?
&lt;/h3&gt;

&lt;p&gt;The reliable way to get news alerts for a specific company is a five-stage filter chain rather than a single query, because no single filter is both precise and complete. Query the company's canonical entity id, union it with a headline keyword search, then filter by language, require the company in the headline with two or more body mentions, drop near-duplicate titles, and match against theme keywords. In this measurement that chain reduced 817 articles a day to 23.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do my company news alerts return irrelevant articles?
&lt;/h3&gt;

&lt;p&gt;Company news alerts return irrelevant articles because a mention is not a subject. Only 32.2% of articles that mention NVIDIA put it in the headline; the rest are passing references in unrelated stories. Requiring a headline match plus &lt;code&gt;frequency &amp;gt;= 2&lt;/code&gt; removes most of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I filter news by ticker symbol?
&lt;/h3&gt;

&lt;p&gt;No — this API has no &lt;code&gt;ticker&lt;/code&gt; parameter, and passing one is silently ignored rather than rejected. The ticker appears inside &lt;code&gt;entities[].metadata.aliases&lt;/code&gt;, so build your own ticker-to-entity-id table once and filter on &lt;code&gt;entity.id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you deduplicate news articles from multiple sources?
&lt;/h3&gt;

&lt;p&gt;The cheapest way to deduplicate news articles across sources is normalised-headline matching, because syndicated copies keep the same words while rewording punctuation and framing. Normalise each headline to a sorted set of its words longer than three characters and treat a repeat as a duplicate. Across 924 source domains that removed 12.5% of the surviving articles, with no similarity library.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is entity resolution enough on its own?
&lt;/h3&gt;

&lt;p&gt;No — entity resolution alone is not enough, because entity extraction has its own recall gap. It missed 180 articles that had the company name in the headline — 9.8% of the headline-matched set. Query both ways and union the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it yours
&lt;/h2&gt;

&lt;p&gt;To point this at another company:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find the entity id.&lt;/strong&gt; Pull any article that mentions the company and read &lt;code&gt;entities[]&lt;/code&gt; — the id, the aliases and &lt;code&gt;is_public_entity&lt;/code&gt; are all in there. Store the mapping; it doesn't change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite &lt;code&gt;THEMES&lt;/code&gt;.&lt;/strong&gt; The stage thresholds transfer between companies, the theme regexes don't — "export controls" matters for a chipmaker and means nothing for a bank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-measure the funnel.&lt;/strong&gt; Count what each stage drops for &lt;em&gt;your&lt;/em&gt; company before you trust the output. A quiet mid-cap won't need stage 5 at all; a bank will need a different stage 3.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mine only looked reasonable after I counted what it threw away.&lt;/p&gt;

&lt;p&gt;Disclosure: I work on APITube, which is the API in the code above — free tier at &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;apitube.io&lt;/a&gt;. The measurement approach works against any news API that exposes entity ids and per-article mention counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.apitube.io" rel="noopener noreferrer"&gt;APITube news API documentation&lt;/a&gt; — endpoints, parameters, response schema&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.google.com/alerts" rel="noopener noreferrer"&gt;Google Alerts&lt;/a&gt; — the no-code baseline: no API, no deduplication, no entity resolution&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://perigon.io/blog/monitor-nvidia-news" rel="noopener noreferrer"&gt;Perigon's NVIDIA monitoring guide&lt;/a&gt; — good on entity resolution as a concept, no code or numbers&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>tutorial</category>
      <category>fintech</category>
    </item>
    <item>
      <title>A week of NVIDIA news is 5,718 articles. Our filter kept 161</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Tue, 08 Sep 2026 17:37:09 +0000</pubDate>
      <link>https://dev.to/apitube/a-week-of-nvidia-news-is-5718-articles-our-filter-kept-161-1eah</link>
      <guid>https://dev.to/apitube/a-week-of-nvidia-news-is-5718-articles-our-filter-kept-161-1eah</guid>
      <description>&lt;p&gt;We wanted a small script that pings us when something real happens to NVIDIA — an earnings surprise, an export-control change, a leadership move. Not a feed. A handful of lines a day we can actually read.&lt;/p&gt;

&lt;p&gt;The first version was fifteen lines and it was wrong in a way we couldn't see from the output. So we pulled a full week of coverage three different ways and counted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A company news watcher is a filter chain, not a query.&lt;/strong&gt; Over seven days (2026-08-30 to 2026-09-05) there were &lt;strong&gt;5,718 articles mentioning NVIDIA&lt;/strong&gt; — about 817 a day. Five filtering stages got that to &lt;strong&gt;161 alerts, or 23 a day&lt;/strong&gt;. This post is the measurements behind each stage, and the finished script.&lt;/p&gt;

&lt;p&gt;This is for developers who want a company news feed they can trust, and analysts who need to know what their alerts are silently dropping.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the week showed, in four lines:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Searching headlines for the company name finds &lt;strong&gt;29.8%&lt;/strong&gt; of the coverage that mentions it.&lt;/li&gt;
&lt;li&gt;Entity resolution alone isn't a fix — it missed &lt;strong&gt;180 articles&lt;/strong&gt; that had "nvidia" in the headline.&lt;/li&gt;
&lt;li&gt;Only &lt;strong&gt;31.6%&lt;/strong&gt; of the coverage is in English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;12.5%&lt;/strong&gt; of what survives filtering is the same story republished under a near-identical headline.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Measured 2026-09-06 against the APITube news API, over articles published 2026-08-30 to 2026-09-05. The pull script (&lt;code&gt;pull.py&lt;/code&gt;) and the resulting counts (&lt;code&gt;data.csv&lt;/code&gt;) sit alongside this post.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The fifteen-line version
&lt;/h2&gt;

&lt;p&gt;Start with the obvious thing: search headlines for the company name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: &lt;/span&gt;&lt;span class="nv"&gt;$APITUBE_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://api.apitube.io/v1/news/everything?title=nvidia&amp;amp;per_page=2&amp;amp;published_at.start=2026-08-30&amp;amp;published_at.end=2026-09-05"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Trimmed to the fields that matter, one article comes back like this:&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3078309425&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;"Nvidia earnings highlight remarkable growth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"published_at"&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-09-02T17:56:16.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;"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;"source"&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;"domain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"proactiveadvisormagazine.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"news"&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;"entities"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1280220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"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;"Nvidia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"organization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"frequency"&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;"sentiment"&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;"score"&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.46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"polarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"positive"&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;"metadata"&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;"aliases"&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;"NVIDIA"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nVidia"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NVDA"&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_public_entity"&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;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;"sentiment"&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;"overall"&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;"score"&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.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;"polarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"positive"&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;"is_duplicate"&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;"story"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3078309425&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;Two fields decide everything later: &lt;code&gt;entities[].frequency&lt;/code&gt; (how many times the company is actually named in the piece) and &lt;code&gt;entities[].id&lt;/code&gt; (the canonical company, independent of spelling).&lt;/p&gt;

&lt;h2&gt;
  
  
  The headline query finds 30% of the coverage
&lt;/h2&gt;

&lt;p&gt;Here is the part that made us throw away version one. We ran the same week twice — once filtering on the headline, once on the canonical entity id.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Unique articles, 7 days&lt;/th&gt;
&lt;th&gt;Share of entity-matched coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;title=nvidia&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,831&lt;/td&gt;
&lt;td&gt;29.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;entity.id=1280220&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5,538&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;union of both&lt;/td&gt;
&lt;td&gt;5,718&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The headline query misses &lt;strong&gt;3,890 articles&lt;/strong&gt; — a supplier's export licence, a hyperscaler's capex call, a competitor's benchmark. Those are the stories that move a position, and they rarely put "NVIDIA" in the headline.&lt;/p&gt;

&lt;p&gt;The obvious fix is to drop the headline query and use entity resolution alone. That is also wrong. &lt;strong&gt;180 articles had "nvidia" in the headline and no NVIDIA entity extracted at all&lt;/strong&gt; — 9.8% of the headline set. Named-entity recognition misses things too, especially on smaller and non-English outlets.&lt;/p&gt;

&lt;p&gt;Neither filter is a superset of the other, so the watcher queries both and unions the results. Unlike a single-strategy filter, the union costs one extra request per window, which means you stop choosing between the two failure modes and just pay for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two thirds of it isn't in English
&lt;/h2&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%2Fp8sx5tjx2fvvnjfbbzxk.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%2Fp8sx5tjx2fvvnjfbbzxk.png" alt="Only 31.6% of NVIDIA coverage is English; German 9.4%, Spanish 7.4%, Chinese 6.4%, French 5.7%" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Language of all 5,718 articles mentioning NVIDIA over the seven days, from the &lt;code&gt;language&lt;/code&gt; field on each article.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Of those 5,718 articles, &lt;strong&gt;1,806 were in English — 31.6%&lt;/strong&gt;. German was 9.4%, Spanish 7.4%, Chinese 6.4%, French 5.7%, Italian 4.9%, Japanese 4.2%, Portuguese 4.1%.&lt;/p&gt;

&lt;p&gt;If you never set a language filter, you don't get a broader watcher — you get a mostly-unreadable one, and your theme keywords (written in English) silently fail against it. If you do set &lt;code&gt;language=en&lt;/code&gt;, be honest that you just dropped two thirds of the world's coverage. We set it, because we can't read the rest. That's a choice, not a default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five stages, with the numbers
&lt;/h2&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%2Ft07he8x1cb1yjx1mpjdr.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%2Ft07he8x1cb1yjx1mpjdr.png" alt="Five filter stages cut 5,718 articles a week to 161: English 1,806, headline plus 2+ mentions 679, dedup 594, theme match 161" width="799" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Each stage applied in order to the same 7-day pull. Counts are articles, not alerts sent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Each stage below is one rule with one threshold, applied in order.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Articles / 7 days&lt;/th&gt;
&lt;th&gt;Per day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Everything mentioning NVIDIA (union of both queries)&lt;/td&gt;
&lt;td&gt;5,718&lt;/td&gt;
&lt;td&gt;817&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;language == "en"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1,806&lt;/td&gt;
&lt;td&gt;258&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;NVIDIA in the headline &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;frequency &amp;gt;= 2&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;679&lt;/td&gt;
&lt;td&gt;97&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Near-duplicates removed&lt;/td&gt;
&lt;td&gt;594&lt;/td&gt;
&lt;td&gt;85&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Matched a watch theme&lt;/td&gt;
&lt;td&gt;161&lt;/td&gt;
&lt;td&gt;23&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Stage 3 is the one worth explaining. Only &lt;strong&gt;32.2% of entity-matched articles have NVIDIA in the headline&lt;/strong&gt;; the other two thirds mention it in passing — a monitor review, a fund's holdings list, an unrelated AI story name-checking the chip. Requiring the headline &lt;em&gt;and&lt;/em&gt; at least two mentions in the body is a cheap proxy for "this article is about NVIDIA", and it cut 258/day to 97/day.&lt;/p&gt;

&lt;p&gt;Stage 4 removed 12.5% of what survived stage 3. Those 924 source domains republish each other constantly — one acquisition story appeared eight times under near-identical headlines. Normalising the title to a sorted set of its long words catches these without any similarity library.&lt;/p&gt;

&lt;p&gt;Stage 5 splits the remainder into themes, which is also the alert budget: earnings 11.4/day, leadership 10.0/day, legal 2.6/day, export controls 2.1/day. Those sum to more than 23 because one article can match two themes — an earnings call that announces a CFO change lands in both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The watcher
&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;json&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;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="n"&gt;API&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.apitube.io/v1/news/everything&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;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;APITUBE_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;NVIDIA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1280220&lt;/span&gt;

&lt;span class="n"&gt;THEMES&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;earnings&lt;/span&gt;&lt;span class="sh"&gt;"&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;earnings|revenue|quarter|guidance|\beps\b|results|forecast&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;leadership&lt;/span&gt;&lt;span class="sh"&gt;"&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;\bceo\b|jensen huang|\bcfo\b|resign|steps down|appoint&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;export&lt;/span&gt;&lt;span class="sh"&gt;"&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;export|sanction|china|\bban\b|restrict|licen[cs]e|blackwell|tariff&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;legal&lt;/span&gt;&lt;span class="sh"&gt;"&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;lawsuit|sue[sd]?\b|antitrust|probe|investigat|settle|court&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;THEME_RE&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="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;THEMES&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;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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;per_page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;url&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API&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;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlencode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&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="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&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;X-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;KEY&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-Agent&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;nvidia-watch/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# urllib's default UA gets a 403
&lt;/span&gt;    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&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;60&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;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&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;meta&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="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;warnings&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WARNING&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# a typo'd filter is ignored, not rejected
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mentions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entity_id&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;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;article&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;entities&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;e&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="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;entity_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;e&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;frequency&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="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;words&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;[^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="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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&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="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&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;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;words&lt;/span&gt; &lt;span class="k"&gt;if&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;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;3&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;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&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;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&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;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity.id&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;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                        &lt;span class="c1"&gt;# stage 1: union
&lt;/span&gt;        &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;q&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;published_at.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;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;published_at.end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;end&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&lt;/span&gt;&lt;span class="sh"&gt;"&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;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;entity_id&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity.id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;q&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;data&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;results&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;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;rows&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;a&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;language&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;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                              &lt;span class="c1"&gt;# stage 2
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&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;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&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="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;title&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;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="c1"&gt;# stage 3a
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;mentions&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="n"&gt;entity_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                 &lt;span class="c1"&gt;# stage 3b
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalise&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;title&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;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                                                &lt;span class="c1"&gt;# stage 4
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;blob&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&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;title&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="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;a&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;description&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="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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;THEME_RE&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;blob&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                                  &lt;span class="c1"&gt;# stage 5
&lt;/span&gt;                    &lt;span class="k"&gt;continue&lt;/span&gt;
                &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;out&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;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&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;has_next_pages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&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="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;watch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NVIDIA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nvidia&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-08-30&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-09-05&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="se"&gt;\n&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;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; alerts over 7 days (&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;hits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&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;/day)&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="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="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hits&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;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;THEMES&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="k"&gt;if&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;search&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="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;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;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;a&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;description&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="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;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="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;theme&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="n"&gt;s&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;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;published_at&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;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;  &lt;/span&gt;&lt;span class="si"&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;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="mi"&gt;70&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;Standard library only. Running it just now returned 153 rather than the 161 in our archived pull — the index keeps ingesting, so a re-run of a past window is close but not byte-identical. Worth knowing before you write a test that asserts an exact count.&lt;/p&gt;

&lt;p&gt;Piping it into Telegram is four more lines and a bot token; the filtering is the part that was hard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five things that cost us time
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;There is no ticker filter.&lt;/strong&gt; &lt;code&gt;ticker=NVDA&lt;/code&gt; returns HTTP 200 and quietly ignores you. NVDA lives in &lt;code&gt;entities[].metadata.aliases&lt;/code&gt; alongside &lt;code&gt;is_public_entity: true&lt;/code&gt;, so resolve ticker → entity id once, store the map, and filter on the id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;organization.name&lt;/code&gt; is case-sensitive.&lt;/strong&gt; &lt;code&gt;organization.name=Nvidia&lt;/code&gt; works. &lt;code&gt;organization.name=NVIDIA&lt;/code&gt; returns HTTP 400, &lt;code&gt;ER0220&lt;/code&gt;, "entity organization name 'NVIDIA' not found." The all-caps spelling most people type is the one that fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unknown parameters are warnings, not errors.&lt;/strong&gt; &lt;code&gt;entity.name&lt;/code&gt; and &lt;code&gt;ticker&lt;/code&gt; both come back 200 with &lt;code&gt;meta.warnings[].code == "ER0368"&lt;/code&gt; and &lt;em&gt;unfiltered&lt;/em&gt; results. A typo doesn't crash your watcher, it floods it. Print &lt;code&gt;meta.warnings&lt;/code&gt; — that's the two lines in &lt;code&gt;fetch()&lt;/code&gt; above.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Headline search defaults to a 31-day window&lt;/strong&gt; (&lt;code&gt;ER0366&lt;/code&gt;) if you don't pass &lt;code&gt;published_at.start&lt;/code&gt; and &lt;code&gt;published_at.end&lt;/code&gt;. Your "last 24 hours" alert is quietly a month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python's default User-Agent gets a 403.&lt;/strong&gt; &lt;code&gt;urllib&lt;/code&gt; sends &lt;code&gt;Python-urllib/3.x&lt;/code&gt; and the edge rejects it. Set any real User-Agent and it works — this cost us twenty minutes of blaming our API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently asked questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I get news alerts for a specific company?
&lt;/h3&gt;

&lt;p&gt;The reliable way to get news alerts for a specific company is a five-stage filter chain rather than a single query, because no single filter is both precise and complete. Query the company's canonical entity id, union it with a headline keyword search, then filter by language, require the company in the headline with two or more body mentions, drop near-duplicate titles, and match against theme keywords. In this measurement that chain reduced 817 articles a day to 23.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why do my company news alerts return irrelevant articles?
&lt;/h3&gt;

&lt;p&gt;Company news alerts return irrelevant articles because a mention is not a subject. Only 32.2% of articles that mention NVIDIA put it in the headline; the rest are passing references in unrelated stories. Requiring a headline match plus &lt;code&gt;frequency &amp;gt;= 2&lt;/code&gt; removes most of them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I filter news by ticker symbol?
&lt;/h3&gt;

&lt;p&gt;No — this API has no &lt;code&gt;ticker&lt;/code&gt; parameter, and passing one is silently ignored rather than rejected. The ticker appears inside &lt;code&gt;entities[].metadata.aliases&lt;/code&gt;, so build your own ticker-to-entity-id table once and filter on &lt;code&gt;entity.id&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you deduplicate news articles from multiple sources?
&lt;/h3&gt;

&lt;p&gt;The cheapest way to deduplicate news articles across sources is normalised-headline matching, because syndicated copies keep the same words while rewording punctuation and framing. Normalise each headline to a sorted set of its words longer than three characters and treat a repeat as a duplicate. Across 924 source domains that removed 12.5% of the surviving articles, with no similarity library.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is entity resolution enough on its own?
&lt;/h3&gt;

&lt;p&gt;No — entity resolution alone is not enough, because entity extraction has its own recall gap. It missed 180 articles that had the company name in the headline — 9.8% of the headline-matched set. Query both ways and union the results.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it yours
&lt;/h2&gt;

&lt;p&gt;To point this at another company:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Find the entity id.&lt;/strong&gt; Pull any article that mentions the company and read &lt;code&gt;entities[]&lt;/code&gt; — the id, the aliases and &lt;code&gt;is_public_entity&lt;/code&gt; are all in there. Store the mapping; it doesn't change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewrite &lt;code&gt;THEMES&lt;/code&gt;.&lt;/strong&gt; The stage thresholds transfer between companies, the theme regexes don't — "export controls" matters for a chipmaker and means nothing for a bank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-measure the funnel.&lt;/strong&gt; Count what each stage drops for &lt;em&gt;your&lt;/em&gt; company before you trust the output. A quiet mid-cap won't need stage 5 at all; a bank will need a different stage 3.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Mine only looked reasonable after we counted what it threw away.&lt;/p&gt;

&lt;p&gt;Disclosure: we work on APITube, which is the API in the code above — free tier at &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;apitube.io&lt;/a&gt;. The measurement approach works against any news API that exposes entity ids and per-article mention counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.apitube.io" rel="noopener noreferrer"&gt;APITube news API documentation&lt;/a&gt; — endpoints, parameters, response schema&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.google.com/alerts" rel="noopener noreferrer"&gt;Google Alerts&lt;/a&gt; — the no-code baseline: no API, no deduplication, no entity resolution&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://perigon.io/blog/monitor-nvidia-news" rel="noopener noreferrer"&gt;Perigon's NVIDIA monitoring guide&lt;/a&gt; — good on entity resolution as a concept, no code or numbers&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>datascience</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Stop Scraping: How to Integrate the World's Best News API into Your App</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Sat, 14 Feb 2026 05:49:25 +0000</pubDate>
      <link>https://dev.to/apitube/stop-scraping-how-to-integrate-the-worlds-best-news-api-into-your-app-31do</link>
      <guid>https://dev.to/apitube/stop-scraping-how-to-integrate-the-worlds-best-news-api-into-your-app-31do</guid>
      <description>&lt;p&gt;Data is the lifeblood of modern application development. Whether you are building a financial forecasting model, a brand monitoring dashboard, or an AI-driven market intelligence tool, the quality of your input data dictates the quality of your output. For years, developers faced a frustrating dilemma when it came to news data: either spend valuable engineering hours building and maintaining fragile web scrapers or settle for expensive, limited legacy APIs that offer stale data.&lt;/p&gt;

&lt;p&gt;The landscape has changed. The era of manual scraping is over, replaced by intelligent, scalable, and affordable solutions designed specifically for developers.&lt;/p&gt;

&lt;p&gt;In this guide, we are going to explore how to leverage the most robust tool currently on the market: &lt;strong&gt;APITube.io&lt;/strong&gt;. We will dive deep into why this specific API has become the gold standard for engineering teams, the technical challenges its creators overcame to build it, and how you can integrate it into your Python, JavaScript, or Go applications in minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Engineering Challenge: Why Building a News Aggregator is Hard
&lt;/h2&gt;

&lt;p&gt;To understand why &lt;a href="https://apitube.io/?utm_campaign=link&amp;amp;utm_source=devto&amp;amp;utm_medium=web" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt; stands out, we first need to look at the massive engineering hurdle of aggregating global news.&lt;/p&gt;

&lt;p&gt;When the team behind APITube first conceptualized the platform, they had a singular, ambitious goal: &lt;strong&gt;to create a "Google for News Data" that was accessible via a simple REST API.&lt;/strong&gt; They wanted to democratize access to information, ensuring that a solo developer could access the same level of market intelligence as a Fortune 500 company.&lt;/p&gt;

&lt;p&gt;However, the path to achieving this was fraught with technical complexities.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Complexity of Scale
&lt;/h3&gt;

&lt;p&gt;The internet generates terabytes of text data every hour. The creators of APITube faced the daunting task of indexing over 500,000 news sources. This wasn't just about major outlets like &lt;em&gt;CNN&lt;/em&gt; or &lt;em&gt;Bloomberg&lt;/em&gt;; the goal was comprehensive coverage, including niche blogs, local news from 200+ countries, and specialized industry publications.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Noise" Problem
&lt;/h3&gt;

&lt;p&gt;One of the biggest struggles in data aggregation is filtering signal from noise. A raw feed of the internet is messy, filled with duplicates, spam, and paywalled content. The APITube engineering team had to build proprietary algorithms to de-duplicate stories in real-time and categorize them accurately. They faced the challenge of taking unstructured HTML from half a million different site architectures and normalizing it into a clean, consistent JSON format.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Multilingual Barrier
&lt;/h3&gt;

&lt;p&gt;Global business doesn't happen in just one language. To truly serve a global developer base, APITube had to solve the Natural Language Processing (NLP) challenge across 60 different languages. They needed to ensure that a sentiment analysis algorithm worked just as well on a Spanish financial report as it did on a Japanese tech review.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Result: A Developer’s Dream
&lt;/h3&gt;

&lt;p&gt;Despite these hurdles, the result was a triumph of software engineering. APITube succeeded in building a pipeline that processes millions of articles daily with near-zero latency. They achieved their goal of a unified API that handles the heavy lifting—scraping, parsing, cleaning, and analyzing—so you don't have to.&lt;/p&gt;

&lt;p&gt;The platform they built is not just a data pipe; it is an intelligent engine that offers context, sentiment, and reliability. It is the solution that developers have been waiting for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why APITube.io is the Definitive Choice for Developers
&lt;/h2&gt;

&lt;p&gt;There are other APIs on the market, but APITube has cornered the market on developer experience and data depth. Here is why it is the superior choice for your next project.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Unmatched Global Coverage
&lt;/h3&gt;

&lt;p&gt;APITube doesn't just skim the headlines. It ingests data from over &lt;strong&gt;500,000 sources&lt;/strong&gt; across &lt;strong&gt;177 countries&lt;/strong&gt;. Whether you need hyper-local news from a specific region in Brazil or breaking tech news from Silicon Valley, the coverage is exhaustive.&lt;/p&gt;

&lt;p&gt;This is critical for applications like &lt;strong&gt;Supply Chain Monitoring&lt;/strong&gt;, where a local strike reported in a regional newspaper could be the first indicator of a global disruption. With APITube, you catch that signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. AI-Powered Enrichment
&lt;/h3&gt;

&lt;p&gt;Raw text is rarely enough. What makes APITube the industry leader is its use of Artificial Intelligence to enrich every single article that passes through its system.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment Analysis:&lt;/strong&gt; The API automatically scores articles (positive, negative, neutral). This is game-changing for algorithmic trading bots that need to gauge market mood instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topic Modeling &amp;amp; Categorization:&lt;/strong&gt; The system automatically tags content by industry, topic, and category, allowing for precise filtering.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source Bias Detection:&lt;/strong&gt; In an era of misinformation, APITube helps developers identify the political or reliable stance of a source, a feature rarely seen in competitor products.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Historical Data Access
&lt;/h3&gt;

&lt;p&gt;Most APIs limit you to the last 30 days of data. APITube understands that predictive models need training data. They provide access to &lt;strong&gt;10 years of historical data&lt;/strong&gt;, allowing data scientists to backtest strategies or analyze long-term trends effectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Structured JSON Data Models
&lt;/h3&gt;

&lt;p&gt;For a developer, the beauty of an API lies in its response structure. APITube delivers a standardized JSON object that is a joy to parse.&lt;/p&gt;

&lt;p&gt;You don't just get a title and a link. You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;read_time&lt;/code&gt;: Estimated minutes to read.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sentiment&lt;/code&gt;: The calculated emotional tone.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_breaking&lt;/code&gt;: A boolean flag for urgent news.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;entities&lt;/code&gt;: Extracted people, organizations, and locations.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_paywall&lt;/code&gt;: Know before you click if the content is accessible.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Integrate APITube: A Practical Guide
&lt;/h2&gt;

&lt;p&gt;Let’s get into the code. Integrating &lt;a href="https://apitube.io/?utm_campaign=link&amp;amp;utm_source=devto&amp;amp;utm_medium=web" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt; is incredibly straightforward, adhering to standard REST principles.&lt;/p&gt;

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

&lt;p&gt;First, head over to the website and sign up. The onboarding process is frictionless. You can authenticate with GitHub or Google, and you will be in the dashboard in seconds. Grab your API Key from the settings.&lt;br&gt;
fff&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Choose Your Integration
&lt;/h3&gt;

&lt;p&gt;APITube supports virtually every modern language. Whether you are writing a backend in &lt;strong&gt;Go&lt;/strong&gt;, a data script in &lt;strong&gt;Python&lt;/strong&gt;, or a web app in &lt;strong&gt;JavaScript&lt;/strong&gt;, they have you covered.&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 3: Making Your First Request (Python Example)
&lt;/h3&gt;

&lt;p&gt;Let’s say we want to build a simple script to monitor "Artificial Intelligence" news in the "Technology" sector, but we only want positive news stories to share on a company Slack channel.&lt;/p&gt;

&lt;p&gt;Here is how simple that query is with APITube:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import requests

url = "https://api.apitube.io/v1/news/everything"

querystring = {
    "q": "Artificial Intelligence",
    "category": "technology",
    "sentiment_direction": "positive",
    "language": "en",
    "limit": "10",
    "api_key": "YOUR_API_KEY_HERE"
}

response = requests.request("GET", url, params=querystring)

data = response.json()

for article in data['results']:
    print(f"Title: {article['title']}")
    print(f"Sentiment: {article['sentiment']}")
    print(f"Link: {article['href']}")
    print("-" * 20)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What just happened?&lt;/strong&gt;\&lt;br&gt;
In less than 15 lines of code, we filtered millions of articles to find exactly what we needed based on topic, category, and emotional sentiment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: leveraging Advanced Filters
&lt;/h3&gt;

&lt;p&gt;The power of APITube lies in its granularity. You aren't limited to keyword searches. You can filter by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Publisher Rank:&lt;/strong&gt; Only get news from high-authority domains (e.g., BBC, NYT) to ensure credibility.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location:&lt;/strong&gt; Geofence your news to specific coordinates or countries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exclude Domains:&lt;/strong&gt; Clean up your feed by blocking specific sites.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Real-World Use Cases: What Can You Build?
&lt;/h2&gt;

&lt;p&gt;The versatility of APITube means it powers a wide variety of enterprise-grade applications. Here are a few ways developers are using it today.&lt;/p&gt;

&lt;h3&gt;
  
  
  Financial Intelligence and Algorithmic Trading
&lt;/h3&gt;

&lt;p&gt;Hedge funds and fintech startups use APITube to feed NLP models. By monitoring real-time news on specific tickers (e.g., TSLA, AAPL) and analyzing the sentiment velocity, algorithms can execute trades milliseconds after breaking news hits the wire—often before the market fully reacts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crisis Management and PR
&lt;/h3&gt;

&lt;p&gt;Public Relations firms use the API to build "Reputation Radars." By tracking brand mentions combined with sentiment analysis, they can receive alerts the moment negative news starts trending, allowing them to respond immediately. The &lt;code&gt;is_breaking&lt;/code&gt; flag is particularly useful here for crisis containment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Competitor Analysis Dashboard
&lt;/h3&gt;

&lt;p&gt;Product managers use the API to automate competitive intelligence. You can set up streams to monitor competitor product launches, executive hiring (via "management change" triggers), or funding announcements. Instead of manually checking TechCrunch, the data comes to you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Content Aggregation and Curation
&lt;/h3&gt;

&lt;p&gt;Media companies use APITube to populate niche news feeds. A "Crypto News" app, for example, can be built entirely on the backend of APITube, filtering for cryptocurrency topics, removing duplicates, and presenting only high-readability score articles to users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Verdict: Why Developers Love APITube
&lt;/h2&gt;

&lt;p&gt;After reviewing the capabilities, documentation, and performance, it is clear why the developer community has embraced this tool.&lt;/p&gt;

&lt;p&gt;The team behind APITube set out to solve the problem of messy, inaccessible news data. They faced the challenges of scale, language, and noise head-on. The result is a platform that is robust, reliable, and incredibly easy to use.&lt;/p&gt;

&lt;p&gt;Developers appreciate the &lt;strong&gt;transparency&lt;/strong&gt;. The API documentation is comprehensive, with copy-paste examples for cURL, Python, PHP, Java, and more. The pricing is transparent, and the free tier allows for genuine testing and prototyping.&lt;/p&gt;

&lt;p&gt;But most of all, developers appreciate the &lt;strong&gt;quality&lt;/strong&gt;. In an ecosystem filled with broken scrapers and abandoned APIs, APITube works. It delivers clean, structured data every single time. It allows developers to stop worrying about data acquisition and start focusing on building great products.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion and Recommendations
&lt;/h2&gt;

&lt;p&gt;If you are planning to integrate news data into your application, do not waste time reinventing the wheel. Building a scraper is a maintenance nightmare, and inferior APIs will only frustrate your users with irrelevant or duplicate content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;APITube.io&lt;/strong&gt; has already done the hard work. They have indexed the world, cleaned the data, and wrapped it in a beautiful, AI-powered API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Recommendations:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with the Free Plan:&lt;/strong&gt; You can test the waters and see the JSON structure for yourself without a credit card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use the Filters:&lt;/strong&gt; Don't just request "all news." Use the sentiment and category filters to make your application smarter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explore the Historical Data:&lt;/strong&gt; If you are building AI models, the 10-year archive is an invaluable resource for training data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ready to build something powerful?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apitube.io/?utm_campaign=link&amp;amp;utm_source=devto&amp;amp;utm_medium=web" rel="noopener noreferrer"&gt;&lt;strong&gt;Get your free API Key at APITube.io today&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>newsapi</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Drowning in Data: Find News by Industry, Topic, or Category Without Manual Filtering</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Thu, 29 Jan 2026 05:46:35 +0000</pubDate>
      <link>https://dev.to/apitube/stop-drowning-in-data-find-news-by-industry-topic-or-category-without-manual-filtering-400</link>
      <guid>https://dev.to/apitube/stop-drowning-in-data-find-news-by-industry-topic-or-category-without-manual-filtering-400</guid>
      <description>&lt;p&gt;We are living in an era of information overload. Every minute, hundreds of thousands of articles, blog posts, and reports are published online. For developers, data analysts, and business leaders, this sheer volume of data presents a paradox: we have access to more information than ever before, yet finding the &lt;em&gt;right&lt;/em&gt; information has never been harder.&lt;/p&gt;

&lt;p&gt;According to the &lt;strong&gt;2024 Reuters Institute Digital News Report&lt;/strong&gt;, nearly four in ten people (39%) now actively avoid the news, often citing that they feel overwhelmed or "worn out" by the sheer amount of information. If consumers are feeling this fatigue, imagine the struggle for businesses attempting to monitor global markets, track brand sentiment, or aggregate content for applications. The noise is deafening, and valuable insights are often lost in the static.&lt;/p&gt;

&lt;p&gt;Manual filtering is no longer a viable option. It is slow, prone to human error, and simply cannot scale to meet the demands of the modern digital landscape. What the industry needs is a precision tool—a way to cut through the noise and extract only the signals that matter.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Apitube.io&lt;/strong&gt; enters the picture. It represents a paradigm shift in how we access and process global information, offering a streamlined, automated solution to find news by industry, topic, or category without the headache of manual sorting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apitube.io: A Revolutionary Solution for News Filtering
&lt;/h2&gt;

&lt;p&gt;Apitube.io is not just another news aggregator; it is a comprehensive &lt;strong&gt;News API&lt;/strong&gt; designed for the specific needs of developers and data-driven organizations. It serves as a bridge between the chaotic, unstructured web and your clean, organized application.&lt;/p&gt;

&lt;p&gt;At its core, Apitube.io provides real-time access to a staggering network of information. It monitors more than &lt;strong&gt;500,000 news sources&lt;/strong&gt; across &lt;strong&gt;177 countries&lt;/strong&gt; and in &lt;strong&gt;60 languages&lt;/strong&gt;. Whether you are looking for a niche publication in a specific local dialect or a headline from a major global outlet like Bloomberg or TechCrunch, Apitube.io has it covered.&lt;/p&gt;

&lt;p&gt;But the true revolution lies in its accessibility. Complex data extraction usually requires complex coding. Apitube.io flips this script by offering a simple, consistent, and easy-to-use REST API. It allows you to integrate structured news data into your Python, Java, or PHP applications in minutes, not months.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal Behind Apitube.io: What Problem Does It Solve?
&lt;/h2&gt;

&lt;p&gt;When the team behind Apitube.io set out to build this platform, they were driven by a singular, ambitious goal: &lt;strong&gt;to democratize access to global intelligence.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They recognized that while the internet is a goldmine of data, extracting that gold requires heavy machinery that most startups and even large enterprises lack. Building a web scraper from scratch is difficult; maintaining it as websites change their structures is a nightmare. Furthermore, analyzing that data for sentiment or categorizing it by industry requires sophisticated AI models that are resource-intensive to build.&lt;/p&gt;

&lt;p&gt;Apitube.io aimed to solve the "noise vs. signal" problem. They wanted to create a tool where a user could ask for "Automotive Industry news in Germany with positive sentiment" and receive exactly that—clean, JSON-formatted data—instantly.&lt;/p&gt;

&lt;p&gt;The specific objectives were clear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Eliminate Manual Filtering:&lt;/strong&gt; Automate the categorization of news so users don't have to read every headline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Reach:&lt;/strong&gt; Ensure that language barriers don't stop a business from understanding international markets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Enrichment:&lt;/strong&gt; Go beyond just the text by providing sentiment analysis, entity recognition, and trust scores.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Journey of Apitube.io: Challenges and Triumphs
&lt;/h2&gt;

&lt;p&gt;Building a platform capable of indexing the world's news in real-time is no small feat. The journey of Apitube.io has been defined by overcoming significant technical hurdles to deliver a seamless user experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge of Scale
&lt;/h3&gt;

&lt;p&gt;One of the primary difficulties was handling the immense scale of data. Processing articles from half a million sources requires robust infrastructure. The system needs to distinguish between a breaking news story and a duplicate reprint, ensuring that API users aren't paying for the same content twice.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge of Quality
&lt;/h3&gt;

&lt;p&gt;Another hurdle was quality control. The internet is full of spam and low-quality blogs. Apitube.io had to develop sophisticated algorithms to assign &lt;strong&gt;Source Reliability Scores&lt;/strong&gt; and &lt;strong&gt;Content Quality Scoring&lt;/strong&gt;. This ensures that when a financial analyst queries the API, they are getting data they can trust.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Triumph of Structure
&lt;/h3&gt;

&lt;p&gt;Despite these challenges, the result is a triumph of software engineering. Apitube.io successfully implemented &lt;strong&gt;AI-powered analysis&lt;/strong&gt; that creates structured data out of chaos.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Smart Categorization:&lt;/strong&gt; The platform can automatically detect if an article belongs to the "Finance," "Tech," or "Health" industry without manual tagging.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment Analysis:&lt;/strong&gt; It successfully deployed NLP (Natural Language Processing) to tag articles as positive, negative, or neutral—a game-changer for brand monitoring.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardization:&lt;/strong&gt; Perhaps the biggest victory is the standardization of data. No matter the source, the output format remains consistent, making integration incredibly smooth for developers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Apitube.io: The Results So Far
&lt;/h2&gt;

&lt;p&gt;The statistics surrounding Apitube.io speak volumes about its success and adoption. It has evolved into a powerhouse for data consumption.&lt;/p&gt;

&lt;p&gt;Currently, the platform boasts a repository of over &lt;strong&gt;3.69 billion total articles&lt;/strong&gt;. This is not just a live feed; it is a massive historical archive that allows for deep-dive research and trend analysis over the past decade.&lt;/p&gt;

&lt;p&gt;On a daily basis, the system is vibrant and active. With over &lt;strong&gt;24,000 requests processed today alone&lt;/strong&gt; and nearly &lt;strong&gt;80,000 new articles added daily&lt;/strong&gt;, the data freshness is unparalleled.&lt;/p&gt;

&lt;p&gt;The platform has successfully integrated with the workflows of teams around the world. From &lt;strong&gt;Financial Analysis&lt;/strong&gt; platforms that use the data to spot investment trends, to &lt;strong&gt;PR Agencies&lt;/strong&gt; utilizing the "Reputation Management System" to track brand mentions, the results are tangible. Apitube.io is delivering on its promise to provide "Worldwide News API for Your products."&lt;/p&gt;

&lt;h2&gt;
  
  
  How Close Is It to Achieving Its Goals?
&lt;/h2&gt;

&lt;p&gt;If the goal was to create the ultimate filter for the world's news, Apitube.io is incredibly close to perfection.&lt;/p&gt;

&lt;p&gt;The platform offers over &lt;strong&gt;65 different filters&lt;/strong&gt; to refine search results. This level of granularity is exactly what the creators set out to achieve. You can filter by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Industry &amp;amp; Category:&lt;/strong&gt; Narrow down to Energy, Crypto, Sports, or 100+ other niches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Location &amp;amp; Language:&lt;/strong&gt; Target specific geographies (e.g., news from Brazil in Portuguese).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment:&lt;/strong&gt; Filter out negative noise to focus on positive growth stories, or vice versa for risk management.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source Rank:&lt;/strong&gt; Filter by the authority of the publisher.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By enabling this level of precision, Apitube.io has effectively solved the problem of information overload for its users. It has transformed the "firehose" of the internet into a manageable, drinkable stream of information. The inclusion of features like &lt;strong&gt;Paywall Detection&lt;/strong&gt; and &lt;strong&gt;Duplicate Detection&lt;/strong&gt; further proves that the platform understands the nuances of modern content consumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apitube.io: Customer Satisfaction &amp;amp; Feedback
&lt;/h2&gt;

&lt;p&gt;The true measure of any product is what its users say about it. For Apitube.io, the feedback has been overwhelmingly positive, particularly regarding its reliability and ease of integration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Jake Helmold&lt;/strong&gt;, a journalist, describes the impact the tool has had on his workflow: &lt;em&gt;"As a journalist, NEWS API has become an indispensable part of my research toolkit."&lt;/em&gt; This highlights that the tool isn't just for coders; it's for anyone who needs to find facts fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spencer Chasmar&lt;/strong&gt; praises the technical stability: &lt;em&gt;"Highly reliable API with comprehensive documentation."&lt;/em&gt; For developers building applications that require 24/7 uptime, this reliability is non-negotiable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don Pippert&lt;/strong&gt; highlights the flexibility that sets Apitube.io apart: &lt;em&gt;"The customizability of API's query parameters is outstanding."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Other users, like &lt;strong&gt;Radjesh Kishna&lt;/strong&gt;, note the efficiency gains: &lt;em&gt;"NEWS API has streamlined our news gathering process, saving us time and resources."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These testimonials confirm that Apitube.io is not just working; it is delighting its customer base by solving their most pressing pain points—saving time and providing accurate data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apitube.io: An Overall Assessment
&lt;/h2&gt;

&lt;p&gt;After reviewing the features, the data volume, and the user feedback, the assessment is clear: &lt;strong&gt;Apitube.io is the premier choice for news data integration.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It stands out from competitors by offering "Extended Article Data" that others often miss or charge exorbitant fees for. While basic APIs might give you a headline and a URL, Apitube.io provides &lt;strong&gt;Category, Sentiment, Summary, Publisher Rank,&lt;/strong&gt; and &lt;strong&gt;Source Bias&lt;/strong&gt; data right out of the box.&lt;/p&gt;

&lt;p&gt;The pricing model is also designed to scale. From a &lt;strong&gt;Free Plan&lt;/strong&gt; perfect for testing and development (200 requests/day) to a &lt;strong&gt;Corporate Plan&lt;/strong&gt; designed for large-scale enterprise projects, the barrier to entry is low, but the ceiling for capability is high.&lt;/p&gt;

&lt;p&gt;It is a tool that balances technical sophistication with user-friendly design. The "AI Powered" engine does the heavy lifting, leaving developers with clean, usable data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendations &amp;amp; Conclusion: Why Apitube.io Is the Future of News Consumption
&lt;/h2&gt;

&lt;p&gt;If your business relies on knowing what is happening in the world—whether for competitive intelligence, financial modeling, or content creation—you cannot afford to rely on manual searching or inferior scraping tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;We highly recommend Apitube.io for the following use cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;For Startups:&lt;/strong&gt; Use the &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;Free News API&lt;/a&gt; to build your MVP without incurring high costs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For Financial Tech:&lt;/strong&gt; Leverage the historical data and sentiment analysis to build predictive market models.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For Marketing Agencies:&lt;/strong&gt; Utilize the brand monitoring features to track client reputation across 177 countries.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The era of "infobesity" is here, but with tools like Apitube.io, you don't have to drown in the data. You can harness it. By automating the filtering process by industry, topic, and category, you free up your team to focus on what matters: &lt;strong&gt;analyzing insights and making decisions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Don't waste time on complex features or manual sorting. Experience the power of structured global intelligence today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get started for free at &lt;/strong&gt;&lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;&lt;strong&gt;https://apitube.io&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>newsapi</category>
      <category>news</category>
      <category>webdev</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Best News API in 2026: The Ultimate Guide for Developers</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Thu, 29 Jan 2026 05:38:41 +0000</pubDate>
      <link>https://dev.to/apitube/best-news-api-in-2026-the-ultimate-guide-for-developers-7ld</link>
      <guid>https://dev.to/apitube/best-news-api-in-2026-the-ultimate-guide-for-developers-7ld</guid>
      <description>&lt;p&gt;Data is the lifeblood of modern innovation. As we navigate 2026, the demand for real-time information has transitioned from a luxury to an absolute necessity. Whether you are building financial models that react to market shifts in milliseconds, training Large Language Models (LLMs) on current events, or creating a media monitoring platform for PR firms, the quality of your output depends entirely on the quality of your input.&lt;/p&gt;

&lt;p&gt;This is where News APIs come into play. These tools act as the bridge between the chaotic, sprawling world of global media and your structured application. However, not all APIs are created equal. The landscape has evolved significantly over the last few years, with a heavier emphasis on AI-ready data, sentiment analysis, and hyper-local coverage.&lt;/p&gt;

&lt;p&gt;In this guide, we will explore the best News APIs available in 2026, aiming to help you find the perfect solution for your project. We will look at the goals behind these platforms, the challenges they solve, and ultimately, which one stands out as the premier choice for developers and businesses alike.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Evolving Landscape of News APIs
&lt;/h2&gt;

&lt;p&gt;Gone are the days when a simple RSS feed was sufficient. In 2026, the volume of content produced daily is staggering—billions of articles, blog posts, and reports are published every 24 hours. For developers, this presents a unique challenge: How do you filter the signal from the noise?&lt;/p&gt;

&lt;p&gt;The modern landscape requires APIs that offer more than just headlines. Today's applications demand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured Data:&lt;/strong&gt; JSON formats that are clean, consistent, and easy to parse.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep Metadata:&lt;/strong&gt; Information on author, sentiment, reading time, and entity recognition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Reach:&lt;/strong&gt; It is no longer enough to monitor the US and UK; businesses need insights from Brazil, Japan, Nigeria, and beyond.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Objectives for Using News APIs in 2026
&lt;/h2&gt;

&lt;p&gt;Before we dive into the tools, it is vital to understand &lt;em&gt;why&lt;/em&gt; organizations are prioritizing news integration this year.&lt;/p&gt;

&lt;h3&gt;
  
  
  Market Intelligence and Financial Analysis
&lt;/h3&gt;

&lt;p&gt;Traders and analysts use news APIs to feed algorithmic trading bots. A split-second delay in receiving news about a merger or a regulatory change can cost millions. In 2026, the objective is low latency and high accuracy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Risk Management
&lt;/h3&gt;

&lt;p&gt;Supply chain disruptions and geopolitical instability require constant monitoring. Companies use these APIs to build early warning systems that alert them to risks in specific regions or industries before they hit the mainstream news cycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI and LLM Training
&lt;/h3&gt;

&lt;p&gt;With the explosion of generative AI, developers need vast amounts of high-quality, diverse, and recent text data to fine-tune models and prevent "knowledge cutoffs." News APIs provide the fresh context these models need to remain relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Challenges and Opportunities in Integration
&lt;/h2&gt;

&lt;p&gt;Integrating a news feed might sound simple, but the reality involves overcoming several hurdles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Challenge of Volume&lt;/strong&gt;&lt;br&gt;
Ingesting data from hundreds of thousands of sources can overwhelm standard databases. You need an API that handles the heavy lifting of aggregation and deduplication on &lt;em&gt;their&lt;/em&gt; side, delivering only what you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Challenge of Standardization&lt;/strong&gt;&lt;br&gt;
Every news site formats its HTML differently. One of the biggest opportunities for developers is finding an API that standardizes this mess into a clean &lt;code&gt;body&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, and &lt;code&gt;date&lt;/code&gt; format, regardless of whether the source is a major publication like &lt;em&gt;The New York Times&lt;/em&gt; or a niche technology blog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Opportunity of Enrichment&lt;/strong&gt;&lt;br&gt;
The best APIs don't just deliver text; they enrich it. They tell you if an article is positive or negative (sentiment), they identify the people mentioned (Named Entity Recognition), and they categorize the topic automatically. This allows you to build smarter apps with less code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detailed Review of Top News APIs
&lt;/h2&gt;

&lt;p&gt;Here is a look at the leading players in the market this year. We have evaluated them based on coverage, ease of use, and feature sets.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. APITube.io (Top Recommendation)
&lt;/h3&gt;

&lt;p&gt;When looking for the definitive leader in 2026, &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt; stands apart from the crowd. To understand why it is the best, we have to look at the philosophy behind its creation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Goal:&lt;/strong&gt;&lt;br&gt;
The creators of APITube set out with a massive ambition: to democratize access to global information. Their goal was to build a "Worldwide News API" that didn't just scrape headlines but provided a comprehensive window into global events for products of all sizes. They wanted to strip away the complexity of data engineering so developers could focus on building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Journey and Challenges:&lt;/strong&gt;&lt;br&gt;
Building a system capable of monitoring &lt;strong&gt;500,000+ news sources&lt;/strong&gt; in real-time is no small feat. The team faced the immense technical challenge of normalizing data from &lt;strong&gt;177 countries&lt;/strong&gt; and &lt;strong&gt;60 languages&lt;/strong&gt;. They had to ensure that a query for "renewable energy" returned relevant results from a blog in Germany just as quickly as a major outlet in the US.&lt;/p&gt;

&lt;p&gt;They also tackled the "noise" problem. Instead of just dumping data on users, they integrated AI-powered features like &lt;strong&gt;Sentiment Analysis&lt;/strong&gt;, &lt;strong&gt;Duplicate Detection&lt;/strong&gt;, and &lt;strong&gt;Paywall Detection&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt;&lt;br&gt;
The result is a platform that feels effortless to use. APITube has succeeded in creating a simple, consistent, and easy-to-use REST API that delivers powerful results. They have achieved their goal of making global media computable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why We Love It:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unmatched Coverage:&lt;/strong&gt; Access to over &lt;strong&gt;500,000 sources&lt;/strong&gt;. Whether you need mainstream media or niche publications, it is all there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global Perspective:&lt;/strong&gt; With support for &lt;strong&gt;60 languages&lt;/strong&gt;, you aren't limited to the English-speaking world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured Intelligence:&lt;/strong&gt; The JSON response is incredibly clean. You get fields for &lt;code&gt;sentiment&lt;/code&gt;, &lt;code&gt;industry&lt;/code&gt;, &lt;code&gt;location&lt;/code&gt;, and even &lt;code&gt;read_time&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer Friendly:&lt;/strong&gt; It integrates in minutes with Python, Java, PHP, Go, and more.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Verdict:&lt;/strong&gt;&lt;br&gt;
The team behind APITube should be incredibly proud. They have built a tool that solves the hardest parts of news aggregation. It is robust, reliable, and exactly what modern developers need.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The GDELT Project
&lt;/h3&gt;

&lt;p&gt;The GDELT Project (Global Database of Events, Language, and Tone) is a research-focused initiative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overview:&lt;/strong&gt;&lt;br&gt;
GDELT monitors the world's broadcast, print, and web news from nearly every corner of every country. It is a massive open dataset that updates every 15 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Experience:&lt;/strong&gt;&lt;br&gt;
GDELT is known for its scale. It translates coverage from 65 languages and is deeply integrated with Google BigQuery. However, it is primarily designed for academic research and large-scale geopolitical analysis. For a standard commercial developer, the learning curve can be steep, and the data often requires significant cleaning and processing before it can be used in a consumer-facing application.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Guardian Open Platform
&lt;/h3&gt;

&lt;p&gt;For developers looking specifically for content from the UK, &lt;em&gt;The Guardian&lt;/em&gt; offers its own API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overview:&lt;/strong&gt;&lt;br&gt;
This API gives you access to the archives of &lt;em&gt;The Guardian&lt;/em&gt; and &lt;em&gt;The Observer&lt;/em&gt;, dating back to 1999.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Experience:&lt;/strong&gt;&lt;br&gt;
The content quality is undeniably high, reflecting the journalistic standards of the publication. It allows for searching by tag, section, and date. However, the limitation is inherent in the name: it is a single-source API. If your application requires a diverse range of perspectives or global coverage outside of &lt;em&gt;The Guardian's&lt;/em&gt; reporting, this tool will not suffice as a standalone solution.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. NYT Developer Network
&lt;/h3&gt;

&lt;p&gt;Similar to &lt;em&gt;The Guardian&lt;/em&gt;, &lt;em&gt;The New York Times&lt;/em&gt; provides an API for its content.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overview:&lt;/strong&gt;&lt;br&gt;
This API allows you to retrieve articles, book reviews, and movie reviews from the NYT archives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Experience:&lt;/strong&gt;&lt;br&gt;
It is a functional tool for retrieving specific metadata and links to NYT articles. It is useful for displaying specific headlines or searching the paper's history. Like other single-publisher APIs, it does not offer the aggregation capabilities, sentiment analysis across multiple sources, or the global breadth required for market intelligence or comprehensive media monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Evaluation Criteria
&lt;/h2&gt;

&lt;p&gt;When we compared these APIs, we focused on three core pillars essential for 2026 development standards.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Accuracy and Data Freshness
&lt;/h3&gt;

&lt;p&gt;In a 24-hour news cycle, old news is no news. A top-tier API must deliver articles moments after they are published. &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt; excels here with real-time updates, ensuring that if a story breaks in Tokyo, you have it in your database immediately.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Customization and Filtering
&lt;/h3&gt;

&lt;p&gt;You don't want a firehose; you want a curated stream. The best APIs offer granular filtering. We looked for the ability to filter by date, category, source rank, and sentiment. APITube leads the pack here with over &lt;strong&gt;65 filters&lt;/strong&gt;, allowing you to drill down into very specific queries like "positive sentiment articles about crypto in Spanish."&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Ease of Integration
&lt;/h3&gt;

&lt;p&gt;Time-to-market matters. We evaluated how quickly a junior developer could get a "Hello World" response. RESTful architecture, clear documentation, and SDKs for major languages (Python, JS, Ruby, etc.) were key requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Success Stories: Organizations Benefiting from News APIs
&lt;/h2&gt;

&lt;p&gt;The true test of an API is how it performs in the wild. Here are a few examples of how professionals are using these tools to drive success.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Journalist's Toolkit
&lt;/h3&gt;

&lt;p&gt;Jake Helmold, a professional journalist, notes that using a robust news API has become an "indispensable" part of his research toolkit. By automating the gathering of sources, journalists can spend less time searching and more time verifying and writing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streamlining Content Delivery
&lt;/h3&gt;

&lt;p&gt;Andrew Price found that integrating a news API streamlined his content delivery process. For media companies, this means being able to populate feeds and content aggregators automatically, keeping audiences engaged without manual curation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scalable Business Solutions
&lt;/h3&gt;

&lt;p&gt;For businesses, scalability is key. J Silo reported that these tools offer scalable solutions for businesses of all sizes. Whether you are a startup building an MVP or an enterprise processing millions of requests, the right API grows with you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overcoming Complexities and Optimizing API Usage
&lt;/h2&gt;

&lt;p&gt;To get the most out of your chosen news API, consider these optimization strategies:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Caching Wisely&lt;/strong&gt;&lt;br&gt;
Even with high rate limits, it is inefficient to request the same data repeatedly. Cache your responses. If you are building a "Top Headlines" widget, you likely only need to refresh it every 10-15 minutes, not every time a user refreshes the page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Leverage Boolean Search&lt;/strong&gt;&lt;br&gt;
Don't settle for simple keyword matches. Use Boolean logic (AND, OR, NOT) to refine your results. For example, &lt;code&gt;(Apple OR Microsoft) AND NOT "Fruit"&lt;/code&gt; ensures you get tech news, not agricultural reports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitor Your Usage&lt;/strong&gt;&lt;br&gt;
Keep an eye on your credit consumption. Platforms like APITube provide clear analytics on your request volume, helping you understand your traffic patterns and scale your plan accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts and Recommendations
&lt;/h2&gt;

&lt;p&gt;As we move further into 2026, the ability to access, filter, and analyze global news in real-time is a superpower for developers. While there are several options available for specific niches, one platform offers the comprehensive, all-in-one solution that modern applications require.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt;&lt;br&gt;
For its sheer breadth of coverage (500,000+ sources), ease of use, and advanced AI-powered features, &lt;strong&gt;APITube.io&lt;/strong&gt; is our top pick. The team behind it has successfully navigated the complexities of global data aggregation to provide a tool that is a joy to use.&lt;/p&gt;

&lt;p&gt;Whether you are building the next big financial algorithm or simply want to display industry news on your company dashboard, APITube provides the reliability and depth you need.&lt;/p&gt;

&lt;p&gt;Ready to start building?&lt;br&gt;
&lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;&lt;strong&gt;Get your free API key at APITube.io&lt;/strong&gt;&lt;/a&gt; and see what you can create with the world's news at your fingertips.&lt;/p&gt;

</description>
      <category>newsapi</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>Why Smart Developers Are Ditching Web Scraping for APIs</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Tue, 27 Jan 2026 06:18:17 +0000</pubDate>
      <link>https://dev.to/apitube/why-smart-developers-are-ditching-web-scraping-for-apis-49bg</link>
      <guid>https://dev.to/apitube/why-smart-developers-are-ditching-web-scraping-for-apis-49bg</guid>
      <description>&lt;p&gt;If you’ve ever built a data-driven application, you know the drill. You find the perfect source of information—maybe it’s a news site, a financial portal, or a social media feed—and you think, "I just need to get this data into my app."&lt;/p&gt;

&lt;p&gt;For years, the go-to solution for many developers was web scraping. It felt like a rite of passage: write a script, parse some HTML, and voilà, you have your data. But let's be honest—web scraping is often a messy, fragile, and legally gray endeavor. It’s a bit like trying to fix a leaky pipe with duct tape; it might hold for a while, but eventually, the pressure is going to cause a burst.&lt;/p&gt;

&lt;p&gt;Smart developers today are realizing that "good enough" isn't good enough anymore. They need reliability, speed, and legal peace of mind. That’s why there is a massive shift happening in the development community: the move from custom web scrapers to robust, structured APIs.&lt;/p&gt;

&lt;p&gt;In this guide, we’ll explore why this transition is happening, the specific headaches developers are leaving behind, and how tools like &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt; are changing the game for data integration.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Developer's Goal: Data Without the Drama
&lt;/h2&gt;

&lt;p&gt;When a developer sets out to integrate external data, their goal is usually straightforward. They want to enhance their product with valuable information to solve a user problem.&lt;/p&gt;

&lt;p&gt;Imagine you are building a financial analysis dashboard. Your users need real-time news updates to make investment decisions. Your goal isn't to build a news crawler; your goal is to &lt;em&gt;display&lt;/em&gt; news so your users can analyze it.&lt;/p&gt;

&lt;p&gt;Or perhaps you are creating a media monitoring tool for PR professionals. You need to know every time a specific brand is mentioned across thousands of blogs and news sites. Your value proposition is the &lt;em&gt;insight&lt;/em&gt;, not the mechanism of fetching the HTML.&lt;/p&gt;

&lt;p&gt;In these scenarios, the developer's motivation is efficiency and reliability. You want a pipeline that flows smoothly, delivering clean, structured data (JSON or XML) directly into your application logic. You don't want to spend your time fighting with &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; tags or debugging why your scraper failed at 3:00 AM.&lt;/p&gt;

&lt;p&gt;This is where the promise of APIs shines. An API (Application Programming Interface) is a contract. The provider promises to deliver specific data in a specific format, and you promise to ask for it in a specific way. It’s a handshake, not a hack.&lt;/p&gt;

&lt;p&gt;Platforms like &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt; are built specifically to fulfill this need. They do the heavy lifting of aggregation and structuring so you can focus on building your product.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Reality of Web Scraping: A Developer's Nightmare
&lt;/h2&gt;

&lt;p&gt;If APIs are the dream, web scraping often turns into a nightmare. Let’s look at the specific challenges developers face when they choose the scraping route.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Fragility of the DOM
&lt;/h3&gt;

&lt;p&gt;Websites are designed for humans, not robots. A website owner might decide to redesign their homepage, change a CSS class name, or switch from server-side rendering to a heavy client-side JavaScript framework like React or Vue.&lt;/p&gt;

&lt;p&gt;For a web scraper, these changes are catastrophic. A script that worked perfectly yesterday might return &lt;code&gt;null&lt;/code&gt; or throw errors today because the HTML structure changed. This forces you into a reactive cycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The site updates.&lt;/li&gt;
&lt;li&gt;Your app breaks.&lt;/li&gt;
&lt;li&gt;You spend hours debugging and rewriting selectors.&lt;/li&gt;
&lt;li&gt;Repeat next week.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. The IP Ban Whack-A-Mole
&lt;/h3&gt;

&lt;p&gt;Most major websites have defenses against scrapers. If you make too many requests from the same IP address, you’ll get blocked. To get around this, you have to manage proxy rotation, user-agent spoofing, and solve CAPTCHAs.&lt;/p&gt;

&lt;p&gt;Suddenly, you aren't just building a data parser; you're building a complex infrastructure just to act like a human. This adds latency, cost, and significant complexity to your codebase.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Legal Gray Area
&lt;/h3&gt;

&lt;p&gt;This is the big one that corporate legal teams worry about. Web scraping often violates a website's Terms of Service (ToS). While the legality of scraping public data is still being debated in courts around the world, the risk is undeniable.&lt;/p&gt;

&lt;p&gt;If you build your entire business model on scraping a competitor or a major publisher without permission, you are building on quicksand. Cease-and-desist letters are real, and they can shut down a project overnight.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Unstructured Chaos
&lt;/h3&gt;

&lt;p&gt;Even when scraping works, the output is raw text or HTML. You have to write complex logic to clean it up.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is that date format DD/MM/YYYY or MM/DD/YYYY?&lt;/li&gt;
&lt;li&gt;Did the author's name get mixed up with the byline?&lt;/li&gt;
&lt;li&gt;Why is the article body truncated?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turning raw HTML into a clean database entry requires constant maintenance and rigorous testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Joy of APIs: Stability, Structure, and Speed
&lt;/h2&gt;

&lt;p&gt;Now, contrast that scraping experience with using a dedicated news API. When you switch to a provider like &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt;, the friction disappears.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Structure by Default
&lt;/h3&gt;

&lt;p&gt;APIs deliver structured data. When you request a news article, you don't get a blob of HTML; you get a JSON object with clear keys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;published_at&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;author&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sentiment&lt;/code&gt; (often pre-calculated for you!)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;content&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't have to guess where the data is. It's exactly where the documentation says it will be. This makes integration trivial—often just a few lines of code in Python, JavaScript, or whatever language you prefer.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Legal Compliance and Peace of Mind
&lt;/h3&gt;

&lt;p&gt;API providers have already done the work of sourcing data legitimately. When you pay for an API subscription, you are paying for the right to use that data. You don't have to worry about a lawsuit because you hit a server too hard or ignored a &lt;code&gt;robots.txt&lt;/code&gt; file. The API provider handles the sourcing relationships and compliance.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Advanced Features Out of the Box
&lt;/h3&gt;

&lt;p&gt;This is where smart developers really see the value. A scraper gets you the text, but an API often gives you intelligence.&lt;/p&gt;

&lt;p&gt;Take &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt; for example. It doesn't just give you the news; it enriches it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment Analysis:&lt;/strong&gt; Is this article positive, negative, or neutral?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Topic Modeling:&lt;/strong&gt; What is this article actually about? (e.g., Finance, Tech, Sports)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Duplicate Detection:&lt;/strong&gt; Do you really need to see the same AP wire story reprinted on 50 different sites? An API can filter that noise out for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translation:&lt;/strong&gt; Accessing news in 60 languages allows you to build truly global applications without needing a localization team.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Scalability
&lt;/h3&gt;

&lt;p&gt;Scrapers don't scale well. If you need to scrape 10 pages, it takes X seconds. If you need to scrape 1 million pages, you need a massive distributed system.&lt;/p&gt;

&lt;p&gt;With an API, scaling is usually just a matter of upgrading your plan. You can go from 100 requests a day to 100,000 without changing a single line of your code. The infrastructure is someone else's problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Results: What Happens When You Switch?
&lt;/h2&gt;

&lt;p&gt;Let’s look at a hypothetical scenario based on real feedback from developers who have made the switch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "Before" State:&lt;/strong&gt;\&lt;br&gt;
A startup team was building a market intelligence platform. They spent 40% of their engineering time maintaining a fleet of scrapers. Every time a major news outlet updated their layout, their dashboard would go blank, leading to angry customer emails. Their "data scientists" were actually spending most of their time acting as "data janitors," cleaning up messy HTML parses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Switch:&lt;/strong&gt;\&lt;br&gt;
They decided to integrate &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt; to handle their news feed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "After" State:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development Time:&lt;/strong&gt; The integration took less than a day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance:&lt;/strong&gt; Reduced to near zero. The API just works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Quality:&lt;/strong&gt; They instantly gained access to 500,000 sources, far more than they were scraping manually.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;New Features:&lt;/strong&gt; Because the API provided sentiment scores, they were able to launch a new "Market Mood" feature in their app within a week—something that would have taken months to build themselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result? The developers were happier because they were building features, not fixing broken scripts. The business owners were happier because their product was more reliable and had better data. And the legal team breathed a sigh of relief.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developer Satisfaction: A Clear Winner
&lt;/h2&gt;

&lt;p&gt;If you browse developer forums or talk to CTOs, the consensus is clear. While web scraping is a useful skill for one-off tasks or academic research, it is not a foundation for a serious business.&lt;/p&gt;

&lt;p&gt;Developers who switch to APIs report significantly higher satisfaction levels. They feel more productive. They spend less time on "grunt work" and more time on "logic work."&lt;/p&gt;

&lt;p&gt;There is a distinct joy in reading clear documentation, sending a request, and getting a perfect 200 OK response with exactly the data you need. It feels professional. It feels secure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why APITube is the Smart Choice
&lt;/h2&gt;

&lt;p&gt;If you've decided that an API is the way to go, the next question is: "Which one?"&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt; stands out for several reasons that appeal directly to the "smart developer" mindset:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Massive Reach:&lt;/strong&gt; Access to over 500,000 sources in 177 countries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multilingual:&lt;/strong&gt; Native support for 60 languages means you aren't limited to English-speaking markets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Historical Data:&lt;/strong&gt; Need to backtest a trading strategy? You can access 10 years of historical news data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smart Filtering:&lt;/strong&gt; With over 65 filtering parameters, you can drill down to exactly what you need—whether it's "crypto news from Brazil in Portuguese" or "tech news about AI with positive sentiment."&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How to Get Started
&lt;/h3&gt;

&lt;p&gt;Getting started with an API is incredibly simple compared to writing a scraper.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Registration:&lt;/strong&gt; &lt;a href="https://apitube.io/#sign-up" rel="noopener noreferrer"&gt;Sign up&lt;/a&gt; and get your free API key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;First Request:&lt;/strong&gt; You can make your first call in minutes. Here is a conceptual example of how easy it is to get the latest news about "Artificial Intelligence":
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import requests

url = "https://api.apitube.io/v1/news/everything"
querystring = {
    "q": "Artificial Intelligence",
    "language": "en",
    "api_key": "YOUR_API_KEY"
}

response = requests.request("GET", url, params=querystring)
print(response.json())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it. No parsing HTML. No proxies. Just data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary and Recommendations
&lt;/h2&gt;

&lt;p&gt;The era of "move fast and break things" is evolving into "move fast and build stable things."&lt;/p&gt;

&lt;p&gt;Web scraping had its time, but for modern, data-driven applications, it is simply too costly in terms of maintenance, risk, and developer sanity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt;\&lt;br&gt;
If your application relies on news data, blog posts, or media monitoring, stop building scrapers. It is a sunk cost trap.&lt;/p&gt;

&lt;p&gt;Instead, leverage a dedicated news API. You will gain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; Uptime you can trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Efficiency:&lt;/strong&gt; Structured data ready for use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligence:&lt;/strong&gt; Built-in sentiment and topic analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale:&lt;/strong&gt; Global coverage without the infrastructure headache.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a solution that offers a perfect balance of power, ease of use, and comprehensive coverage, we highly recommend checking out &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt;. It’s the tool that smart developers are using to power the next generation of intelligent applications.&lt;/p&gt;

&lt;p&gt;Don't let a broken scraper slow you down. Switch to an API and start building what matters.&lt;/p&gt;

</description>
      <category>newsapi</category>
      <category>scrapping</category>
    </item>
    <item>
      <title>Your News Scraper Just Broke Again? Here's the Real Solution</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Tue, 27 Jan 2026 06:05:03 +0000</pubDate>
      <link>https://dev.to/apitube/your-news-scraper-just-broke-again-heres-the-real-solution-3e60</link>
      <guid>https://dev.to/apitube/your-news-scraper-just-broke-again-heres-the-real-solution-3e60</guid>
      <description>&lt;p&gt;It’s 2:00 AM. Your monitoring dashboard is flashing red. The beautiful, custom-built Python script you deployed last week—the one designed to aggregate breaking financial news for your trading algorithm—has crashed. Again.&lt;/p&gt;

&lt;p&gt;You dig into the logs and see the dreaded error messages. Maybe the target website changed a &lt;code&gt;div&lt;/code&gt; class name. maybe Cloudflare decided your requests look suspicious and threw up a CAPTCHA. Or perhaps the site simply updated its layout, rendering your meticulously crafted CSS selectors useless.&lt;/p&gt;

&lt;p&gt;This scenario is the bane of every developer who relies on web scraping for news data. You wanted a stream of information, but what you got was a second job maintaining fragile code. The internet is dynamic; websites are designed for humans, not robots. When you try to force raw HTML into structured data using brittle scripts, you are fighting a losing battle against the natural evolution of the web.&lt;/p&gt;

&lt;p&gt;But it doesn't have to be this way. There is a difference between "scraping" and "data acquisition." One is a constant game of cat-and-mouse; the other is a reliable infrastructure strategy. If you are tired of patching broken scrapers, it's time to look at the real solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why News Scrapers Break (It’s Not Just Bad Code)
&lt;/h2&gt;

&lt;p&gt;When a scraper fails, it’s easy to blame the code. "I should have used a better selector," or "I need to rotate my user agents more frequently." While optimization helps, the fundamental problem isn't usually the script itself—it's the environment it operates in.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Dynamic Web
&lt;/h3&gt;

&lt;p&gt;Modern news websites are no longer static HTML pages delivered from a server. They are complex Single Page Applications (SPAs) built with React, Vue, or Angular. content is loaded dynamically via JavaScript. If your scraper is just making a simple &lt;code&gt;GET&lt;/code&gt; request, it might see an empty page because the content hasn't rendered yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anti-Scraping Defenses
&lt;/h3&gt;

&lt;p&gt;Publishers are protective of their content. To prevent server overload and protect intellectual property, they employ sophisticated anti-bot measures. These systems look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High request rates:&lt;/strong&gt; Too many hits from one IP address in a short time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser fingerprinting:&lt;/strong&gt; inconsistencies in your HTTP headers or TLS handshakes that reveal you aren't using a real browser.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavioral analysis:&lt;/strong&gt; Mouse movements (or lack thereof) that don't match human patterns.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Structural volatility
&lt;/h3&gt;

&lt;p&gt;News sites are constantly A/B testing. They change layouts to optimize ad revenue or user engagement. A slight change in the DOM structure—renaming a class from &lt;code&gt;.article-body&lt;/code&gt; to &lt;code&gt;.story-content&lt;/code&gt;—is enough to break a regex or BeautifulSoup parser instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common "Solutions" and Why They Fail
&lt;/h2&gt;

&lt;p&gt;When faced with these challenges, developers often try to bandage the wound rather than cure the disease. Here are the typical band-aid solutions and why they eventually peel off.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Regex Warrior
&lt;/h3&gt;

&lt;p&gt;Regular Expressions (Regex) are powerful for text searching, but using them to parse HTML is a cardinal sin in programming. HTML is not a regular language. As soon as the nesting depth changes or a new attribute is added to a tag, your regex breaks. It is fragile, unreadable, and impossible to maintain at scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Simple Library (BeautifulSoup/Cheerio)
&lt;/h3&gt;

&lt;p&gt;Libraries like BeautifulSoup (Python) or Cheerio (Node.js) are excellent for parsing static HTML. However, they cannot execute JavaScript. As mentioned earlier, if the news site loads content via AJAX after the initial page load, these libraries will scrape nothing but empty containers.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The "Rotating Proxy" Fix
&lt;/h3&gt;

&lt;p&gt;"My IP got banned? I'll just buy a pool of proxies!" While necessary, proxies alone aren't enough. If your scraper leaks its identity through headers or behaves robotically, you will still get blocked, just from a different IP address. Plus, managing proxy rotation, health checks, and costs is an infrastructure nightmare in itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Real Solution: Robust Scraping Infrastructure
&lt;/h2&gt;

&lt;p&gt;To get reliable news data, you need to stop thinking about &lt;em&gt;scripts&lt;/em&gt; and start thinking about &lt;em&gt;infrastructure&lt;/em&gt;. A robust solution isn't just code that grabs text; it's a system that mimics human behavior, adapts to changes, and handles the heavy lifting of rendering.&lt;/p&gt;

&lt;p&gt;This is where the distinction between "building" and "buying" becomes critical. A true enterprise-grade scraping infrastructure involves several complex layers working in unison.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Components of a Robust Solution
&lt;/h2&gt;

&lt;p&gt;If you were to engineer the "perfect" scraper that never breaks, here is what you would need to build and maintain:&lt;/p&gt;

&lt;h3&gt;
  
  
  Headless Browsers
&lt;/h3&gt;

&lt;p&gt;You need to run actual browsers (like Chrome or Firefox) in headless mode (without a UI) using tools like Puppeteer, Playwright, or Selenium. This allows you to render JavaScript, wait for network idle states, and interact with the page just like a user would. This solves the SPA problem but introduces significant CPU and memory overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intelligent Proxy Networks
&lt;/h3&gt;

&lt;p&gt;You cannot rely on datacenter proxies (AWS, DigitalOcean IPs) because they are easily flagged. You need residential proxies—IP addresses assigned to real home devices. Furthermore, you need logic to rotate them intelligently based on the target site’s sensitivity and geolocation requirements.&lt;/p&gt;

&lt;h3&gt;
  
  
  CAPTCHA Solvers
&lt;/h3&gt;

&lt;p&gt;Eventually, you will hit a CAPTCHA. A robust system needs an automated way to solve or bypass these challenges, often integrating with third-party solving services or using AI to recognize the puzzles.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI-Driven Parsing
&lt;/h3&gt;

&lt;p&gt;This is the frontier of scraping. Instead of hard-coding CSS selectors (&lt;code&gt;div.content &amp;gt; p&lt;/code&gt;), you use Machine Learning models trained to visually identify the "headline," "author," and "body" of an article regardless of the underlying HTML structure. This makes the scraper resilient to layout changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building vs. Buying: The Economics of Data Acquisition
&lt;/h2&gt;

&lt;p&gt;So, you have two choices: build this infrastructure yourself, or use a dedicated API.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Build It Yourself" Path
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Full control over the code.&lt;/li&gt;
&lt;li&gt;No per-request cost (excluding server/proxy costs).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hidden Costs:&lt;/strong&gt; You aren't just paying for servers; you are paying for the engineering time to maintain the scrapers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability Issues:&lt;/strong&gt; Spinning up thousands of headless browsers requires massive compute resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Whack-a-Mole" Factor:&lt;/strong&gt; You will spend your mornings fixing broken parsers instead of building your product.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The "Managed API" Path
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; The provider handles the anti-bot bypasses and DOM changes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured Data:&lt;/strong&gt; You get clean JSON, not raw HTML.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Focus:&lt;/strong&gt; You spend time analyzing data, not acquiring it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct Cost:&lt;/strong&gt; You pay a subscription fee.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most businesses and developers, the opportunity cost of building and maintaining a scraper far outweighs the cost of a subscription to a dedicated News API.&lt;/p&gt;

&lt;h2&gt;
  
  
  How APITube.io Solves This
&lt;/h2&gt;

&lt;p&gt;This is where &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube.io&lt;/a&gt; enters the picture. We didn't just build a scraper; we built a global intelligence engine. We realized that developers need consistency, not just raw access.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Global Reach, Zero Maintenance
&lt;/h3&gt;

&lt;p&gt;APITube aggregates news from over &lt;strong&gt;500,000 sources&lt;/strong&gt; across 177 countries in 60 languages. Whether you need financial news from Bloomberg or local reports from a small outlet in Austria, we have it indexed. You don't need to write a single line of code to handle the scraping logic for these half-million sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Structured, Normalized Data
&lt;/h3&gt;

&lt;p&gt;We turn the chaos of the web into order. When you make a request to our API, you don't get HTML. You get a clean, standardized JSON response containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title and Body:&lt;/strong&gt; Cleanly extracted text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment Analysis:&lt;/strong&gt; AI-driven scoring (Positive/Negative/Neutral).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Entities:&lt;/strong&gt; Extracted people, organizations, and locations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multimedia:&lt;/strong&gt; Links to images and videos.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Advanced Filtering
&lt;/h3&gt;

&lt;p&gt;Instead of scraping a homepage and filtering the results yourself, APITube lets you filter &lt;em&gt;before&lt;/em&gt; you fetch. You can query by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Category:&lt;/strong&gt; Tech, Business, Sports, etc.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment:&lt;/strong&gt; Only show me negative news about Competitor X.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Date and Location:&lt;/strong&gt; What happened in Paris last Tuesday?&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Enterprise-Grade Reliability
&lt;/h3&gt;

&lt;p&gt;We handle the proxies, the headless browsers, and the parsing logic. If a source changes its layout, our system adapts. You just consume the API endpoint, and the data flows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Studies: Success Stories from the Field
&lt;/h2&gt;

&lt;p&gt;Real businesses are already moving away from brittle scrapers to APITube's robust API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Financial Analyst's Edge&lt;/strong&gt;\&lt;br&gt;
&lt;em&gt;Zhao Yi's&lt;/em&gt; analytics team was spending 40% of their time fixing Python scripts that scraped market news. By switching to APITube, they automated their trend analysis dashboard. "Our analytics team relies on NEWS API for accurate trend analysis," Zhao reports. They now focus on building predictive models rather than parsing HTML.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Journalism in Real-Time&lt;/strong&gt;\&lt;br&gt;
For journalists like &lt;em&gt;Jake Helmold&lt;/em&gt;, speed is everything. Waiting for a scraper to finish a batch job isn't an option. "As a journalist, NEWS API has become an indispensable part of my research toolkit," says Jake. The ability to search through historical data (up to 10 years back) and receive real-time updates allows for deeper, faster reporting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seamless Integration for Developers&lt;/strong&gt;\&lt;br&gt;
&lt;em&gt;Chris Bates&lt;/em&gt; needed to integrate news feeds into a mobile app quickly. Building a backend scraper would have delayed the launch by months. "Great and quick integration with APITube!" Chris noted. Using our SDKs for Python, JavaScript, or Java, developers can get their first request working in under 5 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Future of News Scraping
&lt;/h2&gt;

&lt;p&gt;The cat-and-mouse game between scrapers and websites will only get harder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI-Generated Content &amp;amp; Detection&lt;/strong&gt;\&lt;br&gt;
As the web fills with AI-generated content, distinguishing valuable news from noise will be the next major challenge. Simple scrapers won't be able to tell the difference. Advanced APIs like APITube are already implementing AI to detect and categorize content quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semantic Search&lt;/strong&gt;\&lt;br&gt;
Keyword matching is becoming obsolete. The future is vector search—understanding the &lt;em&gt;meaning&lt;/em&gt; behind a query, not just the text. "Show me news about Apple" shouldn't just return results with the word "Apple," but should understand if you mean the fruit or the tech giant based on context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop Fixing, Start Building
&lt;/h2&gt;

&lt;p&gt;You faced a clear goal: getting reliable news data to power your product or analysis. You faced the difficulties of IP bans, broken selectors, and maintenance nightmares. The result of doing it yourself is often frustration and wasted time.&lt;/p&gt;

&lt;p&gt;The result of using a dedicated solution like APITube is reliability. You get closer to your goal of data-driven insights without the headache of infrastructure management.&lt;/p&gt;

&lt;p&gt;If your scraper broke again today, take it as a sign. Stop fighting the internet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ready to switch to a solution that works?&lt;/strong&gt;\&lt;br&gt;
&lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;Sign up for APITube&lt;/a&gt; today and get your free API key. Make your first request in minutes and see the difference between broken code and structured data.&lt;/p&gt;

</description>
      <category>news</category>
      <category>newsapi</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Stop Scraping: How One API Unlocks 500,000+ News Sources Instantly</title>
      <dc:creator>APITube News API</dc:creator>
      <pubDate>Tue, 27 Jan 2026 05:57:33 +0000</pubDate>
      <link>https://dev.to/apitube/stop-scraping-how-one-api-unlocks-500000-news-sources-instantly-560</link>
      <guid>https://dev.to/apitube/stop-scraping-how-one-api-unlocks-500000-news-sources-instantly-560</guid>
      <description>&lt;p&gt;Building a data-driven application is an exciting journey. You have a vision of a dashboard that predicts stock market trends based on global events, or perhaps a media monitoring tool that alerts brands the second they are mentioned online. The goal is clear: you want to ingest the world's information and turn it into actionable insights.&lt;/p&gt;

&lt;p&gt;However, developers and data scientists often hit a massive wall immediately after starting. The internet is a messy, unstructured place. To get the data you need, you might start by writing a web scraper. It works for one site. Then you add another. Then another. Suddenly, you realize that to get a truly global perspective, you need to integrate with thousands of sources.&lt;/p&gt;

&lt;p&gt;The reality of maintaining hundreds—let alone thousands—of individual integrations is a logistical nightmare. This blog post explores the journey from the frustration of manual data collection to the relief of finding a unified solution. We will look at how a single tool, &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube&lt;/a&gt;, can replace 500,000 individual integrations, allowing you to focus on building your product rather than fixing broken scrapers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal: Global Coverage Without the Headache
&lt;/h2&gt;

&lt;p&gt;When you set out to build a market intelligence platform or a news aggregator, the objective is usually ambitious. You aren't satisfied with just checking the New York Times or the BBC. You want the long-tail data. You want to know what local newspapers in Brazil are saying about agricultural exports, or what tech blogs in Japan are saying about robotics.&lt;/p&gt;

&lt;p&gt;The target is &lt;strong&gt;comprehensive situational awareness&lt;/strong&gt;. To achieve this, your data pipeline needs to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Real-time:&lt;/strong&gt; Yesterday’s news is history, not data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multilingual:&lt;/strong&gt; Business happens in every language, not just English.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured:&lt;/strong&gt; You need clean JSON, not messy HTML soup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ambition is high. You want to create a system where a user can type a query and instantly receive relevant articles from 177 countries. But as many developers discover, the gap between "I want this data" and "I have this data" is filled with technical obstacles.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Struggle: The Reality of Web Scraping
&lt;/h2&gt;

&lt;p&gt;If you have ever tried to build your own news aggregator from scratch, you know the pain. You start with Python and Beautiful Soup or Selenium. You write a script to scrape a popular tech news site. It works perfectly—for three days.&lt;/p&gt;

&lt;p&gt;Then, the site updates its DOM structure. Your script breaks. You fix it.&lt;/p&gt;

&lt;p&gt;Next, you get blocked. The site realized you were a bot and banned your IP address. Now you need to manage a proxy rotation service.&lt;/p&gt;

&lt;p&gt;Then comes the CAPTCHA. You need a solver service.&lt;/p&gt;

&lt;p&gt;Now multiply this process by 500,000. It is statistically impossible for a single team to maintain custom scrapers for 500,000 news sources. The "joy" of coding quickly turns into the misery of maintenance. You aren't building an AI product anymore; you are just a digital janitor cleaning up broken links and managing server errors.&lt;/p&gt;

&lt;p&gt;This is the "build vs. buy" dilemma at its most extreme. The difficulty isn't just getting the data; it's the sheer unpredictability of the sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: A Unified Gateway to the World's News
&lt;/h2&gt;

&lt;p&gt;This is where the concept of a News API changes the game. Instead of building the infrastructure to go out and fetch the news, you simply connect to a service that has already done the heavy lifting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube&lt;/a&gt; acts as a single access point to over 500,000 news sources. It standardizes the chaos of the web into a clean, consistent format.&lt;/p&gt;

&lt;h3&gt;
  
  
  How it works
&lt;/h3&gt;

&lt;p&gt;Imagine replacing half a million lines of custom scraping code with a single REST API call. That is the core value proposition here. You send a request specifying what you are looking for—keywords, dates, location, language—and the API returns a structured list of articles.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Joy" of Integration
&lt;/h3&gt;

&lt;p&gt;For a developer, the feeling of switching from scraping to a News API is pure relief. The friction disappears.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No more IP bans:&lt;/strong&gt; The API handles the connections.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No more parsing HTML:&lt;/strong&gt; You get clean data fields like &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;body&lt;/code&gt;, &lt;code&gt;published_at&lt;/code&gt;, and &lt;code&gt;sentiment&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No more language barriers:&lt;/strong&gt; The system supports 60 languages, meaning you don't need to write separate parsers for Arabic, Mandarin, or Spanish sites.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is that you can implement a feature in an afternoon that would have previously taken months of engineering time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep Dive: Features That Replace Manual Work
&lt;/h2&gt;

&lt;p&gt;It's not just about getting the text of an article. To truly replace a sophisticated internal scraping operation, an API needs to provide metadata and intelligence. Here is what you get out of the box, which you would otherwise have to build yourself.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Advanced Filtering and Search
&lt;/h3&gt;

&lt;p&gt;When you are dealing with 500,000 sources, noise is a problem. You don't want &lt;em&gt;all&lt;/em&gt; the news; you want the &lt;em&gt;right&lt;/em&gt; news. APITube offers &lt;strong&gt;over 65 filters&lt;/strong&gt;. You can drill down by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Location:&lt;/strong&gt; Search news specific to a country or region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment:&lt;/strong&gt; Filter for only positive or negative stories (crucial for brand reputation management).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source Rank:&lt;/strong&gt; Prioritize high-authority publications over obscure blogs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Built-in NLP and AI
&lt;/h3&gt;

&lt;p&gt;If you scrape a site yourself, you just get text. You then have to feed that text into a separate Natural Language Processing (NLP) engine to understand it.&lt;/p&gt;

&lt;p&gt;APITube integrates this step. The API provides &lt;strong&gt;Sentiment Analysis&lt;/strong&gt; and &lt;strong&gt;Topic Modeling&lt;/strong&gt; directly in the response. This means the data arrives pre-analyzed. You know immediately if an article is critical of a specific stock or supportive of a political policy, without running your own expensive ML models.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Historical Data Access
&lt;/h3&gt;

&lt;p&gt;Sometimes the goal isn't just monitoring today, but analyzing the past. Building a historical archive of the web is incredibly expensive in terms of storage. APITube provides access to &lt;strong&gt;10 years of historical data&lt;/strong&gt;. This allows financial analysts to backtest trading strategies against past news events without needing to store petabytes of data themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result: Accelerating Innovation
&lt;/h2&gt;

&lt;p&gt;So, what is the outcome of switching to a dedicated News API?&lt;/p&gt;

&lt;p&gt;For the developers and businesses we've observed, the result is a dramatic increase in velocity. Instead of spending 80% of their time on data acquisition and 20% on analysis, they flip the ratio. They spend 100% of their time building value for their users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Success Indicators:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Speed to Market:&lt;/strong&gt; New features involving news feeds can launch in days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reliability:&lt;/strong&gt; The data stream doesn't break when a publisher updates their website theme.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scalability:&lt;/strong&gt; You can go from tracking 100 topics to 10,000 topics instantly without adding more servers.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The feedback from users is generally one of satisfaction—specifically, the satisfaction of things "just working." The technical complexity is abstracted away, leaving a clean interface for innovation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Cases: Who Needs 500,000 Sources?
&lt;/h2&gt;

&lt;p&gt;You might be wondering if you really need access to half a million sources. The answer depends on your industry, but for many, breadth is just as important as depth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Financial Analysis and Algo-Trading
&lt;/h3&gt;

&lt;p&gt;Markets move on information. If a factory shuts down in Vietnam or a strike begins in Germany, that information hits local news sources first. By monitoring global feeds in real-time, financial algorithms can detect risks and opportunities hours before they appear in mainstream Western media.&lt;/p&gt;

&lt;h3&gt;
  
  
  Media Monitoring and PR
&lt;/h3&gt;

&lt;p&gt;For Public Relations professionals, missing a negative story can be a disaster. A comprehensive API allows agencies to track brand mentions across the entire web—not just major outlets, but niche blogs and regional papers—to manage reputation effectively.&lt;/p&gt;

&lt;h3&gt;
  
  
  AI and LLM Training
&lt;/h3&gt;

&lt;p&gt;Large Language Models (LLMs) are hungry for data. To train a model to understand current events or specific industry jargon, you need a massive, diverse stream of high-quality text. This API serves as a firehose of fresh content to keep models up to date.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started: A Recommendation
&lt;/h2&gt;

&lt;p&gt;If you are looking to integrate news data, the recommendation is simple: &lt;strong&gt;Don't build it yourself.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The complexity of the modern web makes manual aggregation a losing battle. The smart move is to leverage specialized infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple Integration Steps
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get an API Key:&lt;/strong&gt; &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;Sign up at APITube&lt;/a&gt; to get your credentials.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Choose Your Language:&lt;/strong&gt; Whether you use Python, Java, JavaScript, PHP, or Go, there is an SDK or library ready for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make Your First Request:&lt;/strong&gt; Use the documentation to construct a query. Start simple, perhaps searching for a specific company name or industry keyword.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refine:&lt;/strong&gt; Use the filters to narrow down by language or sentiment.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is it legal to use news data from an API?
&lt;/h3&gt;

&lt;p&gt;APITube collects and compiles publicly accessible web pages. It provides metadata, summaries, and links to the original content. It does not claim copyright over the articles. It acts as a search and indexing engine, similar to how Google indexes the web.&lt;/p&gt;

&lt;h3&gt;
  
  
  How fresh is the data?
&lt;/h3&gt;

&lt;p&gt;The system is designed for real-time applications. With continuous crawling of 500,000+ sources, new articles appear in the API feed shortly after they are published on the source websites.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I try it for free?
&lt;/h3&gt;

&lt;p&gt;Yes. There is a free plan available that resets credits daily. This allows you to test the integration, check the data quality, and see if it fits your specific use case before committing to a paid subscription.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if I need more data than the plan allows?
&lt;/h3&gt;

&lt;p&gt;The API operates on a credit system. If you scale up and need more requests, you can easily upgrade your plan. However, even on the free plan, the daily reset ensures you can keep developing and testing without interruption.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it support non-Latin scripts?
&lt;/h3&gt;

&lt;p&gt;Absolutely. The API supports UTF-8 and handles 60 languages, including those with non-Latin scripts like Chinese, Japanese, Arabic, and Cyrillic. This is essential for truly global monitoring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The goal of accessing the world's news data is no longer a technical impossibility for small teams. The difficulties of web scraping—the blocks, the bans, and the broken scripts—are problems of the past.&lt;/p&gt;

&lt;p&gt;By using &lt;a href="https://apitube.io" rel="noopener noreferrer"&gt;APITube&lt;/a&gt;, you achieve the result of having a global, real-time, and intelligent news feed integrated directly into your application. The satisfaction comes from reliability and the freedom to focus on what matters: your product's unique value.&lt;/p&gt;

&lt;p&gt;If you are ready to stop fixing scrapers and start analyzing data, we recommend exploring the &lt;a href="https://docs.apitube.io/" rel="noopener noreferrer"&gt;APITube documentation&lt;/a&gt; today.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
