openzoo 0.51.8 → 0.51.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ # OpenZoo Launcher
2
+
3
+ One installer for ChatGPT. Setup starts automatically when you open the launcher, with a floating savings window showing wallet balance, spend, and savings. Node and npm are bundled; no global runtime or terminal is required. The launcher checks npm's stable `openzoo` release on each launch, installs newer versions into its per-user application data directory, and falls back to the bundled or last successfully installed CLI when offline. It never downgrades. The selected CLI downloads the current chat app if missing and preserves existing installations.
4
+
5
+ Build natively with `npm ci`, `npm run prepare:bundle`, then `npm run dist`. Preparation resolves current Node LTS from nodejs.org, checks the archive SHA-256, and bundles this checkout's OpenZoo package. Push a `launcher-v1.0.0` style tag to publish six native installers through Actions; manual dispatch builds downloadable workflow artifacts without publishing a release. macOS uses existing Apple signing/notarization secrets. Windows currently ships unsigned.
6
+
7
+ Downloaded CLI updates execute as the current user, just like installing OpenZoo from npm. Updates are staged and promoted after a CLI smoke check. Launcher/Electron updates require downloading a newer installer. Configuration, wallets, downloaded apps and background proxy processes belong to OpenZoo and survive closing/uninstalling the launcher.
@@ -0,0 +1,34 @@
1
+ const { app, BrowserWindow, ipcMain } = require('electron');
2
+ const path = require('node:path');
3
+ const { resolveCLI, run } = require('./runtime.cjs');
4
+ const { showSavings } = require('./savings.cjs');
5
+ let window, busy = false;
6
+ if (!app.requestSingleInstanceLock({ savingsReady: process.env.OPENZOO_SAVINGS_READY })) app.quit();
7
+ else {
8
+ app.on('second-instance', (_event, argv, _cwd, data) => { if (argv.includes('--savings-only')) {
9
+ const pill = showSavings();
10
+ const ready = () => { if (data?.savingsReady) { try { require('node:fs').writeFileSync(data.savingsReady, 'ready'); } catch {} } };
11
+ if (pill.webContents.isLoading()) pill.webContents.once('did-finish-load', ready); else ready();
12
+ } else { window?.show(); window?.focus(); } });
13
+ app.whenReady().then(() => {
14
+ if (process.argv.includes('--savings-only')) { showSavings(); return; }
15
+ window = new BrowserWindow({ width: 740, height: 660, backgroundColor: '#101110', webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: true } });
16
+ window.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
17
+ window.webContents.on('will-navigate', event => event.preventDefault());
18
+ window.loadFile(path.join(__dirname, 'index.html'));
19
+ ipcMain.handle('launch', async (event, command) => {
20
+ if (event.sender !== window.webContents || command !== 'chatgpt' || busy) throw new Error('Launch unavailable');
21
+ busy = true;
22
+ const log = message => { if (!window.isDestroyed()) window.webContents.send('log', message); };
23
+ try {
24
+ const bundle = app.isPackaged ? path.join(process.resourcesPath, 'bundle') : path.join(__dirname, 'bundle');
25
+ const { node, cli } = await resolveCLI(bundle, app.getPath('userData'), log);
26
+ showSavings();
27
+ await run(node, [cli, 'chatgpt', '--no-pill'], log);
28
+ log('\nLaunched. You can close this window.\n');
29
+ } catch (e) { log(`\n${e.message}\n`); throw e; }
30
+ finally { busy = false; }
31
+ });
32
+ });
33
+ app.on('window-all-closed', () => app.quit());
34
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "openzoo-launcher",
3
+ "version": "1.1.0",
4
+ "private": true,
5
+ "main": "main.cjs",
6
+ "scripts": {
7
+ "prepare:bundle": "node prepare.mjs",
8
+ "dist": "electron-builder",
9
+ "test": "node --test test.cjs"
10
+ },
11
+ "devDependencies": {
12
+ "electron": "44.3.0",
13
+ "electron-builder": "26.15.3"
14
+ },
15
+ "build": {
16
+ "appId": "fun.openzoo.launcher",
17
+ "productName": "OpenZoo Launcher",
18
+ "files": [
19
+ "main.cjs",
20
+ "runtime.cjs",
21
+ "preload.cjs",
22
+ "index.html",
23
+ "savings.cjs",
24
+ "savings-preload.cjs",
25
+ "savings.html",
26
+ "savings-renderer.js"
27
+ ],
28
+ "extraResources": [
29
+ {
30
+ "from": "bundle",
31
+ "to": "bundle"
32
+ },
33
+ {
34
+ "from": "openzoo-overlay.json",
35
+ "to": "openzoo-overlay.json"
36
+ }
37
+ ],
38
+ "artifactName": "OpenZoo-Launcher-${version}-${os}-${arch}.${ext}",
39
+ "mac": {
40
+ "target": "dmg",
41
+ "hardenedRuntime": true
42
+ },
43
+ "win": {
44
+ "target": "nsis"
45
+ },
46
+ "nsis": {
47
+ "oneClick": false,
48
+ "perMachine": false,
49
+ "allowToChangeInstallationDirectory": true
50
+ },
51
+ "linux": {
52
+ "target": "AppImage",
53
+ "category": "Network"
54
+ }
55
+ }
56
+ }
@@ -0,0 +1,59 @@
1
+ const path = require('node:path');
2
+ const fs = require('node:fs/promises');
3
+ const { spawn } = require('node:child_process');
4
+ const stable = v => /^\d+\.\d+\.\d+$/.test(v);
5
+ const newer = (a, b) => {
6
+ if (!stable(a) || !stable(b)) return false;
7
+ const aa = a.split('.').map(Number), bb = b.split('.').map(Number);
8
+ for (let i = 0; i < 3; i++) if (aa[i] !== bb[i]) return aa[i] > bb[i];
9
+ return false;
10
+ };
11
+ function environment(node) {
12
+ const env = { ...process.env, PATH: `${path.dirname(node)}${path.delimiter}${process.env.PATH || ''}` };
13
+ delete env.ELECTRON_RUN_AS_NODE;
14
+ delete env.NODE_OPTIONS;
15
+ return env;
16
+ }
17
+ function run(node, args, log, timeout = 600000) {
18
+ return new Promise((resolve, reject) => {
19
+ const child = spawn(node, args, { env: environment(node), windowsHide: true });
20
+ const timer = setTimeout(() => { child.kill(); reject(new Error('Operation timed out. Please try again.')); }, timeout);
21
+ child.stdout.on('data', b => log(b.toString()));
22
+ child.stderr.on('data', b => log(b.toString()));
23
+ child.on('error', e => { clearTimeout(timer); reject(e); });
24
+ child.on('exit', code => { clearTimeout(timer); code === 0 ? resolve() : reject(new Error(`OpenZoo exited with code ${code}`)); });
25
+ });
26
+ }
27
+ async function resolveCLI(bundle, userData, log) {
28
+ const node = path.join(bundle, 'runtime', process.platform === 'win32' ? 'node.exe' : 'bin/node');
29
+ const npm = path.join(bundle, 'runtime', process.platform === 'win32' ? 'node_modules/npm/bin/npm-cli.js' : 'lib/node_modules/npm/bin/npm-cli.js');
30
+ const pkg = dir => path.join(dir, 'node_modules/openzoo');
31
+ let selected = path.join(bundle, 'sidecar');
32
+ let version = JSON.parse(await fs.readFile(path.join(pkg(selected), 'package.json'), 'utf8')).version;
33
+ const updates = path.join(userData, 'versions');
34
+ await fs.mkdir(updates, { recursive: true });
35
+ for (const v of await fs.readdir(updates)) {
36
+ if (newer(v, version)) {
37
+ try { if (JSON.parse(await fs.readFile(path.join(pkg(path.join(updates, v)), 'package.json'), 'utf8')).version === v) { selected = path.join(updates, v); version = v; } } catch {}
38
+ }
39
+ }
40
+ try {
41
+ log('Checking for the latest OpenZoo…\n');
42
+ const response = await fetch('https://registry.npmjs.org/openzoo/latest', { signal: AbortSignal.timeout(10000) });
43
+ if (!response.ok) throw new Error(`Registry returned ${response.status}`);
44
+ const latest = (await response.json()).version;
45
+ if (newer(latest, version)) {
46
+ const stage = await fs.mkdtemp(path.join(updates, '.install-'));
47
+ try {
48
+ await run(node, [npm, 'install', '--prefix', stage, '--registry=https://registry.npmjs.org', '--omit=dev', '--no-audit', '--no-fund', `openzoo@${latest}`], log);
49
+ await run(node, [path.join(stage, 'node_modules/dugite/script/download-git.js')], log);
50
+ await run(node, [path.join(pkg(stage), 'bin/openzoo.js'), '--help'], () => {}, 30000);
51
+ await fs.rename(stage, path.join(updates, latest));
52
+ selected = path.join(updates, latest); version = latest;
53
+ } finally { await fs.rm(stage, { recursive: true, force: true }); }
54
+ }
55
+ } catch (e) { log(`Update unavailable (${e.message}). Using installed OpenZoo ${version}.\n`); }
56
+ log(`Starting OpenZoo ${version}…\n`);
57
+ return { node, cli: path.join(pkg(selected), 'bin/openzoo.js') };
58
+ }
59
+ module.exports = { newer, environment, resolveCLI, run };
@@ -0,0 +1,5 @@
1
+ const { contextBridge, ipcRenderer } = require('electron');
2
+ contextBridge.exposeInMainWorld('savings', {
3
+ listen: fn => ipcRenderer.on('savings', (_event, value) => fn(value)),
4
+ close: () => ipcRenderer.send('close-savings'),
5
+ });
@@ -0,0 +1,9 @@
1
+ const usd = n => typeof n === 'number' && Number.isFinite(n) ? '$' + Math.abs(n).toFixed(Math.abs(n) < .01 && n !== 0 ? 4 : 2) : '—';
2
+ window.savings.listen(d => {
3
+ for (const [id, value] of [['wallet',d.walletUsd],['credit',d.creditUsd],['spent',d.billing?.calls > 0 ? d.billing.chargedUsd : null],['direct',d.comparison?.calls > 0 ? d.comparison.directUsd : null]]) document.getElementById(id).textContent=usd(value);
4
+ const saved=d.comparison?.calls > 0 ? d.comparison.directUsd-d.comparison.spentUsd : null;
5
+ document.getElementById('saved').textContent=usd(saved);
6
+ document.getElementById('saving-label').textContent=saved!==null && saved<0 ? 'Extra cost' : 'Saved';
7
+ document.getElementById('state').textContent=d.online ? (d.billing?.calls > 0 ? 'Final charges · since connection started' : 'Waiting for final billing · balance is live') : 'Reconnecting · showing last known amounts';
8
+ });
9
+ document.getElementById('close').addEventListener('click',()=>window.savings.close());
@@ -0,0 +1,41 @@
1
+ const { BrowserWindow, screen, ipcMain } = require('electron');
2
+ const path = require('node:path');
3
+ let pill;
4
+ function showSavings(port = Number(process.env.OPENZOO_SAVINGS_PORT) || 8402) {
5
+ if (pill && !pill.isDestroyed()) { pill.show(); return pill; }
6
+ const { x, y, width, height } = screen.getPrimaryDisplay().workArea;
7
+ pill = new BrowserWindow({ width: 420, height: 270, x: x + width - 440, y: y + height - 290,
8
+ frame: false, transparent: true, alwaysOnTop: true, skipTaskbar: true, resizable: false,
9
+ webPreferences: { preload: path.join(__dirname, 'savings-preload.cjs'), contextIsolation: true, sandbox: true, nodeIntegration: false } });
10
+ pill.setAlwaysOnTop(true, 'floating');
11
+ pill.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
12
+ pill.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
13
+ pill.webContents.on('will-navigate', e => e.preventDefault());
14
+ pill.loadFile(path.join(__dirname, 'savings.html'));
15
+ const window = pill;
16
+ let busy = false, last = null;
17
+ const poll = async () => {
18
+ if (busy || window.isDestroyed()) return;
19
+ busy = true;
20
+ try {
21
+ const r = await fetch(`http://127.0.0.1:${port}/v1/info`, { signal: AbortSignal.timeout(2500) });
22
+ const d = await r.json();
23
+ if (!r.ok || d.youAreTalkingTo !== 'openzoo proxy') throw Error('Unavailable');
24
+ last = d;
25
+ if (!window.isDestroyed()) window.webContents.send('savings', { ...d, online: true });
26
+ } catch { if (!window.isDestroyed()) window.webContents.send('savings', { ...last, online: false }); }
27
+ finally { busy = false; }
28
+ };
29
+ window.webContents.on('did-finish-load', () => {
30
+ poll();
31
+ if (process.env.OPENZOO_SAVINGS_READY) {
32
+ try { require('node:fs').writeFileSync(process.env.OPENZOO_SAVINGS_READY, 'ready'); } catch {}
33
+ }
34
+ });
35
+ const timer = setInterval(poll, 3000);
36
+ const close = event => { if (event.sender === window.webContents) window.close(); };
37
+ ipcMain.on('close-savings', close);
38
+ window.on('closed', () => { clearInterval(timer); ipcMain.removeListener('close-savings', close); if (pill === window) pill = null; });
39
+ return pill;
40
+ }
41
+ module.exports = { showSavings };
@@ -0,0 +1,3 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'self'"><title>OpenZoo savings</title><style>
2
+ *{box-sizing:border-box}body{margin:0;padding:8px;background:transparent;font:13px system-ui;color:#fff}.pill{border-radius:20px;background:#18181bf5;border:1px solid #ffffff25;overflow:hidden;box-shadow:0 4px 14px #0004}header{background:#e85d1c;padding:12px 16px;font-weight:650;-webkit-app-region:drag;display:flex;justify-content:space-between;align-items:center}button{-webkit-app-region:no-drag;border:0;background:none;color:inherit;font:18px system-ui;cursor:pointer;padding:0 5px}main{padding:12px 16px}dl{display:grid;grid-template-columns:1fr auto;margin:0;gap:7px}dd{margin:0;font-variant-numeric:tabular-nums}small{color:#bbb;display:block;margin-top:12px;font-size:11px}#state{color:#d8ff76}
3
+ </style></head><body><div class="pill"><header><span>OpenZoo · costs & savings</span><button id="close" aria-label="Close savings pill">×</button></header><main><dl><dt>Wallet balance</dt><dd id="wallet">—</dd><dt>Prepaid credit</dt><dd id="credit">—</dd><dt>Charged · verified calls</dt><dd id="spent">—</dd><dt>Direct cost · verified calls</dt><dd id="direct">—</dd><dt id="saving-label">Saved</dt><dd id="saved">—</dd></dl><small id="state" role="status">Checking your balance…</small><small>Savings cover matching verified calls only. Drag the orange bar to move.</small></main></div><script src="savings-renderer.js"></script></body></html>
package/lib/billing.js ADDED
@@ -0,0 +1,30 @@
1
+ const money = n => typeof n === 'number' && Number.isFinite(n) && n >= 0 ? n : null;
2
+
3
+ // Only final, explicitly reported charges. Never substitute upstream cost or a quote.
4
+ export function finalBilling(x = {}, usage = {}) {
5
+ const charged = [usage.billedUsd, x.billedActualUsd, x.settledBilledUsd, x.chargedUsd,
6
+ x.settledUsd, usage.cost_details?.upstream_inference_cost != null ? usage.cost : null].map(money).find(n => n !== null) ?? null;
7
+ const direct = x.pricing === 'counterfactual' ? money(x.directUsd) : null;
8
+ return { charged, direct, upstream: money(usage.cost_details?.upstream_inference_cost) };
9
+ }
10
+
11
+ // Observe billing without retaining prompts, output text, or changing the stream.
12
+ export function billingObserver(onReceipt) {
13
+ let pending = '';
14
+ return chunk => {
15
+ pending += chunk;
16
+ let i;
17
+ while ((i = pending.indexOf('\n')) >= 0) {
18
+ const line = pending.slice(0, i).trimEnd(); pending = pending.slice(i + 1);
19
+ try {
20
+ if (line.startsWith(': x402 ')) onReceipt(JSON.parse(line.slice(7)));
21
+ else if (line.startsWith('data:')) {
22
+ const event = JSON.parse(line.slice(5).trim());
23
+ const value = event.response || event;
24
+ if (value.usage || value.x402) onReceipt({ ...value.x402, usage: value.usage });
25
+ }
26
+ } catch { /* Not a billing event. */ }
27
+ }
28
+ if (pending.length > 2_000_000) pending = '';
29
+ };
30
+ }
@@ -163,21 +163,41 @@ export function nativeSavingsLauncher({ platform = process.platform, home = os.h
163
163
  return null;
164
164
  }
165
165
 
166
- export async function startChatGptSpend({ profile, proxyPort, spawnImpl = spawn } = {}) {
167
- const native = nativeSavingsLauncher();
168
- if (native && proxyPort === 8402) {
169
- const child = spawnImpl(native, ['--savings-only'], { detached: true, stdio: 'ignore' });
170
- await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
171
- child.unref();
172
- return;
173
- }
174
- fs.mkdirSync(profile, { recursive: true });
175
- const fd = fs.openSync(path.join(profile, 'openzoo-savings.log'), 'a', 0o600);
166
+ export async function startChatGptSpend({ profile, proxyPort = 8402, spawnImpl = spawn } = {}) {
167
+ const app = fileURLToPath(new URL('../launcher/', import.meta.url));
168
+ const electron = path.join(app, 'node_modules/electron/dist', process.platform === 'darwin'
169
+ ? 'Electron.app/Contents/MacOS/Electron' : process.platform === 'win32' ? 'electron.exe' : 'electron');
170
+ const env = { ...process.env, OPENZOO_SAVINGS_PORT: String(proxyPort) };
171
+ delete env.ELECTRON_RUN_AS_NODE;
172
+ const readyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'openzoo-pill-'));
173
+ env.OPENZOO_SAVINGS_READY = path.join(readyDir, 'ready');
174
+ const logDir = profile || path.join(os.homedir(), '.openzoo');
175
+ fs.mkdirSync(logDir, { recursive: true });
176
+ const logPath = path.join(logDir, 'openzoo-savings.log');
177
+ const fd = fs.openSync(logPath, 'a', 0o600);
178
+ let child, failure;
176
179
  try {
177
- const child = spawnImpl(process.execPath, [fileURLToPath(import.meta.url), '--watch', profile, String(proxyPort)], { detached: true, stdio: ['ignore', fd, fd] });
178
- await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
180
+ if (fs.existsSync(electron)) {
181
+ child = spawnImpl(electron, [app, '--savings-only'], { detached: true, stdio: ['ignore', fd, fd], env });
182
+ } else {
183
+ // npm caches the standalone display runtime. The CLI ships the display itself.
184
+ // No ChatGPT debugging port or separate OpenZoo installer is required.
185
+ const args = ['exec', '--yes', '--package=electron@44.3.0', '--', 'electron', app, '--savings-only'];
186
+ const npmScript = process.env.npm_execpath;
187
+ child = npmScript && fs.existsSync(npmScript)
188
+ ? spawnImpl(process.execPath, [npmScript, ...args], { detached: true, stdio: ['ignore', fd, fd], env })
189
+ : spawnImpl(process.platform === 'win32' ? 'npm.cmd' : 'npm', args, { detached: true, stdio: ['ignore', fd, fd], env, shell: process.platform === 'win32' });
190
+ }
191
+ child.once('error', error => { failure = error; });
192
+ child.once('exit', code => { if (code) failure = new Error(`Display exited (${code})`); });
179
193
  child.unref();
180
- } finally { fs.closeSync(fd); }
194
+ const deadline = Date.now() + 120000;
195
+ while (!fs.existsSync(env.OPENZOO_SAVINGS_READY)) {
196
+ if (failure) throw failure;
197
+ if (Date.now() > deadline) throw new Error(`Display did not open. See ${logPath}`);
198
+ await new Promise(resolve => setTimeout(resolve, 200));
199
+ }
200
+ } finally { fs.closeSync(fd); fs.rmSync(readyDir, { recursive: true, force: true }); }
181
201
  }
