feat: AI Pricing Transparency Scanner Chrome Extension v1.0.0

This commit is contained in:
Bun Bun
2026-06-22 05:47:35 +00:00
commit 4906edb65a
18 changed files with 1631 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
dist/
*.log
.verdict
+9
View File
@@ -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"
}
}
+31
View File
@@ -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');
+96
View File
@@ -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.');
+63
View File
@@ -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: {}
});
}
});
+120
View File
@@ -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;
}
+158
View File
@@ -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'
? '<span class="apt-badge apt-badge-save">API saves ' + comparison.savingsPercent + '%</span>'
: comparison.cheaperOption === 'subscription'
? '<span class="apt-badge apt-badge-neutral">Sub cheaper</span>'
: '<span class="apt-badge apt-badge-neutral">≈ Equal</span>';
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 = '<div class="apt-reports"><div class="apt-reports-title">💬 Community Reports (' + tool.communityReports.length + ')</div>';
reports.forEach(function (r) {
reportsHtml += '<div class="apt-report-item"><span class="apt-report-date">' + escapeHtml(r.date) + '</span> ' + escapeHtml(r.report) + '</div>';
});
reportsHtml += '</div>';
}
card.innerHTML =
'<div class="apt-tool-header"><span class="apt-tool-name">' + escapeHtml(tool.name) + '</span>' + cheaperLabel + '</div>' +
'<div class="apt-tool-body">' +
'<div class="apt-metric"><span class="apt-metric-label">Sub cost/msg</span><span class="apt-metric-value">$' + comparison.costPerMessageSubscription.toFixed(4) + '</span></div>' +
'<div class="apt-metric"><span class="apt-metric-label">API cost/msg</span><span class="apt-metric-value">$' + comparison.costPerMessageApi.toFixed(4) + '</span></div>' +
'<div class="apt-metric"><span class="apt-metric-label">API $/month</span><span class="apt-metric-value">' + calc.formatMoney(comparison.apiMonthlyCost) + '</span></div>' +
'<div class="apt-metric"><span class="apt-metric-label">Break-even</span><span class="apt-metric-value">' + (comparison.breakEvenMessages === Infinity ? 'N/A' : comparison.breakEvenMessages.toLocaleString() + ' msgs') + '</span></div>' +
'<div class="apt-free-tier" style="border-left-color:' + freeTier.color + '"><span class="apt-free-label">' + escapeHtml(freeTier.label) + '</span><span class="apt-free-desc">' + escapeHtml(freeTier.description) + '</span></div>' +
'<div class="apt-verdict">' + escapeHtml(comparison.verdict) + '</div>' +
'</div>' + 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 <span class="apt-collapse-icon">▼</span>';
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 =
'<div class="apt-summary-row"><span class="apt-summary-label">Analyzing</span><span class="apt-summary-value">' + tools.length + ' tool(s) on this page</span></div>' +
'<div class="apt-summary-row"><span class="apt-summary-label">Usage estimate</span><span class="apt-summary-value">' + usage.messagesPerMonth + ' msgs/month</span></div>';
content.appendChild(summary);
var patterns = window.AptDB.pricingPatterns;
var patternBox = document.createElement('div');
patternBox.className = 'apt-patterns';
patternBox.innerHTML =
'<div class="apt-patterns-title">📊 Market Intelligence</div>' +
'<div class="apt-pattern-item">💰 <strong>$' + patterns.copycatPricePoint.price + '/mo price point:</strong> ' + patterns.copycatPricePoint.tools.length + ' tools charge this exact amount.</div>' +
'<div class="apt-pattern-item">🔓 <strong>' + patterns.fakeFreeTiers.flaggedIds.length + ' tools</strong> have fake free tiers (product demos).</div>' +
'<div class="apt-pattern-item">📉 API is avg <strong>' + patterns.apiVsSubscriptionSavings.averageMultiplier + 'x cheaper</strong> than subscription for typical usage.</div>';
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 = '<span>Based on ' + usage.messagesPerMonth + ' msgs/month • ' + usage.avgInputTokens + '/' + usage.avgOutputTokens + ' in/out tokens avg</span><br><span>Adjust usage in extension popup</span>';
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 });
})();
+378
View File
@@ -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' };
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 B

