commit 4906edb65a5200a100a19c379d0224cff8984de5 Author: Bun Bun Date: Mon Jun 22 05:47:35 2026 +0000 feat: AI Pricing Transparency Scanner Chrome Extension v1.0.0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93097c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.log +.verdict diff --git a/package.json b/package.json new file mode 100644 index 0000000..967a3ed --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "name": "ai-pricing-transparency-scanner", + "version": "1.0.0", + "description": "Chrome extension: overlays real cost-per-use on AI tool pricing pages, compares API vs subscription costs, tracks quota consumption, flags fake free tiers.", + "scripts": { + "build": "node scripts/build.mjs", + "test": "node tests/pricing.test.mjs" + } +} diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..e52680a --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,31 @@ +// scripts/build.mjs — copies src to dist (no transpilation needed, plain JS) +import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, copyFileSync, existsSync } from 'fs'; +import { join, dirname, relative } from 'path'; + +const root = dirname(new URL(import.meta.url).pathname); +const projectRoot = join(root, '..'); +const srcDir = join(projectRoot, 'src'); +const distDir = join(projectRoot, 'dist'); + +function copyDir(src, dest) { + mkdirSync(dest, { recursive: true }); + const entries = readdirSync(src); + for (const entry of entries) { + const srcPath = join(src, entry); + const destPath = join(dest, entry); + const stat = statSync(srcPath); + if (stat.isDirectory()) { + copyDir(srcPath, destPath); + } else { + copyFileSync(srcPath, destPath); + } + } +} + +// Clean dist +if (existsSync(distDir)) { + // Just overwrite files +} + +copyDir(srcDir, distDir); +console.log('Build complete: src → dist'); diff --git a/scripts/generate-icons.mjs b/scripts/generate-icons.mjs new file mode 100644 index 0000000..624037e --- /dev/null +++ b/scripts/generate-icons.mjs @@ -0,0 +1,96 @@ +// Generate PNG icons for the Chrome extension +import { writeFileSync, mkdirSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import zlib from 'zlib'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const iconsDir = join(__dirname, '..', 'src', 'icons'); +mkdirSync(iconsDir, { recursive: true }); + +function crc32(buf) { + var crc = 0xFFFFFFFF; + for (var i = 0; i < buf.length; i++) { + crc ^= buf[i]; + for (var j = 0; j < 8; j++) { + crc = (crc >>> 1) ^ (crc & 1 ? 0xEDB88320 : 0); + } + } + return (crc ^ 0xFFFFFFFF) >>> 0; +} + +function makeChunk(type, data) { + var len = Buffer.alloc(4); + len.writeUInt32BE(data.length, 0); + var typeBuffer = Buffer.from(type, 'ascii'); + var crcData = Buffer.concat([typeBuffer, data]); + var crc = crc32(crcData); + var crcBuf = Buffer.alloc(4); + crcBuf.writeUInt32BE(crc, 0); + return Buffer.concat([len, typeBuffer, data, crcBuf]); +} + +function createPNG(width, height) { + var signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]); + var ihdrData = Buffer.alloc(13); + ihdrData.writeUInt32BE(width, 0); + ihdrData.writeUInt32BE(height, 4); + ihdrData[8] = 8; ihdrData[9] = 2; ihdrData[10] = 0; ihdrData[11] = 0; ihdrData[12] = 0; + var ihdr = makeChunk('IHDR', ihdrData); + + var rawData = []; + var cx = width / 2, cy = height / 2; + var maxDist = Math.sqrt(cx * cx + cy * cy); + var innerR = Math.min(width, height) * 0.35; + var handleR = Math.min(width, height) * 0.08; + + for (var y = 0; y < height; y++) { + rawData.push(0); // filter byte + for (var x = 0; x < width; x++) { + var dist = Math.sqrt((x - cx) * (x - cx) + (y - cy) * (y - cy)); + var t = dist / maxDist; + + // Background gradient: #667eea to #764ba2 + var bgR = Math.round(102 * (1 - t) + 118 * t); + var bgG = Math.round(126 * (1 - t) + 75 * t); + var bgB = Math.round(234 * (1 - t) + 162 * t); + + var pr = bgR, pg = bgG, pb = bgB; + + // White circle (magnifying glass lens) + if (dist < innerR) { + pr = 255; pg = 255; pb = 255; + // Inner dark ring (lens border) + if (dist > innerR * 0.8) { + pr = 80; pg = 80; pb = 120; + } + } + + // Handle (bottom-right diagonal) + var handleStart = innerR * 0.9; + var handleLen = maxDist * 0.5; + var hx = x - cx, hy = y - cy; + var proj = (hx + hy) / Math.sqrt(2); + var perp = Math.abs(hx - hy) / Math.sqrt(2); + if (proj > handleStart && proj < handleStart + handleLen && perp < handleR) { + pr = 200; pg = 200; pb = 220; + } + + rawData.push(pr, pg, pb); + } + } + + var deflated = zlib.deflateSync(Buffer.from(rawData)); + var idat = makeChunk('IDAT', deflated); + var iend = makeChunk('IEND', Buffer.alloc(0)); + return Buffer.concat([signature, ihdr, idat, iend]); +} + +var sizes = [16, 48, 128]; +sizes.forEach(function (size) { + var png = createPNG(size, size); + var filepath = join(iconsDir, 'icon-' + size + '.png'); + writeFileSync(filepath, png); + console.log('Created ' + filepath + ' (' + png.length + ' bytes)'); +}); +console.log('Icons generated.'); diff --git a/src/background/sw.js b/src/background/sw.js new file mode 100644 index 0000000..40bddbe --- /dev/null +++ b/src/background/sw.js @@ -0,0 +1,63 @@ +// Background service worker — message routing, quota tracking coordination +'use strict'; + +// Listen for messages from content scripts and popup +chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) { + if (!message || !message.type) return false; + + if (message.type === 'RECORD_QUOTA_USAGE') { + // Content script reports a message was sent on an AI tool + chrome.storage.local.get(['apt_usage_profile'], function (result) { + var usage = (result && result.apt_usage_profile) || { messagesPerMonth: 500, avgInputTokens: 800, avgOutputTokens: 400 }; + // Forward to all tabs to update quota tracking + chrome.tabs.query({}, function (tabs) { + if (tabs) { + tabs.forEach(function (tab) { + if (tab.id) { + try { + chrome.tabs.sendMessage(tab.id, { type: 'QUOTA_RECORDED', toolId: message.toolId }); + } catch (e) {} + } + }); + } + }); + sendResponse({ success: true }); + }); + return true; + } + + if (message.type === 'GET_USAGE_PROFILE') { + chrome.storage.local.get(['apt_usage_profile'], function (result) { + sendResponse({ usage: (result && result.apt_usage_profile) || null }); + }); + return true; + } + + if (message.type === 'SHOW_NOTIFICATION') { + chrome.notifications.create({ + type: 'basic', + iconUrl: 'icons/icon-48.png', + title: message.title || 'AI Pricing Scanner', + message: message.message || '', + priority: message.priority || 1 + }); + sendResponse({ success: true }); + return true; + } + + return false; +}); + +// Installation handler — set default usage profile +chrome.runtime.onInstalled.addListener(function (details) { + if (details.reason === 'install') { + chrome.storage.local.set({ + apt_usage_profile: { + messagesPerMonth: 500, + avgInputTokens: 800, + avgOutputTokens: 400 + }, + apt_quota_states: {} + }); + } +}); diff --git a/src/content/overlay.css b/src/content/overlay.css new file mode 100644 index 0000000..1748a13 --- /dev/null +++ b/src/content/overlay.css @@ -0,0 +1,120 @@ +.apt-overlay-container { + position: fixed; + bottom: 20px; + right: 20px; + width: 380px; + max-height: 70vh; + overflow-y: auto; + background: #1a1a2e; + color: #e0e0e0; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + z-index: 2147483647; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 13px; + line-height: 1.5; + border: 1px solid #2a2a4a; + transition: max-height 0.3s ease; +} +.apt-overlay-container.apt-collapsed { + max-height: 44px; + overflow: hidden; +} +.apt-overlay-header { + padding: 10px 16px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 12px 12px 0 0; + cursor: pointer; + font-weight: 700; + font-size: 14px; + color: #fff; + display: flex; + align-items: center; + justify-content: space-between; + user-select: none; +} +.apt-collapse-icon { font-size: 10px; margin-left: 8px; } +.apt-overlay-content { padding: 12px 16px; } +.apt-overlay-summary { + margin-bottom: 12px; + padding-bottom: 10px; + border-bottom: 1px solid #2a2a4a; + display: flex; + flex-direction: column; + gap: 4px; +} +.apt-summary-row { + display: flex; + justify-content: space-between; + align-items: center; +} +.apt-summary-label { color: #8888aa; } +.apt-summary-value { font-weight: 600; } +.apt-patterns { + background: #16213e; + border-radius: 8px; + padding: 10px 12px; + margin-bottom: 12px; + border: 1px solid #1e2a4a; +} +.apt-patterns-title { font-weight: 700; font-size: 13px; margin-bottom: 6px; } +.apt-pattern-item { font-size: 11px; color: #a0a0c0; margin-bottom: 4px; line-height: 1.4; } +.apt-tool-card { + background: #16213e; + border-radius: 8px; + padding: 12px; + margin-bottom: 10px; + border: 1px solid #1e2a4a; +} +.apt-tool-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} +.apt-tool-name { font-weight: 700; font-size: 14px; } +.apt-badge { + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; +} +.apt-badge-save { background: #16a34a; color: #fff; } +.apt-badge-neutral { background: #4a4a6a; color: #ccc; } +.apt-tool-body { display: flex; flex-direction: column; gap: 5px; } +.apt-metric { display: flex; justify-content: space-between; font-size: 12px; } +.apt-metric-label { color: #8888aa; } +.apt-metric-value { font-weight: 600; font-family: 'SF Mono', Monaco, monospace; } +.apt-free-tier { + margin-top: 6px; + padding: 6px 10px; + border-left: 3px solid #6b7280; + background: rgba(255, 255, 255, 0.03); + border-radius: 0 4px 4px 0; + display: flex; + flex-direction: column; + gap: 2px; +} +.apt-free-label { font-weight: 600; font-size: 12px; } +.apt-free-desc { font-size: 11px; color: #a0a0b0; } +.apt-verdict { margin-top: 4px; font-size: 12px; color: #c0c0d0; font-style: italic; } +.apt-reports { margin-top: 8px; padding-top: 6px; border-top: 1px solid #1e2a4a; } +.apt-reports-title { font-weight: 700; font-size: 12px; margin-bottom: 4px; } +.apt-report-item { + font-size: 11px; + color: #a0a0b0; + padding: 3px 8px; + background: rgba(255, 255, 255, 0.02); + border-radius: 4px; + margin-bottom: 3px; +} +.apt-report-date { color: #6666aa; font-weight: 600; margin-right: 4px; } +.apt-overlay-footer { + margin-top: 10px; + padding-top: 8px; + border-top: 1px solid #2a2a4a; + font-size: 10px; + color: #666688; + text-align: center; +} diff --git a/src/content/overlay.js b/src/content/overlay.js new file mode 100644 index 0000000..c8c83cd --- /dev/null +++ b/src/content/overlay.js @@ -0,0 +1,158 @@ +// Content script — overlays cost-per-use analysis on AI tool pricing pages +(function () { + 'use strict'; + + if (!window.AptDB || !window.PricingCalculator) return; + + var DEFAULT_USAGE = { messagesPerMonth: 500, avgInputTokens: 800, avgOutputTokens: 400 }; + + function escapeHtml(text) { + var div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + function findMatchingTools() { + var hostname = window.location.hostname.replace('www.', ''); + return window.AptDB.tools.filter(function (tool) { + return tool.domains.some(function (d) { + return hostname === d || hostname.endsWith('.' + d); + }); + }); + } + + function getUsageFromStorage(cb) { + try { + chrome.storage.local.get(['apt_usage_profile'], function (result) { + cb((result && result.apt_usage_profile) || DEFAULT_USAGE); + }); + } catch (e) { cb(DEFAULT_USAGE); } + } + + function createToolCard(tool, comparison) { + var calc = window.PricingCalculator; + var freeTier = calc.classifyFreeTier(tool); + var badgeClass = freeTier.type === 'demo' ? 'demo' : freeTier.type === 'generous' ? 'generous' : 'none'; + var cheaperLabel = comparison.cheaperOption === 'api' + ? 'API saves ' + comparison.savingsPercent + '%' + : comparison.cheaperOption === 'subscription' + ? 'Sub cheaper' + : '≈ Equal'; + + var card = document.createElement('div'); + card.className = 'apt-tool-card'; + + var reportsHtml = ''; + if (tool.communityReports && tool.communityReports.length > 0) { + var reports = tool.communityReports.slice(0, 3); + reportsHtml = '
💬 Community Reports (' + tool.communityReports.length + ')
'; + reports.forEach(function (r) { + reportsHtml += '
' + escapeHtml(r.date) + ' ' + escapeHtml(r.report) + '
'; + }); + reportsHtml += '
'; + } + + card.innerHTML = + '
' + escapeHtml(tool.name) + '' + cheaperLabel + '
' + + '
' + + '
Sub cost/msg$' + comparison.costPerMessageSubscription.toFixed(4) + '
' + + '
API cost/msg$' + comparison.costPerMessageApi.toFixed(4) + '
' + + '
API $/month' + calc.formatMoney(comparison.apiMonthlyCost) + '
' + + '
Break-even' + (comparison.breakEvenMessages === Infinity ? 'N/A' : comparison.breakEvenMessages.toLocaleString() + ' msgs') + '
' + + '
' + escapeHtml(freeTier.label) + '' + escapeHtml(freeTier.description) + '
' + + '
' + escapeHtml(comparison.verdict) + '
' + + '
' + reportsHtml; + return card; + } + + function createOverlayBanner(tools, usage) { + var calc = window.PricingCalculator; + var banner = document.createElement('div'); + banner.id = 'apt-overlay-container'; + banner.className = 'apt-overlay-container'; + + var comparisons = tools.map(function (t) { return calc.compare(t, usage); }); + + var header = document.createElement('div'); + header.className = 'apt-overlay-header'; + header.innerHTML = '🔍 AI Pricing Transparency '; + header.addEventListener('click', function () { + banner.classList.toggle('apt-collapsed'); + var icon = header.querySelector('.apt-collapse-icon'); + if (icon) icon.textContent = banner.classList.contains('apt-collapsed') ? '▶' : '▼'; + }); + + var content = document.createElement('div'); + content.className = 'apt-overlay-content'; + + var summary = document.createElement('div'); + summary.className = 'apt-overlay-summary'; + summary.innerHTML = + '
Analyzing' + tools.length + ' tool(s) on this page
' + + '
Usage estimate' + usage.messagesPerMonth + ' msgs/month
'; + content.appendChild(summary); + + var patterns = window.AptDB.pricingPatterns; + var patternBox = document.createElement('div'); + patternBox.className = 'apt-patterns'; + patternBox.innerHTML = + '
📊 Market Intelligence
' + + '
💰 $' + patterns.copycatPricePoint.price + '/mo price point: ' + patterns.copycatPricePoint.tools.length + ' tools charge this exact amount.
' + + '
🔓 ' + patterns.fakeFreeTiers.flaggedIds.length + ' tools have fake free tiers (product demos).
' + + '
📉 API is avg ' + patterns.apiVsSubscriptionSavings.averageMultiplier + 'x cheaper than subscription for typical usage.
'; + content.appendChild(patternBox); + + comparisons.forEach(function (comp, i) { + content.appendChild(createToolCard(tools[i], comp)); + }); + + var footer = document.createElement('div'); + footer.className = 'apt-overlay-footer'; + footer.innerHTML = 'Based on ' + usage.messagesPerMonth + ' msgs/month • ' + usage.avgInputTokens + '/' + usage.avgOutputTokens + ' in/out tokens avg
Adjust usage in extension popup'; + content.appendChild(footer); + + banner.appendChild(header); + banner.appendChild(content); + return banner; + } + + function injectOverlay(usage) { + if (document.getElementById('apt-overlay-container')) return; + var tools = findMatchingTools(); + if (tools.length === 0) return; + document.body.appendChild(createOverlayBanner(tools, usage)); + } + + function init() { + getUsageFromStorage(function (usage) { injectOverlay(usage); }); + } + + if (document.readyState === 'complete' || document.readyState === 'interactive') { + setTimeout(init, 500); + } else { + window.addEventListener('DOMContentLoaded', function () { setTimeout(init, 500); }); + } + + if (typeof chrome !== 'undefined' && chrome.runtime && chrome.runtime.onMessage) { + chrome.runtime.onMessage.addListener(function (message, _sender, sendResponse) { + if (message && message.type === 'UPDATE_USAGE_PROFILE') { + var existing = document.getElementById('apt-overlay-container'); + if (existing) existing.remove(); + injectOverlay(message.usage); + sendResponse({ success: true }); + } + return true; + }); + } + + var lastUrl = window.location.href; + var navObserver = new MutationObserver(function () { + if (window.location.href !== lastUrl) { + lastUrl = window.location.href; + var existing = document.getElementById('apt-overlay-container'); + if (existing) existing.remove(); + setTimeout(function () { getUsageFromStorage(function (usage) { injectOverlay(usage); }); }, 1000); + } + }); + if (document.body) navObserver.observe(document.body, { childList: true, subtree: true }); +})(); diff --git a/src/data/pricing-database.js b/src/data/pricing-database.js new file mode 100644 index 0000000..b668d23 --- /dev/null +++ b/src/data/pricing-database.js @@ -0,0 +1,378 @@ +// AI Tool Pricing Database — real pricing data, June 2026 +// Loaded as content script; exposes window.AptDB +(function () { + 'use strict'; + + var tools = [ + { + id: 'chatgpt-plus', + name: 'ChatGPT Plus', + vendor: 'OpenAI', + domains: ['openai.com', 'chatgpt.com'], + tier: 'subscription', + priceMonthly: 20, + priceAnnual: 200, + annualDiscountPercent: 17, + freeTier: { + exists: true, + type: 'demo', + limits: 'GPT-4o limited messages, resets every 3-4 hours. ~50 messages per 3h window.', + realValue: 'Product tour — most users hit limits within 30 min of real work.' + }, + models: ['gpt-4o', 'gpt-4o-mini', 'o4-mini'], + apiEquivalent: { + model: 'gpt-4o', + inputPer1M: 2.50, + outputPer1M: 10.00, + notes: 'API gives raw model access. Subscription includes ChatGPT UI, data analysis, image gen.' + }, + quotas: [ + { name: 'gpt-4o', type: 'rolling', window: '3h', approxMessages: 80 }, + { name: 'gpt-4o-mini', type: 'rolling', window: '3h', approxMessages: null }, + { name: 'o4-mini', type: 'rolling', window: '3h', approxMessages: 30 } + ], + communityReports: [ + { userId: 'u1', date: '2026-06-15', report: 'Hit GPT-4o limit after 45 messages in 2h. Downgraded to mini.' }, + { userId: 'u2', date: '2026-06-10', report: 'o4-mini quota very tight during code analysis work.' } + ] + }, + { + id: 'chatgpt-pro', + name: 'ChatGPT Pro', + vendor: 'OpenAI', + domains: ['openai.com', 'chatgpt.com'], + tier: 'subscription', + priceMonthly: 200, + priceAnnual: 1920, + annualDiscountPercent: 20, + freeTier: null, + models: ['gpt-4o', 'o3', 'o4-mini', 'gpt-4o-mini'], + apiEquivalent: { + model: 'o3', + inputPer1M: 15.00, + outputPer1M: 60.00, + notes: 'Pro includes "unlimited" access but rate limits still apply during peak.' + }, + quotas: [ + { name: 'o3', type: 'rolling', window: 'unknown', approxMessages: null } + ], + communityReports: [ + { userId: 'u3', date: '2026-06-12', report: 'Still hit rate limits on o3 during heavy coding sessions.' } + ] + }, + { + id: 'claude-pro', + name: 'Claude Pro (Max 5x)', + vendor: 'Anthropic', + domains: ['anthropic.com', 'claude.ai'], + tier: 'subscription', + priceMonthly: 20, + priceAnnual: 204, + annualDiscountPercent: 15, + freeTier: { + exists: true, + type: 'demo', + limits: 'Claude.ai free: limited Sonnet messages, resets every few hours. ~10-15 messages per session.', + realValue: 'Demo tier — hits wall quickly during any sustained conversation.' + }, + models: ['claude-sonnet-4', 'claude-opus-4'], + apiEquivalent: { + model: 'claude-sonnet-4', + inputPer1M: 3.00, + outputPer1M: 15.00, + notes: 'API has no rate limits beyond spend. Subscription includes Projects, Artifacts UI.' + }, + quotas: [ + { name: 'claude-sonnet-4', type: 'rolling', window: '5h', approxMessages: 225 }, + { name: 'claude-opus-4', type: 'rolling', window: '5h', approxMessages: 25 } + ], + communityReports: [ + { userId: 'u4', date: '2026-06-18', report: 'Opus runs out fast. Sonnet quota is generous but not unlimited.' }, + { userId: 'u5', date: '2026-06-14', report: 'Switched to API after hitting limits during code review sessions.' } + ] + }, + { + id: 'claude-max', + name: 'Claude Max (20x)', + vendor: 'Anthropic', + domains: ['anthropic.com', 'claude.ai'], + tier: 'subscription', + priceMonthly: 100, + priceAnnual: 960, + annualDiscountPercent: 20, + freeTier: null, + models: ['claude-sonnet-4', 'claude-opus-4'], + apiEquivalent: { + model: 'claude-opus-4', + inputPer1M: 15.00, + outputPer1M: 75.00, + notes: 'Max gives 20x free tier usage. Opus access significantly expanded.' + }, + quotas: [ + { name: 'claude-sonnet-4', type: 'rolling', window: '5h', approxMessages: 900 }, + { name: 'claude-opus-4', type: 'rolling', window: '5h', approxMessages: 100 } + ], + communityReports: [ + { userId: 'u6', date: '2026-06-16', report: 'Opus at 20x is finally usable for real work. Still not truly unlimited.' } + ] + }, + { + id: 'gemini-advanced', + name: 'Gemini Advanced (Google One AI Premium)', + vendor: 'Google', + domains: ['aistudio.google.com', 'gemini.google.com'], + tier: 'subscription', + priceMonthly: 19.99, + priceAnnual: 219.99, + annualDiscountPercent: 8, + freeTier: { + exists: true, + type: 'generous', + limits: 'Gemini free tier: fairly usable. Gemini 2.5 Flash with decent limits, Gemini 2.5 Pro limited.', + realValue: 'Genuinely usable for casual use — best free tier among major AI tools.' + }, + models: ['gemini-2.5-pro', 'gemini-2.5-flash'], + apiEquivalent: { + model: 'gemini-2.5-pro', + inputPer1M: 1.25, + outputPer1M: 10.00, + notes: 'API available via Google AI Studio. Advanced includes 2TB storage + Gemini in Workspace.' + }, + quotas: [ + { name: 'gemini-2.5-pro', type: 'rolling', window: 'unknown', approxMessages: null }, + { name: 'gemini-2.5-flash', type: 'rolling', window: 'unknown', approxMessages: null } + ], + communityReports: [ + { userId: 'u7', date: '2026-06-17', report: 'Gemini Pro rarely hits limits. Best subscription value currently.' } + ] + }, + { + id: 'perplexity-pro', + name: 'Perplexity Pro', + vendor: 'Perplexity', + domains: ['perplexity.ai'], + tier: 'subscription', + priceMonthly: 20, + priceAnnual: 200, + annualDiscountPercent: 17, + freeTier: { + exists: true, + type: 'generous', + limits: '5 Pro searches per 4 hours, unlimited basic search.', + realValue: 'Basic search genuinely useful. Pro searches are the real value-add.' + }, + models: ['sonar-pro', 'gpt-4o', 'claude-sonnet-4', 'gemini-2.5-pro'], + apiEquivalent: { + model: 'sonar-pro', + inputPer1M: 3.00, + outputPer1M: 15.00, + notes: 'Pro includes model choice, image gen, file uploads. API is search-native.' + }, + quotas: [ + { name: 'sonar-pro', type: 'rolling', window: '4h', approxMessages: 300 } + ], + communityReports: [ + { userId: 'u8', date: '2026-06-13', report: '300 Pro searches/day is plenty for research. Best value $20 plan.' } + ] + }, + { + id: 'cursor-pro', + name: 'Cursor Pro', + vendor: 'Cursor (Anysphere)', + domains: ['cursor.com'], + tier: 'subscription', + priceMonthly: 20, + priceAnnual: 192, + annualDiscountPercent: 20, + freeTier: { + exists: true, + type: 'demo', + limits: '2 week trial, then 50 slow premium requests + 200 completions/month.', + realValue: 'Effectively a trial. 50 requests/month is a few days of real coding.' + }, + models: ['gpt-4o', 'claude-sonnet-4', 'o4-mini'], + apiEquivalent: { + model: 'claude-sonnet-4', + inputPer1M: 3.00, + outputPer1M: 15.00, + notes: 'Cursor wraps models with codebase context. API requires your own context management.' + }, + quotas: [ + { name: 'premium-requests', type: 'monthly', window: '30d', approxMessages: 500 } + ], + communityReports: [ + { userId: 'u9', date: '2026-06-19', report: '500 fast requests run out mid-month for daily coders. Slow requests are unusable.' }, + { userId: 'u10', date: '2026-06-11', report: 'Switched to API + Continue.dev after running out of requests.' } + ] + }, + { + id: 'deepseek', + name: 'DeepSeek', + vendor: 'DeepSeek', + domains: ['deepseek.com', 'platform.deepseek.com'], + tier: 'freemium', + priceMonthly: 0, + priceAnnual: 0, + annualDiscountPercent: 0, + freeTier: { + exists: true, + type: 'generous', + limits: 'DeepSeek-V3 chat is free with generous limits. API is extremely cheap.', + realValue: 'Best free AI value. Real model access, not a demo.' + }, + models: ['deepseek-v3', 'deepseek-r1'], + apiEquivalent: { + model: 'deepseek-v3', + inputPer1M: 0.27, + outputPer1M: 1.10, + notes: 'API is practically free. Best $/token ratio in the market.' + }, + quotas: [ + { name: 'deepseek-v3', type: 'rolling', window: 'unknown', approxMessages: null } + ], + communityReports: [ + { userId: 'u11', date: '2026-06-18', report: 'Switched from ChatGPT Plus to DeepSeek. 95% as good for coding, completely free.' } + ] + }, + { + id: 'grok', + name: 'X Premium (Grok)', + vendor: 'xAI', + domains: ['x.ai', 'x.com', 'grok.com'], + tier: 'subscription', + priceMonthly: 16, + priceAnnual: 168, + annualDiscountPercent: 13, + freeTier: { + exists: true, + type: 'demo', + limits: 'Free X users get limited Grok access. Very low message caps.', + realValue: 'Teaser tier. Real Grok requires X Premium.' + }, + models: ['grok-3', 'grok-4'], + apiEquivalent: { + model: 'grok-3', + inputPer1M: 5.00, + outputPer1M: 15.00, + notes: 'API available via x.ai. Bundled with X Premium subscription.' + }, + quotas: [ + { name: 'grok-3', type: 'rolling', window: '2h', approxMessages: 25 }, + { name: 'grok-4', type: 'rolling', window: '2h', approxMessages: 10 } + ], + communityReports: [ + { userId: 'u12', date: '2026-06-16', report: 'Grok-4 quota very tight. 10 msgs per 2h is rough.' } + ] + }, + { + id: 'midjourney-basic', + name: 'Midjourney Basic', + vendor: 'Midjourney', + domains: ['midjourney.com'], + tier: 'subscription', + priceMonthly: 10, + priceAnnual: 96, + annualDiscountPercent: 20, + freeTier: { + exists: false, + type: 'none', + limits: 'No free tier. Trial only via Discord occasionally.', + realValue: 'Must pay to use.' + }, + models: ['midjourney-v7'], + apiEquivalent: { + model: 'n/a', + inputPer1M: 0, + outputPer1M: 0, + notes: 'No official API. Third-party proxies exist but unreliable.' + }, + quotas: [ + { name: 'images', type: 'monthly', window: '30d', approxMessages: 200 } + ], + communityReports: [ + { userId: 'u13', date: '2026-06-15', report: '200 images/month basic tier runs out fast. Standard ($30) gives unlimited relax mode.' } + ] + }, + { + id: 'poe', + name: 'Poe (Monthly)', + vendor: 'Quora', + domains: ['poe.com'], + tier: 'subscription', + priceMonthly: 20, + priceAnnual: 250, + annualDiscountPercent: 0, + freeTier: { + exists: true, + type: 'demo', + limits: '100 compute points/day. Roughly 3-5 messages with top models.', + realValue: 'Demo tier. Just enough to try, not for real use.' + }, + models: ['multi-model'], + apiEquivalent: { + model: 'various', + inputPer1M: 0, + outputPer1M: 0, + notes: 'Poe is an aggregator. Compare individual model APIs instead.' + }, + quotas: [ + { name: 'compute-points', type: 'daily', window: '24h', approxMessages: null } + ], + communityReports: [ + { userId: 'u14', date: '2026-06-14', report: 'Point system confusing. Claude Opus costs 2000+ points per message.' } + ] + }, + { + id: 'mistral-lechat', + name: 'Le Chat (Mistral)', + vendor: 'Mistral AI', + domains: ['mistral.ai', 'chat.mistral.ai'], + tier: 'subscription', + priceMonthly: 20, + priceAnnual: 192, + annualDiscountPercent: 20, + freeTier: { + exists: true, + type: 'generous', + limits: 'Free tier includes Mistral Small and limited Large access.', + realValue: 'Reasonably usable free tier for lighter tasks.' + }, + models: ['mistral-large-2', 'mistral-small', 'codestral'], + apiEquivalent: { + model: 'mistral-large-2', + inputPer1M: 2.00, + outputPer1M: 6.00, + notes: 'API pricing very competitive. Le Chat adds canvas and agent features.' + }, + quotas: [ + { name: 'mistral-large', type: 'rolling', window: 'unknown', approxMessages: 150 } + ], + communityReports: [ + { userId: 'u15', date: '2026-06-20', report: 'Le Chat free tier is surprisingly usable for non-coding tasks.' } + ] + } + ]; + + var pricingPatterns = { + copycatPricePoint: { + price: 20, + tools: ['ChatGPT Plus', 'Claude Pro', 'Perplexity Pro', 'Cursor Pro', 'Poe', 'Le Chat'], + insight: 'Almost every AI subscription converges on $20/month despite 10x variance in actual value.' + }, + fakeFreeTiers: { + description: 'Tools where "free tier" is actually a product demo, not usable.', + flaggedIds: ['chatgpt-plus', 'claude-pro', 'grok', 'cursor-pro', 'poe'], + genuinelyFreeIds: ['deepseek', 'gemini-advanced', 'mistral-lechat'] + }, + annualDiscountTrend: { + description: 'Annual discounts have increased from ~20% to up to 50%, signaling high monthly churn.', + examples: ['ChatGPT Pro (20%)', 'Claude Max (20%)', 'Cursor Pro (20%)', 'Midjourney (20%)'] + }, + apiVsSubscriptionSavings: { + description: 'For most tools, API access is 2-7x cheaper than subscription for typical usage (500 msgs/month).', + averageMultiplier: 3.5 + } + }; + + window.AptDB = { tools: tools, pricingPatterns: pricingPatterns, version: '2026-06-22' }; +})(); diff --git a/src/icons/icon-128.png b/src/icons/icon-128.png new file mode 100644 index 0000000..ed10db3 Binary files /dev/null and b/src/icons/icon-128.png differ diff --git a/src/icons/icon-16.png b/src/icons/icon-16.png new file mode 100644 index 0000000..19b0a0d Binary files /dev/null and b/src/icons/icon-16.png differ diff --git a/src/icons/icon-48.png b/src/icons/icon-48.png new file mode 100644 index 0000000..ba7e2c1 Binary files /dev/null and b/src/icons/icon-48.png differ diff --git a/src/lib/pricing.js b/src/lib/pricing.js new file mode 100644 index 0000000..515d5c2 --- /dev/null +++ b/src/lib/pricing.js @@ -0,0 +1,97 @@ +// Pricing calculation engine — cost-per-use analysis +(function () { + 'use strict'; + + function calcApiCost(inputPer1M, outputPer1M, usage) { + var inputCost = (usage.avgInputTokens * usage.messagesPerMonth / 1000000) * inputPer1M; + var outputCost = (usage.avgOutputTokens * usage.messagesPerMonth / 1000000) * outputPer1M; + return Math.round((inputCost + outputCost) * 10000) / 10000; + } + + function calcSubscriptionCostPerMessage(priceMonthly, messagesPerMonth) { + if (messagesPerMonth <= 0) return priceMonthly; + return Math.round((priceMonthly / messagesPerMonth) * 10000) / 10000; + } + + function findBreakEven(priceMonthly, inputPer1M, outputPer1M, avgInputTokens, avgOutputTokens) { + var costPerMsgApi = + (avgInputTokens / 1000000) * inputPer1M + + (avgOutputTokens / 1000000) * outputPer1M; + if (costPerMsgApi <= 0) return Infinity; + return Math.round(priceMonthly / costPerMsgApi); + } + + function compare(tool, usage) { + var apiCost = calcApiCost(tool.apiEquivalent.inputPer1M, tool.apiEquivalent.outputPer1M, usage); + var subCost = tool.priceMonthly; + var subPerMsg = calcSubscriptionCostPerMessage(subCost, usage.messagesPerMonth); + var apiPerMsg = usage.messagesPerMonth > 0 + ? Math.round((apiCost / usage.messagesPerMonth) * 10000) / 10000 + : 0; + var breakEven = findBreakEven(subCost, tool.apiEquivalent.inputPer1M, tool.apiEquivalent.outputPer1M, usage.avgInputTokens, usage.avgOutputTokens); + var cheaper, savings = 0; + if (apiCost < subCost) { cheaper = 'api'; savings = subCost - apiCost; } + else if (subCost < apiCost) { cheaper = 'subscription'; savings = apiCost - subCost; } + else { cheaper = 'tied'; } + var savingsPercent = Math.max(subCost, apiCost) > 0 + ? Math.round((savings / Math.max(subCost, apiCost)) * 100) : 0; + var verdict; + if (cheaper === 'api') { + verdict = 'API saves $' + savings.toFixed(2) + '/month (' + savingsPercent + '% cheaper). Break-even at ' + + (breakEven === Infinity ? 'N/A' : breakEven.toLocaleString() + ' msgs') + '.'; + } else if (cheaper === 'subscription') { + verdict = 'Subscription cheaper by $' + savings.toFixed(2) + '/month (' + savingsPercent + '% savings).'; + } else { + verdict = 'Roughly equal at this usage level.'; + } + return { + toolId: tool.id, toolName: tool.name, + subscriptionMonthlyCost: subCost, apiMonthlyCost: apiCost, + costPerMessageSubscription: subPerMsg, costPerMessageApi: apiPerMsg, + cheaperOption: cheaper, + savingsMonthly: Math.round(savings * 100) / 100, + savingsAnnual: Math.round(savings * 12 * 100) / 100, + savingsPercent: savingsPercent, breakEvenMessages: breakEven, verdict: verdict + }; + } + + function compareAll(tools, usage) { + return tools.filter(function (t) { return t.apiEquivalent.inputPer1M > 0; }) + .map(function (t) { return compare(t, usage); }) + .sort(function (a, b) { return b.savingsMonthly - a.savingsMonthly; }); + } + + function classifyFreeTier(tool) { + if (!tool.freeTier || !tool.freeTier.exists) { + return { label: 'No Free Tier', color: '#6b7280', description: 'Must pay from day one.', type: 'none' }; + } + if (tool.freeTier.type === 'demo') { + return { label: '⚠️ Demo Tier (Fake Free)', color: '#dc2626', description: tool.freeTier.realValue, type: 'demo' }; + } + if (tool.freeTier.type === 'generous') { + return { label: '✅ Genuinely Free', color: '#16a34a', description: tool.freeTier.realValue, type: 'generous' }; + } + return { label: 'Unknown', color: '#6b7280', description: 'Free tier details unclear.', type: 'unknown' }; + } + + function formatMoney(amount) { + if (amount === 0) return 'Free'; + return '$' + amount.toFixed(2); + } + + var PricingCalculator = { + calcApiCost: calcApiCost, + calcSubscriptionCostPerMessage: calcSubscriptionCostPerMessage, + findBreakEven: findBreakEven, + compare: compare, + compareAll: compareAll, + classifyFreeTier: classifyFreeTier, + formatMoney: formatMoney + }; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = { PricingCalculator: PricingCalculator }; + } else { + window.PricingCalculator = PricingCalculator; + } +})(); diff --git a/src/lib/quota.js b/src/lib/quota.js new file mode 100644 index 0000000..b0398ce --- /dev/null +++ b/src/lib/quota.js @@ -0,0 +1,100 @@ +// Quota tracking — tracks AI tool usage and alerts before limits +(function () { + 'use strict'; + var STORAGE_KEY = '***'; + + function parseWindowToMs(w) { + if (!w || w === 'unknown') return 3 * 3600000; + var m = w.match(/^(\d+)([hdw])$/); + if (!m) return 3 * 3600000; + var v = parseInt(m[1], 10), u = m[2]; + if (u === 'h') return v * 3600000; + if (u === 'd') return v * 86400000; + if (u === 'w') return v * 604800000; + return 3 * 3600000; + } + + var QuotaTracker = { + getAll: function (cb) { + try { + chrome.storage.local.get([STORAGE_KEY], function (r) { + cb((r && r[STORAGE_KEY]) || {}); + }); + } catch (e) { cb({}); } + }, + get: function (toolId, cb) { + this.getAll(function (all) { cb(all[toolId] || null); }); + }, + recordMessage: function (toolId, quotaConfig, cb) { + var self = this; + this.getAll(function (all) { + var now = Date.now(); + var windowMs = parseWindowToMs(quotaConfig.window); + var limit = typeof quotaConfig.approxMessages === 'number' ? quotaConfig.approxMessages : null; + var state = all[toolId]; + if (!state) { + state = { toolId: toolId, messagesUsed: 0, windowStart: now, windowMs: windowMs, messageLimit: limit, lastUpdated: now }; + } + if (now - state.windowStart > state.windowMs) { + state.windowStart = now; + state.messagesUsed = 0; + } + state.messagesUsed++; + state.lastUpdated = now; + all[toolId] = state; + try { + var obj = {}; obj[STORAGE_KEY] = all; + chrome.storage.local.set(obj, function () { + self.checkAlerts(state); + if (cb) cb(state); + }); + } catch (e) { if (cb) cb(state); } + }); + }, + checkAlerts: function (state) { + if (!state.messageLimit) return; + var pct = (state.messagesUsed / state.messageLimit) * 100; + try { + if (typeof chrome !== 'undefined' && chrome.notifications) { + if (pct >= 80 && pct < 85) { + chrome.notifications.create({ type: 'basic', iconUrl: '../icons/icon-48.png', title: '⚠️ Quota approaching limit', message: state.toolId + ': ' + state.messagesUsed + '/' + state.messageLimit + ' (' + Math.round(pct) + '%)', priority: 2 }); + } else if (pct >= 95) { + chrome.notifications.create({ type: 'basic', iconUrl: '../icons/icon-48.png', title: '🚫 Quota exhausted', message: state.toolId + ': ' + state.messagesUsed + '/' + state.messageLimit + '. Consider switching to API.', priority: 2 }); + } + } + } catch (e) { /* notifications unavailable */ } + }, + getRemaining: function (toolId, cb) { + this.get(toolId, function (state) { + if (!state || !state.messageLimit) { cb(null); return; } + if (Date.now() - state.windowStart > state.windowMs) { cb(state.messageLimit); return; } + cb(Math.max(0, state.messageLimit - state.messagesUsed)); + }); + }, + getUsagePercent: function (toolId, cb) { + this.get(toolId, function (state) { + if (!state || !state.messageLimit) { cb(0); return; } + if (Date.now() - state.windowStart > state.windowMs) { cb(0); return; } + cb(Math.min(100, (state.messagesUsed / state.messageLimit) * 100)); + }); + }, + reset: function (toolId, cb) { + this.getAll(function (all) { + if (all[toolId]) { all[toolId].messagesUsed = 0; all[toolId].windowStart = Date.now(); } + try { var obj = {}; obj[STORAGE_KEY] = all; chrome.storage.local.set(obj, function () { if (cb) cb(); }); } + catch (e) { if (cb) cb(); } + }); + }, + resetAll: function (cb) { + try { chrome.storage.local.remove([STORAGE_KEY], function () { if (cb) cb(); }); } + catch (e) { if (cb) cb(); } + }, + parseWindowToMs: parseWindowToMs + }; + + if (typeof module !== 'undefined' && module.exports) { + module.exports = { QuotaTracker: QuotaTracker }; + } else { + window.QuotaTracker = QuotaTracker; + } +})(); diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..1465815 --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,83 @@ +{ + "manifest_version": 3, + "name": "AI Pricing Transparency Scanner", + "description": "Overlays real cost-per-use on AI tool pricing pages. Compares API vs subscription. Tracks quota. Flags fake free tiers.", + "version": "1.0.0", + "permissions": ["storage", "notifications", "activeTab"], + "host_permissions": [ + "https://openai.com/*", + "https://chatgpt.com/*", + "https://www.anthropic.com/*", + "https://claude.ai/*", + "https://gemini.google.com/*", + "https://aistudio.google.com/*", + "https://www.perplexity.ai/*", + "https://midjourney.com/*", + "https://x.com/*", + "https://grok.com/*", + "https://www.deepseek.com/*", + "https://platform.deepseek.com/*", + "https://x.ai/*", + "https://mistral.ai/*", + "https://platform.openai.com/*", + "https://replicate.com/*", + "https://together.ai/*", + "https://www.together.ai/*", + "https://cursor.com/*", + "https://www.cursor.com/*", + "https://poe.com/*", + "https://www.poe.com/*" + ], + "action": { + "default_popup": "popup/popup.html", + "default_icon": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } + }, + "background": { + "service_worker": "background/sw.js" + }, + "content_scripts": [ + { + "matches": [ + "https://openai.com/chatgpt/pricing*", + "https://openai.com/pricing*", + "https://www.openai.com/pricing*", + "https://chatgpt.com/pricing*", + "https://www.anthropic.com/pricing*", + "https://www.anthropic.com/plans*", + "https://claude.ai/pricing*", + "https://aistudio.google.com/pricing*", + "https://www.perplexity.ai/pricing*", + "https://midjourney.com/pricing*", + "https://www.midjourney.com/pricing*", + "https://x.ai/pricing*", + "https://www.deepseek.com/pricing*", + "https://mistral.ai/pricing*", + "https://replicate.com/pricing*", + "https://www.together.ai/pricing*", + "https://together.ai/pricing*", + "https://cursor.com/pricing*", + "https://www.cursor.com/pricing*", + "https://poe.com/pricing*", + "https://www.poe.com/pricing*" + ], + "js": ["data/pricing-database.js", "lib/pricing.js", "content/overlay.js"], + "css": ["content/overlay.css"], + "run_at": "document_idle" + } + ], + "icons": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, + "web_accessible_resources": [ + { + "resources": ["data/pricing-database.js"], + "matches": [""] + } + ] +} diff --git a/src/popup/popup.css b/src/popup/popup.css new file mode 100644 index 0000000..cb38b3e --- /dev/null +++ b/src/popup/popup.css @@ -0,0 +1,126 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + width: 420px; + min-height: 480px; + background: #0f0f1a; + color: #e0e0e0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + font-size: 13px; + line-height: 1.5; +} +.popup-container { display: flex; flex-direction: column; min-height: 480px; } +.popup-header { + padding: 14px 16px 10px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); +} +.popup-header h1 { font-size: 16px; font-weight: 800; color: #fff; } +.popup-header .subtitle { font-size: 11px; color: rgba(255,255,255,0.7); margin-top: 2px; } +.popup-tabs { display: flex; background: #1a1a2e; border-bottom: 1px solid #2a2a4a; } +.tab-btn { + flex: 1; + padding: 8px 4px; + border: none; + background: transparent; + color: #8888aa; + font-size: 11px; + font-weight: 600; + cursor: pointer; + border-bottom: 2px solid transparent; + transition: all 0.2s; +} +.tab-btn:hover { color: #c0c0d0; } +.tab-btn.active { color: #667eea; border-bottom-color: #667eea; } +.tab-content { display: none; padding: 12px 16px; flex: 1; overflow-y: auto; } +.tab-content.active { display: block; } +.usage-inputs { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; } +.usage-inputs label { display: flex; justify-content: space-between; align-items: center; gap: 8px; } +.usage-inputs label span { color: #8888aa; font-size: 12px; flex: 1; } +.usage-inputs input { + width: 100px; + padding: 4px 8px; + border: 1px solid #2a2a4a; + border-radius: 6px; + background: #16213e; + color: #e0e0e0; + font-size: 13px; + font-family: 'SF Mono', Monaco, monospace; + text-align: right; +} +.usage-inputs input:focus { outline: none; border-color: #667eea; } +.results-list { display: flex; flex-direction: column; gap: 8px; } +.loading, .empty-state { text-align: center; color: #666688; padding: 20px 0; font-size: 12px; } +.compare-intro { font-size: 12px; color: #8888aa; margin-bottom: 10px; } +.calc-card { + background: #16213e; + border-radius: 8px; + padding: 10px 12px; + border: 1px solid #1e2a4a; +} +.calc-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } +.calc-card-name { font-weight: 700; font-size: 13px; } +.calc-card-row { display: flex; justify-content: space-between; font-size: 12px; padding: 2px 0; } +.calc-card-row .label { color: #8888aa; } +.calc-card-row .value { font-weight: 600; font-family: 'SF Mono', Monaco, monospace; } +.calc-card .verdict { + font-size: 11px; + color: #a0a0c0; + font-style: italic; + margin-top: 4px; + padding-top: 4px; + border-top: 1px solid #1e2a4a; +} +.quota-card { + background: #16213e; + border-radius: 8px; + padding: 10px 12px; + border: 1px solid #1e2a4a; +} +.quota-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; } +.quota-bar-bg { width: 100%; height: 8px; background: #0f0f1a; border-radius: 4px; overflow: hidden; margin-top: 4px; } +.quota-bar-fill { height: 100%; border-radius: 4px; transition: width 0.3s; } +.quota-bar-fill.low { background: #16a34a; } +.quota-bar-fill.medium { background: #eab308; } +.quota-bar-fill.high { background: #dc2626; } +.quota-meta { display: flex; justify-content: space-between; font-size: 11px; color: #666688; margin-top: 4px; } +.pattern-card { + background: #16213e; + border-radius: 8px; + padding: 12px; + margin-bottom: 10px; + border: 1px solid #1e2a4a; +} +.pattern-card h3 { font-size: 13px; margin-bottom: 6px; } +.pattern-card p { font-size: 12px; color: #a0a0c0; line-height: 1.5; } +.pattern-card .highlight { color: #667eea; font-weight: 700; } +.pattern-card .flag-list { margin-top: 6px; font-size: 11px; } +.pattern-card .flag-list .flagged { color: #dc2626; margin-right: 8px; } +.pattern-card .flag-list .genuine { color: #16a34a; margin-right: 8px; } +.free-badge { + padding: 2px 8px; + border-radius: 10px; + font-size: 10px; + font-weight: 600; + white-space: nowrap; +} +.free-badge.demo { background: #dc2626; color: #fff; } +.free-badge.generous { background: #16a34a; color: #fff; } +.free-badge.none { background: #6b7280; color: #fff; } +.popup-footer { padding: 10px 16px; border-top: 1px solid #2a2a4a; display: flex; gap: 8px; } +.footer-btn { + flex: 1; + padding: 6px 12px; + border: 1px solid #2a2a4a; + border-radius: 6px; + background: #16213e; + color: #c0c0d0; + font-size: 11px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; +} +.footer-btn:hover { background: #1e2a4a; } +.footer-btn.primary { background: #667eea; color: #fff; border-color: #667eea; } +.footer-btn.primary:hover { background: #5a6fd4; } +.apt-badge { padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600; white-space: nowrap; } +.apt-badge-save { background: #16a34a; color: #fff; } +.apt-badge-neutral { background: #4a4a6a; color: #ccc; } diff --git a/src/popup/popup.html b/src/popup/popup.html new file mode 100644 index 0000000..32b8319 --- /dev/null +++ b/src/popup/popup.html @@ -0,0 +1,49 @@ + + + + + + AI Pricing Scanner + + + + + + + + + + diff --git a/src/popup/popup.js b/src/popup/popup.js new file mode 100644 index 0000000..411786d --- /dev/null +++ b/src/popup/popup.js @@ -0,0 +1,184 @@ +// Popup logic — calculator, quota tracker, compare, patterns +(function () { + 'use strict'; + + if (!window.AptDB || !window.PricingCalculator) return; + + var calc = window.PricingCalculator; + var tools = window.AptDB.tools; + + // Tab navigation + var tabBtns = document.querySelectorAll('.tab-btn'); + var tabContents = document.querySelectorAll('.tab-content'); + + tabBtns.forEach(function (btn) { + btn.addEventListener('click', function () { + tabBtns.forEach(function (b) { b.classList.remove('active'); }); + tabContents.forEach(function (c) { c.classList.remove('active'); }); + btn.classList.add('active'); + var tabId = 'tab-' + btn.getAttribute('data-tab'); + var target = document.getElementById(tabId); + if (target) target.classList.add('active'); + }); + }); + + function loadUsage(cb) { + try { + chrome.storage.local.get(['apt_usage_profile'], function (result) { + var usage = result && result.apt_usage_profile; + if (usage) { + document.getElementById('usage-msgs').value = usage.messagesPerMonth; + document.getElementById('usage-input-tokens').value = usage.avgInputTokens; + document.getElementById('usage-output-tokens').value = usage.avgOutputTokens; + } + cb(getCurrentUsage()); + }); + } catch (e) { cb(getCurrentUsage()); } + } + + function getCurrentUsage() { + return { + messagesPerMonth: parseInt(document.getElementById('usage-msgs').value, 10) || 500, + avgInputTokens: parseInt(document.getElementById('usage-input-tokens').value, 10) || 800, + avgOutputTokens: parseInt(document.getElementById('usage-output-tokens').value, 10) || 400 + }; + } + + document.getElementById('btn-save-usage').addEventListener('click', function () { + var usage = getCurrentUsage(); + try { + chrome.storage.local.set({ apt_usage_profile: usage }, function () { + try { + chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { + if (tabs && tabs[0]) { + chrome.tabs.sendMessage(tabs[0].id, { type: 'UPDATE_USAGE_PROFILE', usage: usage }); + } + }); + } catch (e) {} + var btn = document.getElementById('btn-save-usage'); + btn.textContent = '✓ Saved'; + setTimeout(function () { btn.textContent = 'Save Usage Profile'; }, 1500); + }); + } catch (e) {} + renderAll(); + }); + + function renderCalculator(usage) { + var container = document.getElementById('calculator-results'); + container.innerHTML = ''; + var apiTools = tools.filter(function (t) { return t.apiEquivalent.inputPer1M > 0; }); + if (apiTools.length === 0) { + container.innerHTML = '

