openzoo 0.50.72 → 0.50.74

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/lib/botlog.js CHANGED
@@ -59,15 +59,25 @@ export function makeBotLogger({ verbose = false, write = (m) => console.error(m)
59
59
  * The block a new user needs before anything else. `balances` is optional
60
60
  * ({ USDC, TOKEN } in USD) — printed as `?` when unknown so nothing is invented.
61
61
  */
62
- export function payBannerLines({ solana, evm, balances = null, whop = 'https://whop.com/staccoverflow/openzoo', chromeMode = 'own-profile' } = {}) {
62
+ export const MINTS = {
63
+ USDC: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v',
64
+ TOKEN: 'EVULoNF4DeMBN4dGiZiDfpiiTfNZgoCvXWWgaV3epump',
65
+ LEOS: '5xgsnby6P9zqGK71J7H4yJLxzqPvNbC7rDZxNzjHmj7e',
66
+ BASE_USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
67
+ };
68
+ export function payBannerLines({ solana, evm, balances = null, whop = 'https://whop.com/staccoverflow/openzoo', chromeMode = 'own-profile', mints = MINTS } = {}) {
63
69
  const usd = (v) => (typeof v === 'number' && Number.isFinite(v) ? `$${v >= 0.01 || v === 0 ? v.toFixed(2) : v.toFixed(4)}` : '?');
70
+ const units = (v) => (typeof v === 'number' && Number.isFinite(v) ? `${v} units` : '?');
64
71
  const lines = [
65
72
  'openzoo: HOW TO PAY — no account, no API key, every call is paid from this wallet:',
66
73
  ` Solana ${solana}`,
67
- ' send USDC or TOKEN here (TOKEN is half price)',
74
+ ' send any of these here:',
75
+ ` USDC ${mints.USDC}`,
76
+ ` TOKEN ${mints.TOKEN} (half price)`,
77
+ ` LEOS ${mints.LEOS} (half price)`,
68
78
  ` Base ${evm}`,
69
- ' send USDC here',
70
- balances ? ` balance Solana USDC ${usd(balances.USDC)} · TOKEN ${balances.TOKEN_UNITS != null ? `${balances.TOKEN_UNITS} units` : '?'} · Base USDC ${usd(balances.BASE_USDC)} (recheck: openzoo balance)` : ' balance unknown right now — run: openzoo balance',
79
+ ` USDC ${mints.BASE_USDC}`,
80
+ balances ? ` balance Solana USDC ${usd(balances.USDC)} · TOKEN ${units(balances.TOKEN_UNITS)} · LEOS ${units(balances.LEOS_UNITS)} · Base USDC ${usd(balances.BASE_USDC)} (recheck: openzoo balance)` : ' balance unknown right now — run: openzoo balance',
71
81
  ` card ${whop} — paste the Solana address above when it asks`,
72
82
  ];
73
83
  if (chromeMode === 'own-profile') {
@@ -86,3 +96,38 @@ export function walletAddresses(wallet) {
86
96
  evm: privateKeyToAccount(wallet.evmPrivateKey).address,
87
97
  };
88
98
  }
99
+
100
+ /** Live balances for the banner: Solana USDC/TOKEN/LEOS + Base USDC. Never throws; unknowns are null. */
101
+ export async function quickBalances(addrs, { timeoutMs = 6000 } = {}) {
102
+ const within = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))]);
103
+ const out = { USDC: null, TOKEN_UNITS: null, LEOS_UNITS: null, BASE_USDC: null };
104
+ try {
105
+ const { Connection } = await import('@solana/web3.js');
106
+ const { config, FUNDING_ASSETS } = await import('./config.js');
107
+ const { tokenBalance } = await import('./x402.js');
108
+ const { loadOrCreateWallet } = await import('./wallet.js');
109
+ const w = loadOrCreateWallet();
110
+ const conn = new Connection(config.rpcUrl, 'confirmed');
111
+ const find = (sym) => FUNDING_ASSETS.find((a) => a.symbol === sym);
112
+ const [u, t, l] = await within(Promise.all(['USDC', 'TOKEN', 'LEOS'].map((sym) => (find(sym) ? tokenBalance(conn, w.keypair.publicKey, find(sym).mint) : { ui: 0 }))), timeoutMs);
113
+ out.USDC = Number(u?.ui ?? 0);
114
+ out.TOKEN_UNITS = t?.ui != null ? Number(t.ui) : null;
115
+ out.LEOS_UNITS = l?.ui != null ? Number(l.ui) : null;
116
+ } catch { /* solana unreachable */ }
117
+ try {
118
+ const { evmRpcFor, EVM_FUNDING_ASSETS } = await import('./config.js');
119
+ const { evmTokenBalance } = await import('./evm.js');
120
+ const b = (EVM_FUNDING_ASSETS.base || []).find((a) => a.symbol === 'USDC');
121
+ if (b) {
122
+ const r = await within(evmTokenBalance({ rpcUrl: evmRpcFor('base'), owner: addrs.evm, token: b.address }), timeoutMs);
123
+ const raw = typeof r === 'object' && r !== null ? (r.raw ?? r.ui ?? 0) : r;
124
+ out.BASE_USDC = Number(raw) / 10 ** (b.decimals ?? 6);
125
+ }
126
+ } catch { /* base unreachable */ }
127
+ return out;
128
+ }
129
+
130
+ /** The banner as a chat message: same facts, no terminal prefixes. */
131
+ export function payBannerChat(opts) {
132
+ return ['[how to pay]', ...payBannerLines(opts).map((l) => l.replace(/^openzoo: /, '').replace(/^ {9}/, ''))].join('\n');
133
+ }
@@ -1467,6 +1467,29 @@ export function shipNudgeText() {
1467
1467
  'I will create Firstmate (the one bot you talk to) and a crewmate for that repo. Then give Firstmate ship tasks: it runs a worker on a branch, a fresh review of the diff, and opens the PR only when the review is clean. You merge.',
1468
1468
  ].join('\n');
