feat: AI Pricing Transparency Scanner Chrome Extension v1.0.0
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
*.log
|
||||||
|
.verdict
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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');
|
||||||
@@ -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.');
|
||||||
@@ -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: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
})();
|
||||||
@@ -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 |
@@ -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;
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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>"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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; }
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user