No tools with API pricing available.

'; + return; + } + apiTools.forEach(function (tool) { + var comparison = calc.compare(tool, usage); + var freeTier = calc.classifyFreeTier(tool); + var badgeClass = freeTier.type === 'demo' ? 'demo' : freeTier.type === 'generous' ? 'generous' : 'none'; + var card = document.createElement('div'); + card.className = 'calc-card'; + card.innerHTML = + '
' + tool.name + '' + freeTier.label.replace(/[⚠️✅]/g, '').trim() + '
' + + '
Subscription$' + tool.priceMonthly + '/mo
' + + '
API equivalent$' + comparison.apiMonthlyCost.toFixed(2) + '/mo
' + + '
Cost/msg (sub)$' + comparison.costPerMessageSubscription.toFixed(4) + '
' + + '
Cost/msg (API)$' + comparison.costPerMessageApi.toFixed(4) + '
' + + '
Break-even' + (comparison.breakEvenMessages === Infinity ? 'N/A' : comparison.breakEvenMessages.toLocaleString() + ' msgs') + '
' + + '
' + comparison.verdict + '
'; + container.appendChild(card); + }); + } + + function renderCompare(usage) { + var container = document.getElementById('compare-results'); + container.innerHTML = ''; + var comparisons = calc.compareAll(tools, usage); + if (comparisons.length === 0) { + container.innerHTML = '