1469
1469
  }
1470
+ let payNudged = false;
1471
+ /** The terminal banner, on the canvas too: a new user reads the chat, not the shell. */
1472
+ async function seedPayNudge(list, activeId, log = () => {}) {
1473
+ if (payNudged || !activeId) return false;
1474
+ payNudged = true;
1475
+ try {
1476
+ const t = agentTranscript(activeId);
1477
+ if ((t.entries || []).some((e) => /\[how to pay\]/.test(String(e?.content || e?.message?.content || '')))) return false;
1478
+ const { payBannerChat, walletAddresses, quickBalances } = await import('./botlog.js');
1479
+ const { loadOrCreateWallet } = await import('./wallet.js');
1480
+ const addrs = walletAddresses(loadOrCreateWallet());
1481
+ const balances = await quickBalances(addrs);
1482
+ const text = payBannerChat({ ...addrs, balances, chromeMode: chromeStatus().mode });
1483
+ const nonce = `oz-pay-nudge-${activeId}`;
1484
+ const line = fanoutLine(activeId, 'assistant', text, { clientNonce: nonce, requestId: nonce });
1485
+ ssePush('transcript', { ...gatewayEntry(line), agentId: activeId });
1486
+ log(`cursor-backend: how-to-pay painted on ${activeId}`);
1487
+ return true;
1488
+ } catch (e) {
1489
+ log(`cursor-backend: how-to-pay nudge failed ${e.message}`);
1490
+ return false;
1491
+ }
1492
+ }
1470
1493
  let shipNudged = false;
