openzoo 0.49.16 → 0.49.18

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/grokui.mjs CHANGED
@@ -16,12 +16,6 @@ import { routeChatBody } from './modelroute.js';
16
16
  import { peekDirectiveStatus, formatRaceStatus, STALE_THINKING_MS, summarizeRaceFailures, RACE_EVERY_FAILED } from './livestatus.js';
17
17
  import { creditBalance } from './info.js';
18
18
  import { guardFindCwd } from './runguard.js';
19
- import {
20
- SUBSCRIPTIONS_PAGE,
21
- saveSubscription, clearSubscription,
22
- subscriptionPublicView, parseSubscriptionPaste,
23
- billingTiers, billingCheckout, fetchBillingKey, ingestBillingKeyResponse,
24
- } from './subscription.js';
25
19
  import {
26
20
  prepareChildDir, finishChildDir, lockWorktree, unlockWorktree,
27
21
  parsePrRef, fetchSpecsForOrigin, agentSlug,
@@ -3837,24 +3831,8 @@ const APP_HTML = `<!doctype html>
3837
3831
  </div>
3838
3832
  <div id="walletOverlay" data-component="wallet-modal">
3839
3833
  <div id="walletBox">
3840
- <h3>Pay with a card</h3>
3841
- <div class="wsub">Pay with a card. Basic, Pro, and Ultra are first. Wallet/x402 is the other option below — it stays; it is not the lead.</div>
3842
- <div id="subLane" data-component="subscribe-lane">
3843
- <div class="wlanetitle">Subscribe with a card</div>
3844
- <div class="wtag">Subscription key · no x402</div>
3845
- <div class="wsub">Same plans as the public page. Checkout opens in the system browser — never an in-app Stripe window. After Stripe, this app polls the site’s key endpoint with the checkout session, or you paste the key from the success page.</div>
3846
- <div id="subStatus"></div>
3847
- <div id="subTiers">loading plans…</div>
3848
- <div class="wpaste">
3849
- <div class="wlab">I already subscribed — paste key</div>
3850
- <input id="subKeyInp" type="text" autocomplete="off" spellcheck="false"
3851
- placeholder="key, or the /billing/done?session=… URL">
3852
- <button type="button" id="subKeyBtn">Save key</button>
3853
- <button type="button" id="subForgetBtn" hidden>Remove key</button>
3854
- </div>
3855
- <div class="wnote" id="subNote"></div>
3856
- <div class="wquiet"><a id="subPageLink" href="${SUBSCRIPTIONS_PAGE}" target="_blank" rel="noopener">Full subscriptions page</a></div>
3857
- </div>
3834
+ <h3>Pay per call</h3>
3835
+ <div class="wsub">Wallet/x402 is the only pay lane. There is no key to buy and none to paste: every call settles on chain from the burner below.</div>
3858
3836
  <div class="wlane" id="x402Lane" data-component="x402-lane">
3859
3837
  <div class="wlanetitle">Wallet / x402</div>
3860
3838
  <div class="wsub">This is <b>your</b> local burner on this machine (or this box). Keys stay in ~/.openzoo/wallet.json. It is not openzoo’s wallet, not a shared zoo account, not the model’s. You fund these deposit addresses; the app pays x402 per call from this wallet. Public addresses only — the UI never shows the key.</div>
@@ -3910,7 +3888,6 @@ const APP_HTML = `<!doctype html>
3910
3888
  </div>
3911
3889
  <div id="hud">
3912
3890
  <div class="htitle">YOUR WALLET · THIS SESSION</div>
3913
- <div class="hrow" id="hSubRow" hidden><span>subscription</span><span id="hSub" class="hlime">—</span></div>
3914
3891
  <div class="hrow"><span>prepaid credit</span><span id="hCredit" class="hlime">—</span></div>
3915
3892
  <div class="hrow"><span>you've paid</span><span id="hYouSpent">—</span></div>
3916
3893
  <div class="hrow"><span>our cost (cogs)</span><span id="hYouCogs">—</span></div>
@@ -4389,9 +4366,8 @@ const APP_HTML = `<!doctype html>
4389
4366
  if (!w || (!w.solana && !w.evm && w.creditUsd == null)) {
4390
4367
  const p = document.createElement('div');
4391
4368
  p.className = 'wnote wempty';
4392
- p.textContent = 'Could not reach the local openzoo proxy on :8402. It may still be starting — try again in a few seconds. You can still subscribe with a card above.';
4369
+ p.textContent = 'Could not reach the local openzoo proxy on :8402. It may still be starting — try again in a few seconds.';
4393
4370
  walletBody.appendChild(p);
4394
- renderSubLane(w && w.subscription ? w.subscription : null);
4395
4371
  return;
4396
4372
  }
4397
4373
  if (w.creditUsd != null && w.creditUsd !== '') {
@@ -4425,197 +4401,32 @@ const APP_HTML = `<!doctype html>
4425
4401
  note.className = 'wnote';
4426
4402
  // funded === false is the genuinely-empty case. Undefined means the proxy
4427
4403
  // did not say, and guessing "empty" there would send someone to top up a
4428
- // wallet that is fine. A live subscription is the other pay lane — do not
4429
- // nag an empty wallet as fatal when calls already skip x402.
4430
- const subOn = w.subscription && w.subscription.active;
4431
- note.textContent = subOn
4432
- ? ('Wallet is optional while a subscription is active. ' + (w.funding || ''))
4433
- : (w.funded === false
4434
- ? 'This wallet is EMPTY — wallet/x402 calls will fail with HTTP 402 until you fund the addresses above, or subscribe with a card at the top. ' + (w.funding || '')
4435
- : (w.funding || ''));
4436
- if (w.funded === false && !subOn) note.classList.add('wempty');
4404
+ // wallet that is fine.
4405
+ note.textContent = w.funded === false
4406
+ ? 'This wallet is EMPTY — every call will fail with HTTP 402 until you fund the addresses above. ' + (w.funding || '')
4407
+ : (w.funding || '');
4408
+ if (w.funded === false) note.classList.add('wempty');
4437
4409
  if (note.textContent.trim()) walletBody.appendChild(note);
4438
- renderSubLane(w.subscription || null);
4439
- }
4440
- var subPollTimer = null;
4441
- var subBuying = null;
4442
- function setSubNote(text, empty) {
4443
- const el = document.getElementById('subNote');
4444
- if (!el) return;
4445
- el.textContent = text || '';
4446
- el.className = empty ? 'wnote wempty' : 'wnote';
4447
- }
4448
- function renderSubLane(sub) {
4449
- const status = document.getElementById('subStatus');
4450
- const forget = document.getElementById('subForgetBtn');
4451
- if (status) {
4452
- status.innerHTML = '';
4453
- if (sub && sub.active) {
4454
- const p = document.createElement('div');
4455
- p.className = 'wsubon';
4456
- p.textContent = sub.label || (sub.tierName || 'Subscription') + ' · no x402';
4457
- status.appendChild(p);
4458
- }
4459
- }
4460
- if (forget) forget.hidden = !(sub && sub.active);
4461
- loadSubTiers();
4462
- }
4463
- async function loadSubTiers() {
4464
- const box = document.getElementById('subTiers');
4465
- if (!box) return;
4466
- let body = null;
4467
- try {
4468
- const r = await fetch(API + '/billing/tiers');
4469
- body = r.ok ? await r.json() : null;
4470
- } catch (e) { body = null; }
4471
- box.innerHTML = '';
4472
- const tiers = body && body.ok && Array.isArray(body.tiers) ? body.tiers : [];
4473
- if (!tiers.length) {
4474
- const p = document.createElement('div');
4475
- p.className = 'wnote wempty';
4476
- p.textContent = 'Could not load live plans from zoo.openzoo.fun — try again, or use the full subscriptions page.';
4477
- box.appendChild(p);
4478
- return;
4479
- }
4480
- tiers.forEach(function (t) {
4481
- const art = document.createElement('div');
4482
- art.className = 'wtier' + (t.id === 'pro' ? ' hot' : '');
4483
- art.setAttribute('data-tier', t.id);
4484
- const tag = document.createElement('div');
4485
- tag.className = 'wtag';
4486
- tag.textContent = t.id === 'pro' ? 'Most teams want this' : '';
4487
- const name = document.createElement('div');
4488
- name.className = 'wtn';
4489
- name.textContent = t.name || t.id;
4490
- const price = document.createElement('div');
4491
- price.className = 'wtp';
4492
- price.textContent = '$' + ((Number(t.monthlyCents) || 0) / 100).toFixed(0) + '/mo';
4493
- const share = document.createElement('div');
4494
- share.className = 'wts';
4495
- share.textContent = (t.savingsSharePct != null ? t.savingsSharePct : '?') + '% savings share';
4496
- const blurb = document.createElement('div');
4497
- blurb.className = 'wtb';
4498
- blurb.textContent = t.blurb || '';
4499
- const btn = document.createElement('button');
4500
- btn.type = 'button';
4501
- btn.textContent = subBuying === t.id ? 'Opening checkout…' : ('Get ' + (t.name || t.id));
4502
- btn.disabled = subBuying != null;
4503
- btn.addEventListener('click', function () { buyTier(t.id); });
4504
- art.append(tag, name, price, share, blurb, btn);
4505
- box.appendChild(art);
4506
- });
4507
- }
4508
- function openSystemBrowser(url) {
4509
- // Electron's setWindowOpenHandler routes target=_blank to shell.openExternal.
4510
- // Never load Stripe inside this window.
4511
- window.open(url, '_blank', 'noopener,noreferrer');
4512
- }
4513
- function stopSubPoll() {
4514
- if (subPollTimer) { clearInterval(subPollTimer); subPollTimer = null; }
4515
- }
4516
- function startSubPoll(sessionId) {
4517
- stopSubPoll();
4518
- var tries = 0;
4519
- async function tick() {
4520
- tries += 1;
4521
- try {
4522
- const r = await fetch(API + '/billing/key?session=' + encodeURIComponent(sessionId));
4523
- const j = r.ok ? await r.json() : null;
4524
- if (j && j.saved) {
4525
- stopSubPoll();
4526
- setSubNote('Subscription key saved · no x402');
4527
- await openWallet();
4528
- return;
4529
- }
4530
- if (j && j.pending) setSubNote('Waiting for Stripe to confirm…');
4531
- else if (j && j.error && j.error !== 'session required') setSubNote(j.error, true);
4532
- } catch (e) { /* keep polling */ }
4533
- if (tries >= 60) {
4534
- stopSubPoll();
4535
- setSubNote('Still waiting on Stripe — paste the key from the success page if you have it.');
4536
- }
4537
- }
4538
- subPollTimer = setInterval(tick, 2000);
4539
- tick();
4540
- }
4541
- async function buyTier(tier) {
4542
- subBuying = tier;
4543
- setSubNote('');
4544
- loadSubTiers();
4545
- try {
4546
- const r = await fetch(API + '/billing/checkout', {
4547
- method: 'POST', headers: { 'content-type': 'application/json' },
4548
- body: JSON.stringify({ tier: tier }),
4549
- });
4550
- const j = await r.json();
4551
- if (!j || !j.ok || !j.url) throw new Error((j && j.error) || 'checkout failed');
4552
- openSystemBrowser(j.url);
4553
- setSubNote('Checkout opened in your system browser. This window will pick up the key when Stripe confirms.');
4554
- if (j.sessionId) startSubPoll(j.sessionId);
4555
- } catch (e) {
4556
- setSubNote(e.message || String(e), true);
4557
- }
4558
- subBuying = null;
4559
- loadSubTiers();
4560
- }
4561
- async function savePastedSub() {
4562
- const inp = document.getElementById('subKeyInp');
4563
- const paste = inp ? inp.value.trim() : '';
4564
- if (!paste) { setSubNote('Paste a key or the billing/done URL.', true); return; }
4565
- try {
4566
- const r = await fetch(API + '/billing/key', {
4567
- method: 'POST', headers: { 'content-type': 'application/json' },
4568
- body: JSON.stringify({ paste: paste }),
4569
- });
4570
- const j = await r.json();
4571
- if (j && j.pending && j.session) {
4572
- setSubNote('Waiting for Stripe to confirm…');
4573
- startSubPoll(j.session);
4574
- return;
4575
- }
4576
- if (!j || !j.saved) throw new Error((j && j.error) || 'could not save key');
4577
- if (inp) inp.value = '';
4578
- setSubNote('Subscription key saved · no x402');
4579
- await openWallet();
4580
- } catch (e) {
4581
- setSubNote(e.message || String(e), true);
4582
- }
4583
- }
4584
- async function forgetSub() {
4585
- try {
4586
- await fetch(API + '/billing/key', { method: 'DELETE' });
4587
- } catch (e) { /* still refresh */ }
4588
- setSubNote('Subscription key removed. Wallet/x402 is the pay method again.');
4589
- await openWallet();
4590
4410
  }