182
202
 
183
203
  /** One watcher per profile, including concurrent launcher invocations. */
package/lib/proxy.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { billingObserver, finalBilling } from './billing.js';
1
2
  import { liveBalance } from './funding-status.js';
2
3
  import { paymentError } from './payment-error.js';
3
4
  import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
@@ -234,16 +235,10 @@ function relay(res, upstream, onReceipt) {
234
235
  res.on('close', () => body.destroy());
235
236
  body.on('end', resolve);
236
237
  if (!sse || typeof onReceipt !== 'function') { body.pipe(res); return; }
237
- let pending = '';
238
+ const observe = billingObserver(onReceipt);
238
239
  body.on('data', (c) => {
239
240
  res.write(c);
240
- pending += c.toString('utf8');
241
- const lines = pending.split('\n');
242
- pending = lines.pop() ?? ''; // a comment can straddle two chunks
243
- for (const line of lines) {
244
- if (!line.startsWith(': x402 ')) continue;
245
- try { onReceipt(JSON.parse(line.slice(7))); } catch { /* not our frame */ }
246
- }
241
+ observe(c.toString('utf8'));
247
242
  });
248
243
  body.on('end', () => res.end());
249
244
  });
@@ -429,6 +424,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
429
424
  let lastQuoteUsd = null;
