tollkit-wallet 0.3.0 → 0.3.2

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/dist/ui.js CHANGED
@@ -12,18 +12,24 @@ export const PAGE = String.raw `<!doctype html>
12
12
  @media (prefers-color-scheme:light){:root:not([data-theme=dark]){--bg:#eceef3;--card:#fff;--surf:#f4f5f8;--surf2:#e9ebf1;--line:#e1e3ea;--tx:#15161b;--mut:#666a7a}}
13
13
  *{box-sizing:border-box}html,body{margin:0;background:var(--bg);color:var(--tx);font:14px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,system-ui,sans-serif}
14
14
  .wrap{max-width:420px;margin:24px auto;padding:0 16px}
15
- .card{background:var(--card);border:1px solid var(--line);border-radius:20px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.35);display:flex;flex-direction:column;min-height:640px}
15
+ .card{background:var(--card);border:1px solid var(--line);border-radius:20px;overflow:hidden;box-shadow:0 20px 60px rgba(0,0,0,.35);display:flex;flex-direction:column;height:calc(100vh - 96px);height:calc(100dvh - 96px);min-height:520px;max-height:900px}
16
16
  header{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid var(--line)}
17
17
  .logo{width:30px;height:30px;flex:none;display:block}
18
18
  .brand{font:700 15px ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:-.01em;white-space:nowrap}.brand .dot{color:#4a8eff}.brand .sub{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif;font-weight:500;color:var(--mut);margin-left:6px}
19
19
  .cp{margin-left:auto;flex:none;border:0;background:var(--surf2);color:var(--tx);border-radius:9px;padding:6px 10px;font:600 12px inherit;cursor:pointer;display:flex;align-items:center;gap:5px}
20
20
  .cp svg{width:14px;height:14px}.cp.done{color:var(--ok)}header .pill{margin-left:auto;white-space:nowrap;font-size:11px;padding:4px 9px;border-radius:99px;background:var(--surf);color:var(--mut)}
21
- main{flex:1;padding:16px;overflow:auto}
22
- nav{display:grid;grid-template-columns:repeat(6,1fr);border-top:1px solid var(--line);background:var(--card)}
21
+ main{flex:1;min-height:0;padding:16px;overflow:auto;overscroll-behavior:contain}
22
+ nav{flex:none;display:grid;grid-template-columns:repeat(6,1fr);border-top:1px solid var(--line);background:var(--card)}
23
23
  nav button{background:none;border:0;color:var(--mut);padding:10px 0 12px;font:inherit;font-size:11px;cursor:pointer;position:relative}
24
24
  nav button svg{display:block;margin:0 auto 3px;width:20px;height:20px}
25
25
  nav button.on{color:var(--acc)}
26
26
  .badge{position:absolute;top:5px;left:calc(50% + 6px);background:var(--bad);color:#fff;border-radius:99px;font-size:10px;padding:0 5px;min-width:16px}
27
+ .badge.dot{min-width:0;width:8px;height:8px;padding:0;top:7px;left:calc(50% + 8px)}
28
+ nav button.attn{--hi:var(--acc)}nav button.attn-bad{--hi:var(--bad)}nav button.attn-warn{--hi:var(--warn)}nav button.attn-ok{--hi:var(--ok)}
29
+ nav button.attn:not(.on){color:var(--hi)}nav button.attn .badge{background:var(--hi);color:#fff}nav button.attn-warn .badge{color:#1a1206}
30
+ nav button.attn:not(.on)::before{content:"";position:absolute;left:50%;top:6px;width:34px;height:34px;margin-left:-17px;border-radius:50%;background:radial-gradient(circle,color-mix(in srgb,var(--hi) 30%,transparent),transparent 70%);animation:navhi 2.4s ease-in-out infinite;pointer-events:none}
31
+ nav button.attn:not(.on) .emo{opacity:1;filter:none}
32
+ @keyframes navhi{0%,100%{opacity:.35;transform:scale(.85)}50%{opacity:1;transform:scale(1.1)}}
27
33
  .big{font-size:34px;font-weight:700;letter-spacing:-.5px;margin:6px 0 2px}
28
34
  .mut{color:var(--mut)}.sm{font-size:12px}
29
35
  .row{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--surf);border-radius:12px;margin-top:8px}
@@ -170,6 +176,18 @@ input,select{transition:border-color .2s ease,box-shadow .25s ease}input:focus,s
170
176
  .aist{margin:0 0 4px;padding-left:18px;font-size:13px;line-height:1.55}.aist li{margin:5px 0}
171
177
  .aidlg code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em;color:var(--acc);overflow-wrap:anywhere}
172
178
  .kbd{font-family:ui-monospace,monospace;font-size:.85em;background:var(--surf2);border:1px solid var(--line);border-radius:6px;padding:1px 6px}
179
+ .tmdlg{max-width:560px}.tmdlg h2{text-align:center}.tmdlg .ailead{text-align:center}
180
+ .tmopts{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:6px 0 12px}@media(max-width:480px){.tmopts{grid-template-columns:1fr}}
181
+ .tmopt{display:flex;flex-direction:column;gap:6px;text-align:left;background:var(--surf);border:1px solid var(--line);border-radius:16px;padding:14px;color:var(--tx);font:inherit;cursor:pointer;animation:tk-rise .5s var(--ease) both;animation-delay:calc(var(--i,0)*90ms + 120ms);transition:border-color .2s ease,transform .25s var(--ease),box-shadow .25s ease}
182
+ .tmopt:hover,.tmopt:focus-visible{border-color:var(--acc);transform:translateY(-2px);box-shadow:0 14px 34px -18px rgba(var(--glow),.55)}.tmopt b{font-size:15.5px}.tmopt small{color:var(--mut);font-size:12.5px;line-height:1.45}.tmopt .botcta{margin-top:auto}
183
+ .tmico{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;border-radius:12px;flex:none}.tmico svg{width:21px;height:21px}.tmico.sm{width:32px;height:32px;border-radius:10px}.tmico.sm svg{width:18px;height:18px}
184
+ .tmai{background:color-mix(in srgb,#d97757 18%,transparent);color:#d97757}.tmpy{background:color-mix(in srgb,var(--acc) 16%,transparent);color:var(--acc)}
185
+ .pycard{background:var(--surf);border:1px solid color-mix(in srgb,var(--acc) 40%,var(--line));border-radius:14px;padding:12px;margin:12px 0 4px}.pyct{display:flex;align-items:center;gap:10px}.pyct>div{flex:1}
186
+ .pystep{display:flex;align-items:center;gap:8px;margin:16px 0 4px}.pystep b{font-size:14px}.pyn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border-radius:50%;background:color-mix(in srgb,var(--acc) 18%,transparent);color:var(--acc);font-size:12px;font-weight:700}
187
+ .pymore{margin:2px 0 6px}.pymore summary{cursor:pointer;font-size:12.5px;color:var(--mut)}.pymore summary:hover{color:var(--tx)}
188
+ .tmlater{padding:4px 10px 0}.tmopt2{cursor:default}.tmopt2:hover{transform:none}.tmbtns{display:flex;flex-direction:column;align-items:flex-start;gap:7px;margin-top:auto}
189
+ .aiacts{display:flex;align-items:center;flex-wrap:wrap;gap:4px 12px;padding-left:35px}.aiacts .aihand{padding:4px 0 0}.aicopen{margin-top:6px}.aiok{border-color:color-mix(in srgb,var(--ok) 45%,transparent)}
190
+ .tc-fuel{margin-top:8px;display:flex;flex-direction:column;gap:8px;align-items:flex-start}.tc-fuel .b{padding:8px 12px;font-size:13px}.tc-fchk{display:flex;gap:7px;align-items:center;font-size:12.5px;cursor:pointer}.tc-fchk input{margin:0}.tc-own{margin:6px 0 0;font-size:12px}.tmseg{margin:0}.tmseg .bsb{padding:5px 12px}
173
191
  .aicmd{display:flex;gap:8px;align-items:flex-start;background:#08090c;border:1px solid var(--line);border-radius:12px;padding:9px 9px 9px 13px;margin:8px 0}
174
192
  .aicmd pre{flex:1;min-width:0;margin:0;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;color:#cdd6e4;white-space:pre-wrap;overflow-wrap:anywhere}
175
193
  .aicp{padding:6px 11px;font-size:12.5px;border-radius:9px}
@@ -232,7 +250,17 @@ input,select{transition:border-color .2s ease,box-shadow .25s ease}input:focus,s
232
250
  .hfoot{margin-top:8px;font-size:12px;color:var(--mut);text-align:center}
233
251
  .botc{background:var(--surf);border:1px solid var(--line);border-radius:14px;padding:12px;margin-top:8px}.botct{display:flex;align-items:flex-start;gap:8px;justify-content:space-between}.botpill{white-space:nowrap}.botpill.ok{background:rgba(60,207,145,.15);color:var(--ok)}.botpill.warn{background:rgba(245,184,74,.15);color:var(--warn)}.botpill.bad{background:rgba(255,107,107,.15);color:var(--bad)}
234
252
  .botstats{display:grid;grid-template-columns:repeat(4,1fr);gap:6px;margin-top:10px}.botstats div{background:var(--card);border-radius:10px;padding:6px 8px;min-width:0}.botstats small{display:block;color:var(--mut);font-size:11px}.botstats b{font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;display:block}
235
- .botcat{display:grid;grid-template-columns:1fr 1fr;gap:8px}.bottpl{text-align:left;background:var(--surf);border:1px solid var(--line);border-radius:12px;padding:10px;color:var(--tx);font:inherit;cursor:pointer;display:flex;flex-direction:column;gap:4px}.bottpl small{color:var(--mut);font-size:12px;line-height:1.35}.bottpl.on,.bottpl:hover{border-color:var(--acc)}
253
+ .botcat{display:grid;grid-template-columns:1fr 1fr;grid-auto-flow:row dense;gap:8px}.botlim{font-style:normal;color:var(--mut);opacity:.75;font-size:11px;margin-left:4px;white-space:nowrap}.botf input.botcap{animation:botcap .5s ease}@keyframes botcap{0%,100%{border-color:var(--line)}30%{border-color:var(--acc);box-shadow:0 0 0 3px rgba(var(--glow),.25)}}.botwrap{grid-column:1/-1;display:grid;grid-template-rows:1fr}.botin{min-height:0}.botin>.botform{margin:0}.botwrap.drop{animation:botopen .42s var(--ease) both}.botwrap.drop .botform{animation:botslide .42s var(--ease) both}.botwrap.closing{animation:botshut .24s ease-in both}.botwrap.drop .botin,.botwrap.closing .botin{overflow:hidden}
254
+ @keyframes botopen{from{grid-template-rows:0fr}to{grid-template-rows:1fr}}@keyframes botslide{from{opacity:0;transform:translateY(-14px) scale(.98)}to{opacity:1;transform:none}}@keyframes botshut{from{grid-template-rows:1fr;opacity:1}to{grid-template-rows:0fr;opacity:0}}
255
+ nav button .emo{display:block;margin:0 auto 3px;width:20px;height:20px;font-size:17px;line-height:20px;text-align:center;opacity:.75;filter:grayscale(.35);transition:transform .45s var(--ease),opacity .3s,filter .3s}nav button.on .emo{opacity:1;filter:none;transform:translateY(-2px) scale(1.1)}.bottpl{text-align:left;background:var(--surf);border:1px solid var(--line);border-radius:12px;padding:10px;color:var(--tx);font:inherit;cursor:pointer;display:flex;flex-direction:column;gap:4px}.bottpl small{color:var(--mut);font-size:12px;line-height:1.35}.bottpl.on,.bottpl:hover{border-color:var(--acc)}
256
+ .tplh{display:flex;align-items:flex-start;justify-content:space-between;gap:6px}.tplh b{min-width:0}
257
+ .botcta{margin-top:auto;align-self:flex-start;display:inline-flex;align-items:center;gap:6px;padding:6px 11px;border-radius:99px;border:0;background:color-mix(in srgb,var(--acc) 15%,transparent);color:var(--acc);font:inherit;font-size:12.5px;font-weight:600;cursor:pointer;transition:background .2s ease}
258
+ .botcta svg{transition:transform .25s var(--ease)}.bottpl:hover .botcta,.botcta:hover{background:color-mix(in srgb,var(--acc) 24%,transparent)}.bottpl:hover .botcta svg,.botcta:hover svg{transform:translateX(3px)}
259
+ .botcta.g{background:transparent;color:var(--mut);padding-left:6px;padding-right:6px}.botcta.g:hover{background:var(--surf2);color:var(--tx)}
260
+ .bottpl>.botcta{margin-top:auto;padding-top:6px}.bottpl>.boardl{margin-bottom:6px}
261
+ .bottpl.has{cursor:default;border-color:color-mix(in srgb,var(--ok) 45%,var(--line))}.bottpl.has.err{border-color:color-mix(in srgb,var(--bad) 55%,var(--line))}.bottpl.has.on{border-color:var(--acc)}
262
+ .tplbtns{display:flex;flex-wrap:wrap;gap:4px;margin-top:auto;padding-top:6px}.tplbtns .botcta{margin-top:0}
263
+ .botc.botflash{animation:botflash 1.6s ease}@keyframes botflash{0%,60%{border-color:var(--acc);box-shadow:0 0 0 3px color-mix(in srgb,var(--acc) 30%,transparent)}100%{border-color:var(--line);box-shadow:none}}
236
264
  .botgrid{display:grid;grid-template-columns:1fr 1fr;gap:8px}.botf{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--mut)}.botf input,.botf select{background:var(--card);border:1px solid var(--line);color:var(--tx);border-radius:10px;padding:8px;font:inherit;font-size:13px;min-width:0}
237
265
  .botshare{display:flex;gap:8px;align-items:flex-start;font-size:12px;color:var(--mut);margin-top:14px;line-height:1.4}.botshare input{width:auto;flex:0 0 auto;margin:2px 0 0}.addt .crow2 select{flex:0 0 104px;width:104px}.addt .crow2 input{flex:1 1 auto;min-width:0;width:auto}@media(max-width:420px){.botcat,.botgrid{grid-template-columns:1fr}.botstats{grid-template-columns:1fr 1fr}}
238
266
  .tlogo{width:100%;height:100%;border-radius:50%;object-fit:cover;display:block}.tbadge{font-size:10px;padding:1px 6px;vertical-align:1px;margin-left:4px}
@@ -289,7 +317,7 @@ function esc(s){return String(s==null?"":s).replace(/[&<>"']/g,function(c){retur
289
317
  function api(p,body){return fetch("/api"+p,{method:body?"POST":"GET",headers:{"x-tw-token":T,"content-type":"application/json"},body:body?JSON.stringify(body):undefined}).then(function(r){return r.json().then(function(j){if(!r.ok)throw new Error(j.error||("HTTP "+r.status));return j})})}
290
318
  function short(a){a=String(a||"");return a.length>14?a.slice(0,a.indexOf("0x")===0?6:4)+"…"+a.slice(-4):a}
291
319
  var COIN=${COIN_JSON};
292
- var AIIC=${AI_ICON_JSON},AIO=null,AIPW="",AIMSG={},AIOPEN={},AIBUSY=null;
320
+ var AIIC=${AI_ICON_JSON},AIO=null,AIPW="",AIMSG={},AIOPEN={},AIBUSY=null,AICOPIED=null;
293
321
  function aiSvg(k){var i=AIIC[k];return '<svg viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor"'+(i.rule?' fill-rule="'+i.rule+'"':'')+' d="'+i.d+'"/></svg>'}
294
322
  var AIMETA={claude:["claude","Claude"],"claude-code":["claude","Claude Code"],cursor:["cursor","Cursor"],vscode:["copilot","VS Code"],codex:["terminal","Codex"],gemini:["gemini","Gemini CLI"]};
295
323
  function aiBtn(){var on=((S.connect||{}).apps||[]).filter(function(a){return a.added}),rm=(S.connect||{}).remote;
@@ -297,7 +325,7 @@ if(rm&&rm.on)on=[{id:"claude"}];
297
325
  if(!on.length)return '<button class="b aib" onclick="openAI()"><span class="aiic">'+aiSvg("claude")+aiSvg("cursor")+aiSvg("gemini")+'</span>Connect your AI</button>';
298
326
  var icons=[],names=[];on.forEach(function(a){var m=AIMETA[a.id];if(!m)return;names.push(m[1]);if(icons.indexOf(m[0])<0)icons.push(m[0])});
299
327
  var wait=(S.rules||[]).filter(function(r){return r.status==="proposed"}).length;
300
- return '<div class="aiconn"><button class="aich" onclick="openAI()" title="Manage connected AI apps"><span class="aistk">'+icons.slice(0,4).map(function(k){return '<i class="ail-'+k+'">'+aiSvg(k)+'</i>'}).join("")+'</span><span class="aichn"><b>'+esc(rm&&rm.on?"Your AI apps":names.join(" + "))+'</b><small>'+(rm&&rm.on&&!rm.online?'<span class="warn">●</span> Waiting for the wallet':'<span class="ok">●</span> Connected')+'</small></span></button>'+
328
+ return '<div class="aiconn"><button class="aich" onclick="openAI()" title="Manage connected AI apps"><span class="aistk">'+icons.slice(0,4).map(function(k){return '<i class="ail-'+k+'">'+aiSvg(k)+'</i>'}).join("")+'</span><span class="aichn"><b>'+esc(rm&&rm.on?"Your AI apps":names.join(" + "))+'</b><small>'+(rm&&rm.on&&!rm.online?'<span class="warn">●</span> Waiting for the wallet':rm&&rm.on&&!rm.used?'<span class="warn">●</span> Add the address to your AI app':'<span class="ok">●</span> Connected')+'</small></span></button>'+
301
329
  '<button class="b aiap" onclick="goAuto()"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M13 2L4 14h7l-1 8 9-12h-7z" fill="currentColor"/></svg>Autopilot'+(wait?'<span class="aiwt">'+wait+'</span>':'')+'</button></div>'}
302
330
  function goAuto(){go("autopilot");setTimeout(function(){var t=document.getElementById("chin");if(t&&!t.disabled)t.focus()},120)}
303
331
  function aiCfg(){var c=S.connect||{};return c.launch||{command:"npx",args:["-y","tollkit-wallet","mcp"],env:{TOLLKIT_WALLET_PASSWORD_FILE:c.password_file||""}}}
@@ -329,8 +357,8 @@ function svgOk(){return '<svg viewBox="0 0 24 24" width="14" height="14" aria-hi
329
357
  // developers.openai.com/api/docs/guides/developer-mode, code.claude.com/docs/en/mcp-quickstart, cursor.com/docs/mcp,
330
358
  // code.visualstudio.com/docs/copilot/reference/mcp-configuration, developers.openai.com/codex/mcp, geminicli.com/docs/tools/mcp-server.
331
359
  var AIREMOTE=[
332
- {id:"claude",name:"Claude",sub:"Web, desktop and mobile",icon:"claude",hand:function(u){return '<ol class="aist"><li>Open <a href="https://claude.ai/customize/connectors" target="_blank" rel="noopener">claude.ai/customize/connectors</a>, click <b>+</b>, then <b>Add custom connector</b>.</li><li>Paste your address and click <b>Add</b>.</li></ol><p class="aismall">Free plans can add one custom connector. On Team and Enterprise, an Owner adds it (Add → Custom → Web).</p>'}},
333
- {id:"chatgpt",name:"ChatGPT",sub:"Developer mode",icon:"terminal",hand:function(u){return '<ol class="aist"><li>In ChatGPT on the web: <b>Settings → Apps → Advanced settings</b>, turn on <b>Developer mode</b>.</li><li>Click <b>Create app</b>, paste your address, choose <b>No authentication</b>, and create it.</li></ol><p class="aismall">Developer mode depends on your ChatGPT plan; OpenAI’s help pages list which.</p>'}},
360
+ {id:"claude",name:"Claude",sub:"Add it on the web or desktop app",icon:"claude",open:"https://claude.ai/customize/connectors",hand:function(u){return '<ol class="aist"><li>Open <a href="https://claude.ai/customize/connectors" target="_blank" rel="noopener">claude.ai/customize/connectors</a>, click <b>+</b>, then <b>Add custom connector</b>.</li><li>Paste your address and click <b>Add</b>.</li></ol><p class="aismall">Free plans can add one custom connector. On Team and Enterprise, an Owner adds it (Add → Custom → Web).</p>'}},
361
+ {id:"chatgpt",name:"ChatGPT",sub:"Developer mode, paid plans, on the web",icon:"terminal",open:"https://chatgpt.com/",hand:function(u){return '<ol class="aist"><li>In ChatGPT on the web: <b>Settings → Apps → Advanced settings</b>, turn on <b>Developer mode</b>.</li><li>Click <b>Create app</b>, paste your address, choose <b>No authentication</b>, and create it.</li></ol><p class="aismall">Developer mode depends on your ChatGPT plan; OpenAI’s help pages list which.</p>'}},
334
362
  {id:"claude-code",name:"Claude Code",sub:"Terminal",icon:"claude",hand:function(u){return '<p class="aip">Paste into your terminal:</p>'+aiCode("ar-cc","claude mcp add --transport http --scope user tollkit-wallet "+shq(u))}},
335
363
  {id:"cursor",name:"Cursor",sub:"Code editor",icon:"cursor",hand:function(u){return '<p class="aip">Add this to <span class="kbd">~/.cursor/mcp.json</span>:</p>'+aiCode("ar-cu",JSON.stringify({mcpServers:{"tollkit-wallet":{url:u}}},null,2))}},
336
364
  {id:"vscode",name:"VS Code",sub:"With GitHub Copilot",icon:"copilot",hand:function(u){return '<p class="aip">Run <b>MCP: Add Server</b>, choose <b>HTTP</b> and paste your address. Or add to <span class="kbd">mcp.json</span>:</p>'+aiCode("ar-vs",JSON.stringify({servers:{"tollkit-wallet":{type:"http",url:u}}},null,2))}},
@@ -342,8 +370,9 @@ if(!r.on)return h+'<label class="ailbl" for="aipw">Wallet password</label><input
342
370
  '<button class="b aigoap" data-ract="on"'+(AIBUSY?" disabled":"")+'>'+(AIBUSY?"Making it…":"Make my connector address")+'</button>'+(m?'<div class="aim '+(m[0]?"ok":"bad")+'">'+esc(m[1])+'</div>':'')+
343
371
  '<p class="aismall">You get a private web address to add to your AI app. Tollkit’s server passes the app’s requests to this wallet while Chrome is open and the wallet is unlocked. Your keys stay here.</p>';
344
372
  h+='<label class="ailbl">Your connector address</label>'+aiCode("ai-url",r.url)+
345
- '<p class="aismall"><span class="'+(r.online?"ok":"warn")+'">●</span> '+(r.online?"The wallet is reachable.":"Waiting for the wallet to check in…")+' Anyone with this address can use the wallet within your limits, so keep it private.</p>'+
346
- '<div class="ailist">'+AIREMOTE.map(function(x,i){return '<div class="airow" style="--i:'+i+'"><div class="airt">'+aiSvg(x.icon)+'<div class="ainm"><b>'+esc(x.name)+'</b><small>'+esc(x.sub)+'</small></div></div><button class="aihand" data-hand="r-'+x.id+'">'+(AIOPEN["r-"+x.id]?"Hide steps":"How to add it")+'</button>'+(AIOPEN["r-"+x.id]?'<div class="aihb">'+x.hand(r.url)+'</div>':'')+'</div>'}).join("")+'</div>'+
373
+ (r.used?'<div class="aidone aiok"><b>✓ Connected.</b> Your AI app reached the wallet '+esc(ago(new Date(r.used).toISOString()))+'.</div>':'')+
374
+ '<p class="aismall"><span class="'+(r.online?"ok":"warn")+'">●</span> '+(!r.online?"Waiting for the wallet to check in…":r.used?"The wallet is reachable.":"The wallet is ready. Add the address to your AI app below, then ask it something. This turns to Connected when it does.")+' Anyone with this address can use the wallet within your limits, so keep it private.</p>'+
375
+ '<div class="ailist">'+AIREMOTE.map(function(x,i){return '<div class="airow" style="--i:'+i+'"><div class="airt">'+aiSvg(x.icon)+'<div class="ainm"><b>'+esc(x.name)+'</b><small>'+esc(x.sub)+'</small></div></div><div class="aiacts">'+(x.open?'<button class="botcta aicopen" data-copen="'+x.id+'">'+(AICOPIED===x.id?"Copied. Paste it there ✓":"Copy & open "+esc(x.name)+ARROW)+'</button>':'')+'<button class="aihand" data-hand="r-'+x.id+'">'+(AIOPEN["r-"+x.id]?"Hide steps":"How to add it")+'</button></div>'+(AIOPEN["r-"+x.id]?'<div class="aihb">'+x.hand(r.url)+'</div>':'')+'</div>'}).join("")+'</div>'+
347
376
  '<div class="aidone"><b>Check it worked.</b> Ask your AI: <b>“What’s in my Tollkit wallet?”</b></div>'+
348
377
  '<label class="ailbl" for="aipw">Wallet password (for a new address)</label><input id="aipw" class="aiin" type="password" autocomplete="current-password" value="'+esc(AIPW)+'">'+
349
378
  '<div class="btns" style="margin-top:10px"><button class="b g" data-ract="new"'+(AIBUSY?" disabled":"")+'>New address</button><button class="b no" data-ract="off"'+(AIBUSY?" disabled":"")+'>Turn off</button></div>'+(m?'<div class="aim '+(m[0]?"ok":"bad")+'">'+esc(m[1])+'</div>':'')+
@@ -363,7 +392,7 @@ function openAI(){if(!AIO){AIO=document.createElement("div");AIO.className="aio"
363
392
  function closeAI(){if(AIO){AIO.remove();AIO=null;AIPW="";AIMSG={}}}
364
393
  function aiDo(act,id){if(act!=="remove"&&!AIPW){AIMSG[id]=[0,"Type your wallet password first."];aiRender();document.getElementById("aipw").focus();return}
365
394
  AIBUSY=id;AIMSG[id]=null;aiRender();api("/connect",{password:AIPW,action:act,app:id}).then(function(r){AIMSG[id]=[1,r.message];return refresh()}).catch(function(e){AIMSG[id]=[0,e.message]}).then(function(){AIBUSY=null;aiRender()})}
366
- function aiClick(e){var t=e.target.closest("button,a");if(!t)return;if(t.classList.contains("aix"))closeAI();else if(t.hasAttribute("data-ract"))aiRemoteDo(t.getAttribute("data-ract"));else if(t.hasAttribute("data-act"))aiDo(t.getAttribute("data-act"),t.getAttribute("data-id"));else if(t.hasAttribute("data-hand")){var k=t.getAttribute("data-hand");AIOPEN[k]=!AIOPEN[k];aiRender()}else if(t.hasAttribute("data-copy")){var el=document.getElementById(t.getAttribute("data-copy")),v=el?el.textContent:"",done=function(){t.textContent="Copied";setTimeout(function(){t.textContent="Copy"},1500)};if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(v).then(done,function(){fallback(v);done()});else{fallback(v);done()}}}
395
+ function aiClick(e){var t=e.target.closest("button,a");if(!t)return;if(t.classList.contains("aix"))closeAI();else if(t.hasAttribute("data-ract"))aiRemoteDo(t.getAttribute("data-ract"));else if(t.hasAttribute("data-copen")){var xo=AIREMOTE.filter(function(z){return z.id===t.getAttribute("data-copen")})[0],ru=((S.connect||{}).remote||{}).url;if(xo&&ru){if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(ru).catch(function(){fallback(ru)});else fallback(ru);AICOPIED=xo.id;AIOPEN["r-"+xo.id]=true;aiRender();window.open(xo.open,"_blank")}}else if(t.hasAttribute("data-act"))aiDo(t.getAttribute("data-act"),t.getAttribute("data-id"));else if(t.hasAttribute("data-hand")){var k=t.getAttribute("data-hand");AIOPEN[k]=!AIOPEN[k];aiRender()}else if(t.hasAttribute("data-copy")){var el=document.getElementById(t.getAttribute("data-copy")),v=el?el.textContent:"",done=function(){t.textContent="Copied";setTimeout(function(){t.textContent="Copy"},1500)};if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(v).then(done,function(){fallback(v);done()});else{fallback(v);done()}}}
367
396
  document.addEventListener("keydown",function(e){if(AIO&&e.key==="Escape")closeAI()});
368
397
  var BUY={o:null,chain:null,asset:"USDC",amt:"50",custom:"",test:false,err:null,busy:false,done:null};
369
398
  var BUYA={base:["USDC","ETH"],ethereum:["USDC","ETH"],solana:["USDC","SOL"]};
@@ -571,17 +600,21 @@ function fmtAi(t){var B=/^\s*([-•*]|\d+\.)\s+/,h=esc(t).replace(/\*\*([^*]+)\*
571
600
  function tm(ts){var d=new Date(ts);return d.toLocaleTimeString([],{hour:"numeric",minute:"2-digit"})+(Date.now()-d>864e5?" · "+d.toLocaleDateString([],{month:"short",day:"numeric"}):"")}
572
601
  function aiName(){var A=S.ai||{};return A.provider?A.provider.name:"AI"}
573
602
  function ckey(m){return m.role==="you"?"y:"+m.text:(m.id||m.ts)}
574
- function chatMsg(m){var nw=CH.seen&&!CH.seen[ckey(m)]?" tc-new":"";if(m.role==="you")return '<div class="tc-cm tc-you'+nw+'"><div class="tc-cb">'+esc(m.text).replace(/\n/g,"<br>")+'</div></div>';
603
+ function chatMsg(m,last){var nw=CH.seen&&!CH.seen[ckey(m)]?" tc-new":"";if(m.role==="you")return '<div class="tc-cm tc-you'+nw+'"><div class="tc-cb">'+esc(m.text).replace(/\n/g,"<br>")+'</div></div>';
575
604
  if(m.checkin&&m.quiet&&!m.error)return '<div class="tc-cq">'+svgClock()+'Check-in '+tm(m.ts)+' · '+esc(m.text.replace(/^No changes[.:,]?\s*/i,"No changes. "))+'</div>';
576
605
  var cards=(m.rules||[]).map(function(id){var r=(S.rules||[]).filter(function(x){return x.id===id})[0];return r?ruleCard(r):""}).join("");
577
- return '<div class="tc-cm tc-ai'+nw+(m.error?" tc-err":"")+'"><div class="tc-cav">'+aiSvg("claude")+'</div><div class="tc-cbw">'+(m.checkin?'<div class="tc-ctag">Check-in · '+tm(m.ts)+'</div>':'')+'<div class="tc-cb">'+fmtAi(m.text)+'</div>'+(cards?'<div class="tc-ccards">'+cards+'</div>':'')+'</div></div>'}
606
+ return '<div class="tc-cm tc-ai'+nw+(m.error?" tc-err":"")+'"><div class="tc-cav">'+aiSvg("claude")+'</div><div class="tc-cbw">'+(m.checkin?'<div class="tc-ctag">Check-in · '+tm(m.ts)+'</div>':'')+'<div class="tc-cb">'+fmtAi(m.text)+'</div>'+(cards?'<div class="tc-ccards">'+cards+'</div>':'')+(m.error&&last&&m.fuel&&!(S.ai||{}).busy?fuelBox(m):'')+'</div></div>'}
607
+ function fuelBox(m){var f=m.fuel;return '<div class="tc-fuel"><button class="b ok" onclick="topUp(\''+esc(m.id)+'\')">Swap $'+f.usd+' of '+esc(f.sell)+' to USDC and send</button><label class="tc-fchk"><input type="checkbox" id="fuelauto">Top up automatically from now on</label><div class="sm mut">That’s about '+Math.floor(f.usd/0.02)+' steps. Tollkit’s 0.5% fee is about $'+(f.usd*0.005).toFixed(2)+', and your trading limits apply. Or <a href="#" onclick="openAI();return false">connect your own Claude or ChatGPT</a> for free.</div></div>'}
608
+ function topUp(id){var cb=document.getElementById("fuelauto"),auto=!!(cb&&cb.checked);CH.sending=true;CH.err=null;render();api("/ai/topup",{id:id,auto:auto}).then(function(){return refresh()}).catch(function(e){CH.err=e.message}).then(function(){CH.sending=false;render()})}
609
+ function fuelRow(){var f=S.ai.fuel.mode;return '<div class="row"><div style="min-width:0"><div>Paying for Claude</div><div class="sm mut">$0.02 a step, in USDC. '+(f==="auto"?"When USDC runs low, the wallet swaps $2 of SOL or ETH to USDC on its own.":"The chat asks before swapping SOL or ETH to USDC.")+'</div></div><div class="r"><div class="bseg tmseg"><button class="bsb'+(f==="ask"?" on":"")+'" onclick="setFuel(\'ask\')">Ask</button><button class="bsb'+(f==="auto"?" on":"")+'" onclick="setFuel(\'auto\')">Auto</button></div></div></div>'}
610
+ function setFuel(m){api("/ai/fuel",{mode:m}).then(function(){flash=[1,m==="auto"?"Claude tops up its USDC on its own from now on.":"Claude will ask before topping up."];return refresh()}).catch(function(e){flash=[0,e.message];render()})}
578
611
  function svgClock(){return '<svg viewBox="0 0 24 24" width="13" height="13" aria-hidden="true"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 7v5l3 2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>'}
579
612
  function chatBox(){var A=S.ai;if(!A)return "";var M=A.messages||[],busy=A.busy||CH.sending;
580
613
  var head='<div class="tc-chd"><div class="tc-chn">'+aiSvg("claude")+'<div><b>'+esc(aiName())+'</b><small>'+(busy?'<span class="ok">Working…</span>':A.provider?esc(A.provider.note||'Your Claude, on this computer'):'Not set up')+'</small></div></div>'+
581
614
  '<div class="tc-chr"><label class="tc-chk" title="Let the AI review your rules on a schedule">'+svgClock()+'<select onchange="setCheckin(this.value)">'+[[0,"Check-ins off"],[1,"Every hour"],[4,"Every 4 hours"],[12,"Every 12 hours"],[24,"Daily"]].map(function(o){return '<option value="'+o[0]+'"'+(A.checkin.every_hours===o[0]?" selected":"")+'>'+o[1]+'</option>'}).join("")+'</select></label>'+(M.length?'<button class="b g tc-chx'+(CH.confirmNew?" tc-warn":"")+'" onclick="newChat()" title="Start over. Your rules stay on.">'+(CH.confirmNew?'Clear chat?':'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20h4L19 9l-4-4L4 16z M14 6l4 4" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>New chat')+'</button>':'')+'</div></div>';
582
615
  if(!A.provider)return '<div class="tc-chat">'+head+'<div class="tc-cempty"><b>Chat with your AI here</b><p>Install <b>Claude Code</b> on this computer and sign in once (run <code>claude</code> in Terminal). Then reopen this page. It uses your own Claude plan; the wallet never sees your Claude login.</p></div></div>';
583
- var body=M.length?M.map(chatMsg).join(""):'<div class="tc-cempty"><b>What do you want your autopilot to do?</b><p>Say it in your own words. '+esc(aiName())+' checks prices and your balances, then suggests rules you can turn on with one tap.</p><div class="tc-cchips">'+CHIPS.map(function(c,i){return '<button class="tc-cchip" onclick="chatSend(CHIPS['+i+'])">'+esc(c)+'</button>'}).join("")+'</div></div>';
584
- if(busy)body+='<div class="tc-cm tc-ai"><div class="tc-cav">'+aiSvg("claude")+'</div><div class="tc-cbw"><div class="tc-cb tc-ctyping"><i></i><i></i><i></i><span>'+(A.busy&&A.busy.kind==="checkin"?"Checking in on your rules…":"Looking at prices and your wallet…")+'</span></div></div></div>';
616
+ var body=M.length?M.map(function(m,i){return chatMsg(m,i===M.length-1)}).join(""):'<div class="tc-cempty"><b>What do you want your autopilot to do?</b><p>Say it in your own words. '+esc(aiName())+' checks prices and your balances, then suggests rules you can turn on with one tap.</p>'+(tkClaude()?'<p class="tc-own">Have Claude or ChatGPT already? <a href="#" onclick="openAI();return false">Connect it for free</a> and chat there instead.</p>':'')+'<div class="tc-cchips">'+CHIPS.map(function(c,i){return '<button class="tc-cchip" onclick="chatSend(CHIPS['+i+'])">'+esc(c)+'</button>'}).join("")+'</div></div>';
617
+ if(busy)body+='<div class="tc-cm tc-ai"><div class="tc-cav">'+aiSvg("claude")+'</div><div class="tc-cbw"><div class="tc-cb tc-ctyping"><i></i><i></i><i></i><span>'+(A.busy&&A.busy.note?esc(A.busy.note):A.busy&&A.busy.kind==="checkin"?"Checking in on your rules…":"Looking at prices and your wallet…")+'</span></div></div></div>';
585
618
  var pw=!A.ready?'<div class="tc-cpw"><div class="sm">One time: your wallet password lets '+esc(aiName())+' read your balances. It’s saved on this computer, readable only by your account.</div><div class="tc-crow"><input id="chpw" type="password" placeholder="Wallet password" value="'+esc(CH.pw)+'" oninput="CH.pw=this.value"><button class="b" onclick="chatPw()">Save</button></div></div>':"";
586
619
  var inp='<div class="tc-cin"><textarea id="chin" rows="1" placeholder="Message '+esc(aiName())+'…" oninput="CH.draft=this.value;grow(this)" onkeydown="if(event.key===\'Enter\'&&!event.shiftKey){event.preventDefault();chatSend()}"'+(A.ready?"":" disabled")+'>'+esc(CH.draft)+'</textarea><button class="b tc-csend" onclick="chatSend()"'+(busy||!A.ready?" disabled":"")+' aria-label="Send"><svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M5 12h13M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg></button></div>';
587
620
  return '<div class="tc-chat">'+head+'<div class="tc-cbody" id="cbody">'+body+'</div>'+(CH.err?'<div class="msg tc-err" style="margin:0 12px 10px">'+esc(CH.err)+'</div>':'')+pw+inp+'</div>'}
@@ -613,7 +646,7 @@ var prop=R.filter(function(r){return r.status==="proposed"&&!inChat[r.id]}),act=
613
646
  var pv=PV?'<div class="ap" style="margin-top:10px"><div class="sm mut" style="margin-bottom:6px">Preview: nothing runs until you turn it on</div>'+(PV.rules.length?PV.rules.map(function(r){return '<div style="margin:4px 0">• '+esc(r.description)+(r.problem?' <span class="bad sm">('+esc(r.problem)+')</span>':"")+'</div>'}).join(""):'<div class="bad">No rules understood.</div>')+(PV.unparsed.length?'<div class="sm warn" style="margin-top:6px">Not understood: '+esc(PV.unparsed.join(" · "))+'</div>':"")+'<div class="btns"><button class="b g" onclick="PV=null;render()">Discard</button><button class="b ok"'+(PV.rules.length&&!PV.rules.some(function(r){return r.problem})?"":" disabled")+' onclick="addRules()">Turn on</button></div></div>':"";
614
647
  var manualOpen=CH.manual||!S.ai||!(S.ai||{}).provider;
615
648
  return '<div class="sm">'+st+'</div><div class="sm mut" style="margin-top:3px">Every fill follows your limits and approvals, with the same 0.5% fee as any swap.</div>'+msg()+
616
- botsHtml()+'<h3>Ask Claude</h3>'+
649
+ (S.trade_mode==="python"?pyCard():"")+botsHtml()+'<h3>Ask Claude</h3>'+
617
650
  chatBox()+
618
651
  (prop.length?'<h3>Waiting for you</h3>'+prop.map(ruleCard).join(""):"")+
619
652
  '<h3>Your rules</h3>'+(act.length?act.map(ruleCard).join(""):'<div class="mut sm">No rules on yet.</div>')+
@@ -622,12 +655,12 @@ chatBox()+
622
655
  '<div class="toggles" style="margin-top:6px">'+TEMPL.map(function(t,i){return '<button class="chip" style="border:0;cursor:pointer" onclick="document.getElementById(\'stxt\').value=TEMPL['+i+']">'+esc(t)+'</button>'}).join("")+'</div>'+
623
656
  '<div style="margin-top:10px"><button class="b" onclick="preview()">Preview</button></div>'+pv+'</details>'}
624
657
  // ---- Bots: rule-based (no AI) bots, their status and results, and Tollkit's public board ----
625
- var BOT={open:null,tpl:null,chain:"base",token:"ETH",params:{},pv:null,err:null,busy:false,more:{}};
658
+ var BOT={open:null,tpl:null,anim:false,flash:null,chain:"base",token:"ETH",params:{},pv:null,err:null,busy:false,more:{}};
626
659
  function botPill(st){var m={running:["ok","Running"],waiting:["","Waiting for its price"],paused:["warn","Paused"],error:["bad","Stopped by errors"],done:["","Finished"],stopped:["","Stopped"]}[st]||["",st];return '<span class="chip botpill '+m[0]+'">'+m[1]+'</span>'}
627
660
  function money(n){return (n<0?"−":"")+fmtUsd(Math.abs(n))}
628
661
  function botCard(b){var res=b.result_usd!==null?'<span class="'+(b.result_usd>=0?"ok":"bad")+'">'+(b.result_usd>=0?"+":"")+money(b.result_usd)+(b.result_pct!==null?' ('+(b.result_pct>=0?"+":"")+b.result_pct+'%)':'')+'</span>':'<span class="mut">—</span>';
629
662
  var live=b.status==="running"||b.status==="waiting"||b.status==="error",open=BOT.more[b.id];
630
- return '<div class="botc"><div class="botct"><div style="min-width:0"><b>'+esc(b.name)+'</b><div class="sm mut">'+esc(CHAIN_NAMES[b.chain]||b.chain)+(b.last_trade?' · last trade '+ago(b.last_trade):' · no trades yet')+'</div></div>'+botPill(b.status)+'</div>'+
663
+ return '<div class="botc'+(BOT.flash===b.id?" botflash":"")+'" data-botcard="'+esc(b.id)+'"><div class="botct"><div style="min-width:0"><b>'+esc(b.name)+'</b><div class="sm mut">'+esc(CHAIN_NAMES[b.chain]||b.chain)+(b.last_trade?' · last trade '+ago(b.last_trade):' · no trades yet')+'</div></div>'+botPill(b.status)+'</div>'+
631
664
  '<div class="botstats"><div><small>Trades</small><b>'+b.trades_ok+'</b></div><div><small>Failed</small><b class="'+(b.trades_failed?"bad":"")+'">'+b.trades_failed+'</b></div><div><small>Success</small><b>'+(b.success_rate===null?"—":b.success_rate+"%")+'</b></div><div><small>Result</small><b>'+res+'</b></div></div>'+
632
665
  (b.round_trips?'<div class="sm mut" style="margin-top:6px">'+b.round_trips+' buy-and-sell round trip'+(b.round_trips>1?"s":"")+' done</div>':'')+
633
666
  '<div class="btns" style="margin-top:8px"><button class="b g" data-botmore="'+esc(b.id)+'">'+(open?"Hide":"Details")+'</button>'+(b.status==="paused"?'<button class="b ok" data-botst="active" data-bot="'+esc(b.id)+'">Resume</button>':live?'<button class="b g" data-botst="paused" data-bot="'+esc(b.id)+'">Pause</button>':'')+(b.status!=="done"&&b.status!=="stopped"?'<button class="b no" data-botst="cancelled" data-bot="'+esc(b.id)+'">Stop</button>':'')+'</div>'+
@@ -636,22 +669,34 @@ function boardFor(id){var B=S.bot_board;if(!B||!B.templates)return null;for(var
636
669
  function boardLine(id){var x=boardFor(id);if(!x||x.bots<3)return '<div class="sm mut">Not enough shared results yet.</div>';return '<div class="sm mut">'+x.bots+' bots shared · median result '+(x.median_result_pct>=0?"+":"")+x.median_result_pct+'% · '+x.success_rate+'% of trades went through</div>'}
637
670
  function botsHtml(){var L=S.bots||[],T=S.bot_templates||[];
638
671
  var h='<h3>Your bots</h3>'+(L.length?L.map(botCard).join(""):'<div class="mut sm">No bots yet. Start one below; it follows your limits like any trade.</div>');
639
- h+='<h3>Start a bot</h3><div class="sm mut" style="margin-bottom:6px">Plain rules, no AI. You see every step before it starts.</div><div class="botcat">'+T.map(function(t){return '<button class="bottpl'+(BOT.tpl===t.id?" on":"")+'" data-tpl="'+esc(t.id)+'"><b>'+esc(t.name)+'</b><small>'+esc(t.blurb)+'</small>'+boardLine(t.id)+'</button>'}).join("")+'</div>';
640
- if(BOT.tpl)h+=botForm();
672
+ h+='<h3>Start a bot</h3><div class="sm mut" style="margin-bottom:6px">Plain rules, no AI. You see every step before it starts.</div><div class="botcat">'+T.map(function(t){var on=BOT.tpl===t.id;return tplCard(t,on,L)+(on?'<div class="botwrap'+(BOT.anim?" drop":"")+'"><div class="botin">'+botForm()+'</div></div>':'')}).join("")+'</div>';
673
+ BOT.anim=false;
641
674
  h+='<label class="botshare"><input type="checkbox" id="botshare"'+(S.bot_share?" checked":"")+' onchange="botShare(this.checked)"> Share my bots’ results with Tollkit’s public bot board (anonymous: bot type, token, trade counts and result %, no addresses or amounts)</label>';
642
675
  return h}
676
+ var ARROW='<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>';
677
+ function tplCard(t,on,L){var mine=L.filter(function(b){return b.template===t.id}),live=mine.filter(function(b){return b.status!=="done"&&b.status!=="stopped"});
678
+ var head='<div class="tplh"><b>'+esc(t.name)+'</b>'+(live.length===1?botPill(live[0].status):live.length>1?'<span class="chip botpill ok">'+live.length+' bots</span>':'')+'</div>';
679
+ if(!live.length){var past=mine.length?'<div class="sm mut">Your last one: '+esc(({done:"finished",stopped:"stopped"})[mine[0].status]||mine[0].status)+'.</div>':'';
680
+ return '<button class="bottpl'+(on?" on":"")+'" data-tpl="'+esc(t.id)+'" aria-expanded="'+on+'">'+head+'<small>'+esc(t.blurb)+'</small>'+boardLine(t.id)+past+'<span class="botcta">'+(on?'Close setup':(mine.length?'Set up again':'Set up '+esc(t.name)+' bot')+ARROW)+'</span></button>'}
681
+ var b=live[0],line;if(live.length===1){var res=b.result_usd!==null?' · <span class="'+(b.result_usd>=0?"ok":"bad")+'">'+(b.result_usd>=0?"+":"")+money(b.result_usd)+'</span>':'';line=esc(b.token)+' on '+esc(CHAIN_NAMES[b.chain]||b.chain)+' · '+b.trades_ok+' trade'+(b.trades_ok===1?"":"s")+(b.trades_failed?' · <span class="bad">'+b.trades_failed+' failed</span>':'')+res+(b.last_trade?' · last '+ago(b.last_trade):'')}
682
+ else{var c={};live.forEach(function(x){c[x.status]=(c[x.status]||0)+1});line=Object.keys(c).map(function(k){return c[k]+' '+({running:"running",waiting:"waiting for a price",paused:"paused",error:"stopped by errors"})[k]}).join(" · ")}
683
+ return '<div class="bottpl has'+(on?" on":"")+(live.some(function(x){return x.status==="error"})?" err":"")+'">'+head+'<div class="sm">'+line+'</div>'+boardLine(t.id)+'<div class="tplbtns"><button class="botcta" data-botview="'+esc(b.id)+'">'+(live.length>1?'View bots':'View bot')+ARROW+'</button><button class="botcta g" data-tpl="'+esc(t.id)+'" aria-expanded="'+on+'">'+(on?'Close setup':'+ Set up another')+'</button></div></div>'}
643
684
  function botForm(){var t=(S.bot_templates||[]).filter(function(x){return x.id===BOT.tpl})[0];if(!t)return "";
644
- var f=t.fields.map(function(x){var v=BOT.params[x.key]!==undefined?BOT.params[x.key]:x.default;if(x.kind==="choice")return '<label class="botf"><span>'+esc(x.label)+'</span><select data-bp="'+esc(x.key)+'">'+x.choices.map(function(c){return '<option value="'+esc(c[0])+'"'+(String(v)===c[0]?" selected":"")+'>'+esc(c[1])+'</option>'}).join("")+'</select></label>';return '<label class="botf"><span>'+esc(x.label)+'</span><input type="number" step="any" data-bp="'+esc(x.key)+'" value="'+esc(String(v))+'"></label>'}).join("");
685
+ var f=t.fields.map(function(x){var v=BOT.params[x.key]!==undefined?BOT.params[x.key]:x.default;if(x.kind==="choice")return '<label class="botf"><span>'+esc(x.label)+'</span><select data-bp="'+esc(x.key)+'">'+x.choices.map(function(c){return '<option value="'+esc(c[0])+'"'+(String(v)===c[0]?" selected":"")+'>'+esc(c[1])+'</option>'}).join("")+'</select></label>';var lim=x.kind==="pct"&&x.max!==undefined?' <em class="botlim">up to '+Number(x.max).toLocaleString()+'%</em>':'';return '<label class="botf"><span>'+esc(x.label)+lim+'</span><input type="number" step="any" inputmode="decimal" data-bp="'+esc(x.key)+'"'+(x.min!==undefined?' min="'+x.min+'"':'')+(x.max!==undefined?' max="'+x.max+'"':'')+' value="'+esc(String(v))+'"></label>'}).join("");
645
686
  return '<div class="ap botform" style="white-space:normal"><b>'+esc(t.name)+'</b><div class="sm mut" style="margin:4px 0 8px">Risk: '+esc(t.risk)+'</div><div class="botgrid"><label class="botf"><span>Network</span><select id="bot_chain">'+[["base","Base"],["ethereum","Ethereum"],["solana","Solana"]].map(function(o){return '<option value="'+o[0]+'"'+(BOT.chain===o[0]?" selected":"")+'>'+o[1]+'</option>'}).join("")+'</select></label><label class="botf"><span>Token (symbol or address)</span><input id="bot_token" value="'+esc(BOT.token)+'" autocomplete="off" spellcheck="false"></label>'+f+'</div>'+
646
687
  (BOT.err?'<div class="msg err">'+esc(BOT.err)+'</div>':'')+(BOT.pv?'<div class="sm" style="margin-top:8px"><div class="mut">'+(BOT.pv.price?'Today’s price about $'+Number(BOT.pv.price).toLocaleString(undefined,{maximumSignificantDigits:6})+'. ':'')+'It will:</div>'+BOT.pv.rules.map(function(r){return '<div style="margin:4px 0">• '+esc(r.description)+'</div>'}).join("")+'</div>':'')+
647
- '<div class="btns"><button class="b g" onclick="BOT.tpl=null;BOT.pv=null;BOT.err=null;render()">Cancel</button>'+(BOT.pv?'<button class="b ok" onclick="botGo(\'start\')"'+(BOT.busy?" disabled":"")+'>Start bot</button>':'<button class="b" onclick="botGo(\'preview\')"'+(BOT.busy?" disabled":"")+'>Preview</button>')+'</div></div>'}
688
+ '<div class="btns"><button class="b g" onclick="botClose()">Cancel</button>'+(BOT.pv?'<button class="b ok" onclick="botGo(\'start\')"'+(BOT.busy?" disabled":"")+'>Start bot</button>':'<button class="b" onclick="botGo(\'preview\')"'+(BOT.busy?" disabled":"")+'>Preview</button>')+'</div></div>'}
648
689
  function botRead(){var c=document.getElementById("bot_chain"),t=document.getElementById("bot_token");if(c)BOT.chain=c.value;if(t)BOT.token=t.value.trim();document.querySelectorAll("[data-bp]").forEach(function(el){BOT.params[el.getAttribute("data-bp")]=el.tagName==="SELECT"?el.value:Number(el.value)})}
649
690
  function botGo(what){botRead();BOT.busy=true;BOT.err=null;render();api("/bots/"+what,{template:BOT.tpl,chain:BOT.chain,token:BOT.token,params:BOT.params}).then(function(r){BOT.busy=false;if(what==="preview"){BOT.pv=r;render()}else{BOT.tpl=null;BOT.pv=null;flash=[1,"Bot started. It shows under Your bots."];refresh().then(function(){render()})}}).catch(function(e){BOT.busy=false;BOT.err=e.message;render()})}
691
+ function botClose(){var w=document.querySelector(".botwrap");BOT.pv=null;BOT.err=null;var done=function(){BOT.tpl=null;render()};if(!w||(window.matchMedia&&matchMedia("(prefers-reduced-motion: reduce)").matches)){done();return}w.classList.remove("drop");w.classList.add("closing");setTimeout(done,240)}
650
692
  function botShare(on){api("/bots/share",{on:on}).then(function(){refresh()}).catch(function(e){flash=[0,e.message];render()})}
651
- document.addEventListener("click",function(e){var t=e.target.closest&&e.target.closest("[data-tpl],[data-botst],[data-botmore]");if(!t)return;
652
- if(t.hasAttribute("data-tpl")){var id=t.getAttribute("data-tpl");BOT.tpl=BOT.tpl===id?null:id;BOT.params={};BOT.pv=null;BOT.err=null;if(BOT.chain==="solana"&&BOT.token==="ETH")BOT.token="SOL";render();return}
693
+ document.addEventListener("click",function(e){var t=e.target.closest&&e.target.closest("[data-tpl],[data-botst],[data-botmore],[data-botview]");if(!t)return;
694
+ if(t.hasAttribute("data-botview")){var vid=t.getAttribute("data-botview");BOT.more[vid]=true;BOT.flash=vid;render();var card=document.querySelector('[data-botcard="'+vid+'"]');if(card&&card.scrollIntoView)card.scrollIntoView({behavior:RM?"auto":"smooth",block:"center"});setTimeout(function(){if(BOT.flash===vid){BOT.flash=null;var c2=document.querySelector('[data-botcard="'+vid+'"]');if(c2)c2.classList.remove("botflash")}},1600);return}
695
+ if(t.hasAttribute("data-tpl")){var id=t.getAttribute("data-tpl");if(BOT.tpl===id){botClose();return}BOT.tpl=id;BOT.anim=true;BOT.params={};BOT.pv=null;BOT.err=null;if(BOT.chain==="solana"&&BOT.token==="ETH")BOT.token="SOL";render();setTimeout(function(){var w=document.querySelector(".botwrap");if(!w)return;w.classList.remove("drop");if(w.scrollIntoView)w.scrollIntoView({behavior:"smooth",block:"nearest"})},460);return}
653
696
  if(t.hasAttribute("data-botmore")){var k=t.getAttribute("data-botmore");BOT.more[k]=!BOT.more[k];render();return}
654
697
  var st=t.getAttribute("data-botst"),bid=t.getAttribute("data-bot");api("/bots/status",{bot:bid,status:st}).then(function(){flash=[1,st==="paused"?"Paused.":st==="active"?"Resumed.":"Stopped."];refresh().then(function(){render()})}).catch(function(e){flash=[0,e.message];render()})});
698
+ document.addEventListener("input",function(e){var t=e.target;if(!t||!t.hasAttribute||!t.hasAttribute("data-bp")||t.tagName!=="INPUT"||t.value==="")return;var mx=t.getAttribute("max");if(mx!==null&&Number(t.value)>Number(mx)){t.value=mx;t.classList.remove("botcap");void t.offsetWidth;t.classList.add("botcap")}});
699
+ document.addEventListener("focusout",function(e){var t=e.target;if(!t||!t.hasAttribute||!t.hasAttribute("data-bp")||t.tagName!=="INPUT")return;var mn=t.getAttribute("min");if(mn!==null&&(t.value===""||Number(t.value)<Number(mn)))t.value=mn});
655
700
  document.addEventListener("change",function(e){if(e.target&&e.target.id==="bot_chain"){BOT.chain=e.target.value;var ti=document.getElementById("bot_token");if(ti&&(ti.value==="ETH"||ti.value==="SOL")){ti.value=BOT.chain==="solana"?"SOL":"ETH";BOT.token=ti.value}BOT.pv=null}else if(e.target&&(e.target.id==="bot_token"||e.target.hasAttribute&&e.target.hasAttribute("data-bp")))BOT.pv=null});
656
701
  function preview(){var t=document.getElementById("stxt").value;api("/rules/preview",{text:t}).then(function(r){PV=r;flash=null;render()}).catch(function(e){flash=[0,e.message];render()})}
657
702
  function addRules(){var rules=PV.rules.map(function(r){var o={};for(var k in r)if(k!=="description"&&k!=="problem")o[k]=r[k];return o});api("/rules",{rules:rules}).then(function(){PV=null;flash=[1,"Turned on. The autopilot is watching prices."];refresh()}).catch(function(e){flash=[0,e.message];render()})}
@@ -679,15 +724,34 @@ function rowUsd(e){if(Number(e.usd)>0)return usd(e.usd);var m=(e.kind==="stake"?
679
724
  function usd(n){n=Number(n)||0;return "$"+n.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:n<1&&n>0?4:2})}
680
725
  function ago(ts){var s=(Date.now()-Date.parse(ts))/1000;if(s<60)return "just now";if(s<3600)return Math.floor(s/60)+"m ago";if(s<86400)return Math.floor(s/3600)+"h ago";return new Date(ts).toLocaleDateString()}
681
726
  var ICONS={autopilot:'<path d="M13 2L4 14h7l-1 8 9-12h-7z"/>',home:'<path d="M3 11l9-7 9 7v9a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z"/>',activity:'<path d="M3 12h4l3-8 4 16 3-8h4"/>',approvals:'<path d="M5 13l4 4L19 7"/>',settings:'<path d="M4 6h10M18 6h2M4 12h4M12 12h8M4 18h12"/><circle cx="16" cy="6" r="2"/><circle cx="10" cy="12" r="2"/><circle cx="18" cy="18" r="2"/>',withdraw:'<path d="M12 4v12m0 0l-5-5m5 5l5-5M5 20h14"/>'};
682
- var TABS=["home","activity","autopilot","approvals","settings","withdraw"],lastBadge=-1,lastAuto=-1;
683
- function nav(){var el=document.getElementById("nav");if(!el.querySelector("button")){el.innerHTML='<div class="ind" id="ind"></div>'+TABS.map(function(k){return '<button data-k="'+k+'" onclick="go(\''+k+'\')"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'+ICONS[k]+'</svg>'+(k==="autopilot"?"Bots":k[0].toUpperCase()+k.slice(1))+'</button>'}).join("")}
727
+ var TABS=["home","activity","autopilot","approvals","settings","withdraw"];
728
+ function nav(){var el=document.getElementById("nav");if(!el.querySelector("button")){el.innerHTML='<div class="ind" id="ind"></div>'+TABS.map(function(k){return '<button data-k="'+k+'" onclick="go(\''+k+'\')">'+(k==="autopilot"?'<span class="emo" aria-hidden="true">\u{1F916}</span>':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">'+ICONS[k]+'</svg>')+(k==="autopilot"?"Bots":k[0].toUpperCase()+k.slice(1))+'</button>'}).join("")}
684
729
  el.querySelectorAll("button").forEach(function(b){b.classList.toggle("on",b.getAttribute("data-k")===tab)});
685
730
  document.getElementById("ind").style.transform="translateX("+(TABS.indexOf(tab)*100)+"%)";
686
- var na=S&&S.rules?S.rules.filter(function(r){return r.status==="proposed"}).length:0;if(na!==lastAuto){lastAuto=na;var aa=el.querySelector('[data-k="autopilot"]'),o2=aa.querySelector(".badge");if(o2)o2.remove();if(na){var s2=document.createElement("span");s2.className="badge";s2.textContent=na;aa.appendChild(s2)}}
687
- var n=S?S.pending.length:0;if(n!==lastBadge){lastBadge=n;var ab=el.querySelector('[data-k="approvals"]'),old=ab.querySelector(".badge");if(old)old.remove();if(n){var sp=document.createElement("span");sp.className="badge";sp.textContent=n;ab.appendChild(sp)}}}
688
- function go(k){if(k===tab&&!flash)return;tab=k;flash=null;render(true)}
731
+ navMark(el)}
732
+ // Why a tab is worth a tap. Activity and Home only count what's new since this page opened (or since you last looked).
733
+ var SEEN=null;
734
+ function navSeen(k){if(!S||!SEEN)return;if(k==="activity")(S.receipts||[]).forEach(function(r){SEEN.rc[r.id]=r.status});if(k==="home"&&S.live)S.live.events.forEach(function(e){SEEN.ev[e.id]=1})}
735
+ function navWhy(){var w={};if(!S)return w;
736
+ if(!SEEN){SEEN={rc:{},ev:{}};navSeen("activity");navSeen("home")}
737
+ if(tab==="activity"||tab==="home")navSeen(tab);
738
+ var np=(S.pending||[]).length;if(np)w.approvals={n:np,lvl:"bad",t:np+(np>1?" requests are":" request is")+" waiting for your OK"};
739
+ var prop=(S.rules||[]).filter(function(r){return r.status==="proposed"}).length,berr=(S.bots||[]).filter(function(b){return b.status==="error"}).length;
740
+ if(prop||berr)w.autopilot={n:prop+berr,lvl:berr?"bad":"acc",t:[prop?prop+" suggested rule"+(prop>1?"s":"")+" to review":"",berr?berr+" bot"+(berr>1?"s":"")+" stopped by errors":""].filter(Boolean).join(" · ")};
741
+ var bk=S.backup;if(bk&&!bk.done)w.settings={lvl:"warn",t:"Your wallet isn't backed up yet"};
742
+ var nr=0,nf=0;(S.receipts||[]).forEach(function(r){var was=SEEN.rc[r.id];if(was===undefined||(r.status==="failed"&&was!=="failed")){nr++;if(r.status==="failed")nf++}});
743
+ if(nr)w.activity={n:nr,lvl:nf?"bad":"acc",t:nr+" new in Activity"+(nf?" ("+nf+" failed)":"")};
744
+ var nd=S.live?S.live.events.filter(function(e){return e.direction==="in"&&!SEEN.ev[e.id]}).length:0;
745
+ if(nd)w.home={n:nd,lvl:"ok",t:nd+" new deposit"+(nd>1?"s":"")};
746
+ return w}
747
+ function navMark(el){var w=navWhy();el.querySelectorAll("button").forEach(function(b){var k=b.getAttribute("data-k"),r=w[k],name=k==="autopilot"?"Bots":k[0].toUpperCase()+k.slice(1);
748
+ var key=r?r.lvl+"|"+(r.n||"")+"|"+r.t:"";if(b.getAttribute("data-why")===key)return;b.setAttribute("data-why",key);
749
+ b.classList.remove("attn","attn-bad","attn-warn","attn-ok","attn-acc");var old=b.querySelector(".badge");if(old)old.remove();
750
+ b.title=r?r.t:"";b.setAttribute("aria-label",r?name+": "+r.t:name);if(!r)return;
751
+ b.classList.add("attn","attn-"+r.lvl);var sp=document.createElement("span");sp.className="badge"+(r.n?"":" dot");sp.textContent=r.n?(r.n>99?"99+":String(r.n)):"";b.appendChild(sp)})}
752
+ function go(k){if(k==="autopilot"&&S&&!S.trade_mode)openTM();if(k===tab&&!flash)return;var changed=k!==tab;tab=k;flash=null;render(true);if(changed){var mm=document.getElementById("main");if(mm)mm.scrollTop=0}}
689
753
  function msg(){return flash?'<div class="msg '+(flash[0]?"okk":"err")+'">'+esc(flash[1])+'</div>':""}
690
- function render(enter){nav();if(!S)return;var a=S.approvals||"none";var ap=document.getElementById("appr");ap.classList.toggle("live",a!=="none"||!!S.live);ap.textContent=(S.live?"Live":"")+(S.live?" · ":"")+(a==="none"?"Approvals off":"Approvals on");ap.title=a==="none"?"Set up Telegram or keep this page open":"Answered via "+a.replace("+"," and ");ap.onclick=function(){go("settings")};ap.style.cursor="pointer";var m=document.getElementById("main");
754
+ function render(enter){nav();if(!S)return;if(PYO)pyRender();var a=S.approvals||"none";var ap=document.getElementById("appr");ap.classList.toggle("live",a!=="none"||!!S.live);ap.textContent=(S.live?"Live":"")+(S.live?" · ":"")+(a==="none"?"Approvals off":"Approvals on");ap.title=a==="none"?"Set up Telegram or keep this page open":"Answered via "+a.replace("+"," and ");ap.onclick=function(){go("settings")};ap.style.cursor="pointer";var m=document.getElementById("main");
691
755
  if(tab!=="autopilot")LASTAP=null;if(tab!=="settings"){LIMD={};LIMPW=""}var prevTab=m.dataset.tab;m.dataset.tab=tab;if(tab==="home")m.innerHTML=home();else if(tab==="activity")m.innerHTML=activity();else if(tab==="approvals")m.innerHTML=approvals();else if(tab==="settings")m.innerHTML=settings();else if(tab==="autopilot"){var h=autopilot();if(!enter&&h===LASTAP&&prevTab==="autopilot"){chatAfter();return}m.innerHTML=h;LASTAP=h;chatAfter()}else{m.innerHTML=withdraw();tokOpts()}
692
756
  after(m,enter)}
693
757
  var shown=null;
@@ -725,6 +789,46 @@ function limT(id,on,text){var c=lv(id,on);return '<label><input type="checkbox"
725
789
  function setRow(icon,title,sub,btn){return '<div class="row">'+icon+'<div style="min-width:0"><div>'+title+'</div><div class="sm mut">'+sub+'</div></div><div class="r">'+btn+'</div></div>'}
726
790
  var SICO={ai:'<path d="M12 3v3M12 18v3M3 12h3M18 12h3"/><rect x="7" y="7" width="10" height="10" rx="2"/>',tg:'<path d="M21 4L3 11l6 2 2 6 3-4 5 4z"/>',bk:'<path d="M12 3l8 4v5c0 5-3.5 8-8 9-4.5-1-8-4-8-9V7z"/>',ext:'<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3c2.5 2.7 3.8 5.7 3.8 9s-1.3 6.3-3.8 9c-2.5-2.7-3.8-5.7-3.8-9S9.5 5.7 12 3z"/>'};
727
791
  function sIco(k){return '<div class="ic"><svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">'+SICO[k]+'</svg></div>'}
792
+ // Opening the Bots tab before picking: AI trading or Python trading. Python bots get the wallet's bot link (the same one AI apps use).
793
+ var TMO=null,PYO=null,PYPW="",PYMSG=null,PYBUSY=false;
794
+ var CODEICO='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 7l-5 5 5 5M16 7l5 5-5 5M13.5 4l-3 16"/></svg>';
795
+ function openTM(){if(TMO)return;TMO=document.createElement("div");TMO.className="aio tmo";
796
+ TMO.innerHTML='<div class="aidlg tmdlg" role="dialog" aria-modal="true" aria-label="How do you want to trade?"><h2>How do you want to trade?</h2><p class="ailead">Pick one to start. You can switch any time in Settings.</p><div class="tmopts">'+
797
+ (tkClaude()?'<div class="tmopt tmopt2" style="--i:0"><span class="tmico tmai">'+aiSvg("claude")+'</span><b>AI trading</b><small>Tell an AI what you want in plain words. It suggests rules and bots, and nothing runs until you turn it on.</small><div class="tmbtns"><button class="botcta" data-tm="ai-own">Use my Claude or ChatGPT · free'+ARROW+'</button><button class="botcta" data-tm="ai">Use Tollkit’s Claude · $0.02 a step'+ARROW+'</button></div></div>':
798
+ '<button class="tmopt" data-tm="ai" style="--i:0"><span class="tmico tmai">'+aiSvg("claude")+'</span><b>AI trading</b><small>Tell Claude what you want in plain words. It suggests rules and bots, and nothing runs until you turn it on. You can also connect your own AI app.</small><span class="botcta">Start with AI'+ARROW+'</span></button>')+
799
+ '<button class="tmopt" data-tm="python" style="--i:1"><span class="tmico tmpy">'+CODEICO+'</span><b>Python trading</b><small>Run your own Python bots: four starter strategies, backtests on past prices and paper trading with pretend money.</small><span class="botcta">Start with Python'+ARROW+'</span></button>'+
800
+ '</div><p class="aismall" style="text-align:center;margin-bottom:4px">Either way, every trade stays inside your spending limits and approvals.</p><div style="text-align:center"><button class="aihand tmlater" data-tm="later">Decide later</button></div></div>';
801
+ TMO.addEventListener("click",function(e){var b=e.target.closest("[data-tm]");if(!b)return;var m=b.getAttribute("data-tm");if(m==="later"){closeTM();return}pickTM(m,true)});document.body.appendChild(TMO)}
802
+ function closeTM(){if(TMO){TMO.remove();TMO=null}}
803
+ function tkClaude(){return !!(S&&S.ai&&S.ai.provider&&S.ai.provider.id==="tollkit")}
804
+ function pickTM(m,first){var own=m==="ai-own";if(own)m="ai";api("/trade-mode",{mode:m}).then(function(){closeTM();return refresh()}).then(function(){if(!first){render();return}if(own){go("autopilot");openAI();return}if(m==="python"){go("autopilot");openPy()}else{go("autopilot");setTimeout(function(){var c=document.querySelector(".tc-chat");if(c&&c.scrollIntoView)c.scrollIntoView({behavior:RM?"auto":"smooth",block:"start"})},120)}}).catch(function(e){flash=[0,e.message];closeTM();render()})}
805
+ function pyLocalEnv(){var l=(S.connect||{}).launch||{},env=l.env||{};return Object.keys(env).filter(function(k){return /^TOLLKIT_WALLET_/.test(k)}).map(function(k){return 'export '+k+'="'+env[k]+'"'}).join("\n")}
806
+ function pyBody(){var c=S.connect||{},r=c.remote,lim=S.policy||{},m=PYMSG,n=0;
807
+ function step(t){n++;return '<div class="pystep"><span class="pyn">'+n+'</span><b>'+t+'</b></div>'}
808
+ var h='<h2>Python bots</h2><p class="ailead">Run your own trading bots in Python. They trade through this wallet, inside your limits, and can’t withdraw, change limits or see your keys.</p>';
809
+ h+=step("Install")+aiCode("py-i","pip install tollkit-wallet")+'<p class="aismall">Python 3.9 or newer. No other packages needed.</p>';
810
+ var ready=false,env="";
811
+ if(r){h+=step("Your bot link");
812
+ if(!r.on){h+='<label class="ailbl" for="pypw">Wallet password</label><input id="pypw" class="aiin" type="password" autocomplete="current-password" placeholder="Needed to make the link" value="'+esc(PYPW)+'"><button class="b aigoap" data-pyact="on"'+(PYBUSY?" disabled":"")+'>'+(PYBUSY?"Making it…":"Make my bot link")+'</button>'+(m?'<div class="aim '+(m[0]?"ok":"bad")+'">'+esc(m[1])+'</div>':'')+'<p class="aismall">Your bot sends its requests to Tollkit’s server, which passes them to this wallet while Chrome is open and the wallet is unlocked. Your keys stay here.</p>'}
813
+ else{ready=true;env='export TOLLKIT_LINK="'+r.url+'"';h+=aiCode("py-link",r.url)+'<p class="aismall"><span class="'+(r.online?"ok":"warn")+'">●</span> '+(r.online?"The wallet is reachable.":"Waiting for the wallet to check in…")+' Keep this link private: anyone with it can trade inside your limits. It’s the same link your AI apps use. Keep Chrome open with the wallet unlocked while a bot runs.</p>'+
814
+ '<details class="pymore"><summary>New link or turn off</summary><label class="ailbl" for="pypw">Wallet password (for a new link)</label><input id="pypw" class="aiin" type="password" autocomplete="current-password" value="'+esc(PYPW)+'"><div class="btns" style="margin-top:8px"><button class="b g" data-pyact="new"'+(PYBUSY?" disabled":"")+'>New link</button><button class="b no" data-pyact="off"'+(PYBUSY?" disabled":"")+'>Turn off</button></div><p class="aismall">A new link stops the old one, for bots and AI apps alike.</p></details>'+(m?'<div class="aim '+(m[0]?"ok":"bad")+'">'+esc(m[1])+'</div>':'')}}
815
+ else{h+=step("Let your scripts unlock the wallet");
816
+ if(c.ready){ready=true;env=pyLocalEnv();h+='<p class="aismall"><span class="ok">●</span> Saved. Scripts on this computer unlock the wallet with a copy of your password that only your account can read.</p>'}
817
+ else h+='<label class="ailbl" for="pypw">Wallet password</label><input id="pypw" class="aiin" type="password" autocomplete="current-password" value="'+esc(PYPW)+'"><button class="b aigoap" data-pyact="pw"'+(PYBUSY?" disabled":"")+'>'+(PYBUSY?"Saving…":"Save it for my scripts")+'</button>'+(m?'<div class="aim '+(m[0]?"ok":"bad")+'">'+esc(m[1])+'</div>':'')+'<p class="aismall">Saves a copy of the password on this computer, readable only by your account. Anyone who can read it and your wallet folder could trade inside your limits.</p>'}
818
+ if(ready){h+=step("Try it")+aiCode("py-try",(env?env+"\n":"")+"tollkit-bot check\ntollkit-bot backtest dip-buyer\ntollkit-bot run dip-buyer")+'<p class="aismall">Paper trading (pretend money, live prices) unless you add <code>--live</code>. Built in: <code>dca</code>, <code>dip-buyer</code>, <code>momentum</code>, <code>rebalance</code>.'+(r?' On Windows PowerShell: <code>$env:TOLLKIT_LINK="…"</code>.':'')+'</p>'+
819
+ step("Write your own")+aiCode("py-code",'from tollkit_wallet import Strategy, buy, hold\n\nclass BuyDips(Strategy):\n token, chain = "ETH", "base"\n def decide(self, m, notes):\n if m.change_pct(24) is not None and m.change_pct(24) <= -5 and m.cash >= 20:\n return buy(20, "down 5% in a day")\n return hold()')+
820
+ '<p class="aismall"><code>tollkit-bot new my_bot.py</code> writes a full example to edit. Trades over '+usd(lim.approval_above_usd||0)+' wait for you in Approvals.</p>'}
821
+ return h}
822
+ function pyRender(){if(!PYO)return;var d=PYO.querySelector(".aidlg"),sc=d.scrollTop,foc=document.activeElement&&document.activeElement.id==="pypw";PYO.querySelector(".aibody").innerHTML=pyBody();d.scrollTop=sc;var pw=document.getElementById("pypw");if(pw){pw.oninput=function(){PYPW=pw.value};if(foc)pw.focus()}}
823
+ function openPy(){if(!PYO){PYO=document.createElement("div");PYO.className="aio";PYO.innerHTML='<div class="aidlg" role="dialog" aria-modal="true" aria-label="Python bots"><button class="aix" aria-label="Close">×</button><div class="aibody"></div></div>';PYO.addEventListener("mousedown",function(e){if(e.target===PYO)closePy()});PYO.addEventListener("click",pyClick);document.body.appendChild(PYO)}pyRender()}
824
+ function closePy(){if(PYO){PYO.remove();PYO=null;PYPW="";PYMSG=null}}
825
+ function pyDo(act){if(act!=="off"&&!PYPW){PYMSG=[0,"Type your wallet password first."];pyRender();var p=document.getElementById("pypw");if(p)p.focus();return}
826
+ PYBUSY=true;PYMSG=null;pyRender();(act==="pw"?api("/ai/password",{password:PYPW}):api("/connect/remote",{password:PYPW,action:act})).then(function(){PYPW="";PYMSG=act==="off"?[1,"Turned off. No bot or app can reach the wallet now."]:act==="new"?[1,"New link made. Update it in your bots and AI apps."]:null;return refresh()}).catch(function(e){PYMSG=[0,e.message]}).then(function(){PYBUSY=false;pyRender()})}
827
+ function pyClick(e){var t=e.target.closest("button");if(!t)return;if(t.classList.contains("aix"))closePy();else if(t.hasAttribute("data-pyact"))pyDo(t.getAttribute("data-pyact"));else if(t.hasAttribute("data-copy")){var el=document.getElementById(t.getAttribute("data-copy")),v=el?el.textContent:"",done=function(){t.textContent="Copied";setTimeout(function(){t.textContent="Copy"},1500)};if(navigator.clipboard&&navigator.clipboard.writeText)navigator.clipboard.writeText(v).then(done,function(){fallback(v);done()});else{fallback(v);done()}}}
828
+ function pyStatus(){var c=S.connect||{},r=c.remote;if(r)return r.on?(r.online?'<span class="ok">●</span> Bot link on, wallet reachable':'<span class="warn">●</span> Bot link on, waiting for the wallet'):'No bot link yet';return c.ready?'<span class="ok">●</span> Ready for scripts on this computer':'Not set up yet'}
829
+ function pyCard(){return '<div class="pycard"><div class="pyct"><span class="tmico tmpy sm">'+CODEICO+'</span><div style="min-width:0"><b>Python bots</b><div class="sm mut">'+pyStatus()+'</div></div><button class="botcta" onclick="openPy()">'+(((S.connect||{}).remote||{}).on||(!(S.connect||{}).remote&&(S.connect||{}).ready)?"Show setup":"Set up")+ARROW+'</button></div></div>'}
830
+ function tradeRow(){var m=S.trade_mode;return '<div class="row"><div style="min-width:0"><div>Trading with</div><div class="sm mut">'+(m==="python"?"Your own Python bots.":m==="ai"?"Claude and your AI apps.":"Not picked yet.")+' Both use the same limits.</div></div><div class="r"><div class="bseg tmseg"><button class="bsb'+(m==="ai"?" on":"")+'" onclick="pickTM(\'ai\',false)">AI</button><button class="bsb'+(m==="python"?" on":"")+'" onclick="pickTM(\'python\',false)">Python</button></div></div></div>'+
831
+ setRow('<div class="ic">'+CODEICO.replace('aria-hidden','width="17" height="17" aria-hidden')+'</div>',"Python bots",pyStatus(),'<button class="b g sbtn" onclick="openPy()">'+(S.trade_mode==="python"?"Setup":"Set up")+'</button>')}
728
832
  function settings(){var p=S.policy,sp=S.spent_today||{},dirty=Object.keys(LIMD).length>0;
729
833
  var lim='<h3>Spending limits</h3><div class="mut sm">Your AI and autopilot have to stay inside these and can\'t change them. Saving needs your password.</div>'+
730
834
  '<div class="sgrp">Swaps</div><div class="grid2">'+limF("per_trade_usd","Per swap ($)","Largest single swap")+limF("daily_trades_usd","Swaps per day ($)",null,sp.trades_usd||0)+limF("approval_above_usd","Ask me above ($)","Bigger swaps wait for your OK")+limF("max_slippage_bps","Max slippage (bps)","100 bps = 1%")+'</div>'+
@@ -745,7 +849,7 @@ var exr=setRow(sIco("ext"),"Browser extension",ex.paired?'<span class="ok">Paire
745
849
  (EXC?'<div class="row" style="flex-wrap:wrap"><div style="min-width:0"><div class="sm mut">Paste this code into the Tollkit extension. It works once, for 5 minutes.</div><div class="addr" style="font-size:17px;letter-spacing:.06em;margin-top:4px">'+esc(EXC)+'</div></div><div class="r">'+cpBtn(EXC)+'</div></div>':'')+
746
850
  (ex.sites.length?ex.sites.map(function(x){var h=x.origin.replace(/^https?:\/\//,"");return '<div class="row"><div style="min-width:0"><div>'+esc(h)+'</div><div class="sm mut">'+[x.evm?"Base/Ethereum":"",x.solana?"Solana":""].filter(Boolean).join(" + ")+'</div></div><div class="r"><button class="b no sbtn" data-extoff="'+esc(x.origin)+'">Disconnect</button></div></div>'}).join(""):'')+
747
851
  (ex.paired?'<div class="sm" style="margin-top:6px"><a href="#" onclick="extUnpair();return false">Unpair all browsers</a></div>':'');
748
- return lim+'<h3>AI</h3>'+ai+'<h3>Websites</h3>'+exr+'<h3>Approvals</h3>'+tg+'<h3>Recovery</h3>'+bkr}
852
+ return lim+'<h3>How you trade</h3>'+tradeRow()+(S.ai&&S.ai.fuel?fuelRow():'')+'<h3>AI</h3>'+ai+'<h3>Websites</h3>'+exr+'<h3>Approvals</h3>'+tg+'<h3>Recovery</h3>'+bkr}
749
853
  var EXC=null,EXCN=0;
750
854
  function extCode(){api("/extmgr/code",{}).then(function(r){EXC=r.code;EXCN=(S.ext||{}).paired||0;render()}).catch(function(e){flash=[0,e.message];render()})}
751
855
  document.addEventListener("click",function(e){var b=e.target.closest&&e.target.closest("[data-extoff]");if(b)extOff(b.getAttribute("data-extoff"))});
package/dist/ui.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACtE,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACvE,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WA6RnB,SAAS;WACT,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA0eC,CAAC"}
1
+ {"version":3,"file":"ui.js","sourceRoot":"","sources":["../src/ui.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACtE,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;AACvE,MAAM,CAAC,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;WAyTnB,SAAS;WACT,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAsjBC,CAAC"}
package/dist/wallet.d.ts CHANGED
@@ -13,6 +13,7 @@ import { PaperBook } from "./paper.js";
13
13
  import { type Executor } from "./autopilot.js";
14
14
  import { Staking, type StakingOptions } from "./stake.js";
15
15
  import { Sales } from "./sell.js";
16
+ import { type RampAuth, type RampKind } from "./rampauth.js";
16
17
  import { DappBridge } from "./dapp.js";
17
18
  import { Mover, type MoveAsset, type MoverOptions, type MoveQuote } from "./bridge.js";
18
19
  type Fetch = typeof fetch;
@@ -33,6 +34,13 @@ export interface WalletOptions {
33
34
  dapp?: Partial<ConstructorParameters<typeof DappBridge>[0]>;
34
35
  /** Override for tests / other channels. Default: Telegram if configured. */
35
36
  approve?: ((text: string) => Promise<Decision>) | null;
37
+ /** Override for tests: Tollkit's separately-sent Solana fee (recipient, SOL price, balance and sender). */
38
+ solFee?: {
39
+ recipient?: string;
40
+ solUsd?: number;
41
+ balance?: () => Promise<bigint>;
42
+ send?: (lamports: bigint, to: string) => Promise<string>;
43
+ };
36
44
  }
37
45
  export interface SwapResult {
38
46
  ok: boolean;
@@ -119,6 +127,9 @@ export declare class TollkitWallet {
119
127
  private taker;
120
128
  /** Dollar value of a quote. Stable side if there is one; Jupiter reports it; otherwise price the sell side against USDC. */
121
129
  valueOf(q: SwapQuote): Promise<number | null>;
130
+ /** Where and how much Tollkit's separately-sent Solana fee is. */
131
+ private separateFee;
132
+ private sendSolFee;
122
133
  /** A priced, checked quote. Nothing is signed. */
123
134
  quote(req: SwapRequest): Promise<SwapResult>;
124
135
  /** Core, listed (Superchain list on Base/Ethereum, Jupiter-verified on Solana) or added by the owner. */
@@ -129,7 +140,10 @@ export declare class TollkitWallet {
129
140
  /** Fetch a URL, paying through x402 or MPP if it asks, within the owner's rules. */
130
141
  pay(url: string, init?: RequestInit, opts?: {
131
142
  publicOnly?: boolean;
143
+ prefer?: Chain[];
132
144
  }): Promise<PayResult>;
145
+ /** One token's balance on one chain, in atomic units (live, from the chain). */
146
+ balanceOf(chain: Chain, t: Token): Promise<bigint>;
133
147
  /**
134
148
  * Owner withdrawal. Needs the password again, so an agent holding an open
135
149
  * wallet can't move funds out this way. Not subject to the spending limits.
@@ -173,6 +187,8 @@ export declare class TollkitWallet {
173
187
  private dp?;
174
188
  /** Cash-out sales started from the dashboard. */
175
189
  sales(): Sales;
190
+ /** Signs a buy or cash-out request for one of this wallet's own addresses (Tollkit's server checks it). */
191
+ rampAuth(kind: RampKind, network: Chain, address: string): Promise<RampAuth>;
176
192
  private sl?;
177
193
  /**
178
194
  * Send the crypto for a Coinbase sale, after the owner clicked "Cash out now" there. Needs the password.