BetterSSC
A Chrome extension that gives Substack Chat a Discord-style makeover.
Latest release: v0.12.0 (Sep 18, 2026) — 💹 Trades strip + Telegram BUY/SELL alerts (today's buys and sells parsed out of the chat and listed above the Active pane — BUY / SELL·closed / SELL·partial, ticker, who, ET time, click to jump to the message; one Telegram alert per trade as it lands with a Link straight to the comment; opt-in, favorites-only filter, corpus-calibrated regex with 247 pinned fixture rows). Previous: v0.11.1 — 🤖 Google AI provider fixed, 👍 reactions on already-loaded messages, ✈ Telegram favorites-only filter, 🔌 dead proxy tab recovery. 1049/1049 tests passing.
Install · Give feedback · Roadmap
Why this exists
Substack Chat is where a lot of really good traders and writers share their thinking in real time. The trouble is the interface gets in the way:
- Search usually doesn't find what you're looking for.
- If 200 people are posting, there's no easy way to focus on just the 3 you care about.
- You don't get notifications when a specific person posts, or even when someone @mentions you.
- Scroll position resets in weird ways when older messages load.
- Threads of 10 replies all show up as a flat wall of text.
- The whole thing feels like it was built for mobile first and never quite finished for desktop.
BetterSSC keeps your existing Substack account and reads from Substack's own API. It just paints a nicer layout on top so you can actually follow conversations.
What it does (v0.12.0)
BetterSSC is primarily a reader but the send side has caught up — you can now ship images, GIFs (uploaded OR picked from GIPHY), reactions, and replies without leaving the BetterSSC tab.
Read side at a glance: a rolling trending ticker bar with live prices, vi-key navigation, search + filters, inline ticker charts, desktop notifications, AI summaries + free-form Q&A, per-message ✦ Explain (with vision + link reading), silent full-chat prefetch so scrolling-up history is instant. Details below.
Reading the chat
- Two-pane Discord-style layout. Real names, real avatars, real emoji reactions.
- Every message reads as a soft card. A subtle accent tint on each author block makes message boundaries scannable; search hits intensify; the focused row gets a 3px accent bar on the left.
- When someone quotes another message, the quoted block is a clickable accent-color card. Click it to jump to the original and watch it flash amber so you can find it.
- Inline images, click for a full-screen lightbox. If an image fails to load it falls back to a "📎 image (click to open)" link.
- Click any ticker symbol to open a free TradingView chart. Both
$TICKER syntax ($NASA, $DXYZ, $BRK.B) AND bare ALL-CAPS tickers from a curated allowlist (AAPL, TSLA, BTC, SPY, QQQ, etc.) render as accent-pill links. The modal embeds the daily chart with drawing tools (horizontal line, trend line, fib, etc.). $5 / $100 dollar amounts are skipped; Meta / meta lowercase stays text.
- Light theme by default, dark theme one click away. Choice is remembered across reloads.
🧵 Switching between threads
A Substack chat channel isn't one conversation — it's many top-level threads (the author's daily posts, each with its own reply chain). BetterSSC opens the liveliest one, and a rail on the left lists the rest so you're not stuck in just one.
- Liveliest-first, snippet + reply count per row. The open thread is highlighted; click any other row to switch — this reloads the page (not instant, deliberately: it reuses the same tested load path rather than trying to hot-swap a chat's worth of in-memory state).
- "Empty hidden" toggle, on by default. Most threads in a busy channel are same-day link posts with zero replies; the toggle declutters them. The thread you're currently reading is never hidden by this, even if it has no replies yet.
- Collapsible. Click the ‹ / › icon in the rail's header to shrink it to a slim strip, or double-click the thin divider on its edge as a shortcut. Both the hide-empty and collapsed state remember your choice across reloads.
- Only appears for a channel with 2+ switchable threads — a quiet single-thread chat looks exactly like it always has.
📈 The trending ticker bar
A CNBC/Bloomberg-style TRENDING strip sits under the header and scrolls right→left, showing what the chat is talking about right now — pulled live from the messages, no setup.
- Three kinds of chips: trending stock/crypto tickers, @mentioned people, and topic keywords (1–2 words each).
- Click any chip to search. It drops the term into the search box and runs the search instantly — tickers search the bare symbol, people search
@name, topics search the word.
- Ticker searches show a live chart. Click a ticker chip — or type
$HPE, HPE DELL, or any known symbol into search — and a compact TradingView mini-chart per symbol (price · % change · intraday sparkline) pins to the top of the feed, above the matching messages. Click the ⤢ on any card for the full advanced-chart modal. Up to 6 at once; lowercase prose and @/command filters never chart.
- Live prices on ticker chips. Stock and crypto chips show a recent price with a green ▲ / red ▼ percent change, pulled from Yahoo Finance (fetched through the extension's background worker, so no API key and no CORS pain). If a symbol isn't price-able the chip just shows the symbol.
- Hover eases the strip to a halt so the chip under your cursor stops moving and is clickable; move away and it glides back up to speed. Respects
prefers-reduced-motion (no auto-scroll; scroll it by hand instead).
- How chips are chosen: the bar looks at the last 2 hours of chat, scores each candidate by frequency with newer mentions weighted more (so what's hot now leads, but a heavily-discussed earlier mover still shows). Tickers are matched by
$SYMBOL (any symbol, known or unknown) or bare ALL-CAPS symbols from the built-in list; $MSFT and MSFT are deduped to one chip. People come from real @mentions. Topics are gated — a word only trends if two different people said it (and it clears a stopword/length filter), and URLs are stripped first so link fragments (https, www, host words like tradingview) never trend. Tickers rank first, then people, then topics.
🔗 Link previews (opt-in)
When someone posts a link in the chat, BetterSSC can show a Discord-style preview card under the message — the page's site name, title, description, and thumbnail — so you can tell what a link is before clicking it.
- Off by default. Turn it on in Chat preferences (the ⚙ in the member rail). The first time you enable it, your browser asks permission to read the pages you preview — that's the broad host access this feature needs. Decline and the feature simply stays off.
- Local and per-viewer. The preview is built in your browser from the linked page's Open Graph metadata. It is never written back to Substack and never shared with other readers — each person who has the feature on unfurls the links they see, on their own machine.
- Cookieless. The fetch sends no cookies, so it can't act as you on the linked site. Page text is rendered as inert text (no scripts run), and preview images load with no referrer.
- X / Twitter links render as a tweet. A status link gets a tweet-shaped card — avatar + display name + @handle, the full tweet text, and the embedded image rendered large below — built from the tweet's Open Graph data so it matches the original as closely as possible. The avatar comes from unavatar.io; if it can't be resolved, the slot falls back to the X logo.
- Cheap. Each link is fetched once per session and cached; a link that fails to unfurl is remembered as "no card" so it isn't retried.
- Toggle it back off and BetterSSC immediately drops the cards and hands the page-read permission back.
Finding stuff (search + filter)
- 🎯 Focus mode — the answer to "200 people are posting, I only care about 3 of them and one topic." Click the 🎯 Focus button (just left of the search box — it's a separate function, not a search filter) to open a dialog: add terms as chips (
$SPCX, earnings — type several space- or comma-separated and each one is OR'd, so a message matching any term shows) and/or tag people from a searchable list (the people you've selected stay pinned at the top, even while you search the list for more). Everything that isn't about your terms or people is hidden. The filtering is reply-tree aware: a reply to a $SPCX message comes through even if the reply itself never says "$SPCX" — Focus walks up the reply/quote chain and passes any message whose ancestor matches. Same for people, so every reply to someone you've tagged surfaces too. A banner shows your active focus with edit / × exit focus; Esc exits. Composes cleanly with search and the 💬 thread filter (they intersect).
- Full-text search across every message that's been loaded. Type in the box; the feed filters live.
- Type
@boz to see only that person's messages.
- Slash commands (the leading
/ is optional, the : is what makes them unambiguous):
/from:<name> show one person's messages
/me your own messages
/has:link (or /has:links, /has:url, /has:urls) messages containing a URL
/has:image (or /has:images, /has:img, /has:pic, /has:picture, etc.) messages with an image attachment
/has:reaction (or /has:reactions, /has:emoji, /has:emojis) messages with at least one reaction
/since:3 everything from the last 3 days
/help the full reference
n / Shift+N cycle through search hits. Active match gets the strongest accent tint so you can find it as you cycle.
- 💬 thread badge on any message that has replies (quote-replies count too). Click it to focus the stream on just that conversation.
- The "↓ Latest" pill respects your filter. Click the main pill to jump to the bottom of the FILTERED view, not the unfiltered chat. When new messages arrive that don't match your filter, a muted suffix appears — click it to clear the filter and see what you missed.
Getting around (scrolling, paging, vi keys)
The whole feed is keyboard-driven. You can use it without ever touching the mouse.
| Key | What it does |
|---|
j / k or ↓ / ↑ | Next or previous message group |
PageUp / PageDown | Full page up or down |
Ctrl+U / Ctrl+D | Half page up or down (vim style) |
g | Page up — load older history at the top |
Shift+G | Jump to bottom (latest) — clears filter; takes you to absolute newest |
n / Shift+N | Cycle through search hits |
r | Refresh now (also a ⟳ button in the header) |
/ | Focus the search box |
Esc | Close an open dialog/overlay, then exit the thread view, then exit 🎯 focus, then clear search |
? | Show the help overlay |
Focus follows your cursor and your keys. Mouse hover, j / k, arrow keys, and click all drive the same single focus state — the focused author block gets an accent-tinted background and a 3px bar on the left edge. No more "where is the cursor actually" disconnect between mouse and keyboard.
g is a two-tier state machine. First press: scroll to the top of currently loaded messages. Second press (already at top): load one more page of older history and scroll to the new top. Each press does exactly ONE thing — you can keep pressing g to walk back through history indefinitely.
Silent background prefetch. After the initial 25-message page lands, BetterSSC walks every older page in the background (300 ms between requests, exponential backoff on 429) until the chat's full history is in memory. The chat feed itself stays stable — no reflow, no scroll jump — but the footer's N messages · M authors counter ticks up live so you can watch it fill. When it's done, a small ✓ +N loaded pill flashes next to where the Latest pill shows. After completion g is instant — no more network spinner on every page. Default ON; toggle in kebab → Chat preferences.
Following specific people
- 🔔 bell next to each name in the Active rail. Toggle it on and you'll get a desktop notification when that person posts, even if BetterSSC is in another tab.
- ★ Favorites — star people to keep them at the top of the Active rail so you always see them first.
- You're a favorite and auto-bell'd by default. Your own row sits at the top of the rail no matter the sort.
- Sort the rail by most-active (default) or alphabetically.
- The browser tab title shows an unread count while you're away:
(3) Your Chat Name · BetterSSC.
- Auto mark-viewed every 30 seconds (and instantly when you switch back to the tab), so your unread count in native Substack stays in sync.
AI: Explain + Summary + Ask (bring your own key)
- ✦ Explain a single message (v0.5.0) — every message has a small, always-visible ✦ button at its top-right (X/Grok-style). Click it and BetterSSC explains that message inline, in a distinct block attached right under it. It's thread-aware — it walks up the message's reply/quote ancestors so a terse reply gets explained in the context of what it answers. It sees the images — charts/screenshots attached to the message are sent to the model as real vision input. It reads the links — URLs in the message are handed to the model to fetch via web search. And it talks like a professional trader: decodes the jargon, separates the claim from its read, flags the risk. The output is a one-line gist + a few tight bullets + a Sources list. Local-only, same privacy story as the others.
The ✨ AI button in the header is a hover dropdown with two more actions:
- Generate AI Summary — one-click structured summary of the visible chat (themes / takeaways / trades / open questions). The same flow that's been here since v0.1.
- Ask BetterSSC AI (v0.3.0) — opens a textarea, type any question, and the entire visible chat is sent to the configured provider as context. The model answers in three labeled sections: From the chat (with attribution), From the web (with citations, when web search is on), and Synthesis (combined answer). On Anthropic and Google, the model has access to native server-side web search; OpenAI is text-only for now (Responses API migration pending).
Bring your own OpenAI / Anthropic / Google key. See the dedicated AI Insights section below for the full feature: regenerate at length, model + budget + cost + output cap tuning, web search toggle, custom prompts, privacy. The context the model receives now includes reply-target linkage (replying to X: "...") and reaction summaries ([reactions: 👍×2 ❤️×1]) so it stops misattributing replies and can tell when a claim got group agreement.
Live updates
Polling once every 12 seconds, which is the same thing Substack's own native client does. The status pill in the header shows you what's live: 🟢 live poll or 🟢 ws on. WebSocket support is on the roadmap, polling handles things in the meantime.
A second, slower (20s) poll picks up reactions added to messages you've already loaded — the main poll only catches genuinely new messages, so a reaction on an older message used to sit invisible until you reloaded the tab. This refresh is scoped to the most recent page of the thread; a reaction on something further back still needs a reload.
✈ Telegram bridge (stream the chat to a bot)
Stream the live Substack Chat feed to your own Telegram bot — read it, post to it, and react from your phone — without leaving Telegram.
- Set it up in ~1 minute. Create a bot with @BotFather and copy its token. In BetterSSC, click the ✈ button in the header, paste the token (validated via
getMe), then send your bot any message in Telegram so it can reach you (a bot can't message you until you message it first). BetterSSC captures the chat automatically — then hit Start streaming.
- Stream (read): every new chat message appears in your Telegram chat with a bold author header (the bot is the nominal sender, so the original author rides in the text). Images forward as photos; other attachments get a
📎 marker.
- Only favorites, optionally: a checkbox in the bridge dialog ("Only forward messages from favorites") reuses the same ★ favorites as the member list — turn it on to mirror just your favorites' messages to Telegram instead of the whole feed. Off by default (mirrors everyone); your own messages always forward since you're a favorite by default.
- Post back: type a message to the bot and it posts to the Substack thread as you. (Messages starting with
/ are treated as bot commands and not posted.)
- React back: react to a streamed message in Telegram and the closest Substack reaction lands on the matching comment. Unmapped reactions are skipped, never guessed.
- Runs only while the BetterSSC tab is open — no server, no always-on background process. The bot token lives only in
chrome.storage and is never logged. Latency is ~12s with the tab active (longer when the tab is hidden, due to browser timer throttling).
- BUY/SELL alerts ride the same bot, independently of streaming — see the trades strip. The bridge's own favorites-only checkbox covers the mirror; the strip's covers the alerts.
- Not supported (yet): replies/threading, AI features in Telegram, operation while Chrome is closed, sending media from Telegram, and multiple threads. Reaction changes/removals are lossy.
💹 Trades strip + Telegram BUY/SELL alerts (opt-in)
Members announce fills in prose — "Bought: CBRS at 12.40", "trimmed half of NVDA", "out of HOOD", "close $QQQ 718 lottos". BetterSSC reads those out of the chat and lists today's buys and sells above the Active pane, and can push each one to your Telegram bot as it lands.
- Off by default. Turn on Show the trades strip in Chat preferences. A "TRADES TODAY · N" section appears above Active, newest first: a coloured badge (🟢 BUY / 🔴 SELL·closed / 🟠 SELL·partial / 🔴 SELL), the ticker(s), who, and the time in ET. Hover a row for the original message; click it to jump to the message (a member's comment flashes in the stream; the author's own "Bought: …" root posts open the post view; a root from another thread switches to that thread).
- Roots and comments both count. The publication author's trades are usually thread root posts, members' trades are comments in the daily thread — the strip reads both. Comments in other threads of the channel aren't loaded by the app and don't appear; the channel's thread roots refresh every 60s while the strip is on.
- "Today" is the calendar day in New York, midnight to midnight, whatever your own timezone. Rows drop at ET midnight on their own.
- Narrow window? Under 800px wide the members pane is hidden as always, but with the strip on it becomes a slim trades bar across the top of the chat (capped at about a third of the window; the chevron collapses it to one line).
- Favorites only (second toggle) narrows the strip — and the Telegram alerts — to your ★ favorites, the same list as the Active pane.
- Telegram alerts (third toggle, on by default once the strip is on and a bot is connected via ✈): one Telegram message per chat message that contains a trade, in the same shape —
🔴 **SELL·closed QQQ** — Jordan Kerner: close $QQQ 718 lottos — followed by a Link that opens the thread scrolled to that exact comment. Fires whether or not the full chat mirror is streaming; never for history, only for messages that arrive while the tab is open; never twice for the same trade, even across a reload (the day's alerts are remembered). Your own messages alert too.
- Best-effort regex, no AI. The parser is calibrated against a committed corpus of real chat messages plus hand-built near-misses (hypotheticals, questions, "closed the gap", third-party "Cramer bought", past-tense "back in March", all-caps shouting…) and every row is asserted by the test suite. It looks for a trade verb with a ticker next to it in the same sentence; lowercase tickers are accepted right after the verb ("bought orcu") and marked with a ? in the strip and "unconfirmed ticker" in Telegram. Known misses, on purpose: "long AMZN" / "I'm short oil" (position statements, not fills), a ticker in an earlier sentence ("NBIS looks good. Bought it."), and an edited message never re-alerts.
Sending (when you do want to write)
- Type and send messages, with optimistic UI so your message lands instantly without waiting for the 12s poll cycle.
- Discord-style composer — icons sit on the right of the textarea (image, GIF, emoji, Send). The composer is constrained to the chat-feed width; the members rail extends the full height of the page. No
+ button — the image icon does what + would.
- Send images and GIFs — paperclip-style image button (📷), drag-drop onto the composer, or paste from clipboard. PNG / JPEG / GIF / WebP up to 10 MB. Wire decoded from real captures: Substack's 3-step flow (register → PUT binary → comment POST with a shared client-generated UUID as an idempotency key). Optimistic preview chip via a local
blob: URL renders instantly; the real CDN URL swaps in once the server reconciles.
- GIPHY GIF picker — a
GIF button opens a debounced search + 3-column grid. First time you click it, an inline onboarding walkthrough gets you a free GIPHY API key in about 30 seconds. Key is BYOK (stays in chrome.storage.local, BetterSSC never sees it). Picked GIFs re-upload as image/gif through the same Substack pipeline, so they render natively and animate in the browser. See GIPHY GIF picker setup below.
- Emoji popover — 😊 button opens a small popover with 6 categories of common Unicode glyphs (Smileys / Hands / Hearts / Symbols / Markets / Food). Click to insert at the cursor. Re-click toggles closed.
- React with any emoji from Substack's full ~392-emoji catalog — the picker has a search box, a "Frequently used" row derived from the reactions actually in this chat, and the complete catalog grouped into scrollable categories.
- Quick-react strip on hover. Hovering any message shows the top 4 emojis used in the current chat right before the
+ (picker) and ↩ (reply) buttons. Click any to react directly, no picker dance.
- Click any existing reaction pill under a message to add your own reaction of that type. The hover state + cursor pointer make it discoverable.
- @mention autocomplete pulls from the people you've already seen in chat.
- Reply UI puts a Discord-style quoted block on your message locally so you can see what you're answering.
- Failed sends keep your text and show a Retry button. If the message had an attachment, the original
File is stashed on the pending row so retry re-runs the full register + PUT, not just the comment POST — so a failed-then-retried GIF doesn't silently send as text-only.
🛡️ AI moderation before posting (bring your own key)
Turn it on in Chat preferences and your own outgoing messages get reviewed before they post — never anyone else's.
- Send normally. A plain-text message waits briefly (configurable, default 2s) to catch a rapid-fire burst of follow-ups and merge them into one reviewed message, then your AI provider reviews it. A message with a mention, reply, or attachment skips straight to review — no merge window, since a reword could otherwise mangle an
@name token.
- Three outcomes. Clean: posts as typed, nothing changes. Blunt or poorly-framed but not outright offensive (the default verdict for tone problems): the composer shows the AI's reworded version and stops — you edit it or press Send yourself, it never auto-posts on a timer. Offensive, or a reply to a message the AI classifies as political: blocked. Send stays disabled until you actually edit the flagged text; your original message, any staged attachment, and your reply target are all still right there.
- Fails closed. A network error, timeout, or a response BetterSSC can't parse never results in a silent unreviewed post — you're told what happened and your text is intact, ready to resend.
- Skip review is a one-click settings toggle once you trust it — bypasses AI review and sends immediately.
- Same BYOK key as AI Insights below (OpenAI / Anthropic / Google) — nothing new to configure. Uses the cheapest model on your provider and your last ~100 messages as background context for tone; web search is off here, for speed.
AI Insights
A one-click summarizer for whatever's currently visible in the chat. Built BYOK (Bring Your Own Key) so the privacy story stays clean — your messages don't pass through a BetterSSC server because there is no BetterSSC server.
Two modes — Summary and Ask
Hover (or click, or focus) the ✨ AI button in the header to reveal the dropdown:
-
Generate AI Summary — the one-click structured summary. The model reads the messages you're currently seeing (respecting your active search filter and thread filter) and emits these sections:
- Themes — what's being discussed
- Key takeaways — most important claims or conclusions
- Notable trades / ideas — specific tickers, entries, theses (default lens is trading; editable)
- Open questions — what's unresolved or being asked
-
Ask BetterSSC AI — opens a textarea. Type a free-form question (e.g. "What's the bull thesis on CRWV?" or "Who's been bearish on SPX this week?"). The full visible chat is stuffed into the system prompt and sent to your configured provider. Responses render in three labeled sections:
- 💬 From the chat — what was actually said, with attribution to specific authors and direct quotes when load-bearing
- 🌐 From the web — facts pulled in via web search (on Anthropic + Google; OpenAI not yet wired). The model only invokes search when the chat alone can't answer. Citations appear as a numbered "Sources" list of clickable links.
- ✦ Synthesis — the combined answer addressing your question directly
Your question echoes in a Q badge at the top so multiple Ask responses in the feed stay distinguishable. The response is local-only — same privacy story as Summary.
Both modes appear in the feed as accent-tinted messages authored by "✨ BetterSSC AI", with a footer like Only visible to you · anthropic · 342 messages analyzed. Nothing gets POSTed back to Substack, the message never appears for other people, and it isn't included in the polling cursor (so it doesn't break the live-update path).
Dismiss any AI message with the Dismiss button. All AI messages clear on a reload (in-memory only for now).
Author-aware perspective
When you narrow the chat to one person — via @name, /from:<name>, or /me — the summary automatically reframes every observation in third person from that person's viewpoint: "In Jordan's view, …", "Jordan thinks …". Without this the model tends to summarize "the chat is discussing X," which is wrong when the chat at that point IS just one person.
This rule isn't editable — it's mechanical (driven by the search filter, not the prompt) and breaking it would generate output that looks like a bug.
Concise / Elaborate — regenerate at length
Every AI insight ships with two buttons at the bottom:
- ↓ Concise — generates a new summary in 3-4 bullets total, headlines only, no preamble.
- ↑ Elaborate — generates a longer summary with direct quotes and 2-3 sentences per section.
Each click produces a new insight appended to the bottom of the feed (the original stays). Compare lengths side-by-side, dismiss the ones you don't want.
The kebab ⋮ menu — three settings
Top-right of the header, next to your avatar.
1. Tune AI model
- Provider · Model dropdown — every (provider, model) combo where you have a configured API key, no others.
- Input context budget slider — 6K to 200K characters. Defaults to 60K (~15K tokens, under 12% of every supported provider's context window). Bigger budget = more chat history reaches the model = more accurate summary, at higher latency and higher per-call cost. Ask mode bypasses this slider — Ask sends the entire visible chat, capped only by the provider's context window.
- Summary output cap (v0.3.0) — radio:
1024 / 2048 / 4096 tokens. Default 2048. Controls max_tokens for the Summary call. Raise to 4096 if long briefings (multi-stock + Open Questions tail) keep truncating mid-sentence; drop to 1024 to save cost on short summaries.
- Ask output cap (v0.3.0) — same options, default
4096 since sectioned Ask responses with citations run longer.
- Ask web search (v0.3.0) — checkbox, default on. Auto-disabled when the active provider is OpenAI (native web search requires the Responses API, not yet wired). On Anthropic the tool is
web_search_20250305 with a 5-call cap; on Google it's google_search grounding.
- Live per-call cost estimate — updates on every change. Uses each model's published per-1M-token pricing for both input + output (output cost scales with the chosen cap × 0.78 fill rate).
Default models are the cheap-fast tier for each provider:
| Provider | Default model | Upgrade option | Web search |
|---|
| OpenAI | gpt-4o-mini | gpt-4o (~17× cost) | Not yet (Responses API migration pending) |
| Anthropic | claude-haiku-4-5 | claude-sonnet-4-6 (~3× cost) | ✅ native web_search_20250305 |
| Google | gemini-3.6-flash | gemini-2.5-pro (~2× cost) | ✅ native google_search grounding |
Per-call cost at the 60K char default (~15K input tokens) computes to (Summary mode, 2048 cap → ~1600 output tokens at the 0.78 fill rate):
| Provider | Cheap model | Capable model |
|---|
| OpenAI | ~$0.003 | ~$0.054 |
| Anthropic | ~$0.023 | ~$0.069 |
| Google | ~$0.017 | ~$0.035 |
Ask mode runs ~2× this when the whole chat fits (no input-budget cap) + ~2× output cap. These are static estimates against each provider's published per-million-token rates at the time of writing. The dialog shows the live computed number on every change — trust the dialog, not this table, if pricing has moved.
2. Tune prompt
Two editable textareas:
- Lens hint — what kind of chat is this? The default is trading-flavored (financial / markets / trade ideas / tickers). If you use BetterSSC for a book club, a dev team, or anything else, swap this for the appropriate framing. The model uses it to know where to focus.
- Response format template — the section block. Want a single-paragraph summary instead of bullet sections? Just one section instead of four? A haiku? Edit this.
Each field has a per-field "Reset to default" button. The author-aware perspective rule above stays locked.
3. Reset all saved data
Wipes everything BetterSSC has saved on your device: favorites/watched members, AI provider + API key, theme, member-sort, notify-all toggle, WebSocket-enabled flag. Confirmation dialog lists exactly what gets cleared. The page reloads to land cleanly at defaults.
This does NOT touch anything on Substack's side — your actual chats stay where they are.
Privacy
Already covered in the Privacy section below, but the short version: your key lives in chrome.storage.local on this device, the chat content you choose to analyze goes directly from your browser to the provider's API (not through Substack and not through any BetterSSC server, because there is no BetterSSC server), and the whole feature is opt-in — until you click the ✨ AI button and configure a key, none of the AI endpoints are contacted.
Long-form write-up
For a narrative version of the above — why I built this, the author-aware reframe in more depth, the cost / latency / quality tradeoff behind the budget slider — see posts/ai-insights.md. Same story, different voice.
GIPHY GIF picker setup
The GIF button in the composer is wired to GIPHY's v1 API. BYOK so the privacy story matches the AI keys — your key stays in chrome.storage.local, BetterSSC never sees it.
Get a free key (~30 seconds, one-time):
- Go to developers.giphy.com/dashboard and sign in (free).
- Click Create an App → choose API (NOT SDK — they're different products) → fill in any name and description → submit.
- Copy the generated API Key.
- In BetterSSC, click the
GIF button in the composer. First time you do this, an onboarding modal appears. Paste the key, click Test & Save. BetterSSC pings GIPHY with the key; if valid, it saves and opens the picker.
Use it:
- Click
GIF → picker opens with trending GIFs.
- Type to search — debounced 300 ms, hits GIPHY's
/search endpoint.
- Click any tile → BetterSSC downloads the GIF from GIPHY's CDN, then re-uploads it through Substack's media endpoint as
image/gif. Same path as a clipboard-pasted GIF — appears inline and animates natively in the browser.
- "Powered by GIPHY" + a "Change key" link in the footer.
Notes:
- Beta key tier (the default): 100 search calls/hour per IP, 1000/day per app, 42 results per search max. Way more than personal use needs.
- Picker revalidates your key on every open. If the key was revoked or expired since last session, it bounces you back to the onboarding modal cleanly instead of surfacing an error inside the picker.
- GIFs above 10 MB are filtered out client-side (the picker uses GIPHY's pre-disclosed
original.size field — no wasted downloads).
- If GIPHY is unreachable, the picker shows a clear status message; the composer still works for clipboard-paste / drag-drop GIF uploads.
How it works under the hood
It's a Chrome MV3 extension that opens one page (app.html) when you click the toolbar icon. That page makes authenticated REST calls to Substack by piggy-backing on your open Substack tab via chrome.scripting.executeScript. The requests run inside Substack's own origin so your session cookie comes along for the ride. There is no backend server.
You click the toolbar icon
↓
background.js looks at your active Substack chat tab's URL
↓
Opens app.html?pub=<pubId>&post=<postUuid> in a new tab
↓
app.js gets to work:
- Reads your identity from window._analyticsConfig
- Pulls initial messages from /api/v1/community/posts/<postUuid>/comments
- Polls every 12s with ?after=<ISO> for new messages
- declarativeNetRequest rewrites the Referer header on image requests
so Substack's S3 bucket serves them properly
Nothing leaves your browser except calls to substack.com. No analytics, no tracking, no third-party scripts. Your preferences (theme, favorites, watched users, sort order) live in chrome.storage.local.
Install
This takes about 3 minutes the first time. You only do it once. No technical knowledge required, but if you've never installed a Chrome extension this way, the steps below walk you through every click.
BetterSSC isn't in the Chrome Web Store yet, so you install it directly from this repo. That's safer than the Web Store in one way — there's no automatic update, and you can read every line of code before it runs.
Step 1 — Download the extension
Click the green Code button at the top of this page, then Download ZIP. Find the file in your Downloads folder and double-click it to unzip. You'll get a folder called betterssc-main (or similar). Move that folder somewhere you won't accidentally delete it. Your Documents folder is fine.
Step 2 — Open the extensions page in your browser
Works in Chrome, Brave, Arc, Edge, or anything Chromium-based.
Copy this and paste it into your address bar (where you'd normally type a website), then press Enter:
chrome://extensions
You should now see a page listing any extensions you already have installed.
Step 3 — Turn on Developer Mode
Look in the top-right corner of that page for a toggle labeled Developer mode. Click it to turn it on. (The name sounds scary but it just means "let me install extensions that aren't from the Web Store." This is the same toggle every Chrome developer uses every day. It's safe.)
Once it's on, three new buttons appear at the top of the page: Load unpacked, Pack extension, Update.
Step 4 — Load BetterSSC
Click Load unpacked. A file picker opens.
Find the betterssc-main folder you saved in Step 1. Select that folder (don't open it — just select it once and click Open or Choose, depending on your OS).
BetterSSC should now appear in the list of installed extensions. You'll see its icon and name.
Step 5 — Pin the icon to your toolbar (optional but nice)
Look in the top-right of your browser window for a puzzle-piece icon (). Click it. You'll see a list of extensions with little pin icons next to them. Click the pin next to BetterSSC so its icon stays visible in the toolbar.
Step 6 — Open Substack Chat, then click BetterSSC
In a new tab, go to one of your Substack chats. The URL will look something like substack.com/chat/12345/post/abcdef-1234.
Now click the BetterSSC icon (the one you pinned). A new tab opens, and your chat appears in a Discord-style layout.
That's it.
A small thing to remember
Keep at least one Substack tab open while you're using BetterSSC. The extension talks to Substack through whatever Substack tab you have open — it doesn't have its own login. If you close every Substack tab, BetterSSC will tell you it can't reach Substack until you open one again. (You don't need to do anything in the Substack tab — it just needs to exist.)
When something goes wrong
- "Open a Substack tab first" message — you closed every Substack tab. Open
substack.com in a new tab; BetterSSC will start working again.
- The icon doesn't do anything — make sure you're on a Substack chat URL, not just
substack.com. The icon needs to know which chat to open.
- Chrome keeps showing "Access requested" for BetterSSC in the extensions menu — that means Chrome has the extension set to "Ask on every site" instead of automatic. Go to
chrome://extensions, click Details on BetterSSC, scroll to Site access, and pick "On all sites" (or just "On specific sites" → https://*.substack.com/*). The "Access requested" prompt goes away and notifications start firing properly.
- You pinned the icon but it's not showing in the toolbar — Chrome occasionally resets pin state when a sideloaded extension auto-updates. Click the puzzle-piece icon () in the top-right of your browser, find BetterSSC, click the pin icon next to it. The icon should appear in the toolbar. If the pin icon looks struck-through (with a diagonal line through it), it's currently NOT pinned — click it once to pin.
- The first time you click, it asks for permission — that's Chrome confirming the extension can talk to Substack. Click Allow. The permissions are scoped to Substack only.
- Notifications aren't firing — open
chrome://extensions, click "service worker" under BetterSSC to see the background console. It logs permission level: every time it tries to notify. If it says anything other than granted, check macOS System Settings → Notifications → Google Chrome and chrome://settings/content/notifications. On macOS Tahoe 26.3, Chrome may report granted but the OS still drops banners — known OS quirk, no fix on our end.
- You want to uninstall — go back to
chrome://extensions, find BetterSSC, click Remove. Done. No leftover files anywhere except the folder you originally downloaded, which you can delete.
Feedback and bug reports
👉 Open an issue
Bugs, feature requests, "this is weird on my chat," questions, all welcome.
If you're filing a bug, these things help me fix it faster:
- Chrome version and OS
- What you did to make it happen
- Whatever red text is in the DevTools console (open with Cmd-Opt-I on Mac, F12 on Windows)
- A screenshot if it's a visual bug
The roadmap below is my current wish list. What you actually need will reshape it.
Roadmap
- v0.2 ✅ Send messages, add reactions, reply, @mention autocomplete with optimistic UI.
- v0.3 ✅ Ask BetterSSC AI mode + tunable output cap + native web search on Anthropic & Google.
- v0.4 ✅ 🎯 Focus mode — filter the feed to terms + tagged people, reply-tree aware. Discord-style composer with icon cluster on the right + chat-column-only width. Send images + GIFs (PNG / JPEG / GIF / WebP) via 📷 + drag-drop + clipboard paste. GIPHY GIF picker (BYOK, with inline onboarding to get a free key). Emoji popover. Silent background prefetch of full chat history.
- v0.5 ✅ ✦ Explain — per-message, always-visible AI button that explains that message inline. Thread-aware (walks reply/quote ancestors), reads embedded chart images via vision, reads referenced links via web search, professional-trader voice.
- v0.7 ✅ 🔗 Link previews — opt-in, local-only Open Graph unfurl cards under messages that contain a link. Fetched in your browser, cookieless, gated behind an on-demand host permission, never shared back to Substack.
- v0.9 ✅ ✈ Telegram bridge — stream Substack Chat to your own Telegram bot, post + react back from Telegram.
- v0.10 ✅ 🛡️ AI moderation before posting — opt-in, BYOK review of your own outgoing messages before they post; reword-with-confirmation for tone, hard block for offensive content and replies to political messages.
- v0.6 OpenAI Responses API migration so Ask/Explain web search works on OpenAI too. Edit + delete your own messages. Multi-image attachments per send.
- v0.6.x Multi-chat support: left rail across every chat you're in, unread badges, Cmd-K quick switcher.
- DMs + Tenor parity Direct messages. Tenor GIF picker as an alternative to GIPHY if a user prefers it (TOS allows; would need to recapture Substack DM wire shape since group-chat shape doesn't always match).
- WebSocket protocol Right now the WS handshake returns "Invalid message" after auth and we fall back to polling. Cracking that protocol needs a side-by-side capture of a working native session vs ours. Polling at 12s matches Substack's own client so this is a latency win, not a correctness one.
Privacy
The core extension is server-less. Nothing about your messages, identity, reactions, search queries, favorites, watched users, or theme ever gets sent to me, to a third party, or to anyone else. I cannot see what you read or write in your chats. Nobody else can either.
- There is no BetterSSC server. There is no BetterSSC database. There is no BetterSSC backend at all.
- Every Substack-side network request goes directly from your browser to
substack.com, using the session cookie you already have from being logged in.
- No analytics. No telemetry. No "anonymous usage data." No crash reporters. No third-party scripts. None.
- Settings (theme, favorites, watched users, sort preference, notify-all toggle, AI provider + key) live in
chrome.storage.local. That's a per-browser-profile bucket on your disk. They never leave your machine.
- Failed-image URLs are cached in memory only for the lifetime of the tab, then forgotten.
- The source is all here in this repo. Read it, audit it, fork it. Roughly 3000 lines of vanilla JS with no build step. What you see is what runs.
AI Insights — opt-in BYOK
When you choose to use the ✨ AI feature (Summary or Ask), the privacy story changes only for that feature, and only when you trigger it:
- You bring your own API key for OpenAI, Anthropic, or Google. Your key stays in
chrome.storage.local on this device.
- The chat content you choose to analyze (whatever is currently visible in the feed at click time) is sent directly from your browser to your chosen AI provider's API using your key. BetterSSC has no server in this path either — but the provider sees what you send. Ask mode sends the full visible chat (subject to the provider's context window); Summary mode sends up to the budget you set in the Tune dialog.
- BetterSSC does NOT route AI calls through the Substack proxy tab. We use a separate fetch to
api.openai.com / api.anthropic.com / generativelanguage.googleapis.com so Substack's network trail doesn't contain any AI traffic.
- Web search (Ask mode, Anthropic + Google only) invokes the provider's native server-side search tool when you have the toggle on. Search queries are constructed by the model and run inside the provider's infrastructure; the URLs and titles of the results come back as citations rendered in the response. The chat content + your question + the search queries + the search results all live with the provider for the duration of the call. Toggle off in the Tune dialog if you don't want this.
- Provider error strings are bounded at 200 chars and
sk-… patterns are masked before rendering — defense-in-depth so an accidental key fragment in a verbose error body can't survive into the visible DOM.
- The feature is fully opt-in. Until you open the dropdown and configure a key, none of these endpoints are contacted.
- If you don't want any chat content reaching a third party, simply don't use AI. Everything else works exactly the same.
AI moderation — opt-in BYOK
Same key, same direct-to-provider path as AI Insights above, triggered by your own outgoing sends instead of a button click:
- Only YOUR outgoing messages are ever reviewed — nothing anyone else posts is read, analyzed, or sent anywhere by this feature.
- What's sent to the provider: the draft message you're about to post, plus your last ~100 chat messages as background context (so the model can judge tone in context, not in a vacuum) and, if you're replying, the parent message you're replying to (so it can judge whether that parent is political).
- Uses the same
chrome.storage.local key as AI Insights — no separate key, no separate setup.
- Web search is off for this feature (unlike Ask mode) — nothing beyond the chat context above ever leaves your browser for this call.
- Fully opt-in, off by default. Turn it off in Chat preferences at any time and your sends go back to posting exactly as before, with zero AI involvement.
GIPHY picker — opt-in BYOK
The GIF button in the composer is the same story, smaller surface:
- You bring your own GIPHY API key. Free at developers.giphy.com/dashboard. Stored in
chrome.storage.local. BetterSSC has no GIPHY app of its own.
- What GIPHY sees: your search queries and the IP they're sent from. They do NOT see any of your Substack chat content — the search is independent of the chat. Once you pick a GIF, the binary downloads to your browser and is re-uploaded to Substack from your browser. GIPHY never knows which chat you sent the GIF in or who saw it.
- What Substack sees: the same thing as if you'd dragged a GIF in from your desktop — an
image/gif upload from your own session.
- Fully opt-in. Until you click
GIF and configure a key, GIPHY's API isn't contacted.
💹 Trades strip + alerts — local parsing, your own bot
- The trade parser runs entirely in the extension page on messages the chat already delivered — no request leaves for it, no AI provider is involved.
- Telegram alerts go only to the bot and chat you configured for the bridge, carrying the member's name, their message text (capped), and a Substack link to the comment. Nothing is sent when the strip or the alerts toggle is off.
- The per-day "already alerted" list lives in
chrome.storage.local as comment ids only.
🔗 Link previews — opt-in, cookieless
The link preview cards (v0.7) are off until you enable them, and they're the one feature that reaches outside Substack to arbitrary websites:
- You grant the access explicitly. Enabling previews in Chat preferences triggers a Chrome permission prompt for broad host access (
http://*/*, https://*/*) — declared as an optional permission, so it is NOT requested at install and NOT held unless you turn the feature on. Turn the feature off and BetterSSC relinquishes the permission.
- What the linked site sees: when a preview is built, your browser makes one request to that page — so the site (and its image host) sees your IP, the same as if you'd clicked the link. The request is cookieless (
credentials: 'omit'), so it carries no session and can't act as you. This is inherent to any link unfurl; it's why the feature is opt-in.
- What Substack sees: nothing new. Previews are built and rendered entirely in your browser and are never written back to Substack or shared with other readers.
- No code execution. Page metadata is rendered as inert text (
textContent, never HTML), preview images are https-only and loaded with no referrer, and responses are size-capped. A hostile page can't run script or smuggle a tracking pixel through the card.
- Previews are cached in memory for the tab's lifetime only, then forgotten.
Known issues
Things I know are broken or unfinished as of v0.5.0. PRs welcome. Bug reports help me prioritize.
- Reply quotes are local-only. When you click Reply and send, BetterSSC shows the quoted parent message above yours. The actual wire payload sent to Substack is just plain text, so anyone else (including you on your phone in native Substack) sees a first-class message with no quote. Substack's reply API needs a fresh wire capture before this is shippable end-to-end.
- Image upload isn't built yet. Incoming images render inline with a lightbox. Sending an image from BetterSSC isn't implemented. Use native Substack to upload for now. WS capture confirmed Substack creates a separate
type: "media" post, so the architecture is known. Implementation pending.
- Live updates use 12-second polling. WebSocket support is stubbed but not protocol-decoded. Substack's WS returns "Invalid message" after auth and we fall back to REST polling, same cadence as native. Reactions and new messages land on the next poll, not instantly.
- macOS Tahoe 26.3 notification banners can silently drop.
chrome.notifications.create returns permission: granted and the call reports success, but the OS sometimes doesn't render the banner. This is outside the extension. Notification permission alone doesn't fix it. Affects native Chrome notifications generally on this OS.
- You'll see notifications for your own cross-device posts. Self is auto-watched by default. If you send a message from your phone while this tab is hidden, the desktop will notify you. Some people find this useful, some find it noisy. Click the 🔔 on your own row in the Active rail to silence; the auto-watch will re-add itself on next session.
- Thread filter intersected with search occasionally hides too aggressively. If you have an active thread filter AND start typing in the search box, the intersection logic can hide the parent. Clear one to recover. Will be tightened when we ship the next search pass.
- Starting a new top-level thread isn't wired. The v0.11.0 thread rail lets you switch between a channel's existing threads and reply inside any of them, but posting a brand-new top-level post (the way an author starts a fresh daily thread) isn't implemented — that write endpoint hasn't been captured yet. Use native Substack to start a new thread.
Tracker: GitHub issues.
Tech notes
No framework. Just vanilla JS modules and plain HTML/CSS. About 3000 lines across app.js, app.css, app.html, lib/*.js, and the manifest. No build step. What you see in the source is what runs.
Why no framework: this is a tool I use daily, and hopefully now you will too. Vanilla makes it easier to read, easier to debug, and easier to keep up with Substack's evolving API without dependency-update churn. Lift the hood and the engine is sitting right there.
The protocol was reverse-engineered through five rounds of probing (see commit history before v0.1.0). A few notable detours that became ship-blocker fixes:
- v0.1.2: cross-origin cookies don't attach from
chrome-extension://, so REST calls now route through the Substack tab via scripting.executeScript.
- v0.1.5: REST replies come back wrapped as
{comment, user} rather than flat. New unwrapComment normalizes both shapes.
- v0.1.8: author info lives as a sibling of
comment, not nested inside it. Attached raw.user as c.author during unwrap.
- v0.1.20: threaded replies live nested under their parents in the response. Added a recursive
flattenReplies walker.
- v0.1.24: quote-reply counts aren't in
reply_count. Built a client-side thread index that counts both styles.
License
MIT. See LICENSE.
Built by Inder Sabharwal. Not affiliated with Substack.