430
425
  let sessionCogs = restored.cogsUsd;
431
426
  let sessionDirect = restored.directUsd;
427
+ let comparedSpent = 0, comparedDirect = 0, comparedCalls = 0;
428
+ let verifiedCharged = 0, verifiedCalls = 0;
432
429
  const rememberSpend = () => {
433
430
  saveSessionSpend({ spentUsd: sessionSpent, cogsUsd: sessionCogs, directUsd: sessionDirect, paidCalls });
434
431
  };
@@ -659,6 +656,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
659
656
  creditUsd,
660
657
  // WHAT THE SAME CALLS WOULD HAVE COST DIRECT. Spend on its own is a
661
658
  // bill; spend beside the counterfactual is the product.
659
+ billing: { chargedUsd: verifiedCharged, calls: verifiedCalls, scope: "verified calls since proxy startup" },
660
+ comparison: { spentUsd: comparedSpent, directUsd: comparedDirect, calls: comparedCalls },
662
661
  directUsd: sessionDirect,
663
662
  savedUsd: Math.max(0, sessionDirect - sessionSpent),
664
663
  savingX: sessionSpent > 0 ? Number((sessionDirect / sessionSpent).toFixed(4)) : null,
@@ -965,36 +964,39 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
965
964
  }
966
965
  scheduleRefresh(4000); // settlement lands on-chain in a few seconds
