openzoo 0.44.1 → 0.45.1

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/bin/openzoo.js CHANGED
@@ -192,6 +192,17 @@ async function main() {
192
192
  case 'balance':
193
193
  await (await import('../lib/info.js')).printBalance();
194
194
  break;
195
+ case 'topup':
196
+ case 'prepay':
197
+ await (await import('../lib/info.js')).topUp(process.argv[3]);
198
+ break;
199
+ case 'credit':
200
+ case 'credits': {
201
+ const bal = await (await import('../lib/info.js')).creditBalance();
202
+ console.log(`prepaid credit: $${bal.toFixed(6)}`);
203
+ if (bal <= 0) console.log('buy some with: npx openzoo topup 10');
204
+ break;
205
+ }
195
206
  case 'address':
196
207
  (await import('../lib/info.js')).printAddress();
197
208
  break;
package/lib/info.js CHANGED
@@ -73,3 +73,57 @@ export async function printBalance() {
73
73
  console.log(` or USDC on Base to ${evmAddress}`);
74
74
  }
75
75
  }
76
+
77
+ /**
78
+ * PREPAY. Buys gateway credit in ONE settlement so later calls skip the
79
+ * per-call payment round trip entirely.
80
+ *
81
+ * That round trip is where the latency lives, not the model: MEASURED against
82
+ * the live gateway, a 402 challenge comes back in 0.12s while a full paid call
83
+ * takes 9-37s end to end. The gateway already applies credit automatically
84
+ * whenever a balance covers the quote — nothing could BUY it until now.
85
+ *
86
+ * Credit is keyed by the signed namespace, so it belongs to this wallet and
87
+ * cannot be spent by anyone else.
88
+ */
89
+ export async function topUp(usdArg) {
90
+ const usd = Number(usdArg);
91
+ if (!Number.isFinite(usd) || usd < 1 || usd > 500) {
92
+ throw new Error('usage: openzoo topup <usd> (1-500)');
93
+ }
94
+ const { PayClient } = await import('./pay.js');
95
+ const client = new PayClient();
96
+ const url = `${config.apiBase}/v1/credits/topup`;
97
+
98
+ const before = await creditBalance();
99
+ console.log(`credit before: $${before.toFixed(6)}`);
100
+ console.log(`buying $${usd.toFixed(2)} of credit — one on-chain settlement...`);
101
+
102
+ const { response, paid } = await client.fetch(url, {
103
+ method: 'POST',
104
+ headers: { 'content-type': 'application/json' },
105
+ body: JSON.stringify({ usd }),
106
+ });
107
+ const body = await response.json().catch(() => ({}));
108
+ if (!response.ok) {
109
+ throw new Error(`topup failed (HTTP ${response.status}): ${body.error || body.detail || 'unknown'}`);
110
+ }
111
+ console.log('');
112
+ console.log(`credited: $${Number(body.creditedUsd ?? usd).toFixed(2)}${paid ? '' : ' (from existing credit)'}`);
113
+ console.log(`balance: $${Number(body.balanceUsd ?? 0).toFixed(6)}`);
114
+ if (body.tx) console.log(`tx: ${body.tx}`);
115
+ console.log('');
116
+ console.log('calls now settle against this balance instead of paying on-chain each time.');
117
+ }
118
+
119
+ /** Current prepaid credit for this wallet's namespace. */
120
+ export async function creditBalance() {
121
+ const { withNamespace } = await import('./namespace.js');
122
+ try {
123
+ const r = await fetch(`${config.apiBase}/v1/credits`, { headers: withNamespace({}) });
124
+ const j = await r.json();
125
+ return Number(j.balanceUsd) || 0;
126
+ } catch {
127
+ return 0;
128
+ }
129
+ }
package/lib/proxy.js CHANGED
@@ -722,6 +722,22 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
722
722
  if (isChat && response.ok && upCt.includes('application/json')) {
723
723
  let data = null;
724
724
  try { data = await response.clone().json(); } catch { /* not JSON after all */ }
725
+ // PREPAID CALLS STILL COST MONEY. The block above only meters calls
726
+ // where THIS proxy answered a 402 and paid. When prepaid credit covers
727
+ // the quote the gateway serves 200 on the FIRST request, so there is
728
+ // no 402, no payment and no receipt — and the session read $0.05 / 2
729
+ // calls while the credit balance had actually fallen $3.017 -> $1.395
730
+ // over a 30-question run. The receipt still rides the response body,
731
+ // so meter it from there.
732
+ if (!paid && data?.x402 && typeof data.x402.billedUsd === 'number') {
733
+ const x = data.x402;
734
+ sessionSpent += x.billedUsd;
735
+ sessionCogs += typeof x.cogsUsd === 'number' ? x.cogsUsd : x.billedUsd / MARKUP;
736
+ sessionDirect += typeof x.directUsd === 'number' ? x.directUsd : x.billedUsd;
737
+ paidCalls += 1;
738
+ if (viaTunnel) tunnelSpent += x.billedUsd;
739
+ say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
740
+ }
725
741
  if (data?.object === 'chat.completion') {
726
742
  if (rKey) replayPut(rKey, data, response.headers.get('x-payment-response'));
727
743
  // Anthropic-shaped caller gets an Anthropic-shaped answer, streamed
@@ -779,6 +795,34 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
779
795
  server.listen(config.port, bindHost, resolve);
780
796
  });
781
797
 
798
+ // AUTO-PREPAY. Paying on-chain per call is where the latency lives: the
799
+ // gateway answers its 402 challenge in ~0.12s while a full settled call
800
+ // MEASURED 9-37s end to end. Credit is applied automatically server-side
801
+ // whenever a balance covers the quote, so buying it once makes every later
802
+ // call skip verify+settle entirely.
803
+ //
804
+ // Runs in the background — never block the listener on a payment — and only
805
+ // when this wallet actually has funds, so a fresh/empty wallet is untouched.
806
+ // Opt out with OPENZOO_NO_AUTOTOPUP=1; size it with OPENZOO_AUTOTOPUP_USD.
807
+ if (!process.env.OPENZOO_NO_AUTOTOPUP) {
808
+ (async () => {
809
+ try {
810
+ const { creditBalance, topUp } = await import('./info.js');
811
+ const have = await creditBalance();
812
+ const want = Number(process.env.OPENZOO_AUTOTOPUP_USD || 5);
813
+ // Only top up when nearly dry, so restarting the proxy does not keep
814
+ // buying credit on top of a healthy balance.
815
+ if (have >= Math.min(1, want)) return;
816
+ say(`prepaid credit $${have.toFixed(4)} — topping up $${want} so calls stop settling on-chain each time...`);
817
+ await topUp(want);
818
+ } catch (e) {
819
+ // A wallet with no funds, or a gateway that refuses, must never stop
820
+ // the proxy from serving: calls just fall back to paying per call.
821
+ say(`auto top-up skipped: ${String(e.message || e).slice(0, 120)}`);
822
+ }
823
+ })();
824
+ }
825
+
782
826
  if (!silent) {
783
827
  // VERSION IN THE BANNER, deliberately. `npx openzoo` can serve a STALE
784
828
  // cached copy — npx reuses a cache entry that matches the bare spec, so a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.44.1",
3
+ "version": "0.45.1",
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",