+97
View File
@@ -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;
}
})();
+100
View File
@@ -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;
}
})();
+83
View File
@@ -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": ["<all_urls>"]
}
]
}
+126
View File
@@ -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; }
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Pricing Scanner</title>
<link rel="stylesheet" href="popup.css" />
</head>
<body>
<div class="popup-container">
<header class="popup-header">
<h1>🔍 AI Pricing Scanner</h1>
<p class="subtitle">Real cost-per-use for AI tools</p>
</header>
<nav class="popup-tabs">
<button class="tab-btn active" data-tab="calculator">Calculator</button>
<button class="tab-btn" data-tab="quota">Quota</button>
<button class="tab-btn" data-tab="compare">Compare</button>
<button class="tab-btn" data-tab="patterns">Insights</button>
</nav>
<section id="tab-calculator" class="tab-content active">
<div class="usage-inputs">
<label><span>Messages / month</span><input type="number" id="usage-msgs" value="500" min="1" max="50000" /></label>
<label><span>Avg input tokens / msg</span><input type="number" id="usage-input-tokens" value="800" min="0" max="100000" /></label>
<label><span>Avg output tokens / msg</span><input type="number" id="usage-output-tokens" value="400" min="0" max="100000" /></label>
</div>
<div id="calculator-results" class="results-list"><p class="loading">Calculating…</p></div>
</section>
<section id="tab-quota" class="tab-content">
<div id="quota-list" class="quota-list"><p class="empty-state">Visit an AI tool page to start tracking.</p></div>
</section>
<section id="tab-compare" class="tab-content">
<div class="compare-intro">All tools ranked by API savings vs subscription:</div>
<div id="compare-results" class="results-list"><p class="loading">Comparing…</p></div>
</section>
<section id="tab-patterns" class="tab-content">
<div id="patterns-content"></div>
</section>
<footer class="popup-footer">
<button id="btn-save-usage" class="footer-btn primary">Save Usage Profile</button>
<button id="btn-reset-quotas" class="footer-btn">Reset Quotas</button>
</footer>
</div>
<script src="../data/pricing-database.js"></script>
<script src="../lib/pricing.js"></script>
<script src="../lib/quota.js"></script>
<script src="popup.js"></script>
</body>
</html>
+184
View File
@@ -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 = '<p class="empty-state">No tools with API pricing available.</p>';
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 =
'<div class="calc-card-header"><span class="calc-card-name">' + tool.name + '</span><span class="free-badge ' + badgeClass + '">' + freeTier.label.replace(/[⚠️✅]/g, '').trim() + '</span></div>' +
'<div class="calc-card-row"><span class="label">Subscription</span><span class="value">$' + tool.priceMonthly + '/mo</span></div>' +
'<div class="calc-card-row"><span class="label">API equivalent</span><span class="value">$' + comparison.apiMonthlyCost.toFixed(2) + '/mo</span></div>' +
'<div class="calc-card-row"><span class="label">Cost/msg (sub)</span><span class="value">$' + comparison.costPerMessageSubscription.toFixed(4) + '</span></div>' +
'<div class="calc-card-row"><span class="label">Cost/msg (API)</span><span class="value">$' + comparison.costPerMessageApi.toFixed(4) + '</span></div>' +
'<div class="calc-card-row"><span class="label">Break-even</span><span class="value">' + (comparison.breakEvenMessages === Infinity ? 'N/A' : comparison.breakEvenMessages.toLocaleString() + ' msgs') + '</span></div>' +
'<div class="verdict">' + comparison.verdict + '</div>';
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 = '<p class="empty-state">No tools with API pricing to compare.</p>';
return;
}
comparisons.forEach(function (comp) {
var card = document.createElement('div');
card.className = 'calc-card';
var badgeHtml = comp.cheaperOption === 'api'
? '<span class="apt-badge apt-badge-save">API saves ' + comp.savingsPercent + '%</span>'
: '<span class="apt-badge apt-badge-neutral">Sub cheaper</span>';
card.innerHTML =
'<div class="calc-card-header"><span class="calc-card-name">' + comp.toolName + '</span>' + badgeHtml + '</div>' +
'<div class="calc-card-row"><span class="label">Sub: $' + comp.subscriptionMonthlyCost + '/mo</span><span class="value">vs API: $' + comp.apiMonthlyCost.toFixed(2) + '/mo</span></div>' +
'<div class="calc-card-row"><span class="label">Monthly savings</span><span class="value">$' + comp.savingsMonthly.toFixed(2) + '</span></div>' +
'<div class="calc-card-row"><span class="label">Annual savings</span><span class="value">$' + comp.savingsAnnual.toFixed(2) + '</span></div>' +
'<div class="verdict">' + comp.verdict + '</div>';
container.appendChild(card);
});
}
function renderQuotas() {
var container = document.getElementById('quota-list');
container.innerHTML = '<p class="loading">Loading…</p>';
if (!window.QuotaTracker) {
container.innerHTML = '<p class="empty-state">Quota tracker not available.</p>';
return;
}
window.QuotaTracker.getAll(function (states) {
container.innerHTML = '';
var keys = Object.keys(states);
if (keys.length === 0) {
container.innerHTML = '<p class="empty-state">No quotas being tracked. Visit an AI tool page to start tracking.</p>';
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 =
'<div class="quota-card-header"><span class="calc-card-name">' + state.toolId + '</span><span class="free-badge ' + (pct >= 80 ? 'demo' : 'generous') + '">' + (expired ? 'Reset' : Math.round(pct) + '%') + '</span></div>' +
'<div class="quota-bar-bg"><div class="quota-bar-fill ' + barClass + '" style="width:' + pct + '%"></div></div>' +
'<div class="quota-meta"><span>' + state.messagesUsed + ' / ' + (state.messageLimit || '∞') + ' messages</span><span>' + windowLabel + '</span></div>';
container.appendChild(card);
});
});
}
function renderPatterns() {
var container = document.getElementById('patterns-content');
var patterns = window.AptDB.pricingPatterns;
container.innerHTML =
'<div class="pattern-card"><h3>💰 $20/mo Copycat Price Point</h3><p>Almost every AI subscription has converged on <span class="highlight">$20/month</span> despite 10x variance in actual usage value.</p><p style="margin-top:4px;font-size:11px;color:#8888aa;">Tools charging this: ' + patterns.copycatPricePoint.tools.join(', ') + '</p></div>' +
'<div class="pattern-card"><h3>🔓 Fake Free Tiers</h3><p>Many "free tiers" are actually product tours — not genuinely usable for real work.</p><div class="flag-list" style="margin-top:6px;"><div style="margin-bottom:3px;"><strong style="color:#dc2626;">⚠️ Demo tiers:</strong> ' + patterns.fakeFreeTiers.flaggedIds.map(function (id) { var t = tools.find(function (x) { return x.id === id; }); return t ? t.name : id; }).join(', ') + '</div><div><strong style="color:#16a34a;">✅ Genuinely free:</strong> ' + patterns.fakeFreeTiers.genuinelyFreeIds.map(function (id) { var t = tools.find(function (x) { return x.id === id; }); return t ? t.name : id; }).join(', ') + '</div></div></div>' +
'<div class="pattern-card"><h3>📉 API vs Subscription Gap</h3><p>For most tools, API access is <span class="highlight">' + patterns.apiVsSubscriptionSavings.averageMultiplier + 'x cheaper</span> than subscription for typical usage (500 msgs/month).</p></div>' +
'<div class="pattern-card"><h3>📊 Annual Discount Trends</h3><p>' + patterns.annualDiscountTrend.description + '</p><p style="margin-top:4px;font-size:11px;color:#8888aa;">Examples: ' + patterns.annualDiscountTrend.examples.join(', ') + '</p></div>' +
'<div class="pattern-card"><h3>🗃️ Database Info</h3><p style="font-size:11px;">Tracking <span class="highlight">' + tools.length + ' tools</span> from ' + tools.reduce(function (set, t) { set.add(t.vendor); return set; }, new Set()).size + ' vendors.</p><p style="font-size:11px;margin-top:2px;">Last updated: ' + window.AptDB.version + '</p></div>';
}
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();
});
})();
+133
View File
@@ -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);