1471
1494
  function seedShipNudge(list, activeId) {
1472
1495
  if (shipNudged || !activeId) return false;
@@ -1847,9 +1870,11 @@ const MODEL_ALIASES = {
1847
1870
  'glm-5.3': 'zai-org/glm-5.3-flash',
1848
1871
  'glm-5.3-flash': 'zai-org/glm-5.3-flash',
1849
1872
  flash: 'zai-org/glm-5.3-flash',
1850
- auto: 'auto',
1851
- 'openrouter/auto': 'auto',
1852
- 'openzoo/auto': 'auto',
1873
+ // NEVER OPENROUTER for Grok Bot: `auto` is the gateway router over
1874
+ // OpenRouter's catalog, so it lands on the door-only id instead.
1875
+ auto: 'grok-4.6',
1876
+ 'openrouter/auto': 'grok-4.6',
1877
+ 'openzoo/auto': 'grok-4.6',
1853
1878
  deepseek: 'deepseek/deepseek-v4-pro',
1854
1879
  'deepseek-pro': 'deepseek/deepseek-v4-pro',
1855
1880
  'deepseek-flash': 'deepseek/deepseek-v4-flash',
@@ -1863,7 +1888,7 @@ const MODEL_ALIASES = {
1863
1888
  * — the bazaar (x402 upstream) row, not OpenRouter's `x-ai/grok-4.6`. The
1864
1889
  * gateway serves the bare id off an x402 door with an on-chain cogs receipt,
1865
1890
  * so an OpenRouter credit outage cannot take it down. */
1866
- export const DEFAULT_ZOO_MODEL = 'auto';
1891
+ export const DEFAULT_ZOO_MODEL = 'grok-4.6';
1867
1892
  async function resolveModelId(raw) {
1868
1893
  const s = String(raw || '').trim();
1869
1894
  if (!s) return null;
@@ -3350,6 +3375,7 @@ async function handleHijackedPodHttp(req, res, full, body, log) {
3350
3375
  });
3351
3376
  }
3352
3377
  if (seedShipNudge(list, active)) log(`cursor-backend: ship nudge painted on ${active}`);
3378
+ if (active && !payNudged) setTimeout(() => { seedPayNudge(list, active, log).catch(() => {}); }, 4000);
3353
3379
  log(`cursor-backend: listAgents local n=${list.length} account=${activeAccountId || 'none'} active=${active || 'none'}`);
3354
3380
  return true;
3355
3381
  }
package/lib/grokcli.js CHANGED
@@ -334,35 +334,8 @@ export async function runBot(argv = []) {
334
334
  try {
335
335
  const { loadOrCreateWallet } = await import('./wallet.js');
336
336
  const addrs = walletAddresses(loadOrCreateWallet());
337
- let balances = null;
338
- try {
339
- const { Connection } = await import('@solana/web3.js');
340
- const { config, FUNDING_ASSETS } = await import('./config.js');
341
- const { tokenBalance } = await import('./x402.js');
342
- const { evmTokenBalance } = await import('./evm.js');
343
- const { loadOrCreateWallet: lw } = await import('./wallet.js');
344
- const w = lw();
345
- const conn = new Connection(config.rpcUrl, 'confirmed');
346
- const within = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms))]);
347
- const usdc = FUNDING_ASSETS.find((a) => a.symbol === 'USDC');
348
- const tok = FUNDING_ASSETS.find((a) => a.symbol === 'TOKEN');
349
- const [u, t] = await within(Promise.all([
350
- usdc ? tokenBalance(conn, w.keypair.publicKey, usdc.mint) : { ui: 0 },
351
- tok ? tokenBalance(conn, w.keypair.publicKey, tok.mint) : { ui: 0 },
352
- ]), 6000);
353
- let baseUsdc = null;
354
- try {
355
- const { evmRpcFor, EVM_FUNDING_ASSETS } = await import('./config.js');
356
- const b = (EVM_FUNDING_ASSETS.base || []).find((a) => a.symbol === 'USDC');
357
- if (b) {
358
- const r = await within(evmTokenBalance({ rpcUrl: evmRpcFor('base'), owner: addrs.evm, token: b.address }), 6000);
359
- const raw = typeof r === 'object' && r !== null ? (r.raw ?? r.ui ?? 0) : r;
360
- baseUsdc = Number(raw) / 10 ** (b.decimals ?? 6); // raw units → dollars
361
- }
362
- } catch { /* base unreachable: print ? */ }
363
- // TOKEN is priced at the 402, not here: show units, never an invented $.
364
- balances = { USDC: Number(u?.ui ?? 0), TOKEN_UNITS: t?.ui != null ? Number(t.ui) : null, BASE_USDC: baseUsdc };
365
- } catch { balances = null; }
337
+ const { quickBalances } = await import('./botlog.js');
338
+ const balances = await quickBalances(addrs);
366
339
  let chromeMode = 'own-profile';
