openzoo 0.51.11 → 0.51.13
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.
- package/launcher/savings-preload.cjs +6 -0
- package/launcher/savings-renderer.js +44 -1
- package/launcher/savings.cjs +28 -3
- package/launcher/savings.html +2 -2
- package/lib/billing.js +1 -1
- package/lib/proxy.js +11 -2
- package/lib/receipt-ledger.js +22 -0
- package/package.json +1 -1
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
2
|
contextBridge.exposeInMainWorld('savings', {
|
|
3
3
|
listen: fn => ipcRenderer.on('savings', (_event, value) => fn(value)),
|
|
4
|
+
deposit: () => ipcRenderer.send("open-deposit"),
|
|
5
|
+
back: () => ipcRenderer.send("close-deposit"),
|
|
6
|
+
copy: chain => ipcRenderer.send("copy-deposit", chain),
|
|
7
|
+
card: () => ipcRenderer.send("deposit-card"),
|
|
8
|
+
wallet: fn => ipcRenderer.on("deposit-wallet", (_event, value) => fn(value)),
|
|
9
|
+
copied: fn => ipcRenderer.on("deposit-copied", (_event, value) => fn(value)),
|
|
4
10
|
close: () => ipcRenderer.send('close-savings'),
|
|
5
11
|
});
|
|
@@ -1,9 +1,52 @@
|
|
|
1
|
+
let onboarding = false;
|
|
2
|
+
try { onboarding = localStorage.getItem('openzoo-onboarded-v1') !== 'yes'; } catch {}
|
|
3
|
+
const finishOnboarding = () => {
|
|
4
|
+
try { localStorage.setItem('openzoo-onboarded-v1','yes'); } catch {}
|
|
5
|
+
onboarding=false;
|
|
6
|
+
document.getElementById('welcome').hidden=true;
|
|
7
|
+
document.getElementById('totals').hidden=false;
|
|
8
|
+
document.getElementById('later').hidden=true;
|
|
9
|
+
document.getElementById('funding').hidden=true;
|
|
10
|
+
window.savings.back();
|
|
11
|
+
};
|
|
1
12
|
const usd = n => typeof n === 'number' && Number.isFinite(n) ? '$' + Math.abs(n).toFixed(Math.abs(n) < .01 && n !== 0 ? 4 : 2) : '—';
|
|
2
13
|
window.savings.listen(d => {
|
|
14
|
+
if (onboarding) {
|
|
15
|
+
const funded=d.online && ((d.walletUsd || 0)+(d.creditUsd || 0)>0);
|
|
16
|
+
document.getElementById("back").textContent=funded ? "Ready — start chatting" : "Waiting for funds…";
|
|
17
|
+
document.getElementById("back").disabled=!funded;
|
|
18
|
+
}
|
|
3
19
|
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
20
|
const saved=d.comparison?.calls > 0 ? d.comparison.directUsd-d.comparison.spentUsd : null;
|
|
5
|
-
|
|
21
|
+
const pct=d.comparison?.directUsd > 0 && saved !== null ? Math.abs(saved)/d.comparison.directUsd*100 : null;
|
|
22
|
+
document.getElementById('saved').textContent=usd(saved)+(pct===null?'':` (${pct.toFixed(1)}%)`);
|
|
6
23
|
document.getElementById('saving-label').textContent=saved!==null && saved<0 ? 'Extra cost' : 'Saved';
|
|
7
24
|
document.getElementById('state').textContent=d.online ? (d.billing?.calls > 0 ? 'Final charges · saved receipts' : 'Waiting for final billing · balance is live') : 'Reconnecting · showing last known amounts';
|
|
8
25
|
});
|
|
9
26
|
document.getElementById('close').addEventListener('click',()=>window.savings.close());
|
|
27
|
+
|
|
28
|
+
document.getElementById('deposit').addEventListener('click', () => {
|
|
29
|
+
document.getElementById('funding').hidden=false;
|
|
30
|
+
if (!onboarding) { document.getElementById('back').disabled=false; document.getElementById('back').textContent='Back to savings'; }
|
|
31
|
+
window.savings.deposit();
|
|
32
|
+
});
|
|
33
|
+
window.savings.wallet(d => {
|
|
34
|
+
document.getElementById('fund-status').textContent=d.error || 'Fund this wallet. Click an address to copy it.';
|
|
35
|
+
for (const chain of ['solana','evm']) {
|
|
36
|
+
document.getElementById(chain+'-address').textContent=d[chain] || '';
|
|
37
|
+
document.getElementById('copy-'+chain).disabled=!d[chain];
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
for (const chain of ['solana','evm']) document.getElementById('copy-'+chain).addEventListener('click',()=>window.savings.copy(chain));
|
|
41
|
+
window.savings.copied(chain => { document.getElementById('fund-status').textContent=(chain==='solana'?'Solana':'Base')+' address copied.'; });
|
|
42
|
+
document.getElementById('card').addEventListener('click',()=>window.savings.card());
|
|
43
|
+
document.getElementById('back').addEventListener('click',finishOnboarding);
|
|
44
|
+
document.getElementById('later').addEventListener('click',finishOnboarding);
|
|
45
|
+
if (onboarding) {
|
|
46
|
+
document.getElementById('welcome').hidden=false;
|
|
47
|
+
document.getElementById('totals').hidden=true;
|
|
48
|
+
document.getElementById('later').hidden=false;
|
|
49
|
+
document.getElementById('funding').hidden=false;
|
|
50
|
+
if (!onboarding) { document.getElementById('back').disabled=false; document.getElementById('back').textContent='Back to savings'; }
|
|
51
|
+
window.savings.deposit();
|
|
52
|
+
}
|
package/launcher/savings.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { BrowserWindow, screen, ipcMain } = require('electron');
|
|
1
|
+
const { BrowserWindow, screen, ipcMain, clipboard, shell } = require('electron');
|
|
2
2
|
const path = require('node:path');
|
|
3
3
|
let pill;
|
|
4
4
|
function showSavings(port = Number(process.env.OPENZOO_SAVINGS_PORT) || 8402) {
|
|
@@ -13,7 +13,32 @@ function showSavings(port = Number(process.env.OPENZOO_SAVINGS_PORT) || 8402) {
|
|
|
13
13
|
pill.webContents.on('will-navigate', e => e.preventDefault());
|
|
14
14
|
pill.loadFile(path.join(__dirname, 'savings.html'));
|
|
15
15
|
const window = pill;
|
|
16
|
-
let busy = false, last = null;
|
|
16
|
+
let busy = false, last = null, wallet = null;
|
|
17
|
+
const resize = height => {
|
|
18
|
+
const b = window.getBounds(), area = screen.getDisplayMatching(b).workArea;
|
|
19
|
+
window.setBounds({ ...b, height: Math.min(height, area.height), y: Math.max(area.y, Math.min(b.y, area.y + area.height - height)) });
|
|
20
|
+
};
|
|
21
|
+
const deposit = async event => {
|
|
22
|
+
if (event.sender !== window.webContents) return;
|
|
23
|
+
resize(620);
|
|
24
|
+
try {
|
|
25
|
+
const r = await fetch(`http://127.0.0.1:${port}/v1/wallet`, { signal: AbortSignal.timeout(5000) });
|
|
26
|
+
if (!r.ok) throw Error("Unavailable");
|
|
27
|
+
wallet = await r.json();
|
|
28
|
+
if (!window.isDestroyed()) window.webContents.send("deposit-wallet", { solana: wallet.solana, evm: wallet.evm });
|
|
29
|
+
} catch { if (!window.isDestroyed()) window.webContents.send("deposit-wallet", { error: "Couldn’t load your wallet. Try again." }); }
|
|
30
|
+
};
|
|
31
|
+
const copy = (event, chain) => {
|
|
32
|
+
if (event.sender !== window.webContents || !["solana", "evm"].includes(chain) || !wallet?.[chain]) return;
|
|
33
|
+
clipboard.writeText(wallet[chain]);
|
|
34
|
+
window.webContents.send("deposit-copied", chain);
|
|
35
|
+
};
|
|
36
|
+
const card = event => { if (event.sender === window.webContents) shell.openExternal("https://whop.com/staccoverflow/openzoo"); };
|
|
37
|
+
const back = event => { if (event.sender === window.webContents) resize(270); };
|
|
38
|
+
ipcMain.on("open-deposit", deposit);
|
|
39
|
+
ipcMain.on("copy-deposit", copy);
|
|
40
|
+
ipcMain.on("deposit-card", card);
|
|
41
|
+
ipcMain.on("close-deposit", back);
|
|
17
42
|
const poll = async () => {
|
|
18
43
|
if (busy || window.isDestroyed()) return;
|
|
19
44
|
busy = true;
|
|
@@ -35,7 +60,7 @@ function showSavings(port = Number(process.env.OPENZOO_SAVINGS_PORT) || 8402) {
|
|
|
35
60
|
const timer = setInterval(poll, 3000);
|
|
36
61
|
const close = event => { if (event.sender === window.webContents) window.close(); };
|
|
37
62
|
ipcMain.on('close-savings', close);
|
|
38
|
-
window.on('closed', () => { clearInterval(timer); ipcMain.removeListener('close-savings', close); if (pill === window) pill = null; });
|
|
63
|
+
window.on('closed', () => { clearInterval(timer); for (const [name, fn] of [["open-deposit",deposit],["copy-deposit",copy],["deposit-card",card],["close-deposit",back]]) ipcMain.removeListener(name,fn); ipcMain.removeListener('close-savings', close); if (pill === window) pill = null; });
|
|
39
64
|
return pill;
|
|
40
65
|
}
|
|
41
66
|
module.exports = { showSavings };
|
package/launcher/savings.html
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
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>
|
|
2
|
+
*{box-sizing:border-box}body{max-height:100vh;overflow-y:auto;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}.actions{display:flex;align-items:center;gap:8px}#deposit,.fund-button{font-size:12px;border:1px solid #ffffff60;border-radius:7px;padding:5px 10px}.fund-button{display:block;width:100%;text-align:left;margin:9px 0;overflow-wrap:anywhere;line-height:1.5}.fund-button span{display:block;font-size:11px;color:#ccc;user-select:text}[hidden]{display:none!important}#welcome p{font-size:12px;line-height:1.5;color:#ccc}#funding{border-top:1px solid #ffffff25;margin-top:12px}#funding p{font-size:12px;color:#ccc}
|
|
3
|
+
</style></head><body><div class="pill"><header><span>OpenZoo · costs & savings</span><div class="actions"><button id="deposit">Deposit</button><button id="close" aria-label="Close savings pill">×</button></div></header><main><section id="welcome" hidden><h3>Welcome to OpenZoo</h3><p>Pay per request from your OpenZoo wallet. Add funds before your first chat.</p><p>1. Choose card or crypto below.<br>2. Add funds to this wallet.<br>3. Once your balance appears, start chatting.</p></section><div id="totals"><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></div><section id="funding" hidden><h3>Add funds</h3><p id="fund-status" role="status">Loading your wallet…</p><button class="fund-button" id="copy-solana" disabled>Solana · USDC / TOKEN / LEOS<span id="solana-address"></span><span>Click to copy address</span></button><button class="fund-button" id="copy-evm" disabled>Base · USDC<span id="evm-address"></span><span>Click to copy address</span></button><button class="fund-button" id="card">Pay by card · Whop ↗</button><p>At checkout, paste your Solana address to link the purchase to this wallet. Your balance updates automatically after confirmation.</p><button class="fund-button" id="back">Back to savings</button><button class="fund-button" id="later" hidden>Set up later</button></section></main></div><script src="savings-renderer.js"></script></body></html>
|
package/lib/billing.js
CHANGED
|
@@ -24,7 +24,7 @@ export function billingObserver(onReceipt) {
|
|
|
24
24
|
else if (line.startsWith('data:')) {
|
|
25
25
|
const event = JSON.parse(line.slice(5).trim());
|
|
26
26
|
const value = event.response || event;
|
|
27
|
-
if (value.usage || value.x402) onReceipt({ ...value.x402, usage: value.usage, _completedResponse: event.type === 'response.completed' || value.status === 'completed' });
|
|
27
|
+
if (value.usage || value.x402) onReceipt({ ...value.x402, usage: value.usage, _responseId: value.id, _completedResponse: event.type === 'response.completed' || value.status === 'completed' });
|
|
28
28
|
}
|
|
29
29
|
} catch { /* Not a billing event. */ }
|
|
30
30
|
}
|
package/lib/proxy.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { receiptLedger } from './receipt-ledger.js';
|
|
1
2
|
import { billingObserver, finalBilling } from './billing.js';
|
|
2
3
|
import { liveBalance } from './funding-status.js';
|
|
3
4
|
import { paymentError } from './payment-error.js';
|
|
@@ -35,7 +36,7 @@ async function catalogIdsCached(modelsUrl, headers) {
|
|
|
35
36
|
return ids;
|
|
36
37
|
}
|
|
37
38
|
import { withNamespace } from './namespace.js';
|
|
38
|
-
import { loadSessionSpend, saveSessionSpend } from './session.js';
|
|
39
|
+
import { loadSessionSpend, saveSessionSpend, sessionSpendFile } from './session.js';
|
|
39
40
|
import { creditBalance, quotedPrices } from './info.js';
|
|
40
41
|
import { priceHoldings } from './livestatus.js';
|
|
41
42
|
import { receiptUsedCogs, receiptDirectUsd, pairActualBilled } from './racesettle.js';
|
|
@@ -426,6 +427,12 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
426
427
|
let sessionDirect = restored.directUsd;
|
|
427
428
|
let comparedSpent = restored.comparison?.spentUsd ?? 0, comparedDirect = restored.comparison?.directUsd ?? 0, comparedCalls = restored.comparison?.calls ?? 0;
|
|
428
429
|
let verifiedCharged = restored.billing?.chargedUsd ?? 0, verifiedCalls = restored.billing?.calls ?? 0;
|
|
430
|
+
const ledger = receiptLedger(sessionSpendFile() + '.receipts.json', { billing: restored.billing, comparison: restored.comparison });
|
|
431
|
+
const applyLedger = ({billing, comparison}) => {
|
|
432
|
+
verifiedCharged = billing.chargedUsd; verifiedCalls = billing.calls;
|
|
433
|
+
comparedSpent = comparison.spentUsd; comparedDirect = comparison.directUsd; comparedCalls = comparison.calls;
|
|
434
|
+
};
|
|
435
|
+
applyLedger(ledger.totals());
|
|
429
436
|
const rememberSpend = () => {
|
|
430
437
|
saveSessionSpend({ spentUsd: sessionSpent, cogsUsd: sessionCogs, directUsd: sessionDirect, paidCalls, billing: { chargedUsd: verifiedCharged, calls: verifiedCalls }, comparison: { spentUsd: comparedSpent, directUsd: comparedDirect, calls: comparedCalls } });
|
|
431
438
|
};
|
|
@@ -964,6 +971,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
964
971
|
}
|
|
965
972
|
scheduleRefresh(4000); // settlement lands on-chain in a few seconds
|
|
966
973
|
}
|
|
974
|
+
const requestReceiptId = crypto.randomUUID();
|
|
967
975
|
let accounted = paid && receipt?.ok && typeof receipt.billedUsd === 'number' ? receipt.billedUsd : 0;
|
|
968
976
|
let counted = paid && receipt?.ok && typeof receipt.billedUsd === 'number';
|
|
969
977
|
let finalX = {}, finalUsage = {}, comparison = null, actualPair = null, verified = null;
|
|
@@ -989,6 +997,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
989
997
|
if (actualPair) { sessionActual -= actualPair.upstream; billedWithActual -= actualPair.charged; actualCalls--; }
|
|
990
998
|
actualPair = final.upstream === null ? null : final;
|
|
991
999
|
if (actualPair) { sessionActual += final.upstream; billedWithActual += final.charged; actualCalls++; }
|
|
1000
|
+
applyLedger(ledger.record(finalX.settle?.transaction || finalX._responseId || receipt?.tx || requestReceiptId, final));
|
|
992
1001
|
rememberSpend();
|
|
993
1002
|
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
1003
|
};
|
|
@@ -996,7 +1005,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
|
|
|
996
1005
|
if (isPaidPost && response.ok && upCt.includes('application/json')) {
|
|
997
1006
|
let data = null;
|
|
998
1007
|
try { data = await response.clone().json(); } catch { /* not JSON after all */ }
|
|
999
|
-
meterFinal({ ...data?.x402, usage: data?.usage, _completedResponse: response.ok });
|
|
1008
|
+
meterFinal({ ...data?.x402, usage: data?.usage, _responseId: data?.id, _completedResponse: response.ok });
|
|
1000
1009
|
if (data?.object === 'chat.completion') {
|
|
1001
1010
|
if (statsOff) {
|
|
1002
1011
|
// The gateway already dropped its half; drop ours, and any block an
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function receiptLedger(file, base = {}) {
|
|
4
|
+
let state = { base, receipts: {} };
|
|
5
|
+
try { const saved=JSON.parse(fs.readFileSync(file,'utf8')); if(saved.receipts && saved.base)state=saved; } catch {}
|
|
6
|
+
const totals = () => {
|
|
7
|
+
const billing={chargedUsd:state.base.billing?.chargedUsd || 0,calls:state.base.billing?.calls || 0};
|
|
8
|
+
const comparison={spentUsd:state.base.comparison?.spentUsd || 0,directUsd:state.base.comparison?.directUsd || 0,calls:state.base.comparison?.calls || 0};
|
|
9
|
+
for(const r of Object.values(state.receipts)) {
|
|
10
|
+
billing.chargedUsd+=r.charged;billing.calls++;
|
|
11
|
+
if(r.direct!==null){comparison.spentUsd+=r.charged;comparison.directUsd+=r.direct;comparison.calls++;}
|
|
12
|
+
}
|
|
13
|
+
return {billing,comparison};
|
|
14
|
+
};
|
|
15
|
+
return { totals, record(id, receipt) {
|
|
16
|
+
if(!id || typeof receipt.charged!=='number' || !Number.isFinite(receipt.charged) || receipt.charged<0) return totals();
|
|
17
|
+
state.receipts[id]={charged:receipt.charged,direct:typeof receipt.direct==='number'&&Number.isFinite(receipt.direct)&&receipt.direct>=0?receipt.direct:null};
|
|
18
|
+
fs.mkdirSync(path.dirname(file),{recursive:true});
|
|
19
|
+
const tmp=file+'.tmp';fs.writeFileSync(tmp,JSON.stringify(state),{mode:0o600});fs.renameSync(tmp,file);
|
|
20
|
+
return totals();
|
|
21
|
+
}};
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzoo",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.13",
|
|
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",
|