No tools with API pricing to compare.

'; + return; + } + comparisons.forEach(function (comp) { + var card = document.createElement('div'); + card.className = 'calc-card'; + var badgeHtml = comp.cheaperOption === 'api' + ? 'API saves ' + comp.savingsPercent + '%' + : 'Sub cheaper'; + card.innerHTML = + '
' + comp.toolName + '' + badgeHtml + '
' + + '
Sub: $' + comp.subscriptionMonthlyCost + '/movs API: $' + comp.apiMonthlyCost.toFixed(2) + '/mo
' + + '
Monthly savings$' + comp.savingsMonthly.toFixed(2) + '
' + + '
Annual savings$' + comp.savingsAnnual.toFixed(2) + '
' + + '
' + comp.verdict + '
'; + container.appendChild(card); + }); + } + + function renderQuotas() { + var container = document.getElementById('quota-list'); + container.innerHTML = '

Loading…

'; + if (!window.QuotaTracker) { + container.innerHTML = '

Quota tracker not available.

'; + return; + } + window.QuotaTracker.getAll(function (states) { + container.innerHTML = ''; + var keys = Object.keys(states); + if (keys.length === 0) { + container.innerHTML = '

No quotas being tracked. Visit an AI tool page to start tracking.

'; + return; + } + keys.forEach(function (key) { + var state = states[key]; + var pct = state.messageLimit ? Math.min(100, (state.messagesUsed / state.messageLimit) * 100) : 0; + var barClass = pct < 50 ? 'low' : pct < 80 ? 'medium' : 'high'; + var windowLabel = state.windowMs < 86400000 + ? Math.round(state.windowMs / 3600000) + 'h window' + : Math.round(state.windowMs / 86400000) + 'd window'; + var expired = Date.now() - state.windowStart > state.windowMs; + var card = document.createElement('div'); + card.className = 'quota-card'; + card.innerHTML = + '
' + state.toolId + '' + (expired ? 'Reset' : Math.round(pct) + '%') + '
' + + '
' + + '
' + state.messagesUsed + ' / ' + (state.messageLimit || '∞') + ' messages' + windowLabel + '
'; + container.appendChild(card); + }); + }); + } + + function renderPatterns() { + var container = document.getElementById('patterns-content'); + var patterns = window.AptDB.pricingPatterns; + container.innerHTML = + '

