.env.id, persona, prompt, model. That's it.Open the Chrome Web Store listing, hit Add to Chrome, confirm the manifest permissions. Works in Chromium and Edge too — same file.
Click the puzzle icon, pin Peanut Gallery, then Ctrl+Shift+P to dock the panel. It stays on while you browse.
Load any tab with audio — a livestream, a podcast, a webinar, a Zoom call, a YouTube video. The staff clocks in automatically as soon as the first caption lands. No button, no signup, no modal.
That is the entire install path for the default experience. The hosted backend uses our keys, routes through our server, and costs you nothing. It's rate-limited per installation — if you're a heavy user or you don't trust our pipeline, skip to § 02 and run the whole thing yourself.
~4 hours of streaming per day on the shared backend, split across all four critics. Hit the wall, the feed pauses politely and tells you so. Self-host removes the cap — the cap then becomes your API spend.
.env file. Your machine is now the wire room.The backend is a small Node service that brokers between the extension and the four upstream APIs. It exists so your API keys never ship to the browser — and so you can swap models or add caching without touching extension code.
You need Node 20+ and git. The server has no database and no state — just a process and four outbound connections.
$ git clone https://github.com/peanut-gallery/server.git $ cd server $ npm install # ~200 deps, 20 seconds on a decent pipe
Copy .env.example to .env and drop in the four keys you collected in § 03. Brave is optional; if you omit it, the fact-checker falls back to model-only claims and flags itself as unverified.
$ cp .env.example .env $ nano .env # paste four keys, save, exit
Dev mode reloads on change; production mode is what you pin to a tmux window or a systemd unit. Either way it binds to one port and listens on localhost by default.
$ npm run dev › peanut-gallery/server listening on :8787 › deepgram ready (nova-3) › anthropic ready (claude-haiku-4-5) › xai ready (grok-4-fast) › brave ready (search-v1) › wire room is open. go heckle something.
In the extension's Tweaks panel, switch Backend from Hosted to Custom and paste http://localhost:8787. The panel badge flips to SELF-HOSTED when the health-check clears.
If you want to run the server somewhere other than localhost, put it behind a reverse proxy with TLS (Caddy does this in three lines) and restrict ALLOWED_ORIGIN to your extension ID. The extension refuses non-HTTPS custom backends except for localhost.
Deepgram bills by streaming minute, Anthropic & xAI by token. A 90-minute podcast at full staff runs roughly $0.18–$0.35 of API. Set usage caps on each provider's dashboard before you walk away from it.
Each vendor's signup is roughly the same ritual: create an account, verify email, create an API key, copy it into your .env. Free tiers on all four are enough to get through a few hours of testing — which is enough to decide whether you want to wire in a real card.
If you swap models — say, you prefer Claude Sonnet for the comedy writer or Grok Mini for the troll — the model string lives in each pack file, not the server. See § 04. The server doesn't care which model you pick as long as the vendor is configured.
Two packs ship by default: the Stern staff (Baba Booey, The Troll, Fred Norris, Jackie Martling) and the TWiST staff (Jason, Molly, Dwight, Ampersand). Everything else you'd ever want is a pack you write yourself.
A note on the rosters. Pack names nod to public media figures. Peanut Gallery is an unofficial work of commentary and parody — not affiliated with, endorsed by, or approved by any of them. Any of those packs can be renamed, swapped, or replaced wholesale; the schema below is the only thing that's load-bearing.
The roles are fixed — fact, dunk, cue, bit — because they pair with specific UI treatments (the tag colors, the sound-cue playback, the receipts badge). The personas filling those roles are entirely up to you.
| Field | Type | Description | |
|---|---|---|---|
id |
string | Required | Lowercase, no spaces. Appears in the pack picker and the URL params. e.g. stern, twist, mst3k. |
name |
string | Required | Human-readable label shown in the Tweaks menu. Keep it under 28 chars. |
tagline |
string | Optional | A one-line pitch for the pack picker. Italicized in the UI. |
staff |
array[4] | Required | Exactly four persona entries, one per role. Extras are ignored; missing roles throw. |
staff[].role |
enum | Required | One of fact · dunk · cue · bit. Exactly once each. |
staff[].persona |
string | Required | Display name. Shown on the mug card, the wire tag, and the clipboard export. |
staff[].model |
string | Required | Vendor-prefixed model ID. anthropic/claude-haiku-4-5, xai/grok-4-fast, etc. The server routes by prefix. |
staff[].prompt |
string | Required | The full system prompt. ~400 tokens is the sweet spot. Longer = slower. Include voice, cadence, and two or three example reactions in-line. |
staff[].temperature |
number | Optional | Default 0.7. Trolls & comedy writers like 0.9. Fact-checker wants 0.2. |
staff[].cooldown_ms |
number | Optional | Minimum gap between this persona's posts. Keeps the troll from monologuing. Default 4000. |
mst3k.json packHosted backend: upload via the Packs tab in the extension Tweaks panel. Self-hosted: drop it in ./packs/ on the server and it's picked up on next request — no restart.
Run npm run pack:lint packs/mst3k.json. It checks the schema, pings each model once to confirm the vendor is reachable, and prints token counts for each prompt.
In the extension, open Tweaks → Pack and pick your new entry. The feed clears, the mugs re-render, and the next transcript partial runs through the new cast.
Give each persona three short in-line examples of the shape of reaction you want. "Short zinger under 12 words" > "Be funny." Include a negative example or two: "Don't explain the joke. Don't caveat. Don't apologize." These cost you nothing and save the model from hedging.
| Action | How | What it does |
|---|---|---|
| Open the panel | Ctrl+Shift+P | Docks the wire room in Chrome's native side panel. Stays open across tabs. |
| Swap the pack | Tweaks → Pack | Instantly retires the current staff and brings in the new one. Feed clears; history kept per-session. |
| Mute a critic | Click the bottom-row pill | That persona stops posting. Other three keep going. Un-mute re-enables mid-stream. |
| Clip a line | Hover a line → clip icon | Copies a formatted quote block to clipboard. Includes timestamp, persona, source URL. |
| Export the feed | Tweaks → Export | Dumps the full session as Markdown. Useful for sharing a particularly brutal broadcast. |
| Kill the panel | Tweaks → Off-air | Extension stays installed; mic stays off; wire goes dark. Pick it back up later. |
The extension does not run on tabs you haven't opened the panel against. There is no background heckling. If the panel is closed, the mic is not listening, the server is not called, and nothing is happening.
SELF-HOSTED · UNREACHABLE./health on your backend. Confirm the server is up (curl http://localhost:8787/health), your ALLOWED_ORIGIN matches the extension ID, and you're not on a different network than you think (corporate VPNs love to rewrite localhost).npm run pack:lint <pack>.json, check token counts, confirm the model ID still exists at the vendor. Vendors rename models; packs need updating when they do.BRAVE_SEARCH_API_KEY is missing or invalid. Fact-checker falls back to model-only claims when search fails; it flags itself with a small NO RX badge when it does. Add the key or accept the fallback.If none of the above applies, file an issue on the github with your server.log, the pack you're using, and the video URL. We read them. We do not promise to fix them. It's a newspaper run by four AIs.