367
340
  try { const { chromeStatus } = await import('./mcpbridge.js'); chromeMode = chromeStatus().mode; } catch { /* */ }
368
341
  for (const l of payBannerLines({ ...addrs, balances, chromeMode })) console.error(l);
package/lib/proxy.js CHANGED
@@ -640,7 +640,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
640
640
  return;
641
641
  }
642
642
  }
643
- const init = { method: req.method, headers: upstreamHeaders(req) };
643
+ let init = { method: req.method, headers: upstreamHeaders(req) };
644
644
  if (req.method !== 'GET' && req.method !== 'HEAD') init.body = bodyBuf;
645
645
 
646
646
  // Harnesses validate their configured model BEFORE ever POSTing — some
@@ -698,15 +698,45 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
698
698
  }
699
699
 
700
700
  try {
701
+ // OUTAGE FALLBACK. OpenRouter dry does not have to mean dead bots: the
702
+ // x402 door for bare grok-4.6 settles cogs on chain and never touches
703
+ // OpenRouter. When a model is gated (or comes back "Insufficient
704
+ // credits" below), the same request goes out once more on the fallback.
705
+ const FALLBACK_MODEL = String(process.env.OPENZOO_OUTAGE_FALLBACK || 'grok-4.6');
706
+ const withModel = (i, model) => {
707
+ try { const b = JSON.parse(String(i.body || '{}')); b.model = model; return { ...i, body: JSON.stringify(b) }; } catch { return i; }
708
+ };
709
+ let usedFallback = false;
701
710
  {
702
711
  const gate = upstreamOutage_.get(outageKey(init));
703
712
  if (gate && Date.now() < gate.until) {
704
- log(`upstream outage: not paying (gate) model=${outageKey(init)}`);
705
- jsonErr(res, 503, gate.msg);
706
- return;
713
+ if (FALLBACK_MODEL && outageKey(init) !== FALLBACK_MODEL) {
714
+ log(`upstream outage: ${outageKey(init)} gated -> ${FALLBACK_MODEL}`);
715
+ init = withModel(init, FALLBACK_MODEL);
716
+ usedFallback = true;
717
+ } else {
718
+ log(`upstream outage: not paying (gate) model=${outageKey(init)}`);
719
+ jsonErr(res, 503, gate.msg);
720
+ return;
721
+ }
722
+ }
723
+ }
724
+ let result = await client.fetch(url, init);
725
+ // Paid, then the gateway said its upstream is out of credits: gate this
726
+ // model for 60s and buy the same completion from the door instead.
727
+ if (result.paid && result.response?.status === 402 && !usedFallback && FALLBACK_MODEL && outageKey(init) !== FALLBACK_MODEL) {
728
+ let q402 = null;
729
+ try { q402 = await result.response.clone().json(); } catch { q402 = null; }
730
+ if (upstreamOutage(q402)) {
731
+ const tx = q402?.x402?.settle?.transaction || result.receipt?.tx || '';
732
+ const msg = `openzoo gateway upstream is out of credits (OpenRouter: "Insufficient credits") for ${outageKey(init)}; that payment${tx ? ` (tx ${tx})` : ''} is credited back by the gateway. Routing to ${FALLBACK_MODEL} (x402 door) for 60s.`;
733
+ upstreamOutage_.set(outageKey(init), { until: Date.now() + 60_000, msg });
734
+ log(`upstream outage: ${outageKey(init)} -> ${FALLBACK_MODEL}`);
735
+ init = withModel(init, FALLBACK_MODEL);
736
+ usedFallback = true;
737
+ result = await client.fetch(url, init);
707
738
  }
708
739
  }
709
- const result = await client.fetch(url, init);
710
740
  const { response, paid, receipt, accept } = result;
711
741
  if (paid && receipt) {
712
742
  if (receipt.ok && typeof receipt.billedUsd === 'number') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.72",
3
+ "version": "0.50.74",
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",