4591
4411
  document.getElementById('walletBtn').addEventListener('click', () => {
4592
4412
  echoSlash('/pay');
4593
4413
  openWallet();
4594
4414
  });
4595
- const subKeyBtn = document.getElementById('subKeyBtn');
4596
- if (subKeyBtn) subKeyBtn.addEventListener('click', savePastedSub);
4597
- const subForgetBtn = document.getElementById('subForgetBtn');
4598
- if (subForgetBtn) subForgetBtn.addEventListener('click', forgetSub);
4599
- const subKeyInp = document.getElementById('subKeyInp');
4600
- if (subKeyInp) subKeyInp.addEventListener('keydown', function (e) {
4601
- if (e.key === 'Enter') { e.preventDefault(); savePastedSub(); }
4602
- });
4603
- // First launch: if the burner is empty AND there is no subscription, open
4604
- // the pay modal once so they see card plans first — and burner addresses
4605
- // below. localStorage so a funded session, a saved key, or a dismiss does
4606
- // not keep popping it.
4415
+ // First launch: if the burner is empty, open the pay modal once so the
4416
+ // deposit addresses are the first thing seen. localStorage so a funded
4417
+ // session or a dismiss does not keep popping it.
4607
4418
  (async function maybeOpenWalletOnce() {
4608
4419
  if (localStorage.getItem('openzoo.wallet.seen')) return;
4609
4420
  for (let i = 0; i < 8; i++) {
4610
4421
  try {
4611
4422
  const r = await fetch(API + '/wallet');
4612
4423
  const w = r.ok ? await r.json() : null;
4613
- if (!w || (!w.solana && !w.evm && !(w.subscription && w.subscription.active))) {
4424
+ if (!w || (!w.solana && !w.evm)) {
4614
4425
  await new Promise((res) => setTimeout(res, 400));
4615
4426
  continue;
4616
4427
  }
4617
4428
  localStorage.setItem('openzoo.wallet.seen', '1');
4618
- if (w.funded === false && !(w.subscription && w.subscription.active)) await openWallet();
4429
+ if (w.funded === false) await openWallet();
4619
4430
  return;
4620
4431
  } catch (e) {
4621
4432
  await new Promise((res) => setTimeout(res, 400));
@@ -6077,16 +5888,6 @@ const APP_HTML = `<!doctype html>
6077
5888
  const you = await (await fetch(API + '/hud-summary')).json();
6078
5889
  const creditEl = document.getElementById('hCredit');
6079
5890
  if (creditEl) creditEl.textContent = (you.creditUsd == null) ? '—' : usd(Number(you.creditUsd) || 0);
6080
- const subRow = document.getElementById('hSubRow');
6081
- const subEl = document.getElementById('hSub');
6082
- if (subRow && subEl) {
6083
- if (you.subscription && you.subscription.active) {
6084
- subRow.hidden = false;
6085
- subEl.textContent = you.subscription.label || you.subscription.tierName || 'Subscription key · no x402';
6086
- } else {
6087
- subRow.hidden = true;
6088
- }
6089
- }
6090
5891
  const spent = Number(you.spentUsd) || 0;
6091
5892
  const cogs = Number(you.cogsUsd) || 0;
6092
5893
  const direct = Number(you.directUsd) || 0;
@@ -6185,116 +5986,12 @@ const server = http.createServer((req, res) => {
6185
5986
  try {
6186
5987
  w.creditUsd = await creditBalance();
6187
5988
  } catch { /* leave credit off if the gateway is down */ }
6188
- // Subscription is local (~/.openzoo/subscription.json). Merge it here so
6189
- // an older :8402 that does not yet know about Stripe still shows the lane.
6190
- w.subscription = subscriptionPublicView();
6191
5989
  res.writeHead(200, { 'content-type': 'application/json' });
6192
5990
  res.end(JSON.stringify(w));
6193
5991
  })();
6194
5992
  return;
6195
5993
  }
6196
5994
 
6197
- // Live Stripe plans — never a stale hardcoded $9/$29/$99. Same origin so
6198
- // the renderer does not have to talk to zoo.openzoo.fun itself.
6199
- if (req.method === 'GET' && req.url === '/billing/tiers') {
6200
- (async () => {
6201
- try {
6202
- const body = await billingTiers();
6203
- res.writeHead(200, { 'content-type': 'application/json' });
6204
- res.end(JSON.stringify(body));
6205
- } catch (e) {
6206
- res.writeHead(502, { 'content-type': 'application/json' });
6207
- res.end(JSON.stringify({ ok: false, error: e.message }));
6208
- }
6209
- })();
6210
- return;
6211
- }
6212
-
6213
- if (req.method === 'POST' && req.url === '/billing/checkout') {
6214
- const chunks = [];
6215
- req.on('data', (d) => chunks.push(d));
6216
- req.on('end', async () => {
6217
- let tier = '';
6218
- try { tier = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}').tier || ''; }
6219
- catch { /* ignore */ }
6220
- try {
6221
- const body = await billingCheckout(tier);
6222
- res.writeHead(200, { 'content-type': 'application/json' });
6223
- res.end(JSON.stringify(body));
6224
- } catch (e) {
6225
- res.writeHead(502, { 'content-type': 'application/json' });
6226
- res.end(JSON.stringify({ ok: false, error: e.message }));
6227
- }
6228
- });
6229
- return;
6230
- }
6231
-
6232
- // GET ?session= polls the same endpoint the public /billing/done page uses.
6233
- // On a key, persist it locally and return a public view — never the secret
6234
- // (this UI can sit on a public box URL).
6235
- if (req.method === 'GET' && (req.url || '').startsWith('/billing/key')) {
6236
- (async () => {
6237
- const q = new URL(req.url, 'http://x').searchParams;
6238
- const session = q.get('session') || q.get('session_id') || '';
6239
- try {
6240
- const body = await fetchBillingKey(session);
6241
- res.writeHead(200, { 'content-type': 'application/json' });
6242
- res.end(JSON.stringify(ingestBillingKeyResponse(body, {
6243
- sessionId: session,
6244
- tier: q.get('tier') || null,
6245
- })));
6246
- } catch (e) {
6247
- res.writeHead(502, { 'content-type': 'application/json' });
6248
- res.end(JSON.stringify({ ok: false, error: e.message }));
6249
- }
6250
- })();
6251
- return;
6252
- }
6253
-
6254
- if (req.method === 'POST' && req.url === '/billing/key') {
6255
- const chunks = [];
6256
- req.on('data', (d) => chunks.push(d));
6257
- req.on('end', async () => {
6258
- let paste = '';
6259
- let tier = '';
6260
- try {
6261
- const j = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}');
6262
- paste = j.paste || j.key || '';
6263
- tier = j.tier || '';
6264
- } catch { /* ignore */ }
6265
- const parsed = parseSubscriptionPaste(paste);
6266
- if (parsed.error) {
6267
- res.writeHead(400, { 'content-type': 'application/json' });
6268
- res.end(JSON.stringify({ ok: false, error: parsed.error }));
6269
- return;
6270
- }
6271
- if (parsed.session) {
6272
- try {
6273
- const body = await fetchBillingKey(parsed.session);
6274
- const out = ingestBillingKeyResponse(body, { sessionId: parsed.session, tier });
6275
- if (out.pending) out.session = parsed.session;
6276
- res.writeHead(200, { 'content-type': 'application/json' });
6277
- res.end(JSON.stringify(out));
6278
- } catch (e) {
6279
- res.writeHead(502, { 'content-type': 'application/json' });
6280
- res.end(JSON.stringify({ ok: false, error: e.message }));
6281
- }
6282
- return;
6283
- }
6284
- saveSubscription({ key: parsed.key, tier: tier || null });
6285
- res.writeHead(200, { 'content-type': 'application/json' });
6286
- res.end(JSON.stringify({ ok: true, saved: true, ...subscriptionPublicView() }));
6287
- });
6288
- return;
6289
- }
6290
-
6291
- if (req.method === 'DELETE' && req.url === '/billing/key') {
6292
- clearSubscription();
6293
- res.writeHead(200, { 'content-type': 'application/json' });
6294
- res.end(JSON.stringify({ ok: true, saved: false, ...subscriptionPublicView() }));
6295
- return;
6296
- }
6297
-
6298
5995
  // Restart grokui in place — and ACTUALLY PICK UP THE NEW BUILD.
6299
5996
  //
6300
5997
  // Exiting is the restart: on a production box, box-server's ensureOz() poll
@@ -6351,7 +6048,6 @@ const server = http.createServer((req, res) => {
6351
6048
  try {
6352
6049
  you.creditUsd = await creditBalance();
6353
6050
  } catch { /* credit is advisory */ }
6354
- you.subscription = subscriptionPublicView();
6355
6051
  res.writeHead(200, { 'content-type': 'application/json' });
6356
6052
  res.end(JSON.stringify(you));
6357
6053
  })();
package/lib/info.js CHANGED
@@ -39,8 +39,11 @@ function fmtUi(raw, decimals) {
39
39
  * The quote already carries `extra.tokenUsd` for every asset it settles in —
40
40
  * the very "priced at the 402" this file kept promising in a footnote while
41
41
  * printing `$?`. So a wallet holding 845,486 TOKEN summed to "≈ $0.11 known".
42
- * Symbols come back wrapped (wTOKENx, wLEOSx, wUSDGx); strip the wrapper so a
43
- * row for the plain token the user actually holds finds its price.
42
+ *
43
+ * Symbols are the plain tickers the wallet holds. They used to arrive as
44
+ * wrapper twins that had to be mapped back to the underlying before a holding
45
+ * could find its price; the gateway quotes the native mint now, so the symbol
46
+ * on the row is already the symbol in the wallet.
44
47
  */
45
48
  export async function quotedPrices() {
46
49
  const out = {};
@@ -71,10 +74,7 @@ export async function quotedPrices() {
71
74
  const usd = Number(row?.extra?.tokenUsd);
72
75
  const sym = String(row?.extra?.symbol || '');
73
76
  if (!sym || !Number.isFinite(usd)) continue;
74
- // wTOKENx -> TOKEN, wUSDGx -> USDG, yUSDCx -> USDC
75
- const plain = sym.replace(/^[wy]/, '').replace(/x$/, '').toUpperCase();
76
77
  out[sym.toUpperCase()] = usd;
77
- if (!(plain in out)) out[plain] = usd;
78
78
  }
79
79
  } catch { /* offline: fall back to printing what we know */ }
80
80
  return out;
@@ -179,11 +179,10 @@ export async function affordableUsd() {
179
179
  const perUsd = BigInt(row.maxAmountRequired || '0');
180
180
  if (perUsd <= 0n) continue;
181
181
  try {
182
- // SPENDABLE, not merely wrapped. Every quote names the WRAPPER, and the
183
- // plain token is converted at payment time (topUpQuotedAsset /
184
- // acquireWrappedIfNeeded) — so counting only pre-wrapped balance told a
185
- // user holding 14,847 TOKEN that their wallet "covers only $1.0230" and
186
- // sent them off to fund an account that was already funded.
182
+ // The quote names the raw mint, so the wallet's balance of it IS what it
183
+ // can spend. Back when rows named a Token-2022 twin this had to count the
184
+ // convertible plain token too, and not doing so told a user holding
185
+ // 14,847 TOKEN their wallet "covers only $1.0230".
187
186
  let usd;
188
187
  if (client.spendableUsdForAccept) {
189
188
  usd = await client.spendableUsdForAccept(row);
package/lib/mcp.js CHANGED
@@ -8,7 +8,6 @@ import { tokenBalance } from './x402.js';
8
8
  import { askWithContext, contextCacheDisabled, BIND_MIN_CHARS } from './hrr.js';
9
9
  import { listContexts } from './contexts.js';
10
10
  import { withNamespace } from './namespace.js';
11
- import { subscriptionPublicView } from './subscription.js';
12
11
 
13
12
  const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
14
13
  // The model zoo_ask uses when the caller does not name one. Opus 5 by default:
@@ -383,10 +382,9 @@ export function buildMcpServer() {
383
382
  railsLiveNow: rails?.live ?? null,
384
383
  fundWith: hint || `${FUNDING_ASSETS.map((a) => a.symbol).join(' or ')} on Solana`,
385
384
  solanaMints: Object.fromEntries(FUNDING_ASSETS.map((a) => [a.symbol, a.mint])),
386
- fundHint: 'send a few cents of a listed asset to the address for that rail — Solana assets to solanaAddress, Base assets to evmAddress. The shim converts to whatever the 402 quotes, at payment time. Force a rail with OPENZOO_RAIL=solana|base|robinhood.',
385
+ fundHint: 'send a few cents of a listed asset to the address for that rail — Solana assets to solanaAddress, Base and Robinhood assets to evmAddress. These are the exact mints the 402 quotes: whatever you fund is what gets spent, with no conversion or wrapping step. Force a rail with OPENZOO_RAIL=solana|base|robinhood.',
387
386
  balances,
388
387
  receipts: client.receipts.map((r) => ({ at: r.at, line: r.line })),
389
- subscription: subscriptionPublicView(),
390
388
  });
391
389
  });
392
390
 
package/lib/openclaw.js CHANGED
@@ -133,7 +133,13 @@ async function fetchCatalogRows() {
133
133
  const r = await fetchHeaders(`${config.apiBase}/v1/models`);
134
134
  if (!r.ok) throw new Error(`model catalog fetch failed: HTTP ${r.status}`);
135
135
  const d = await r.json();
136
- return quoteableRows(d.data).filter((m) => !isAutoModel(m.id));
136
+ // AUTO STAYS IN THE CATALOG. It used to be stripped here, which made it
137
+ // unreachable by EVERY route — `--models openzoo/auto` failed with "not in
138
+ // the live catalog", and OpenClaw refused a hand-edited config with
139
+ // `model not allowed: openzoo/auto`. The price objection below is about the
140
+ // cost BLOCK, and the answer to that is to quote a ceiling, not to delete
141
+ // the router we tell people to use.
142
+ return quoteableRows(d.data);
137
143
  }
138
144
 
139
145
  function pickRows(rows, { all, wanted }) {
@@ -146,10 +152,25 @@ function pickRows(rows, { all, wanted }) {
146
152
  return wanted.map((id) => byId.get(id));
147
153
  }
148
154
  if (all) return rows;
149
- // Short honest default: same flagship picker Claude Code gets, minus Auto
150
- // (Auto's price varies per route — a fixed cost block would be a lie).
151
- const picked = pickClaudePickerRows(rows).filter((m) => !isAutoModel(m.id));
152
- return picked.length ? picked : rows.slice(0, 8);
155
+ // Same flagship picker Claude Code gets, WITH Auto pinned first.
156
+ //
157
+ // Auto's price genuinely varies per route, so no fixed cost block is exact.
158
+ // But omitting it entirely is the worse lie: it tells the harness the router
159
+ // does not exist. The command already prints "prices are the CEILING
160
+ // (openrouter-direct)" over every row it writes, and a ceiling is precisely
161
+ // what an upper bound across the picked set is — see the cost fix-up in
162
+ // setupOpenClaw.
163
+ const picked = pickClaudePickerRows(rows);
164
+ const withoutAuto = picked.filter((m) => !isAutoModel(m.id));
165
+ // PREFER THE BARE `auto` ID. The catalog offers `auto`, `openzoo/auto` and
166
+ // `openzoo-auto` for the same router. OpenClaw renders a model as
167
+ // `<provider>/<id>` and our provider key is already `openzoo`, so taking the
168
+ // prefixed row writes `openzoo/openzoo/auto` — which is not the string the
169
+ // user types and OpenClaw rejects it as "model not allowed".
170
+ const autos = rows.filter((m) => isAutoModel(m.id));
171
+ const auto = autos.find((m) => m.id === "auto") ?? autos[0];
172
+ const ordered = auto ? [auto, ...withoutAuto] : withoutAuto;
173
+ return ordered.length ? ordered : rows.slice(0, 8);
153
174
  }
154
175
 
155
176
  /**
@@ -170,6 +191,18 @@ export async function setupOpenClaw(argv = []) {
170
191
  const rows = pickRows(await fetchCatalogRows(), opt);
171
192
  if (!rows.length) throw new Error('live catalog returned no quoteable models');
172
193
  const entries = rows.map(openclawModelEntry);
194
+ // AUTO IS PRICED AT THE CEILING OF WHAT IT CAN ROUTE TO. Its own catalog row
195
+ // carries no usable price (the route is chosen per request), and a zero there
196
+ // reads to a budgeting harness as "free", which is the one number guaranteed
197
+ // wrong. The max across everything written alongside it cannot be exceeded by
198
+ // any route Auto picks from that set, so it over-states and never under-states.
199
+ const autoEntry = entries.find((e) => isAutoModel(e.id));
200
+ if (autoEntry) {
201
+ const others = entries.filter((e) => e !== autoEntry);
202
+ const ceil = (k) => others.reduce((m, e) => Math.max(m, Number(e.cost?.[k]) || 0), 0);
203
+ autoEntry.cost = { input: ceil("input"), output: ceil("output"), cacheRead: 0, cacheWrite: 0 };
204
+ }
205
+ // Auto is written first, so it is also the default unless overridden.
173
206
  const defaultId = opt.defaultId || entries[0].id;
174
207
  if (!entries.some((e) => e.id === defaultId)) {
175
208
  throw new Error(`--default ${defaultId} is not among the written models (add it via --models)`);