openzoo 0.50.10 → 0.50.12

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
@@ -280,9 +280,21 @@ async function main() {
280
280
  }
281
281
  case 'ask': {
282
282
  const question = process.argv[3];
283
- if (!question) throw new Error('usage: openzoo ask "<question>" [--context <id>] [--model <id>]');
283
+ if (!question) throw new Error('usage: openzoo ask "<question>" [--context <id>] [--model <id>] [--system <text>]');
284
284
  const ci = process.argv.indexOf('--context');
285
285
  const mi = process.argv.indexOf('--model');
286
+ // A BARE QUESTION IS A DIFFERENT PRODUCT FROM A BRIEFED ONE.
287
+ //
288
+ // This path drives PayClient straight at the gateway, so it never passes
289
+ // through the local proxy that injects lib/brief.js — the model gets the
290
+ // user's words and nothing else. OBSERVED from the Omarchy bar widget:
291
+ // "do you even love omarchy thru this uiux?!?" came back "I think you
292
+ // mean *anarchy*?" from deepseek, while the same question in the terminal
293
+ // (Claude Code: full system prompt + the omarchy skill) answered about
294
+ // DHH, Hyprland and theming. Same gateway, same product, one had context.
295
+ // A caller that knows where it is running can now say so.
296
+ const si = process.argv.indexOf('--system');
297
+ const system = si !== -1 ? process.argv[si + 1] : '';
286
298
  const { PayClient } = await import('../lib/pay.js');
287
299
  const { config } = await import('../lib/config.js');
288
300
  const client = new PayClient();
@@ -293,7 +305,9 @@ async function main() {
293
305
  headers,
294
306
  body: JSON.stringify({
295
307
  model: (mi !== -1 && process.argv[mi + 1]) || process.env.OPENZOO_DEFAULT_MODEL || 'anthropic/claude-opus-5',
296
- messages: [{ role: 'user', content: question }],
308
+ messages: system
309
+ ? [{ role: 'system', content: system }, { role: 'user', content: question }]
310
+ : [{ role: 'user', content: question }],
297
311
  max_tokens: Number(process.env.OPENZOO_ASK_MAX_TOKENS || 1024),
298
312
  }),
299
313
  });
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/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.10",
3
+ "version": "0.50.12",
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",