<?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: RSS Atlas</title>
    <description>The latest articles on DEV Community by RSS Atlas (@rss-atlas).</description>
    <link>https://dev.to/rss-atlas</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%2F4076571%2Fd5b3f4a5-7e52-4391-bfc1-0b483d4c5b46.png</url>
      <title>DEV Community: RSS Atlas</title>
      <link>https://dev.to/rss-atlas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rss-atlas"/>
    <language>en</language>
    <item>
      <title>How the rankings work — and what we refuse to count</title>
      <dc:creator>RSS Atlas</dc:creator>
      <pubDate>Fri, 14 Aug 2026 16:57:52 +0000</pubDate>
      <link>https://dev.to/rss-atlas/how-the-rankings-work-and-what-we-refuse-to-count-4d6d</link>
      <guid>https://dev.to/rss-atlas/how-the-rankings-work-and-what-we-refuse-to-count-4d6d</guid>
      <description>&lt;p&gt;Ads in RSS Atlas &lt;strong&gt;take no part in the ranking calculation&lt;/strong&gt;. There are ad slots, but they are separate and labelled "Ad".&lt;/p&gt;

&lt;p&gt;Anyone can write that sentence, so here is what is actually counted — and, more importantly, what is deliberately not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three signals
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What it represents&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Favorites&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Someone decided they wanted to come back to it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Page views&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The feed's detail page was opened&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI fetches&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An AI tool retrieved the feed over MCP&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are combined with &lt;strong&gt;time decay&lt;/strong&gt;, so recent interest counts for more. A feed read heavily last year sits below one being read this week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ordering is public&lt;/strong&gt;: favorites weigh most, then views, then AI fetches. The &lt;strong&gt;coefficients and thresholds are not&lt;/strong&gt;. Publishing the exact numbers would amount to publishing which signal is cheapest to inflate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why count AI fetches at all
&lt;/h3&gt;

&lt;p&gt;This is the axis other directories do not have.&lt;/p&gt;

&lt;p&gt;Search traffic is falling while assistants increasingly compose the answer, which makes &lt;strong&gt;what the AI reads&lt;/strong&gt; a separate fact from what humans open. Feeds exist — reference documentation is the obvious case — that people rarely click through to but agents pull repeatedly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is deliberately excluded
&lt;/h2&gt;

&lt;p&gt;This half matters more.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Ads and sponsor slots.&lt;/strong&gt; Inserted at the render layer only; they touch neither the score nor the ordering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payment.&lt;/strong&gt; There is no way to pay for a position. Slots may be sold later, but a slot is not a rank&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Article volume.&lt;/strong&gt; A site publishing ten posts a day has no claim over a good weekly one&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Age.&lt;/strong&gt; Having existed since 2005 says nothing about being worth reading now&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unreviewed feeds.&lt;/strong&gt; Submissions that have not passed review stay out of rankings entirely&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Making the numbers hard to inflate
&lt;/h2&gt;

&lt;p&gt;Publishing what you count invites people to manufacture it. AI fetches are the exposed surface, since there is an unauthenticated demo path, so:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deduplicated per client per day.&lt;/strong&gt; The same client pulling the same feed a thousand times counts once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribution scales logarithmically&lt;/strong&gt; in the number of distinct clients, so spinning up more of them does not pay linearly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day-over-day outliers are held back.&lt;/strong&gt; A sudden spike has that day's increment withheld pending a look&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of that is airtight, which is why &lt;strong&gt;AI fetches carry the smallest weight&lt;/strong&gt; of the three.&lt;/p&gt;

&lt;h2&gt;
  
  
  No ads in MCP responses
&lt;/h2&gt;

&lt;p&gt;When an AI tool calls &lt;code&gt;get_rankings&lt;/code&gt;, what comes back is &lt;strong&gt;purely organic&lt;/strong&gt;. The sponsored-placement data is not consulted by that query at all.&lt;/p&gt;

&lt;p&gt;The website shows ads. &lt;strong&gt;MCP responses, search results, OPML exports and badges never will.&lt;/strong&gt; Mixing advertising into the data an agent reads would remove the reason for this directory to exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking it yourself
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://rssatlas.com/rankings" rel="noopener noreferrer"&gt;rankings&lt;/a&gt; table shows position, feed count, favorites, AI fetches and score as columns. &lt;strong&gt;You can follow why something sits where it does&lt;/strong&gt; without taking our word for it.&lt;/p&gt;

