openzoo 0.49.16 → 0.49.17

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