Chrome extension that auto-claims Twitch bonus chests while you watch · survives Twitch's obfuscated CSS churn with a semantic selector cascade · zero backend
Architecture
Chrome injects content.js into every twitch.tv page. A readyState guard starts the loop whether or not load already fired, then polls every 5 s. Each tick, findClaimButton() scopes the search to the community points summary and walks a cascade of 4 semantic selectors; when the chest appears it climbs to the enclosing <button> with closest(), checks it is not disabled, and clicks. On a successful claim it increments claimedBonusChestCount in chrome.storage.local. The Stats popup reads that counter on open and derives channel points earned.
Deployed: Extension → Chrome Web Store · MV3, everything runs client-side
Outcomes
- ≤ 5 s · chest visible → claimed
- 0 · servers · all logic runs in the tab
- 4 · fallback selectors, semantic before cosmetic
- 2 · permissions total: storage + twitch.tv host
Skills demonstrated: resilient DOM selector strategy · Chrome extension MV3 · SPA-aware content scripts · minimal permission surface
Problems and solutions
Every choice answers one constraint: the extension lives inside a page it does not control and that changes without notice.
▓ Outliving Twitch's CSS churn
Problem: Twitch builds its UI with styled-components: class names are compiler hashes like ScCoreButton-sc-ocjdkq-0 that change on every frontend rebuild. Early versions pinned to those hashes and silently broke, four separate releases in the git history are selector fixes.
I replaced the hash selector with a prioritized cascade of 4 semantic hooks: the chest icon class, two of Twitch's own data-test-selector attributes, and a data-a-target. A match may be an inner icon, so the code climbs to the clickable element with closest('button').
- Rejected hash classes after repeated breakage: they are compiler output, not an API
- Rejected matching button text: it breaks in every non-English locale
- Test hooks are still Twitch internals, so 4 independent fallbacks instead of trusting any single one
▒ Polling instead of MutationObserver
Problem: The chest appears at unpredictable moments, and detection runs inside a tab that is already busy playing video. A wrong choice here costs CPU on every chat message.
A plain 5 s setInterval running one scoped querySelector per tick: roughly 180 cheap no-op checks per chest at Twitch's ~15 min cadence.
- Tried MutationObserver first (the experiment survives as content copy.js): Twitch chat mutates constantly, so the callback fired in storms
- Accepted up to 5 s of claim latency in exchange for a loop that is trivially correct
- The interval also survives Twitch's SPA navigation, since the document persists across channel switches
▚ No background service worker
Problem: MV3 pushes extension logic toward service workers: more moving parts, more lifecycle bugs, more Web Store review scrutiny.
The whole extension is one content script and one popup. The only permission beyond the twitch.tv host is storage.
- Rejected a background worker with the alarms API: there is nothing to do when no Twitch tab is open
- A 2-entry permission surface means faster store review and an install prompt users can actually trust
- Trade-off: no cross-tab coordination, each tab polls independently, acceptable because clicking a claimed chest is a no-op (disabled check)
▧ Injection timing on a SPA
Problem: The content script can run before or after the page's load event, and Twitch navigates between channels with pushState, never reloading. An early version missed every chest after the user switched channels.
A readyState guard starts the loop immediately if load already fired and listens for it otherwise; the interval then outlives every in-app navigation.
- Rejected re-injection via webNavigation events: an extra permission for a problem the persistent interval already solves
- The guard closes the race where a late-injected script waits for a load event that never comes
- One interval covers every channel the user browses to in that tab
▤ Stats without a backend
Problem: Users want proof the extension is working, but a telemetry server is cost, maintenance, and a privacy liability for a free tool.
A single counter in chrome.storage.local, incremented on each claim; the popup reads it and derives channel points earned (x50).
- Rejected remote analytics entirely: zero infrastructure, data never leaves the browser
- Trade-off: the counter is device-local and does not sync across machines, fine for a vanity stat
- Read-modify-write through the async storage API keeps the popup and content script decoupled