&lt;p&gt;Category and tag rankings work the same way, ordered by the summed score of the feeds they contain.&lt;/p&gt;




&lt;p&gt;The transparency is not altruism. &lt;strong&gt;A directory whose ordering cannot be trusted has no value left&lt;/strong&gt; — the moment positions become purchasable, it is a list of adverts, not a directory.&lt;/p&gt;

</description>
      <category>rss</category>
      <category>ai</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Letting an AI ask 'what shipped in this field this week?'</title>
      <dc:creator>RSS Atlas</dc:creator>
      <pubDate>Thu, 13 Aug 2026 16:48:03 +0000</pubDate>
      <link>https://dev.to/rss-atlas/letting-an-ai-ask-what-shipped-in-this-field-this-week-1nk0</link>
      <guid>https://dev.to/rss-atlas/letting-an-ai-ask-what-shipped-in-this-field-this-week-1nk0</guid>
      <description>&lt;p&gt;Ask an assistant about recent developments and you get one of two things: whatever was in its training data, or the top few web search results. What you cannot ask is the thing you actually want — &lt;em&gt;"what came out in Rust tooling in the last two weeks"&lt;/em&gt; — scoped to a field, across the sources that cover it.&lt;/p&gt;

&lt;p&gt;Those sources exist. They are RSS feeds. They are just not reachable from an AI tool.&lt;/p&gt;

&lt;p&gt;So I built a directory of them and gave it an MCP server. This post is about four decisions that only became obvious after shipping, not a tour of the product.&lt;/p&gt;

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

&lt;p&gt;2,907 RSS feeds across 19 languages and 37 categories, holding 51,483 articles. An MCP server exposes it to AI tools.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add &lt;span class="nt"&gt;--transport&lt;/span&gt; http rss-atlas https://mcp.rssatlas.com/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other clients take it in &lt;code&gt;settings.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rss-atlas"&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;"httpUrl"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://mcp.rssatlas.com/mcp"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It runs on Cloudflare Workers (site and MCP server), Supabase (Postgres), and Cloud Run Jobs (crawler). There is a no-auth demo at &lt;code&gt;https://mcp.rssatlas.com/demo&lt;/code&gt; if you want to poke at it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Four tools, and adding a parameter instead of a fifth
&lt;/h2&gt;

&lt;p&gt;The server exposes exactly four tools: &lt;code&gt;search_feeds&lt;/code&gt;, &lt;code&gt;get_feed&lt;/code&gt;, &lt;code&gt;get_articles&lt;/code&gt;, &lt;code&gt;get_rankings&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Building "fetch articles from only my favourited feeds" wanted to be a fifth tool — &lt;code&gt;get_favorite_articles&lt;/code&gt;. It became a parameter instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;favorites_only&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Restrict results to the signed-in user's favorites&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is that every added tool is another choice the model has to get right. With &lt;code&gt;get_articles&lt;/code&gt; and &lt;code&gt;get_favorite_articles&lt;/code&gt; side by side, the model decides between them on every call. As a parameter it is not a decision at all — it is an argument. One less branch.&lt;/p&gt;

&lt;p&gt;The rule now is: before adding a tool, check whether an existing tool's parameters can carry it.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Auth failures must be HTTP 401, not a 200 with an error
&lt;/h2&gt;

&lt;p&gt;This one cost the most time.&lt;/p&gt;

&lt;p&gt;MCP lets a tool return &lt;code&gt;isError: true&lt;/code&gt; in its result. I used that for unauthenticated requests at first — HTTP 200, error in the payload.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clients do not show a sign-in prompt for that.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;They decide "authentication needed" from &lt;strong&gt;HTTP 401 plus a &lt;code&gt;WWW-Authenticate&lt;/code&gt; header&lt;/strong&gt;. A 200 means the call succeeded as far as the transport is concerned, so whatever is inside gives the client no reason to start an auth flow. To the user it looks like a tool that throws a confusing error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;401&lt;/span&gt; &lt;span class="ne"&gt;Unauthorized&lt;/span&gt;
&lt;span class="na"&gt;WWW-Authenticate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer resource_metadata="https://mcp.rssatlas.com/.well-known/oauth-protected-resource"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The authorization server is discovered from that header. So: &lt;strong&gt;authentication is expressed at the HTTP layer, not in tool results.&lt;/strong&gt; Ordinary failures inside a tool call — no matches, bad argument — are still fine as &lt;code&gt;isError&lt;/code&gt;. Do not mix the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Take the user id out of the arguments
&lt;/h2&gt;

