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 +25 -9
- package/lib/proxy.js +19 -0
- package/lib/xbot.js +18 -3
- package/package.json +1 -1
package/lib/pay.js
CHANGED
|
@@ -125,17 +125,28 @@ export function resetRailMemory() {
|
|
|
125
125
|
|
|
126
126
|
export class PayClient {
|
|
127
127
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
this.
|
|
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
|
-
|
|
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.
|
|
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",
|