967
966
  }
967
+ let accounted = paid && receipt?.ok && typeof receipt.billedUsd === 'number' ? receipt.billedUsd : 0;
968
+ let counted = paid && receipt?.ok && typeof receipt.billedUsd === 'number';
969
+ let finalX = {}, finalUsage = {}, comparison = null, actualPair = null, verified = null;
970
+ const meterFinal = (x = {}) => {
971
+ const { usage, ...fields } = x;
972
+ finalX = { ...finalX, ...fields };
973
+ finalUsage = { ...finalUsage, ...usage };
974
+ const final = finalBilling(finalX, finalUsage);
975
+ if (final.charged === null) return;
976
+ if (verified === null) verifiedCalls++;
977
+ verifiedCharged += final.charged - (verified ?? 0);
978
+ verified = final.charged;
979
+ const delta = final.charged - accounted;
980
+ sessionSpent += delta;
981
+ if (viaTunnel) tunnelSpent += delta;
982
+ if (!counted) { paidCalls++; counted = true; }
983
+ accounted = final.charged;
984
+ if (comparison) { comparedSpent -= comparison.charged; comparedDirect -= comparison.direct; comparedCalls--; comparison = null; }
985
+ if (final.direct !== null) {
986
+ comparison = final;
987
+ comparedSpent += final.charged; comparedDirect += final.direct; comparedCalls++;
988
+ }
989
+ if (actualPair) { sessionActual -= actualPair.upstream; billedWithActual -= actualPair.charged; actualCalls--; }
990
+ actualPair = final.upstream === null ? null : final;
991
+ if (actualPair) { sessionActual += final.upstream; billedWithActual += final.charged; actualCalls++; }
992
+ rememberSpend();
993
+ log(`final billing: charged $${final.charged.toFixed(6)} · upstream ${final.upstream === null ? 'unreported' : '$' + final.upstream.toFixed(6)} · direct ${final.direct === null ? 'unreported' : '$' + final.direct.toFixed(6)}`);
994
+ };
968
995
  const upCt = response.headers.get('content-type') || '';
