The first version of TokenPulse's notification system was simple: fire a notification when usage crossed 75%, 90%, or 100%. Users turned it off within a day.
The problem was obvious in retrospect — the notification fired every time the condition was checked, not just when the threshold was first crossed. If you were at 82% and the extension polled every 5 minutes, you got a "you're at 75%" notification every 5 minutes indefinitely.
Here is the three-iteration journey to a notification system users actually keep enabled.
What a good threshold notification system needs
Before writing code, define the behavior precisely:
- Fire once when usage first crosses a threshold
- Do not fire again for the same threshold in the same window
- Reset when usage drops below the threshold
- Support multiple thresholds independently
- Persist state across service worker restarts Most notification systems get 1 and 2 right but fail on 3 and 5.
Version 1 — fires every poll (wrong)
// Polled every 5 minutes
function checkNotifications(utilization) {
if (utilization >= 0.75) {
chrome.notifications.create({
type: 'basic',
iconUrl: chrome.runtime.getURL('icons/icon48.png'),
title: 'TokenPulse',
message: `You are at ${Math.round(utilization * 100)}% of your limit`,
})
}
}
Problem: fires on every poll cycle once above threshold. User gets a notification every 5 minutes.
Version 2 — tracks last notified (better, still wrong)
let lastNotifiedThreshold = 0 // In-memory — dies with service worker
function checkNotifications(utilization) {
const thresholds = [75, 90, 100]
const crossed = thresholds.filter(t => utilization * 100 >= t).pop() || 0
if (crossed > lastNotifiedThreshold) {
lastNotifiedThreshold = crossed
fireNotification(crossed, utilization)
}
}
Problem: lastNotifiedThreshold is in memory. When Chrome kills and restarts the service worker, it resets to 0. The user gets duplicate notifications after every service worker restart.
Also: never resets when usage drops. Once you hit 90%, you never get a 75% notification again even after the window resets.
Version 3 — persisted, threshold-aware, resetting (correct)
// service-worker.js
const THRESHOLDS = [50, 75, 90, 100]
async function shouldNotify(stateKey, currentPct, settings) {
// Filter to enabled thresholds
const enabled = THRESHOLDS.filter(t => {
if (t === 50) return settings.notify_50 !== false
if (t === 75) return settings.notify_75 !== false
if (t === 90) return settings.notify_90 !== false
if (t === 100) return settings.notify_100 !== false
return false
})
if (enabled.length === 0) return null
// Find the highest threshold crossed
const crossed = enabled.filter(t => currentPct >= t).pop() || 0
// Read persisted state
const stored = await chrome.storage.local.get('lastNotified')
const lastNotified = stored.lastNotified || {}
const last = lastNotified[stateKey] || 0
// Reset: if usage has dropped below the last notified threshold
if (crossed === 0 && last > 0) {
lastNotified[stateKey] = 0
await chrome.storage.local.set({ lastNotified })
return null
}
// No new threshold crossed
if (crossed <= 0 || crossed <= last) return null
// New threshold crossed — persist and notify
lastNotified[stateKey] = crossed
await chrome.storage.local.set({ lastNotified })
return crossed
}
The key insight: stateKey is a string that identifies which window and platform we are tracking — for example 'claude_5hour' or 'claude_7day'. This allows independent threshold tracking for each limit.
Firing the notification
async function checkRateLimitNotifications(usage) {
const settings = await Storage.getSettings()
// Check 5-hour limit
const sessionPct = Math.round((usage.five_hour?.utilization || 0) * 100)
const sessionThreshold = await shouldNotify(
'claude_5hour',
sessionPct,
settings
)
if (sessionThreshold) {
const messages = {
50: 'Halfway through your 5-hour session limit.',
75: 'Only 25% of your session remaining.',
90: 'Almost out — consider wrapping up your session.',
100: 'Session limit reached. Claude will be restricted.',
}
chrome.notifications.create(`tp_session_${sessionThreshold}`, {
type: 'basic',
iconUrl: chrome.runtime.getURL('icons/icon48.png'),
title: `Claude session at ${sessionThreshold}%`,
message: messages[sessionThreshold],
priority: sessionThreshold >= 90 ? 2 : 1,
})
}
// Check 7-day limit (same pattern, different key)
const weeklyPct = Math.round((usage.seven_day?.utilization || 0) * 100)
const weeklyThreshold = await shouldNotify(
'claude_7day',
weeklyPct,
settings
)
if (weeklyThreshold) {
chrome.notifications.create(`tp_weekly_${weeklyThreshold}`, {
type: 'basic',
iconUrl: chrome.runtime.getURL('icons/icon48.png'),
title: `Claude weekly limit at ${weeklyThreshold}%`,
message: `Your 7-day usage is at ${weeklyThreshold}%.`,
priority: weeklyThreshold >= 90 ? 2 : 1,
})
}
}
The reset mechanism explained
The reset logic is the part most implementations get wrong.
When does a threshold reset? When the usage drops below the threshold level. If you were notified at 90% and your session resets, usage drops to 0%. The next check sees crossed === 0 and last === 90, so it clears lastNotified[stateKey] to 0.
On the next poll at, say, 30% usage, crossed === 0 still, so nothing fires. When usage climbs back to 76%, crossed === 75 and last === 0, so the 75% notification fires again.
This is exactly the right behavior: you get warned at each threshold once per window, and the warnings reset with the window.
Giving notifications meaningful content
Generic percentage notifications are less useful than contextual ones:
function buildNotificationMessage(threshold, limitType, utilization) {
const pct = Math.round(utilization * 100)
// Estimate time remaining based on burn rate
// (simplified — real implementation tracks usage over time)
const timeHints = {
50: 'You probably have enough messages for another 2 hours at your current pace.',
75: 'Consider wrapping up long sessions — roughly 30-45 minutes of usage left.',
90: 'Almost out. Finish your current task before the limit hits.',
100: 'Limit reached. Start a fresh session after the reset.',
}
return timeHints[threshold] || `Usage at ${pct}%.`
}
"You probably have enough messages for another 45 minutes" is more actionable than "You are at 75%." Users make better decisions with context.
User controls
Notifications the user cannot control get disabled. Add per-threshold toggles in settings:
// Default settings
const DEFAULT_SETTINGS = {
notify_50: false, // Off by default — too early for most users
notify_75: true,
notify_90: true,
notify_100: true,
notify_response_ready: true,
}
50% off by default reduces notification fatigue for new users. They can enable it if they want earlier warnings.
The response-ready notification
One notification that users love and is easy to miss: "your response is ready."
When Claude is generating a long response, developers often switch tabs. The response finishes but they don't notice for several minutes. A notification when generation completes brings them back:
// content-script.js
function watchForResponseCompletion() {
const observer = new MutationObserver(() => {
const isGenerating = document.querySelector('[data-generating="true"]')
const wasGenerating = window._tpWasGenerating
if (wasGenerating && !isGenerating) {
chrome.runtime.sendMessage({ type: 'RESPONSE_READY' })
}
window._tpWasGenerating = !!isGenerating
})
observer.observe(document.body, { childList: true, subtree: true })
}
// service-worker.js
if (msg.type === 'RESPONSE_READY') {
const settings = await Storage.getSettings()
if (settings.notify_response_ready === false) return false
chrome.notifications.create('tp_response_ready', {
type: 'basic',
iconUrl: chrome.runtime.getURL('icons/icon48.png'),
title: 'Response ready',
message: 'Your Claude response has finished generating.',
priority: 1,
})
return false
}
Full implementation at github.com/anu-ship-it/TokenPulse.
TokenPulse — free AI usage tracker for Claude, ChatGPT, Gemini, DeepSeek and Grok.
Top comments (0)