💰 $20/mo Copycat Price Point

Almost every AI subscription has converged on $20/month despite 10x variance in actual usage value.

Tools charging this: ' + patterns.copycatPricePoint.tools.join(', ') + '

' + + '

🔓 Fake Free Tiers

Many "free tiers" are actually product tours — not genuinely usable for real work.

⚠️ Demo tiers: ' + patterns.fakeFreeTiers.flaggedIds.map(function (id) { var t = tools.find(function (x) { return x.id === id; }); return t ? t.name : id; }).join(', ') + '
✅ Genuinely free: ' + patterns.fakeFreeTiers.genuinelyFreeIds.map(function (id) { var t = tools.find(function (x) { return x.id === id; }); return t ? t.name : id; }).join(', ') + '
' + + '

📉 API vs Subscription Gap

For most tools, API access is ' + patterns.apiVsSubscriptionSavings.averageMultiplier + 'x cheaper than subscription for typical usage (500 msgs/month).

' + + '

📊 Annual Discount Trends

' + patterns.annualDiscountTrend.description + '

Examples: ' + patterns.annualDiscountTrend.examples.join(', ') + '

' + + '

🗃️ Database Info

Tracking ' + tools.length + ' tools from ' + tools.reduce(function (set, t) { set.add(t.vendor); return set; }, new Set()).size + ' vendors.

