Six weeks ago I started building TokenPulse after getting cut off mid-debugging session by Claude's rate limit one too many times. The extension is now live with active users across five platforms. Here is what I actually learned — the engineering decisions, product mistakes, and things I would do differently.
The problem was real but I almost built the wrong solution
My initial instinct was to build a browser extension that scraped Claude's rate limit data and sent it to a backend that users could check via a dashboard. I spent three days on the backend architecture before I stopped and asked the obvious question: why would someone open a separate dashboard to check information they need while they are already inside Claude?
The right solution was obvious once I asked it: inject the information directly into the page they are already on. A slim bar above the input box. Always visible, never intrusive, no tab switching required.
The lesson: solve the problem in the context where it actually occurs. Do not add steps.
MV3 was harder than the documentation suggested
Chrome's Manifest V3 requirements were the first major technical challenge. The biggest constraint is Content Security Policy — no inline scripts, no eval, no remotely loaded JavaScript.
This broke my initial implementation in three ways:
Inline event handlers. Every onclick="..." attribute in my HTML failed silently. The fix is addEventListener for everything, wired after DOMContentLoaded. Simple in hindsight, took me a day to fully debug.
Service worker lifecycle. MV3 background scripts are service workers — Chrome can kill them at any time. Code that assumes the background script is alive between messages will fail intermittently and be almost impossible to reproduce. Everything has to go through chrome.storage.local.
Message channel management. The chrome.runtime.onMessage listener has a specific contract: return true if you are going to call sendResponse asynchronously, return false (or nothing) if you are not. Getting this wrong causes the dreaded "message channel closed before response was received" error that appears randomly and is hard to trace.
The dual selector problem
Every platform — Claude, ChatGPT, Gemini, DeepSeek, Grok — updates its frontend regularly. I learned this the hard way when a ChatGPT update broke my content script three days after I published.
The solution is defensive selector arrays with fallbacks:
const INPUT_SELECTORS = [
'#prompt-textarea', // primary
'[data-id="prompt-textarea"]', // fallback 1
'div[contenteditable="true"]', // fallback 2
]
function findInput() {
for (const selector of INPUT_SELECTORS) {
const el = document.querySelector(selector)
if (el) return el
}
return null
}
Never rely on a single selector. Always have at least two fallbacks. Check for null before every DOM operation.
I shipped a feature nobody asked for and skipped one everyone wanted
I spent significant time building a detailed token tips panel — a collapsible section in the popup with optimization advice. Nobody uses it. Zero feedback mentions it. It adds visual weight and code complexity for no user value.
Meanwhile, the most common piece of feedback in the first two weeks was "can you show me how much I've spent this week across all platforms?" — a weekly summary. I had cost tracking per conversation but no weekly rollup.
The lesson is not "do user research before building" — I had done that. The lesson is that users cannot tell you what they want until they are using the product. The tips panel sounded useful in theory. The weekly summary revealed itself from actual usage patterns.
Ship the minimum, watch what users do, build what they actually reach for.
The notification system needed three rewrites
My first notification implementation fired every time the threshold was crossed, which meant multiple notifications within the same session. Users turned it off immediately.
My second implementation stored the last notified percentage and only fired when the user crossed a higher threshold. Better — but it never reset, so once you hit 90% and your session reset, you would not get notified at 75% in the next session.
The third implementation tracks the last notified threshold per window, resets it when usage drops below the threshold, and fires once per crossing per window. This is the version that users actually keep enabled.
The algorithm sounds simple when written out. Getting there took three iterations and specific user feedback about each failure mode.
Choosing Vercel over AWS was correct
I spent two days considering hosting options for the marketing site before choosing Vercel. The alternative I seriously considered was AWS — EC2 with nginx and PM2.
Vercel was correct for three reasons:
-
Zero operational overhead. No instance management, no SSL renewal, no process monitoring. Deployment is
git push. - The credentials problem doesn't change. Whether you deploy on Vercel or EC2, you still need Google Sheets API credentials in environment variables. Switching infrastructure doesn't make that easier.
- Revenue is zero. Until the product generates money, infrastructure cost is a constraint. Vercel hobby tier is free. EC2 t3.micro is $8-10/month. The right time to consider AWS is when you have specific requirements Vercel cannot meet — custom runtime environments, GPU access, VPC networking, specific regional compliance. For a Next.js marketing site with API routes, those requirements do not exist yet.
The Chrome Web Store review process is slower than you expect
First submission: 3 days to review, rejected for missing privacy policy disclosures in the store listing.
Second submission after fixing: 2 days to review, approved.
This means plan for a minimum 5-7 day runway between "finished" and "published." If you are building toward a launch date, submit to the Chrome Web Store at least a week before you want to launch.
Also: the Chrome Web Store's description field is SEO. The keywords in your listing title and description affect where your extension appears in the store's own search. "TokenPulse — Claude Rate Limit & Token Tracker" performs better than "TokenPulse" alone.
What I would do differently
Build the popup last. The in-page bar is the core value. I built the popup first because it felt more like a "real" extension. The bar is what users actually use 90% of the time.
Write the content scripts with a test harness from day one. Testing content scripts means opening the browser, loading the extension, navigating to the target page, and observing behavior. This loop takes 2-3 minutes per test. A mock DOM environment for unit testing would have saved significant time on the notification system rewrites.
Set up error tracking earlier. I added error logging after shipping. I had no visibility into content script failures for the first two weeks. Users were silently experiencing broken functionality that I only discovered through direct feedback.
Launch on one platform, add others after. I launched with Claude and ChatGPT support simultaneously. The dual-platform debugging effort at launch was chaotic. Claude alone first, then ChatGPT, would have been cleaner.
TokenPulse is free and open source at github.com/anu-ship-it/TokenPulse. If you build with AI tools daily and want visibility into your usage, install it here — works on Claude, ChatGPT, Gemini, DeepSeek and Grok with no API key.
Happy to answer questions about any of the technical decisions in the comments.
Top comments (0)