openzoo 0.51.18 → 0.51.19

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/README.md CHANGED
@@ -230,6 +230,14 @@ Pin the key with `OPENZOO_TUNNEL_TOKEN` if your IDE stores it. Keys never leave
230
230
 
231
231
  **MCP clients don't need any of this.** If your tool speaks MCP, use the hosted server at `https://mcp.openzoo.fun/mcp` — cloud-reachable, no local process, mint a wallet with `zoo_wallet`.
232
232
 
233
+ ## Memory authentication
234
+
235
+ Every memory request uses a wallet-signed namespace. Signing errors stop the request; unsigned requests and the shared default bucket are rejected by the gateway. Voice and Sonar now use that same gateway, including scored recall. Re-ingest contexts previously stored in a local daemon or the old shared bucket.
236
+
237
+ The gateway signs each storage request over its exact payload, route, timestamp, tenant and context. The backing store verifies that proof and retains signed receipts with saved contexts or the tenant's memory notes. Signing authenticates access and integrity; it does not encrypt uploaded text. Never include private keys in an uploaded corpus.
238
+
239
+ Public proxy tunnels require the configured token for memory and inference. Arbitrary first-use tokens and anonymous binds are no longer accepted. Quote discovery uses an unfunded signing identity so it cannot consume the operator's credit.
240
+
233
241
  ## The wallet model
234
242
 
235
243
  - **Burner, local, yours.** A keypair in `~/.openzoo/wallet.json`, created on first run, chmod 600. Keys never leave your machine — the zoo only ever sees signed transfers.
