I built a Chrome extension to manage my 1,200+ Substack subscriptions
Get it free or build your own
Disclosure: This article was written by me, a human, because I just had to build a Chrome extension while working on outreach for another project. Claude helped edit the copy and repackage my existing specification into feature descriptions. Use the Substack API endpoints I’ve shared at your own risk.
The extension is free to anyone who subscribes to Wondering About AI. The discount code appears in the welcome email you receive right after you sign up. If you’re an existing subscriber, DM me for the code.
I checked Substack the other day and discovered I have more than 1,200 subscriptions. Yes, that’s right, I have more subscriptions than subscribers.
How did this happen? Well, I subscribe to most folks who subscribe to me, especially if they’re just starting out and have posted at least a few interesting-looking articles. I have a broad definition of “interesting,” and my massive subscription pool lets me peer outside my algorithmic bubble and wrestle with a steady stream of new ideas.
The problem was that I had no way to manage any of it.
When I shut down StackDigest, my production-grade analytics app that included subscription management tools, I felt the void immediately. I started missing articles from creators I loved because I could no longer easily get a list of their latest posts. Several former StackDigest users reached out feeling the same way, and my DMs became a hub for debate about the legal and technical risks of using Substack’s undocumented API in production systems.
For context: StackDigest was a platform that processed tens of thousands of Substack articles. It used vector databases, semantic search, and machine learning clustering to help users discover new newsletters, articles, and even platform-wide themes. At its peak, 165 users relied on it for AI-powered digests and competitive intelligence. But after legal review, I made the difficult decision to shut it down. Accessing platform content programmatically, especially at scale, could potentially violate Substack’s terms of use.
But it did occur to me that the risk of building a tool that helps users work with their own data locally, even if it tapped into Substack’s API, would be much lower. I ultimately wrote a specification for a Chrome extension that focused on data individual users’ “own,” relied on local storage, and didn’t interact with article content at all beyond collecting links.
And, after thinking more about the legal and compliance issues, I decided to build it. (You can download it here if you don’t want to wait for the CTA!)
Why a Chrome extension?
While I’m familiar now with how to build an enterprise-scalable production app, I know a lot less about smaller-scale development techniques. So I researched options for creating something that could:
Work entirely on the user’s own machine (no servers processing anyone’s data)
Only access data that the user already has access to (their subscriptions, their likes, their restacks)
Support something simple enough for non-technical Substack readers to use
Be built with components and coding languages that I already know how to use
Chrome extensions, which rely on HTML, CSS, and JavaScript, seemed like the perfect fit. They run locally in your browser, they can access the same authenticated session you already have with Substack, and they store data in your browser’s local storage, so nothing leaves your machine.
Plus, I’d never built one before, and I love learning new things.
How Chrome extensions work
If you’ve never looked under the hood of a Chrome extension, here’s the basic structure:
manifest.json—This is the configuration file that tells Chrome what your extension does, what permissions it needs, and where to find its various components. Think of it as the extension’s ID card.
popup/—The little window that appears when you click the extension icon. This is where most of the user interface lives. Mine has tabs for Dashboard, Subscriptions, Engagement, Collections, and Reading Lists.
background/—A service worker that runs in the background and handles things like API calls and data processing. It stays active even when the popup is closed.
lib/—Utility modules for things like API calls, storage helpers, and export functions.
For my extension, I used Manifest V3 (Chrome’s latest standard), Tailwind CSS for styling, and vanilla JavaScript instead of React or Vue, because I wanted to keep things simple.
What I wanted to build (and how I specified it)
I’ve learned (sometimes the hard way) that jumping straight into code with AI assistance leads to meandering, inconsistent results. So I started by writing a detailed specification, and I did it by working iteratively with Claude.
I kicked things off with an initial prompt that described the project and summarized the key features in just six bullet points:
View, tag, sort, and filter subscriptions
Export subscriptions as CSV with RSS feeds
Show stats about my subscription landscape
Track which newsletters I engage with most
Organize subscriptions into collections
Create reading lists from multiple newsletters
From there, Claude helped me expand it into a proper spec document including user flows, data models, UI guidelines, and technical architecture. The final specification was about 10 pages long and covered these main features:
The Dashboard shows a quick overview of all your subscriptions, including total count, breakdown of paid vs. free, and distribution by size tier.
The Subscriptions tab lists all your subscriptions with search and filter options. Each entry displays the newsletter name, author, subscriber count (when available), a widget for defining custom tags (like “Must read” or “Crocheting tips”), and any tags you’ve already added. Export to CSV gives you newsletter names, URLs, RSS feed URLs (useful for feed readers), author info, and subscriber counts.
The Engagement tab fetches your recent likes and restacks, then ranks newsletters by total engagement. The leaderboard shows exactly where you’re spending your attention, highlighting which publications you interact with most.
The Collections tab lets you group newsletters into named collections with tag rules that automatically include matching newsletters. For example, you could create a collection called “AI Writers,” add a tag rule for “AI,” so all newsletters you tag are also instantly added to that collection.
It also lets you export collections as JSON or a simple URL list.
The Reading List lets you select a source (All Newsletters, a Collection, or a tag), a timeframe, and optional keyword filters. The extension fetches recent articles from each newsletter’s archive and compiles them into a searchable, exportable list.
Defining the data requirements
To build these features, each of which would have its own tab in the extension, I needed specific data:
For Subscriptions: A complete list of every newsletter I subscribe to, including newsletter names, Substack URLs, RSS feed URLs (so users can add newsletters to feed readers), author names, whether the newsletter offers paid subscriptions, and approximate subscriber counts.
For Engagement: A history of every post, note, and comment I’ve liked or restacked, with enough metadata to identify which publication each one came from.
For the Reading List: Recent articles from any newsletter in my subscription list, filtered by date.
None of this information is available through Substack’s official data export. You can download your own posts, comments, and subscribers, but not a list of the newsletters you subscribe to, your engagement history, or what shows up on your Substack feed.
So I turned to the undocumented API.
Getting to know Substack’s undocumented API
Substack doesn’t offer a public API. They offer no documentation, no developer portal, and no official, approved way to programmatically access your data. But that doesn’t mean the data is inaccessible.
Every web application makes API calls behind the scenes. When you load your Substack homepage, scroll through your feed, or click on a newsletter, your browser is sending requests to Substack’s servers and receiving structured data back. The website then renders that data into the page you see.
These internal API calls aren’t meant for external developers, but they’re not hidden, either. You can see exactly what’s happening using your browser’s developer tools.
How to find API endpoints yourself
It’s a simple process in which you:
Open Chrome DevTools (right-click → Inspect → Network tab)
Navigate around Substack while watching the network requests
Filter by “XHR” or “Fetch” to see API calls
Click on interesting requests to see the URL, headers, and response data
I spent a couple of hours loading different pages, clicking around, and documenting every interesting endpoint I found. Some were dead ends (404s, or data I didn’t need). But I eventually mapped out exactly what I needed.
A note about user IDs: The endpoints below reference {user_id}, which is the numerical identifier Substack assigns to each account. It’s not your username or handle; instead, it’s a number like 363410124. You can find yours by going to your Substack profile page and inspecting the network requests in DevTools. Look for API calls that include your profile data; the user ID will be in the URL or response. Once the extension is authenticated, it automatically extracts this from the session.
Here’s a quick overview of how to find API endpoints:
And here are the endpoints I’m using in my extension:
User profile & subscriptions
Endpoint:
https://substack.com/api/v1/user/{user_id}-{username}/public_profile/self
This is the motherload. One API call returns your profile info plus your complete list of subscriptions. Each subscription includes the newsletter name, subdomain, author info, whether it’s paid or free, and approximate subscriber counts.
Your likes
Endpoint:
https://substack.com/api/v1/reader/feed/profile/{user_id}?types[]=like
This endpoint returns everything you’ve liked, including posts, notes, and comments. Each item includes the publication it came from, so I can calculate which newsletters you engage with most.
Your restacks
Endpoint:
https://substack.com/api/v1/reader/feed/profile/{user_id}?types[]=restack
Same structure as likes, but for restacks. One gotcha I discovered is that the types[]=restack filter misses Notes restacks. You have to fetch the full feed and filter in JavaScript.
Newsletter archive
Endpoint:
https://{subdomain}substack.com/api/v1/archive
This endpoint fetches recent posts from a specific newsletter. I use this to build reading lists from your subscriptions.
A word of caution about undocumented APIs
I have mixed feelings about how Substack manages (or, more accurately, doesn’t manage) their API. Ideally, they would publish guidelines for developers and even official, robust documentation, which is something many other platforms do.
As it is, using Substack’s API comes with a variety of risks:
The API could break. Substack hasn’t published these endpoints, so they could change without notice. I’ve built in error handling and graceful degradation, but there’s no guarantee my extension (and any software built on this API) will work forever.
Rate limiting is unclear. Substack doesn’t publish rate limits. I’ve added delays between requests and aggressive caching to minimize API calls, but heavy usage might trigger blocks.
Access policies are opaque. Substack’s TOS prohibits “copying” and “scraping” data, but it’s unclear if that applies to accessing metadata through their own API.
This isn’t legal advice. I’m not a lawyer, and I can’t tell you whether using Substack’s undocumented API is the right decision for your specific use case. The extension only accesses your own subscriptions and engagement data, which may be less risky than processing other users’ engagement data, articles, etc. But you should seek out your own counsel and make decisions based on your own individual situation and risk tolerance.
Building with Claude
I built the extension iteratively with Claude, prompting it to work through the spec one feature at a time. For each feature, Claude generated code and ran automated tests until they all succeeded. Then I loaded the extension into Chrome and tested whatever Claude had built. Depending on the results, I’d either make tweaks myself or ask Claude for corrections.
Overall, it took me about a day and a half of intermittent effort to get this done.
What worked
Prescriptive prompts that spelled out exactly what the user experience should look like and referenced the written spec produced better code. Instead of “add a search feature,” I’d write: “Add a search input to the subscriptions list. When the user types, filter the displayed subscriptions by newsletter name or author name.”
Showing exact error messages helped Claude debug issues. I’d paste the console output, the relevant code, and what I expected to happen. This would usually produce a fix on the first try.
Asking Claude to explain why something broke taught me more than just getting fixes. “Why does this CORS error happen in extensions?” led to understanding host permissions. “Why is this API returning 403?” led to a helpful lesson on cookie authentication.
What required human judgment
Every UX decision. Claude would suggest options, but deciding what “felt right” when actually using the extension was on me.
Scope management. Claude will build above and beyond your stated scope, unless you set limits. I had to be the one saying “that’s out of scope for this version” when it suggested functionality that I wasn’t ready to support.
Testing edge cases. AI doesn’t use the product like a real person. I found bugs by actually trying to manage 1,200+ subscriptions, an edge case Claude never would have anticipated.
The most educational bugs
Building this extension involved plenty of debugging. A few highlights:
The pagination mystery. I expected the API to use a cursor parameter for pagination, but the actual parameter is nextCursor (camelCase). This caused lots of seemingly inexplicable failures that were only resolved when I took a very close look at the raw API response.
The paid subscription glitch. Claude expected membership_state === ‘active’ for paid subscriptions. Turns out it’s membership_state === ‘subscribed’. I only found this by grepping through actual API responses: grep “membership_state” subscriptions.json | sort | uniq -c. The output showed values like “free_signup” and “subscribed”—nothing called “active” anywhere.
CORS headaches. Chrome extensions have strict rules about cross-origin requests. My initial manifest only had permissions for https://substack.com/*, but I had to add https://*.substack.com/* to the host permissions to cover all possible subdomains and https://*/* to cover custom domains.
Subscriber count formatting. The API returns subscriber counts in multiple formats: sometimes as raw numbers, sometimes as strings like “155,000”, sometimes as shorthand like “155K+”. Different fields have different formats. I ended up asking Claude to write parsing logic to handle all variations and display them consistently.
Caching race conditions. When loading data from cache while simultaneously checking for updates, I ran into race conditions where the UI would flicker between cached and fresh data. I fixed this by showing cached data immediately and only updating the UI when fresh data actually differs from what’s cached.
Benefits of building local-only
Despite the possible API risks, the local-only architecture has real advantages:
Privacy. Your data never leaves your browser. No servers, no databases, no analytics. I literally cannot see what you’re doing with the extension.
No accounts. It works immediately if you’re logged into Substack. No signup, no email verification, no password to remember.
No ongoing costs. One-time download, works forever (or until the API breaks). I don’t need to maintain servers or pay hosting bills.
Works offline. Once you’ve loaded your subscriptions, the cached data is available even without internet.
Get it free (or build your own)
I’m offering the extension for free on Gumroad as a special holiday present to all of you lovely folks who read my stuff.
Here’s a five-minute tour of the extension, if you’d like to see it in action:
Install it, pin it to your Chrome toolbar, and click the icon while logged into Substack…and that’s it. Your subscriptions load automatically, and everything stays in your browser’s local storage.
You can also build your own version. Paste this prompt into Claude Code or Cursor, tell it you want to build a Chrome extension, and iterate from there:
Build a Chrome extension (Manifest V3) that helps users manage their Substack subscriptions.
Authentication: Check for substack.sid cookie. If missing, show “Please log into Substack first.”
API Endpoints:
- User profile + subscriptions: GET https://substack.com/api/v1/user/{user_id}/public_profile/self
- User’s likes: GET https://substack.com/api/v1/reader/feed/profile/{user_id}?types[]=like
- User’s restacks: GET https://substack.com/api/v1/reader/feed/profile/{user_id}
- Newsletter archive: GET https://{subdomain}.substack.com/api/v1/archive
Features (implement in order):
1. Dashboard showing total subscriptions, paid vs free count
2. Subscriptions list with search and CSV export
3. Engagement tab showing likes/restacks by publication
4. Collections for grouping newsletters
5. Reading list generator
UI: Popup window 400px wide. Tab navigation. Use Tailwind CSS.
Store all user data in chrome.storage.local.This is enough to get a working MVP. I suggest starting with just the subscriptions list and export feature, then adding engagement tracking, collections, and reading lists, in that order. Building incrementally is key; trying to implement everything at once can lead to more complex bugs spanning multiple, interrelated functions.
Tips if you’re building your own
Save raw API responses to JSON files early on. You’ll need them for debugging when things don’t work as expected.
Test with real data from the start. My subscription list has quirks (newsletters with special characters, custom domains, missing subscriber counts) that I never would have thought to include in a fake dataset.
Cache aggressively. The extension loads much faster when it can show cached data immediately while fetching updates in the background.
Handle errors gracefully. Every API call should have a try/catch and clear error messaging. Users blame the extension, not the API, when things break.
What’s next
This extension is intentionally limited in scope. I didn’t build publisher analytics (who engages with YOUR content) because that’s a different use case with different data requirements. That might be a separate tool someday.
In fact, I enjoyed building this extension so much, I might just create a few more tools, even as I continue chatting with academics and research experts to determine if there’s a real, addressable market for Future Scan.
If you try the extension, I’d love to hear what you think. And if you build your own version using the spec I’ve shared, tell me about it. I’m always curious what other builders create.
Get the Substack Reader extension: karenspinner.gumroad.com/l/vlwcrf




thank you so much Karen, it feels like the exact same story I have been doing for the past two weeks but for scheduling the notes. I just built and successfully tested the chrome plugin for myself as I was struggling with n8n workflow so I vibe-coded with Google Gemini 3.. I will publish more about it in next weeks
omg this is exactly what I wanted for Christmas! You rock Karen!