Six weeks and three platform rewrites later A developer's six-week effort to build a browser extension for tracking AI platform usage nearly failed due to context-switching design, Chrome Manifest V3 constraints, and selector rot, but recovered by shipping a minimal viable product and iterating based on user behavior. The extension, which injects a usage bar into Claude and ChatGPT interfaces, saw engagement spike after adding a weekly spend rollup in v1.2, while a token optimization tips panel built over two weeks received zero interactions. Six weeks and three platform rewrites later The context trap almost killed the product My first instinct was a backend dashboard — scrape rate limits, push to a server, let users check a separate tab. Three days into the API design I caught myself: why would anyone context-switch to a dashboard when they're staring at the rate limit error inside Claude /en/tags/claude/ ? The fix was obvious once I stopped overthinking: inject a slim bar directly above the input box. Always visible, zero tab switching, problem solved where it actually happens. Lesson learned: solve the friction in the exact context it appears. Every extra step is a failure point. MV3 is hostile to rapid iteration Chrome's Manifest V3 constraints bit me in three specific ways: CSP kills inline everything. Every onclick="handler " in my popup HTML failed silently. The fix: addEventListener wired after DOMContentLoaded for every single interaction. Took a day to trace because the errors don't surface in the console the way you'd expect. Service workers die on you. Background scripts are now ephemeral — Chrome kills them between messages. Any state held in memory vanishes. Everything persistent must go through chrome.storage.local . I learned this when users reported intermittent "limit not updating" bugs that I couldn't reproduce locally. Message channels have a contract. chrome.runtime.onMessage listeners must return true if you'll call sendResponse asynchronously, false or nothing if you won't. Get this wrong and you get "message channel closed before response was received" — an error that appears randomly and leaves no stack trace. // Correct async pattern chrome.runtime.onMessage.addListener msg, sender, sendResponse = { if msg.type === 'GET USAGE' { fetchUsage .then data = sendResponse data return true // critical: tells Chrome we'll respond async } return false } Selector rot is inevitable — plan for it Every platform updates its frontend weekly. ChatGPT /en/tags/chatgpt/ broke my content script three days post-launch. The only sustainable pattern is defensive selector arrays with explicit fallbacks: js const INPUT SELECTORS = ' prompt-textarea', // primary ' data-id="prompt-textarea" ', // fallback 1 'div contenteditable="true" ', // fallback 2 'textarea placeholder ="Message" ' // fallback 3 function findInput { for const selector of INPUT SELECTORS { const el = document.querySelector selector if el return el } return null } Never trust a single selector. Always null-check before DOM operations. I now ship with 3-4 fallbacks per platform and a background job that logs selector failures so I know when to update. I built a feature zero people used Spent two weeks on a "token optimization tips" panel in the popup — collapsible, categorized, pretty. Total interactions in week one: zero. Not a single piece of feedback mentioned it. Meanwhile, the 1 request: "show me weekly spend across all platforms." I had per-conversation cost tracking but no rollup. Shipped the weekly summary in v1.2 — immediate engagement spike. The lesson isn't "do user research" I did . It's that users can't articulate what they need until they're using the product. The tips panel sounded useful in theory. The weekly view revealed itself from actual behavior. Ship the minimum. Watch what they reach for. Build that. Notifications took three rewrites First attempt: chrome.notifications.create for every limit warning. Result — notification spam during long coding sessions, users disabled permissions. Second: in-page toast system. Better, but z-index fights with platform modals and the toasts got buried. Third current : a persistent but collapsible banner injected at the top of the chat viewport, color-coded by severity yellow at 70%, orange at 90%, red at 95% . Dismissible per-session, respects prefers-reduced-motion . Finally zero complaints. The extension is live as TokenPulse if anyone wants to poke at the implementation. Happy to share the manifest config or the platform detection logic if it saves someone the MV3 learning curve. Next After six months with Cursor, here's where the real leverage actually shows up → /en/threads/7178/ a guide to making money with AI https://tanyan888.com/ , with plenty of directly applicable cases.