&lt;p&gt;For anything scoped to a user, the user id is not a tool parameter. The only identity used is &lt;code&gt;sub&lt;/code&gt; from the verified access token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;favorites_only&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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;fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;favorites_only requires a signed-in user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validation would have caught a wrong id. But with MCP &lt;strong&gt;the model composes the arguments&lt;/strong&gt;, so an argument that can name another user is a shape that invites the mistake. If the field does not exist, the class of bug does not exist.&lt;/p&gt;

&lt;p&gt;Structure beats validation for anything that must never be passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Workers Rate Limiting binding does not block
&lt;/h2&gt;

&lt;p&gt;Cloudflare Workers has a Rate Limiting binding. I used it for the MCP server's limits.&lt;/p&gt;

&lt;p&gt;It did not work — more precisely, it did not &lt;em&gt;block&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The docs say plainly that it is not intended for precise counting. It is distributed, counts do not settle instantly across edges, and near the threshold requests get through. It is for shaping rough traffic, not for reliably stopping the 31st call.&lt;/p&gt;

&lt;p&gt;Rewrote it on a Durable Object, which serialises to a single instance and therefore counts exactly. Measured afterwards: the 31st request returns 429 for authenticated users, the 11th for the demo.&lt;/p&gt;

&lt;p&gt;The habit that came out of this: &lt;strong&gt;after implementing a rate limit, hammer past the threshold and watch it actually refuse.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  No AI on the site itself
&lt;/h2&gt;

&lt;p&gt;Worth stating because it drove several decisions: there is no summarisation and no AI classification in the product. Classification is rules and thresholds. Summarising happens in your own LLM.&lt;/p&gt;

&lt;p&gt;The reason is that the reader is already paying for a model. Calling one server-side means they pay twice, indirectly. Over MCP the summary belongs on their side anyway — faster, cheaper, and their choice of model.&lt;/p&gt;

&lt;p&gt;For the same reason feed content is capped at &lt;strong&gt;title + 500-character excerpt + link&lt;/strong&gt;. Nothing is stored in full. You read it at the source, which is what RSS was for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: 500 healthy feeds silently stopped updating
&lt;/h2&gt;

&lt;p&gt;Not MCP, but it ate a comparable amount of time.&lt;/p&gt;

&lt;p&gt;Conditional GET plus redirects: &lt;code&gt;/feed&lt;/code&gt; → &lt;strong&gt;301&lt;/strong&gt; → &lt;code&gt;/feed/&lt;/code&gt; → &lt;strong&gt;304 Not Modified&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;304 is a 3xx. Treat "3xx means redirect" literally and you go looking for a &lt;code&gt;Location&lt;/code&gt; header, which a 304 does not have, and you error out.&lt;/p&gt;

&lt;p&gt;The nasty part is that &lt;strong&gt;only feeds that once succeeded well enough to store an ETag can hit this&lt;/strong&gt; — so your best-behaved sources break first, and because the listing still renders, they just quietly go stale.&lt;/p&gt;

&lt;p&gt;Fixing the redirect check to mean "3xx except 304" and requeueing took failures from 501 to 13.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Solve it with a parameter before adding a tool; every tool is a decision the model can get wrong&lt;/li&gt;
&lt;li&gt;Auth failures are HTTP 401 — &lt;code&gt;isError&lt;/code&gt; on a 200 never triggers a sign-in prompt&lt;/li&gt;
&lt;li&gt;Delete arguments that must never be passed; do not merely validate them&lt;/li&gt;
&lt;li&gt;The Workers Rate Limiting binding does not block. Use a Durable Object when the count must be right&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Demo, no auth required: &lt;code&gt;https://mcp.rssatlas.com/demo&lt;/code&gt; — and the directory is at &lt;a href="https://rssatlas.com" rel="noopener noreferrer"&gt;rssatlas.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>rss</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