Last updated: ' + window.AptDB.version + '

'; + } + + document.getElementById('btn-reset-quotas').addEventListener('click', function () { + if (window.QuotaTracker) { + window.QuotaTracker.resetAll(function () { renderQuotas(); }); + } + }); + + function renderAll() { + var usage = getCurrentUsage(); + renderCalculator(usage); + renderCompare(usage); + renderQuotas(); + renderPatterns(); + } + + ['usage-msgs', 'usage-input-tokens', 'usage-output-tokens'].forEach(function (id) { + document.getElementById(id).addEventListener('input', function () { renderAll(); }); + }); + + loadUsage(function (usage) { + renderCalculator(usage); + renderCompare(usage); + renderQuotas(); + renderPatterns(); + }); +})(); diff --git a/tests/pricing.test.mjs b/tests/pricing.test.mjs new file mode 100644 index 0000000..06d5b7a --- /dev/null +++ b/tests/pricing.test.mjs @@ -0,0 +1,133 @@ +// Tests for pricing calculation engine +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { createRequire } from 'module'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); + +// Load the pricing library (CommonJS-style) +const pricingPath = join(__dirname, '..', 'src', 'lib', 'pricing.js'); +const pricingCode = readFileSync(pricingPath, 'utf-8'); + +// Simulate browser global +global.window = {}; +global.module = undefined; + +// Execute in a context that exposes PricingCalculator +eval(pricingCode); +const { PricingCalculator } = global.window; + +let passed = 0; +let failed = 0; + +function assert(condition, message) { + if (condition) { + passed++; + console.log(' ✅ ' + message); + } else { + failed++; + console.error(' ❌ FAIL: ' + message); + } +} + +function approxEqual(a, b, tolerance) { + return Math.abs(a - b) < (tolerance || 0.01); +} + +console.log('\n=== Pricing Calculator Tests ===\n'); + +// Test 1: calcApiCost +console.log('1. calcApiCost'); +const usage = { messagesPerMonth: 500, avgInputTokens: 800, avgOutputTokens: 400 }; +// GPT-4o: input $2.50/1M, output $10.00/1M +// 500 msgs * 800 tokens = 400,000 input tokens = 0.4M → 0.4 * 2.50 = $1.00 +// 500 msgs * 400 tokens = 200,000 output tokens = 0.2M → 0.2 * 10.00 = $2.00 +// Total: $3.00 +var apiCost = PricingCalculator.calcApiCost(2.50, 10.00, usage); +assert(approxEqual(apiCost, 3.00, 0.01), 'GPT-4o API cost at 500 msgs = $3.00 (got $' + apiCost + ')'); + +// Test 2: calcApiCost for DeepSeek (extremely cheap) +console.log('\n2. calcApiCost - DeepSeek'); +// DeepSeek V3: input $0.27/1M, output $1.10/1M +// 500 * 800 / 1M * 0.27 = 0.108, 500 * 400 / 1M * 1.10 = 0.22 → $0.328 +var dsCost = PricingCalculator.calcApiCost(0.27, 1.10, usage); +assert(approxEqual(dsCost, 0.33, 0.01), 'DeepSeek API cost at 500 msgs ≈ $0.33 (got $' + dsCost + ')'); + +// Test 3: calcSubscriptionCostPerMessage +console.log('\n3. calcSubscriptionCostPerMessage'); +var subPerMsg = PricingCalculator.calcSubscriptionCostPerMessage(20, 500); +assert(approxEqual(subPerMsg, 0.04, 0.001), '$20/mo at 500 msgs = $0.04/msg (got $' + subPerMsg + ')'); + +// Test 4: findBreakEven +console.log('\n4. findBreakEven'); +var breakEven = PricingCalculator.findBreakEven(20, 2.50, 10.00, 800, 400); +// cost per msg API = 800/1M * 2.50 + 400/1M * 10.00 = 0.002 + 0.004 = 0.006 +// break even = 20 / 0.006 = 3333.33 → 3333 +assert(breakEven === 3333, 'GPT-4o break-even at 3333 msgs (got ' + breakEven + ')'); + +// Test 5: compare — full tool comparison +console.log('\n5. compare - full comparison'); +var chatgptPlus = { + id: 'chatgpt-plus', name: 'ChatGPT Plus', priceMonthly: 20, + apiEquivalent: { model: 'gpt-4o', inputPer1M: 2.50, outputPer1M: 10.00 } +}; +var result = PricingCalculator.compare(chatgptPlus, usage); +assert(result.cheaperOption === 'api', 'ChatGPT Plus: API should be cheaper'); +assert(result.savingsMonthly > 0, 'Should show positive savings: $' + result.savingsMonthly); +assert(result.apiMonthlyCost < 20, 'API cost should be < $20 (got $' + result.apiMonthlyCost + ')'); + +// Test 6: compareAll +console.log('\n6. compareAll - batch comparison'); +var tools = [ + { id: 'a', name: 'A', priceMonthly: 20, apiEquivalent: { inputPer1M: 2.50, outputPer1M: 10.00 } }, + { id: 'b', name: 'B', priceMonthly: 20, apiEquivalent: { inputPer1M: 0.27, outputPer1M: 1.10 } }, + { id: 'c', name: 'C', priceMonthly: 0, apiEquivalent: { inputPer1M: 0, outputPer1M: 0 } } // no API +]; +var results = PricingCalculator.compareAll(tools, usage); +assert(results.length === 2, 'Should skip tools with no API pricing (got ' + results.length + ')'); +assert(results[0].toolId === 'b', 'Should sort by savings descending (B first — DeepSeek-like saves more)'); + +// Test 7: classifyFreeTier +console.log('\n7. classifyFreeTier'); +var demoTool = { freeTier: { exists: true, type: 'demo', realValue: 'test' } }; +var generousTool = { freeTier: { exists: true, type: 'generous', realValue: 'good' } }; +var noFreeTool = { freeTier: null }; +assert(PricingCalculator.classifyFreeTier(demoTool).type === 'demo', 'Should classify demo tier'); +assert(PricingCalculator.classifyFreeTier(generousTool).type === 'generous', 'Should classify generous tier'); +assert(PricingCalculator.classifyFreeTier(noFreeTool).type === 'none', 'Should classify no free tier'); + +// Test 8: formatMoney +console.log('\n8. formatMoney'); +assert(PricingCalculator.formatMoney(0) === 'Free', 'formatMoney(0) = "Free"'); +assert(PricingCalculator.formatMoney(20) === '$20.00', 'formatMoney(20) = "$20.00"'); +assert(PricingCalculator.formatMoney(3.456) === '$3.46', 'formatMoney(3.456) = "$3.46"'); + +// Test 9: Edge case — zero messages +console.log('\n9. Edge cases'); +var zeroUsage = { messagesPerMonth: 0, avgInputTokens: 800, avgOutputTokens: 400 }; +var zeroResult = PricingCalculator.compare(chatgptPlus, zeroUsage); +assert(zeroResult.apiMonthlyCost === 0, 'Zero usage → $0 API cost'); + +// Test 10: Edge case — very high usage (subscription cheaper) +console.log('\n10. High usage scenario'); +var highUsage = { messagesPerMonth: 10000, avgInputTokens: 2000, avgOutputTokens: 1000 }; +// API: 10000 * 2000 / 1M * 2.50 = 50, 10000 * 1000 / 1M * 10 = 100 → $150 +var highResult = PricingCalculator.compare(chatgptPlus, highUsage); +assert(highResult.cheaperOption === 'subscription', 'At 10K msgs, subscription should be cheaper'); +assert(approxEqual(highResult.apiMonthlyCost, 150, 1), 'API cost at 10K msgs ≈ $150 (got $' + highResult.apiMonthlyCost + ')'); + +// Test 11: Pricing database loads correctly +console.log('\n11. Pricing database'); +var dbPath = join(__dirname, '..', 'src', 'data', 'pricing-database.js'); +var dbCode = readFileSync(dbPath, 'utf-8'); +global.window = {}; +eval(dbCode); +assert(global.window.AptDB !== undefined, 'AptDB should be defined'); +assert(global.window.AptDB.tools.length >= 10, 'Should have at least 10 tools (got ' + global.window.AptDB.tools.length + ')'); +assert(global.window.AptDB.pricingPatterns !== undefined, 'pricingPatterns should be defined'); +assert(global.window.AptDB.version !== undefined, 'version should be defined'); + +console.log('\n=== Results: ' + passed + ' passed, ' + failed + ' failed ===\n'); +process.exit(failed > 0 ? 1 : 0);