openzoo 0.50.9 → 0.50.11

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/pay.js CHANGED
@@ -125,17 +125,28 @@ export function resetRailMemory() {
125
125
 
126
126
  export class PayClient {
127
127
  /**
128
- * Always this machine's ~/.openzoo/wallet.json. The constructor used to take
129
- * a per-user derived burner so the X bot could make each asker pay their own
130
- * way; that bot is gone and nothing else ever passed one, so the override was
131
- * a second wallet path with no caller and no test.
128
+ * This machine's ~/.openzoo/wallet.json, OR a derived burner passed in.
129
+ *
130
+ * The override was removed once with the note "that bot is gone and nothing
131
+ * else ever passed one" THE BOT WAS NOT GONE. xbot.js kept calling
132
+ * `new PayClient(burner)`, the argument was silently swallowed, and every
133
+ * "asker pays" answer settled from the OPERATOR's shared wallet instead:
134
+ * measured live 2026-08-27 as gateway `paid_200 payer:0x640944…` (the
135
+ * machine EVM key) for calls the log attributed to a per-asker burner. The
136
+ * paid lane's entire economics ran backwards and nothing errored.
137
+ *
138
+ * A burner carries BOTH keys (Solana + EVM from one HMAC), so an override
139
+ * must never fall back to the machine wallet for either rail — an
140
+ * underfunded burner is the FUNDING REPLY path, not "bill the operator".
132
141
  */
133
- constructor() {
134
- const w = loadOrCreateWallet();
142
+ constructor(wallet) {
143
+ const w = wallet?.keypair ? wallet : loadOrCreateWallet();
135
144
  this.keypair = w.keypair;
136
- this.evmPrivateKey = w.evmPrivateKey;
137
- this.walletCreated = w.created;
138
- this.walletPath = w.path;
145
+ // ?? null, NOT || machine key: a burner without an EVM key must fail
146
+ // underfunded on EVM rails rather than quietly spending the operator's.
147
+ this.evmPrivateKey = w.evmPrivateKey ?? null;
148
+ this.walletCreated = w.created ?? false;
149
+ this.walletPath = w.path ?? (w.xUserId ? `derived burner for X user ${w.xUserId}` : 'passed-in wallet');
139
150
  this.connection = new Connection(config.rpcUrl, 'confirmed');
140
151
  this.receipts = []; // last paid calls, newest last
141
152
  // EVERY OFFERED RAIL IS A FALLBACK, INCLUDING ROBINHOOD.
@@ -335,6 +346,11 @@ export class PayClient {
335
346
  const receipt = {
336
347
  at: new Date().toISOString(),
337
348
  line: receiptLine(accept, settle),
349
+ // The settle SIGNATURE as its own field, not only baked into `line`.
350
+ // The xbot printed a burner ADDRESS prefix in its PAID log and the
351
+ // operator spent an evening searching Solscan for it as a tx hash —
352
+ // an id a machine can log must be one a human can look up.
353
+ tx: settle?.transaction || settle?.txHash || settle?.signature || null,
338
354
  rail: railOf(accept),
339
355
  billedUsd: accept.extra?.billedUsd,
340
356
  directUsd: accept.extra?.directUsd,
package/lib/proxy.js CHANGED
@@ -752,6 +752,25 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
752
752
  rememberSpend();
753
753
  say(`credit -> $${x.billedUsd.toFixed(6)} · session $${sessionSpent.toFixed(6)}`);
754
754
  };
755
+ // A 402 AFTER we attempted payment is a SETTLEMENT failure, not a quote —
756
+ // the client-side balance check is advisory, so a wallet that looks
757
+ // fundable can still fail on-chain, and the gateway answers the paid
758
+ // retry with a fresh 402. Relaying that raw meant Claude Code printed a
759
+ // half-kilobyte accepts[] blob at the user instead of the one thing they
760
+ // need: the price, what the wallet holds, and where to send funds.
761
+ if (response.status === 402) {
762
+ let quoted = '';
763
+ try {
764
+ const q = await response.clone().json();
765
+ const usd = Number(q?.accepts?.[0]?.extra?.billedUsd);
766
+ if (Number.isFinite(usd)) quoted = ` This call needs ≈$${usd.toFixed(4)}.`;
767
+ } catch { /* body was not the quote after all */ }
768
+ jsonErr(res, 402,
769
+ `openzoo wallet underfunded — payment did not settle.${quoted} `
770
+ + `Fund it and retry: send USDC (or TOKEN/LEOS for half price) to ${client.address} on Solana, `
771
+ + `or USDC to ${client.evmAddress} on Base. Check with: openzoo balance`);
772
+ return;
773
+ }
755
774
  await relay(res, response, meterStreamed);
756
775
  } catch (err) {
757
776
  if (err instanceof QuoteTooHighError) {
package/lib/xbot.js CHANGED
@@ -1258,7 +1258,12 @@ export async function askZooPaid(question, { burner, thread = '', maxTokens = AN
1258
1258
  // Same shared context as the free lane — a paid asker should recall
1259
1259
  // everything the bot has read, not start from an empty corpus.
1260
1260
  call: async (body) => (await pay.chat(body, { headers: contextId ? { 'x-hrr-context': contextId } : {} })).data,
1261
- });
1261
+ }).then((result) => ({
1262
+ ...result,
1263
+ // Every on-chain settle signature this answer paid with, so the PAID log
1264
+ // can print something Solscan actually finds.
1265
+ txSigs: pay.receipts.map((r) => r.tx).filter(Boolean),
1266
+ }));
1262
1267
  }
1263
1268
 
1264
1269
  /**
@@ -2604,9 +2609,19 @@ export async function runXBot({ once = false, intervalMs = POLL_MS, dryRun = fal
2604
2609
  // Print the SAME comparison the free lane does. This showed billed
2605
2610
  // alone, so a paid answer gave no way to see whether the asker beat
2606
2611
  // buying direct — the one thing the receipt exists to demonstrate.
2607
- console.error(` ${t.id} @${t.author_id}: PAID ${burner.address.slice(0, 8)}… ${result.routedModel} ${usd(result.billedUsd)}`
2612
+ // The tx SIGNATURE, or an explicit REPLAYED marker — never a bare
2613
+ // "PAID" that might not have settled. The 8-char burner prefix here
2614
+ // was read as a tx hash and hunted on Solscan for an evening; and a
2615
+ // restart re-asking answered questions got cached 200s whose echoed
2616
+ // receipts printed as fresh payments. Both lies die here: label the
2617
+ // wallet as a wallet, print the settle sig when one exists, and say
2618
+ // REPLAYED when none does.
2619
+ const sigs = Array.isArray(result.txSigs) ? result.txSigs : [];
2620
+ console.error(` ${t.id} @${t.author_id}: ${sigs.length ? 'PAID' : 'REPLAYED (no new settlement)'}`
2621
+ + ` payer ${burner.address.slice(0, 8)}… ${result.routedModel} ${usd(result.billedUsd)}`
2608
2622
  + (result.directUsd > 0 ? ` (direct ${usd(result.directUsd)})` : '')
2609
- + (result.actualUsd > 0 ? ` [cost ${usd(result.actualUsd)}]` : ''));
2623
+ + (result.actualUsd > 0 ? ` [cost ${usd(result.actualUsd)}]` : '')
2624
+ + (sigs.length ? `\n tx ${sigs[sigs.length - 1]}` : ''));
2610
2625
  await postAndLog({ creds, text, inReplyTo: t.id, state, dryRun, tag: 'paid', conversationId: t.conversation_id });
2611
2626
  state.answered[t.id] = 'paid';
2612
2627
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.50.9",
3
+ "version": "0.50.11",
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",