969
996
  if (isPaidPost && response.ok && upCt.includes('application/json')) {
970
997
  let data = null;
971
998
  try { data = await response.clone().json(); } catch { /* not JSON after all */ }
972
- // WHAT IT REALLY COST, straight from the provider — rides every
973
- // completion already; no extra call.
974
- {
975
- // PAIR THE NUMERATOR WITH THE DENOMINATOR: usage.cost with the
976
- // post-completion billed twin, never the quote reserve.
977
- const pair = pairActualBilled(data?.x402, data?.usage);
978
- if (pair) {
979
- sessionActual += pair.upstreamUsd;
980
- actualCalls += 1;
981
- billedWithActual += pair.billedUsd;
982
- }
983
- }
984
- // PREPAID CALLS STILL COST MONEY. When prepaid credit covers the quote
985
- // the gateway serves 200 on the FIRST request — no 402, no payment, no
986
- // receipt. The receipt still rides the response body, so meter it there.
987
- if (!paid && data?.x402 && typeof data.x402.billedUsd === 'number') {
988
- const x = data.x402;
989
- sessionSpent += x.billedUsd;
990
- sessionCogs += receiptUsedCogs(x);
991
- noteQuote(x);
992
- sessionDirect += receiptDirectUsd(x);
993
- paidCalls += 1;
994
- if (viaTunnel) tunnelSpent += x.billedUsd;
995
- rememberSpend();
996
- say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
997
- }
999
+ meterFinal({ ...data?.x402, usage: data?.usage });
998
1000
  if (data?.object === 'chat.completion') {
999
1001
  if (statsOff) {
1000
1002
  // The gateway already dropped its half; drop ours, and any block an
@@ -1020,23 +1022,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
1020
1022
  // A STREAMED call is metered from the gateway's trailing SSE comment —
1021
1023
  // same figures the JSON path reads out of `data.x402`, same counters, so
1022
1024
  // the status line does not care which transport served the answer.
1023
- const meterStreamed = (x) => {
1024
- const pair = pairActualBilled(x, x?.usage);
1025
- if (pair) {
1026
- sessionActual += pair.upstreamUsd;
1027
- actualCalls += 1;
1028
- billedWithActual += pair.billedUsd;
1029
- }
1030
- if (paid || typeof x?.billedUsd !== 'number') return;
1031
- sessionSpent += x.billedUsd;
1032
- sessionCogs += receiptUsedCogs(x);
1033
- noteQuote(x);
1034
- sessionDirect += receiptDirectUsd(x);
1035
- paidCalls += 1;
1036
- if (viaTunnel) tunnelSpent += x.billedUsd;
1037
- rememberSpend();
1038
- say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
1039
- };
1025
+ const meterStreamed = meterFinal;
1040
1026
  // A 402 AFTER we attempted payment is a SETTLEMENT failure, not a quote.
1041
1027
  // The client-side balance check is advisory, so a funded wallet can still
1042
1028
  // fail on-chain / at the facilitator, and the gateway answers the paid
package/lib/x402.js CHANGED
@@ -432,49 +432,22 @@ export async function tokenBalance(connection, owner, mintStr) {
432
432
  export function receiptLine(accept, settle) {
433
433
  const x = accept.extra || {};
434
434
  const usd = x.billedUsd != null ? `$${Number(x.billedUsd).toFixed(6)}` : `${accept.maxAmountRequired} raw units`;
435
- // "1.0× cheaper than direct" is a sentence that means nothing, and a user
436
- // read it as a bug — rightly. Since the gateway repriced to an OpenRouter
437
- // CEILING, an uncompressed call bills exactly the direct rate, so the ratio
438
- // is 1.0 by design rather than by accident. Say what actually happened:
439
- // below 1.05× there is no saving to report, so report the price instead.
440
- //
441
- // The saving comes from leCore forwarding fewer tokens. A short body never
442
- // reaches the spill threshold, so there is nothing to compress and nothing
443
- // to save — which is worth saying out loud, because the fix on the caller's
444
- // side is to BIND a corpus, not to change models.
445
435
  const billed = Number(x.billedUsd);
446
436
  const direct = x.directUsd != null ? Number(x.directUsd) : null;
447
437
  const saved = x.savedUsd != null ? Number(x.savedUsd)
448
438
  : (Number.isFinite(billed) && Number.isFinite(direct) ? Math.max(0, direct - billed) : null);
449
439
  const ratio = x.savesVsDirect != null ? Number(x.savesVsDirect)
450
440
  : (Number.isFinite(billed) && billed > 0 && Number.isFinite(direct) ? direct / billed : null);
451
- // Wallet path: OpenRouter price, plus 33% of savings vs direct when any.
452
- // Never print extra.markup — that field is leftover 3× and is not the quote.
453
- //
454
- // THE BASIS DECIDES THE CLAIM, NOT THE RATIO'S SIZE.
455
- //
456
- // This used to gate the discount purely on `ratio >= 1.05`, which is the
457
- // wrong test: on the `markup` basis the gateway sets directUsd to its OWN
458
- // forwarded cost so downstream sums stay like-for-like (quote.ts), and
459
- // savesVsDirect is computed against that same internal number — so the ratio
460
- // can read well above 1 while billedUsd and directUsd are IDENTICAL.
461
- // MEASURED 2026-08-28 on a live short-body call: billed $0.0013178, direct
462
- // $0.0013178, savesVsDirect 1.457 — and this line printed "1.5× cheaper than
463
- // direct" on a call that by its own two dollar figures saved nothing.
464
- //
465
- // The 402's own `description` already guards this correctly by branching on
466
- // q.pricing; the receipt did not, so the challenge told the truth and the
467
- // receipt for the same call did not. Only the counterfactual basis — leCore
468
- // actually spilled, so "what this would have cost sent whole" is a real
469
- // computed number — has earned the discount claim.
441
+ // Only counterfactual pricing supplies a retail comparison. Other modes
442
+ // may put internal costs in directUsd; they do not establish price parity.
470
443
  const counterfactual = x.pricing === 'counterfactual';
471
444
  const saves = !counterfactual
472
- ? ' (at OpenRouter price — nothing to compress; bind a corpus to save)'
445
+ ? ' (direct-price comparison unavailable)'
473
446
  : ratio != null && ratio >= 1.05
474
447
  ? ` (${ratio.toFixed(1)}× cheaper than direct)`
475
448
  : Number.isFinite(saved) && saved > 0
476
449
  ? ` ($${saved.toFixed(4)} saved vs direct)`
477
- : ' (at OpenRouter price — nothing to compress; bind a corpus to save)';
450
+ : ' (direct-price comparison unavailable)';
478
451
  const tx = settle?.transaction || settle?.txHash || settle?.signature;
479
452
  const rail = railOf(accept);
480
453
  return `paid ${usd}${saves}${rail ? ` · rail ${rail}` : ''}${tx ? ` · tx ${tx}` : ''}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.51.8",
3
+ "version": "0.51.10",
4
4
  "description": "Local x402-paying proxy + MCP server for openzoo.fun — point any OpenAI-compatible harness (Cursor, Claude Code, aider, SDKs) at localhost and it pays per call from a local burner wallet. Solana and Base rails live; Robinhood experimental.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,7 +10,14 @@
10
10
  "files": [
11
11
  "bin",
12
12
  "lib",
13
- "README.md"
13
+ "README.md",
14
+ "launcher/main.cjs",
15
+ "launcher/runtime.cjs",
16
+ "launcher/package.json",
17
+ "launcher/savings.cjs",
18
+ "launcher/savings.html",
19
+ "launcher/savings-preload.cjs",
20
+ "launcher/savings-renderer.js"
14
21
  ],
15
22
  "engines": {
16
23
  "node": ">=18"