package/lib/config.js CHANGED
@@ -207,9 +207,10 @@ export function unfundableRails(liveRailNames) {
207
207
  * currently offering are reported separately so nobody funds a dead lane.
208
208
  */
209
209
  export async function liveRails() {
210
+ const { withQuoteNamespace } = await import('./namespace.js');
210
211
  const r = await fetchHeaders(`${config.apiBase}/v1/chat/completions`, {
211
212
  method: 'POST',
212
- headers: { 'content-type': 'application/json' },
213
+ headers: withQuoteNamespace({ 'content-type': 'application/json' }),
213
214
  body: JSON.stringify({ model: 'nvidia/nemotron-3.5-lightning', messages: [{ role: 'user', content: 'ping' }], max_tokens: 1 }),
214
215
  });
215
216
  if (r.status !== 402) { await r.text().catch(() => {}); return null; }
package/lib/demo.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { withNamespace } from './namespace.js';
1
2
  import { config, fundingLine } from './config.js';
2
3
  import { PayClient, UnderfundedError } from './pay.js';
3
4
  import { parse402, pickAccept, tokenBalance } from './x402.js';
@@ -38,7 +39,7 @@ async function quoteFor(bodyObj, headers = {}) {
38
39
  const spin = startSpinner(big ? `uploading ${mb}MB body` : 'asking the zoo for a live quote');
39
40
  try {
40
41
  const r = await postWithUploadSignal(`${config.apiBase}/v1/chat/completions`, body, {
41
- headers,
42
+ headers: withNamespace(headers),
42
43
  onUploaded: () => { if (big) spin.update(`zoo pricing ~${approxTokens}k tokens`); },
43
44
  });
44
45
  if (r.status !== 402) throw new Error(`expected 402 quote, got HTTP ${r.status}: ${(await r.text()).slice(0, 300)}`);
package/lib/info.js CHANGED
@@ -62,10 +62,11 @@ export async function quotedPrices() {
62
62
  // that number.
63
63
  //
64
64
  // The chat challenge prices each asset at its real spot (DexScreener), and
65
- // needs no namespace signature, so it is both correct and simpler.
65
+ // uses an unfunded signing identity so discovery cannot spend wallet credit.
66
+ const { withQuoteNamespace } = await import('./namespace.js');
66
67
  const r = await fetchHeaders(`${config.apiBase}/v1/chat/completions`, {
67
68
  method: 'POST',
68
- headers: { 'content-type': 'application/json' },
69
+ headers: withQuoteNamespace({ 'content-type': 'application/json' }),
69
70
  body: JSON.stringify({ model: config.defaultModel || 'anthropic/claude-sonnet-5', max_tokens: 1, messages: [{ role: 'user', content: 'x' }] }),
70
71
  });
71
72
  if (r.status !== 402) return out;
package/lib/namespace.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import crypto from 'node:crypto';
2
+ import { Keypair } from '@solana/web3.js';
2
3
  import { loadOrCreateWallet } from './wallet.js';
3
4
 
4
5
  /**
@@ -59,22 +60,16 @@ import { loadOrCreateWallet } from './wallet.js';
59
60
  * requests across time. A value every caller sends identically discloses
60
61
  * nothing at all.
61
62
  *
62
- * BREAKING: corpora bound under the old per-wallet namespace live in a
63
- * different tenant and will not be found. The gateway's tenantsToTry fallback
64
- * reaches pre-existing context ids by id, but anything relying on the old
65
- * namespace should be re-bound.
63
+ * BREAKING: old namespaces stay isolated. Re-bind their corpora under the
64
+ * authenticated namespace; there is no shared-memory fallback.
66
65
  */
67
66
  export const STACC_NAMESPACE = 'stacc';
68
67
 
69
68
  export function namespaceHeaderValue(wallet) {
70
- try {
71
- // Still require a wallet: the namespace is meaningless without a signer to
72
- // prove it, and sending one unsigned drops you into the SHARED tenant.
73
- if (!wallet?.keypair) loadOrCreateWallet();
74
- return STACC_NAMESPACE;
75
- } catch {
76
- return ''; // no wallet (read-only use): fall back to the shared tenant
77
- }
69
+ // A failed signer must fail the operation; never send an unsigned request.
70
+ if (wallet !== undefined && !wallet?.keypair) throw new Error('The supplied wallet cannot sign.');
71
+ if (!wallet?.keypair) loadOrCreateWallet();
72
+ return STACC_NAMESPACE;
78
73
  }
79
74
 
80
75
  /**
@@ -121,11 +116,14 @@ function signNamespace(namespace, wallet) {
121
116
  /** Merge the namespace header — and its proof — into any headers object. */
122
117
  export function withNamespace(headers = {}, wallet) {
123
118
  const ns = namespaceHeaderValue(wallet);
124
- if (!ns) return headers;
125
- // No unsigned fallback. The gateway REQUIRES the signature, so a bare
126
- // namespace buys nothing — it would just be silently demoted to the shared
127
- // tenant, which looks like "my corpus vanished" instead of a clear failure.
128
- return { ...headers, 'x-openzoo-namespace': ns, ...signNamespace(ns, wallet) };
119
+ if (!ns) throw new Error('A wallet-signed namespace is required; shared memory is disabled.');
120
+ // The gateway rejects an unsigned namespace. Signing failure must stop
121
+ // the operation locally instead of sending an unauthenticated request.
122
+ const clean = Object.fromEntries(new Headers(headers).entries());
123
+ for (const name of Object.keys(clean)) {
124
+ if (name === 'x-openzoo-session' || name.startsWith('x-openzoo-namespace')) delete clean[name];
125
+ }
126
+ return { ...clean, 'x-openzoo-namespace': ns, ...signNamespace(ns, wallet) };
129
127
  }
130
128
 
131
129
  // Bitcoin-alphabet base58, matching what the gateway's bs58 decode expects.
@@ -147,3 +145,8 @@ function base58encode(bytes) {
147
145
  for (let i = digits.length - 1; i >= 0; i--) out += B58[digits[i]];
148
146
  return out;
149
147
  }
148
+
149
+ /** Price discovery must never consume the operator's existing gateway credit. */
150
+ export function withQuoteNamespace(headers = {}) {
151
+ return withNamespace(headers, { keypair: Keypair.generate() });
152
+ }
package/lib/pay.js CHANGED
@@ -426,7 +426,7 @@ export class PayClient {
426
426
  onStage?.('paying');
427
427
  let response = await fetchHeaders(url, {
428
428
  ...init,
429
- headers: { ...stripAuthorization(init.headers || {}), ...paymentHeaders(payment.header) },
429
+ headers: withNamespace({ ...stripAuthorization(init.headers || {}), ...paymentHeaders(payment.header) }, { keypair: this.keypair }),
430
430
  });
431
431
 
432
432
  // A REFUSED ATOMIC ROW MUST NOT TAKE THE WHOLE CALL DOWN.
@@ -450,7 +450,7 @@ export class PayClient {
450
450
  const retry = await this.buildPaymentFor(cand, onStage, quote);
451
451
  const again = await fetchHeaders(url, {
452
452
  ...init,
453
- headers: { ...stripAuthorization(init.headers || {}), ...paymentHeaders(retry.header) },
453
+ headers: withNamespace({ ...stripAuthorization(init.headers || {}), ...paymentHeaders(retry.header) }, { keypair: this.keypair }),
454
454
  });
455
455
  if (again.status !== 402) {
456
456
  accept = cand; payment = retry; response = again;
package/lib/proxy.js CHANGED
@@ -640,7 +640,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
640
640
  // Requests that arrived over the public quick-tunnel URL carry cloudflared's
641
641
  // headers; nothing dialing 127.0.0.1 directly does. That distinction is what
642
642
  // lets localhost stay keyless while the SAME port is safely public.
643
- const viaTunnel = !requireToken && tunnelGate
643
+ const viaTunnel = !requireToken
644
644
  && Boolean(req.headers['cf-connecting-ip'] || req.headers['cf-ray']);
645
645
  // Auth first: refuse before reading a body, forwarding, quoting or paying.
646
646
  if (requireToken) {
@@ -709,7 +709,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
709
709
  upstream: config.apiBase,
710
710
  payment: 'x402 per request from the operator\'s local burner wallet - no API key, no account',
711
711
  auth: viaTunnel
712
- ? 'this public URL requires the oz_… bearer for paid endpoints; /v1/models and /v1/hrr/bind are free'
712
+ ? 'this public URL requires the configured oz_… bearer for memory and inference; /v1/models is public'
713
713
  : 'localhost is keyless',
714
714
  shim: 'pure passthrough — request bodies are forwarded byte-for-byte; all message/model handling is on the backend',
715
715
  tools: ['zoo_bind', 'zoo_ask', 'zoo_status', 'zoo_models', 'zoo_wallet', 'zoo_contexts'],
@@ -721,21 +721,8 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
721
721
 
722
722
  if (viaTunnel) {
723
723
  const got = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
724
- // TRUST ON FIRST USE, so a stale key still works without weakening the
725
- // tunnel to "any key forever". The printed token always works, and the
726
- // FIRST other key to present itself claims the tunnel for the session.
727
- // OPENZOO_TUNNEL_STRICT=1 restores exact-match only.
728
- const strict = process.env.OPENZOO_TUNNEL_STRICT === '1';
729
- let authed = got === tunnelGate.token;
730
- if (!authed && !strict && got.length >= 8) {
731
- if (!tunnelGate.adopted) {
732
- tunnelGate.adopted = got;
733
- log(`public url: adopted the caller's key (first-use) — later keys must match it`);
734
- authed = true;
735
- } else {
736
- authed = got === tunnelGate.adopted;
737
- }
738
- }
724
+ // Only the configured token authorizes signing with the operator's wallet.
725
+ const authed = Boolean(tunnelGate?.token) && got === tunnelGate.token;
739
726
  const p = (req.url || '').split('?')[0];
740
727
  // DISCOVERY IS FREE. Reads that cost nothing and leak nothing go through
741
728
  // without the key; money paths stay gated.
@@ -759,9 +746,7 @@ export async function startProxy({ silent = false, requireToken = null, sessionM
759
746
  // paths below stay gated; the worst a stranger can do here is spend
760
747
  // the sidecar's disk.
761
748
  const freeRead = req.method === 'GET' && (p === '/v1/models' || p.startsWith('/v1/models/'));
762
- const freeBind = req.method === 'POST' && p === '/v1/hrr/bind';
763
- if (freeBind) log(`public url: unauthenticated bind allowed (free endpoint) from ${req.socket.remoteAddress}`);
764
- if (!freeRead && !freeBind) {
749
+ if (!freeRead) {
765
750
  log(`public url: 401 ${req.method} ${req.url}`);
766
751
  jsonErr(res, 401, 'unauthorized: this openzoo public URL requires the api key printed at the operator\'s proxy startup — GET / for discovery, GET /v1/models is open');
767
752
  return;
package/lib/sonar.js CHANGED
@@ -38,6 +38,7 @@ import crypto from 'node:crypto';
38
38
  import { PublicKey } from '@solana/web3.js';
39
39
  import { loadDotenv } from './dotenv.js';
40
40
  import { config } from './config.js';
41
+ import { withNamespace } from './namespace.js';
41
42
 
42
43
  // Before config.js is consulted, so OPENZOO_RPC from .env wins over the
43
44
  // public default without anyone pasting a credentialed URL onto a
@@ -1588,9 +1589,6 @@ export function vocabularyFor(combo) {
1588
1589
  * Every recalled name is still verified by hashing, so a bad recall costs
1589
1590
  * a few microseconds and never a wrong answer.
1590
1591
  */
1591
- const LECORE = process.env.OPENZOO_LECORE_URL || 'http://127.0.0.1:8787';
1592
- const LECORE_TOKEN = process.env.OPENZOO_LECORE_TOKEN || 'hrr-lab-token';
1593
- const LECORE_TENANT = process.env.OPENZOO_LECORE_TENANT || 'claude-code';
1594
1592
 
1595
1593
  export async function bindCorpus({ log = () => {}, sample = 0 } = {}) {
1596
1594
  let comps = { archetypes: [] };
@@ -1616,11 +1614,10 @@ export async function bindCorpus({ log = () => {}, sample = 0 } = {}) {
1616
1614
  let contextId = null;
1617
1615
  const BATCH = 500;
1618
1616
  for (let i = 0; i < items.length; i += BATCH) {
1619
- const res = await fetch(`${LECORE}/internal/v1/hrr/bind`, {
1617
+ const res = await fetch(`${config.apiBase}/v1/hrr/bind`, {
1620
1618
  method: 'POST',
1621
- headers: { 'content-type': 'application/json', authorization: `Bearer ${LECORE_TOKEN}` },
1619
+ headers: withNamespace({ 'content-type': 'application/json' }),
1622
1620
  body: JSON.stringify({
1623
- tenant_id: LECORE_TENANT,
1624
1621
  items: items.slice(i, i + BATCH),
1625
1622
  ...(contextId ? { context_id: contextId } : {}),
1626
1623
  }),
@@ -1643,10 +1640,10 @@ export async function recallVocabulary(query, { topK = 24 } = {}) {
1643
1640
  const ctx = lecoreContext();
1644
1641
  if (!ctx) return [];
1645
1642
  try {
1646
- const res = await fetch(`${LECORE}/internal/v1/hrr/recall`, {
1643
+ const res = await fetch(`${config.apiBase}/v1/hrr/recall`, {
1647
1644
  method: 'POST',
1648
- headers: { 'content-type': 'application/json', authorization: `Bearer ${LECORE_TOKEN}` },
1649
- body: JSON.stringify({ tenant_id: LECORE_TENANT, context_id: ctx, query, top_k: topK }),
1645
+ headers: withNamespace({ 'content-type': 'application/json' }),
1646
+ body: JSON.stringify({ context_id: ctx, query, top_k: topK }),
1650
1647
  });
1651
1648
  if (!res.ok) return [];
1652
1649
  const items = (await res.json()).items || [];
package/lib/voice.js CHANGED
@@ -13,8 +13,7 @@
13
13
  * TIERED CONTEXTS, CASCADED RETRIEVAL (leCore's retrieval_dispatch shape):
14
14
  * the corpus binds into progressively larger tiers — cream (best few
15
15
  * thousand turns), telegram (all of it), twitter (all of it) — and every
16
- * bind lands TWICE: on the gateway (attachable via x-hrr-context) and on
17
- * the local leCore daemon (scored recall). A rewrite recalls against
16
+ * bind uses the authenticated gateway for both text and scored item recall. A rewrite recalls against
18
17
  * cream first; when the top-1/top-2 margin says the match is ambiguous,
19
18
  * it ESCALATES into the full tiers and fuses. Exemplars go inline; the
20
19
  * gateway context rides as x-hrr-context only when the daemon is down.
@@ -29,6 +28,7 @@ import os from 'node:os';
29
28
  import path from 'node:path';
30
29
  import { splitIntoParts } from './bindpath.js';
31
30
  import { config } from './config.js';
31
+ import { withNamespace } from './namespace.js';
32
32
 
33
33
  /**
34
34
  * VOICE CALLS ARE SLOW CALLS, AND THE DEFAULT TIMEOUT ASSUMES THEY ARE NOT.
@@ -44,9 +44,6 @@ if (!process.env.OPENZOO_UPSTREAM_HEADERS_MS) {
44
44
  }
45
45
 
46
46
  const GATEWAY = config.apiBase;
47
- const DAEMON = process.env.OPENZOO_LECORE_URL || 'http://127.0.0.1:8787';
48
- const DAEMON_TOKEN = process.env.OPENZOO_LECORE_TOKEN || 'hrr-lab-token';
49
- const DAEMON_TENANT = process.env.OPENZOO_LECORE_TENANT || 'claude-code';
50
47
 
51
48
  const STATE_FILE = process.env.OPENZOO_VOICE_STATE || path.join(os.homedir(), '.openzoo', 'voice.json');
52
49
  const CARD_FILE = process.env.OPENZOO_VOICE_CARD || path.join(os.homedir(), '.openzoo', 'voice-card.md');
@@ -217,7 +214,7 @@ async function bindGateway(corpus, log, label) {
217
214
  for (let i = 0; i < parts.length; i++) {
218
215
  const r = await fetch(`${GATEWAY}/v1/hrr/bind`, {
219
216
  method: 'POST',
220
- headers: { 'content-type': 'application/json' },
217
+ headers: withNamespace({ 'content-type': 'application/json' }),
221
218
  body: JSON.stringify(contextId ? { context_id: contextId, corpus: parts[i] } : { corpus: parts[i] }),
222
219
  });
223
220
  if (!r.ok) throw new Error(`gateway bind ${label} ${i + 1}/${parts.length}: HTTP ${r.status}`);
@@ -228,7 +225,9 @@ async function bindGateway(corpus, log, label) {
228
225
  }
229
226
 
230
227
  /**
231
- * Local daemon bind — what makes SCORED recall (and the cascade) possible.
228
+ * Scored item bind through the wallet-authenticated gateway.
229
+ * Legacy daemonCtx state fields now refer to private gateway contexts;
230
+ * old daemon contexts must be re-ingested.
232
231
  *
233
232
  * ONE ITEM PER TURN, deliberately: the daemon indexes discrete items, so a
234
233
  * turn stays whole and recall returns something imitable. Chunking a
@@ -247,11 +246,10 @@ async function bindDaemon(turns, log, label) {
247
246
  text: renderTurn(t),
248
247
  metadata: { source: t.source, where: t.where, when: t.when },
249
248
  }));
250
- const r = await fetch(`${DAEMON}/internal/v1/hrr/bind`, {
249
+ const r = await fetch(`${config.apiBase}/v1/hrr/bind`, {
251
250
  method: 'POST',
252
- headers: { 'content-type': 'application/json', authorization: `Bearer ${DAEMON_TOKEN}` },
251
+ headers: withNamespace({ 'content-type': 'application/json' }),
253
252
  body: JSON.stringify({
254
- tenant_id: DAEMON_TENANT,
255
253
  items,
256
254
  ...(contextId ? { context_id: contextId } : {}),
257
255
  }),
@@ -374,10 +372,10 @@ export async function distillStyleCard(turns, log = () => {}) {
374
372
  // ---------------------------------------------------------------- recall
375
373
 
376
374
  async function daemonRecall(contextId, query, topK) {
377
- const r = await fetch(`${DAEMON}/internal/v1/hrr/recall`, {
375
+ const r = await fetch(`${config.apiBase}/v1/hrr/recall`, {
378
376
  method: 'POST',
379
- headers: { 'content-type': 'application/json', authorization: `Bearer ${DAEMON_TOKEN}` },
380
- body: JSON.stringify({ tenant_id: DAEMON_TENANT, context_id: contextId, query, top_k: topK }),
377
+ headers: withNamespace({ 'content-type': 'application/json' }),
378
+ body: JSON.stringify({ context_id: contextId, query, top_k: topK }),
381
379
  });
382
380
  if (!r.ok) throw new Error(`recall HTTP ${r.status}`);
383
381
  return (await r.json()).items || [];
package/lib/xbot.js CHANGED
@@ -22,6 +22,7 @@ import os from 'node:os';
22
22
  import path from 'node:path';
23
23
  import crypto from 'node:crypto';
24
24
  import { FUNDING_ASSETS } from './config.js';
25
+ import { withNamespace, withQuoteNamespace } from './namespace.js';
25
26
  import { deriveBurner } from './xburner.js';
26
27
 
27
28
  const GATEWAY = process.env.OPENZOO_GATEWAY || 'https://x402-tokens.fly.dev';
@@ -214,7 +215,7 @@ async function openzooFacts() {
214
215
  try {
215
216
  const r = await fetch(`${GATEWAY}/v1/chat/completions`, {
216
217
  method: 'POST',
217
- headers: { 'content-type': 'application/json' },
218
+ headers: withQuoteNamespace({ 'content-type': 'application/json' }),
218
219
  body: JSON.stringify({ model: 'openai/gpt-4o-mini', max_tokens: 1, messages: [{ role: 'user', content: 'hi' }] }),
219
220
  });
220
221
  const j = await r.json().catch(() => ({}));
@@ -269,7 +270,7 @@ export async function ensureSharedContext(state) {
269
270
  const facts = await openzooFacts();
270
271
  const res = await fetch(`${GATEWAY}/v1/hrr/bind`, {
271
272
  method: 'POST',
272
- headers: { 'content-type': 'application/json' },
273
+ headers: withNamespace({ 'content-type': 'application/json' }),
273
274
  body: JSON.stringify({ corpus: `openzoobot shared corpus. Threads the bot reads are appended here.\n\n${facts}` }),
274
275
  });
275
276
  if (!res.ok) throw new Error(`bind ${res.status}: ${(await res.text()).slice(0, 160)}`);
@@ -661,7 +662,7 @@ export async function seedFromMentions(creds, contextId, { maxPages = 10 } = {})
661
662
 
662
663
  const b = await fetch(`${GATEWAY}/v1/hrr/bind`, {
663
664
  method: 'POST',
664
- headers: { 'content-type': 'application/json' },
665
+ headers: withNamespace({ 'content-type': 'application/json' }),
665
666
  body: JSON.stringify({ context_id: contextId, corpus }),
666
667
  });
667
668
  if (b.ok) {
@@ -796,7 +797,7 @@ export async function bindThread(contextId, chain, mention) {
796
797
  try {
797
798
  const res = await fetch(`${GATEWAY}/v1/hrr/bind`, {
798
799
  method: 'POST',
799
- headers: { 'content-type': 'application/json' },
800
+ headers: withNamespace({ 'content-type': 'application/json' }),
800
801
  body: JSON.stringify({ context_id: contextId, corpus }),
801
802
  });
802
803
  if (!res.ok) return 0;
@@ -1080,14 +1081,14 @@ export async function askZoo(question, { key, maxTokens = ANSWER_TOKENS, thread
1080
1081
  call: async (body) => {
1081
1082
  const res = await fetch(`${FREE_GATEWAY}/v1/chat/completions`, {
1082
1083
  method: 'POST',
1083
- headers: {
1084
+ headers: withNamespace({
1084
1085
  'content-type': 'application/json',
1085
1086
  // NO x-hrr-top-k. The gateway already scales breadth to the corpus
1086
1087
  // (scaleTopK), and a client-sent X-HRR-Top-K "wins over everything" —
1087
1088
  // pinning a number replaces a curve that grows with the thread.
1088
1089
  ...(contextId ? { 'x-hrr-context': contextId } : {}),
1089
1090
  ...(key ? { authorization: `Bearer ${key}` } : {}),
1090
- },
1091
+ }),
1091
1092
  body: JSON.stringify(body),
1092
1093
  });
1093
1094
  const json = await res.json().catch(() => ({}));
@@ -1180,7 +1181,6 @@ const TOPUP_CAP_USD = Number(process.env.OPENZOO_XBOT_TOPUP_CAP || 500);
1180
1181
 
1181
1182
  export async function ensureCredit(burner) {
1182
1183
  const { PayClient } = await import('./pay.js');
1183
- const { withNamespace } = await import('./namespace.js');
1184
1184
  const ns = (h) => withNamespace(h, { keypair: burner.keypair });
1185
1185
 
1186
1186
  let balance = 0;
@@ -1258,9 +1258,9 @@ export async function askZooPaid(question, { burner, thread = '', maxTokens = AN
1258
1258
  messages,
1259
1259
  maxTokens,
1260
1260
  allowTools: Boolean(WEB_SEARCH),
1261
- // Same shared context as the free lane — a paid asker should recall
1262
- // everything the bot has read, not start from an empty corpus.
1263
- call: async (body) => (await pay.chat(body, { headers: contextId ? { 'x-hrr-context': contextId } : {} })).data,
1261
+ // The operator's archive belongs to its signer. Burner calls use only
1262
+ // the explicitly supplied thread; they cannot attach that private context.
1263
+ call: async (body) => (await pay.chat(body)).data,
1264
1264
  }).then((result) => ({
1265
1265
  ...result,
1266
1266
  // Every on-chain settle signature this answer paid with, so the PAID log
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzoo",
3
- "version": "0.51.18",
3
+ "version": "0.51.19",
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",