lightnode-sdk 0.7.15 → 0.7.16

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/add.d.ts CHANGED
@@ -70,6 +70,12 @@ export declare function addChatWeb3(opts?: AddOpts): {
70
70
  network: Network;
71
71
  needsWagmi: boolean;
72
72
  };
73
+ export declare function addWagmiSetup(opts?: AddOpts): {
74
+ written: WrittenFile[];
75
+ install: string;
76
+ template: Template;
77
+ network: Network;
78
+ };
73
79
  export declare function addJudge(opts?: AddOpts): {
74
80
  written: WrittenFile[];
75
81
  install: string;
package/dist/add.js CHANGED
@@ -31,6 +31,75 @@ function detectTemplate(cwd) {
31
31
  return "hono";
32
32
  return "node";
33
33
  }
34
+ const HOSTING_GUIDE = `# Hosting LightChain AI inference - your options
35
+
36
+ A single LightChain mainnet inference takes **60-90 seconds** under normal
37
+ load (the workers do the model run, attest the result on-chain, and return
38
+ the answer). If your server-side route has a function timeout shorter than
39
+ that, every call will fail with a generic timeout error.
40
+
41
+ This file is a quick reference for picking a host that actually finishes
42
+ the request. If you used \`lightnode add chat-web3\`, ignore everything
43
+ below - that path has NO server-side route to worry about.
44
+
45
+ ## TL;DR
46
+
47
+ | Need | Use |
48
+ |---------------------------------------|--------------------------------|
49
+ | Zero infra, users pay (Web3 dApp) | \`lightnode add chat-web3\` |
50
+ | Cheapest server-side host that works | Railway / Fly.io / Render |
51
+ | Already on Vercel and need it to fit | Vercel Pro (60s) + streaming |
52
+ | You self-host already (Docker, VPS) | Anywhere - no timeout to fight |
53
+
54
+ ## The detailed table
55
+
56
+ | Host | Free / Hobby timeout | Paid timeout | Verdict |
57
+ |---------------------|----------------------------|-----------------------------|---------|
58
+ | Vercel Hobby (free) | **10 seconds** | - | **DOES NOT WORK** for mainnet inference. Every call times out. |
59
+ | Vercel Pro ($20/mo) | - | 60s default, up to 800s with \`maxDuration\` config | Works if calls finish under 60s; 70-80s calls cut it close. Stream tokens to keep the connection warm. |
60
+ | Cloudflare Workers | 30s CPU | unbounded with Durable Objects | Free tier 30s is too tight; Workers + DO works but is complex. |
61
+ | Railway | none | none | **Great fit.** $5/mo for the smallest container, no timeout. Deploy a plain Node server or Next.js. |
62
+ | Fly.io | none | none | **Great fit.** Free tier covers small apps, scales to paid. |
63
+ | Render | none | none | **Great fit.** $7/mo for the smallest web service. |
64
+ | Netlify Functions | 26s sync / 15min background | 26s sync / 15min background | Use background functions for inference; sync functions time out. |
65
+ | AWS Lambda | 15 min | 15 min | Works, but WebSocket setup for streaming is involved. |
66
+ | Self-host (Docker) | no limit | no limit | **Works anywhere.** ECS, Cloud Run, your own VPS, fine. |
67
+
68
+ ## What I recommend
69
+
70
+ 1. **Building a Web3 app where users have wallets?** Switch to \`lightnode add
71
+ chat-web3\`. Each user signs and pays from their own wallet. You host the
72
+ static page (Vercel/Netlify/Cloudflare Pages free tier all work). No timeout
73
+ to fight because there is no server-side inference route.
74
+
75
+ 2. **Building a SaaS chatbot where users do NOT have wallets?** Deploy your
76
+ inference route on Railway or Fly. Both have no function timeout, scale
77
+ to traffic, and cost ~$5/mo for small apps. Cheaper than Vercel Pro.
78
+
79
+ 3. **Already committed to Vercel?** Upgrade to Pro and add streaming. The
80
+ chat template generated by \`lightnode add chat\` already uses streaming
81
+ tokens, so the connection stays warm while the model runs.
82
+
83
+ 4. **Have a Docker setup already?** Run the inference route in your existing
84
+ container. No timeout, no per-call cost beyond your existing infra.
85
+
86
+ ## Why the request is slow at all
87
+
88
+ LightChain inference is not a synchronous LLM call. Each request:
89
+ 1. Negotiates an ECDH-encrypted session with a worker (off-chain).
90
+ 2. Sends a \`createSession\` tx on-chain (one confirmation).
91
+ 3. Uploads the encrypted prompt blob to the gateway.
92
+ 4. Sends a \`submitJob\` tx on-chain (one confirmation).
93
+ 5. Waits for the worker to run inference + post the encrypted result.
94
+ 6. Decrypts the result client-side.
95
+
96
+ Steps 2, 4, and 5 are the slow part - each waits for a block confirmation
97
+ and worker pickup. The protocol's verifiable-AI guarantee comes from doing
98
+ all of this on-chain instead of just hitting an OpenAI-style API, which is
99
+ the reason the call takes 60-90s instead of 1-2s. There is no way to
100
+ shortcut this on the SDK side; the host just has to allow long-running
101
+ functions.
102
+ `;
34
103
  const ENV_EXAMPLE = (net) => `# Funded private key. Testnet works free (faucet at https://lightfaucet.ai).
35
104
  PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
36
105
 
@@ -182,6 +251,7 @@ export function addInference(opts = {}) {
182
251
  const written = [];
183
252
  if (template === "nextjs-api") {
184
253
  written.push(writeFile(path.join(cwd, "app/api/inference/route.ts"), NEXTJS_ROUTE, force));
254
+ written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
185
255
  }
186
256
  else if (template === "hono") {
187
257
  written.push(writeFile(path.join(cwd, "lightchain-inference.ts"), HONO_HANDLER, force));
@@ -590,20 +660,47 @@ export default function Chat() {
590
660
  setTurns(next);
591
661
  setDraft("");
592
662
  setBusy(true);
663
+ // Reserve the assistant bubble immediately so tokens can stream into it.
664
+ setTurns([...next, { role: "assistant", text: "" }]);
593
665
  try {
594
- // Concatenate prior turns so the model has the conversation context.
595
666
  const prompt =
596
667
  next.map((t) => (t.role === "user" ? \`User: \${t.text}\` : \`Assistant: \${t.text}\`)).join("\\n") +
597
668
  "\\nAssistant:";
598
669
  const r = await fetch("/api/inference", {
599
670
  method: "POST",
600
671
  headers: { "Content-Type": "application/json" },
601
- body: JSON.stringify({ prompt }),
602
- }).then((r) => r.json()) as { answer?: string; txs?: Record<string, string>; error?: string };
603
- if (!r.answer) throw new Error(r.error ?? "no answer");
604
- setTurns([...next, { role: "assistant", text: r.answer, txs: r.txs }]);
672
+ body: JSON.stringify({ prompt, stream: true }),
673
+ });
674
+ if (!r.ok || !r.body) {
675
+ const err = await r.text().catch(() => "");
676
+ throw new Error(\`route returned \${r.status}: \${err.slice(0, 200)}\`);
677
+ }
678
+ const reader = r.body.getReader();
679
+ const decoder = new TextDecoder();
680
+ let assembled = "";
681
+ let txs: Record<string, string> | undefined;
682
+ while (true) {
683
+ const { done, value } = await reader.read();
684
+ if (done) break;
685
+ const text = decoder.decode(value, { stream: true });
686
+ // The route ends the stream with one line of JSON on a new \\u0000
687
+ // delimiter (a NUL byte we put between the streamed prose and the
688
+ // metadata). Anything before is body; anything after is metadata.
689
+ const nulIdx = text.indexOf("\\u0000");
690
+ if (nulIdx === -1) {
691
+ assembled += text;
692
+ setTurns((prev) => prev.map((t, i) => i === prev.length - 1 ? { ...t, text: assembled } : t));
693
+ } else {
694
+ assembled += text.slice(0, nulIdx);
695
+ try { txs = JSON.parse(text.slice(nulIdx + 1)) as Record<string, string>; } catch { /* ignore */ }
696
+ setTurns((prev) => prev.map((t, i) => i === prev.length - 1 ? { ...t, text: assembled, txs } : t));
697
+ }
698
+ }
605
699
  } catch (e) {
606
- setTurns([...next, { role: "assistant", text: \`(error: \${(e as Error).message})\` }]);
700
+ // Replace the empty assistant bubble with the error.
701
+ setTurns((prev) => prev.map((t, i) =>
702
+ i === prev.length - 1 ? { ...t, text: \`(error: \${(e as Error).message})\` } : t
703
+ ));
607
704
  } finally {
608
705
  setBusy(false);
609
706
  }
@@ -664,13 +761,99 @@ export default function Chat() {
664
761
  </button>
665
762
  </div>
666
763
  <p style={{ marginTop: 16, fontSize: 12, color: "#888" }}>
667
- Make sure <code>/api/inference</code> is mounted - run <code>npx lightnode add inference</code> if you have
668
- not. The route signs every call server-side with the PRIVATE_KEY in your .env.
764
+ Your server signs every call with the PRIVATE_KEY in <code>.env</code>. Cost per turn is paid from that wallet.
765
+ See <code>LIGHTNODE-HOSTING.md</code> for picking a host that handles 60-90s function calls.
669
766
  </p>
670
767
  </main>
671
768
  );
672
769
  }
673
770
  `;
771
+ /**
772
+ * Streaming inference route, paired with NEXTJS_CHAT_PAGE.
773
+ *
774
+ * Why a second route: the plain NEXTJS_ROUTE returns JSON in one shot,
775
+ * which is fine for one-off calls but means the chat UI shows nothing for
776
+ * 60-90s. This streams decrypted chunks as they arrive (via
777
+ * runInferenceStream), so the user sees tokens land live and the host
778
+ * keeps the connection warm. After the last chunk we append a NUL byte
779
+ * (\\u0000) and a JSON line with the tx hashes so the client can render
780
+ * the on-chain receipt under the assistant bubble.
781
+ */
782
+ const NEXTJS_INFERENCE_STREAM_ROUTE = `// app/api/inference/route.ts
783
+ // Generated by 'lightnode add chat'. Streaming inference route - tokens
784
+ // arrive live as the model produces them, then a NUL byte separates them
785
+ // from a final JSON line with the on-chain tx hashes.
786
+ //
787
+ // Pass { stream: true } in the body to get streaming. Without that flag
788
+ // the same route still returns one-shot JSON, which is fine for non-chat
789
+ // integrations.
790
+ //
791
+ // Mainnet 8b inference takes 60-90s. Vercel Hobby caps function execution
792
+ // at 10s and will time out. See LIGHTNODE-HOSTING.md for hosts that work.
793
+ import { NextResponse } from "next/server";
794
+ import { runInferenceWithKey, runInferenceStream } from "lightnode-sdk";
795
+
796
+ export const runtime = "nodejs";
797
+ export const dynamic = "force-dynamic";
798
+ export const maxDuration = 120;
799
+
800
+ const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
801
+ const MODEL = process.env.MODEL ?? "llama3-8b";
802
+
803
+ export async function POST(req: Request) {
804
+ if (!process.env.PRIVATE_KEY?.startsWith("0x")) {
805
+ return NextResponse.json({ error: "PRIVATE_KEY not configured" }, { status: 500 });
806
+ }
807
+ const body = (await req.json().catch(() => ({}))) as { prompt?: string; system?: string; stream?: boolean };
808
+ const prompt = body.prompt?.trim();
809
+ if (!prompt) return NextResponse.json({ error: "prompt is required" }, { status: 400 });
810
+
811
+ const args = {
812
+ network: NETWORK,
813
+ privateKey: process.env.PRIVATE_KEY as \`0x\${string}\`,
814
+ model: MODEL,
815
+ system: body.system?.trim() || undefined,
816
+ prompt,
817
+ };
818
+
819
+ // One-shot JSON path: same shape as the original 'add inference' route.
820
+ if (!body.stream) {
821
+ try {
822
+ const { answer, worker, txs, jobId } = await runInferenceWithKey(args);
823
+ return NextResponse.json({
824
+ answer, worker, jobId: jobId.toString(),
825
+ txs: { createSession: txs.createSession, submitJob: txs.submitJob, jobCompleted: txs.jobCompleted },
826
+ });
827
+ } catch (e) {
828
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
829
+ }
830
+ }
831
+
832
+ // Streaming path: tokens, NUL byte, then tx metadata as JSON.
833
+ const stream = runInferenceStream(args);
834
+ const encoder = new TextEncoder();
835
+ const body$ = new ReadableStream({
836
+ async start(controller) {
837
+ try {
838
+ for await (const chunk of stream) controller.enqueue(encoder.encode(chunk));
839
+ const { worker, txs, jobId } = await stream.done;
840
+ const meta = JSON.stringify({
841
+ worker, jobId: jobId.toString(),
842
+ createSession: txs.createSession, submitJob: txs.submitJob, jobCompleted: txs.jobCompleted,
843
+ });
844
+ controller.enqueue(encoder.encode("\\u0000" + meta));
845
+ controller.close();
846
+ } catch (e) {
847
+ controller.enqueue(encoder.encode(\`\\u0000{"error":\${JSON.stringify((e as Error).message)}}\`));
848
+ controller.close();
849
+ }
850
+ },
851
+ });
852
+ return new Response(body$, {
853
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" },
854
+ });
855
+ }
856
+ `;
674
857
  const NEXTJS_CHAT_WEB3_PAGE = `// app/chat-web3/page.tsx
675
858
  // Generated by 'lightnode add chat-web3'. User-pays chat: each visitor's own
676
859
  // wallet signs SIWE + createSession per turn. Your app holds zero funds.
@@ -1101,7 +1284,12 @@ export function addChat(opts = {}) {
1101
1284
  const force = !!opts.force;
1102
1285
  const written = [];
1103
1286
  if (template === "nextjs-api") {
1287
+ // 'add chat' is now self-contained: it writes BOTH the chat page AND
1288
+ // the streaming inference route. Previously the user had to remember
1289
+ // to also run 'add inference' separately, which was easy to miss.
1104
1290
  written.push(writeFile(path.join(cwd, "app/chat/page.tsx"), NEXTJS_CHAT_PAGE, force));
1291
+ written.push(writeFile(path.join(cwd, "app/api/inference/route.ts"), NEXTJS_INFERENCE_STREAM_ROUTE, force));
1292
+ written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
1105
1293
  }
1106
1294
  else {
1107
1295
  written.push(writeFile(path.join(cwd, "chat-repl.ts"), NODE_CHAT_REPL, force));
@@ -1149,6 +1337,151 @@ export function addChatWeb3(opts = {}) {
1149
1337
  needsWagmi: !hasWagmi,
1150
1338
  };
1151
1339
  }
1340
+ const WAGMI_CONFIG_FILE = `// lib/wagmi.ts
1341
+ // Generated by 'lightnode add wagmi-setup'. Minimal wagmi setup for
1342
+ // LightChain mainnet (9200) + testnet (8200). Use this as a starting
1343
+ // point; swap in RainbowKit / Reown AppKit / ConnectKit if you want a
1344
+ // richer connect UI.
1345
+ import { createConfig, http } from "wagmi";
1346
+ import { injected } from "wagmi/connectors";
1347
+ import type { Chain } from "viem";
1348
+
1349
+ export const lightchainMainnet: Chain = {
1350
+ id: 9200,
1351
+ name: "LightChain",
1352
+ nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 },
1353
+ rpcUrls: { default: { http: ["https://rpc.mainnet.lightchain.ai"] } },
1354
+ blockExplorers: { default: { name: "Lightscan", url: "https://mainnet.lightscan.app" } },
1355
+ };
1356
+
1357
+ export const lightchainTestnet: Chain = {
1358
+ id: 8200,
1359
+ name: "LightChain Testnet",
1360
+ nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 },
1361
+ rpcUrls: { default: { http: ["https://rpc.testnet.lightchain.ai"] } },
1362
+ blockExplorers: { default: { name: "Lightscan", url: "https://testnet.lightscan.app" } },
1363
+ };
1364
+
1365
+ export const wagmiConfig = createConfig({
1366
+ chains: [lightchainMainnet, lightchainTestnet],
1367
+ // \`injected\` covers MetaMask, Rabby, OKX, Phantom EVM, and any browser
1368
+ // wallet that follows EIP-1193. Add walletConnect() / coinbaseWallet()
1369
+ // here if you want explicit support for those.
1370
+ connectors: [injected()],
1371
+ transports: {
1372
+ [lightchainMainnet.id]: http(),
1373
+ [lightchainTestnet.id]: http(),
1374
+ },
1375
+ });
1376
+ `;
1377
+ const WAGMI_PROVIDERS_FILE = `// app/providers.tsx
1378
+ // Generated by 'lightnode add wagmi-setup'. Wraps the app with the
1379
+ // wagmi + react-query providers needed for any wagmi hook to work.
1380
+ //
1381
+ // Import this from your root layout:
1382
+ //
1383
+ // // app/layout.tsx
1384
+ // import { Providers } from "./providers";
1385
+ // export default function RootLayout({ children }: { children: React.ReactNode }) {
1386
+ // return (
1387
+ // <html lang="en">
1388
+ // <body><Providers>{children}</Providers></body>
1389
+ // </html>
1390
+ // );
1391
+ // }
1392
+ "use client";
1393
+
1394
+ import { WagmiProvider } from "wagmi";
1395
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
1396
+ import { wagmiConfig } from "@/lib/wagmi";
1397
+ import type { ReactNode } from "react";
1398
+
1399
+ const queryClient = new QueryClient();
1400
+
1401
+ export function Providers({ children }: { children: ReactNode }) {
1402
+ return (
1403
+ <WagmiProvider config={wagmiConfig}>
1404
+ <QueryClientProvider client={queryClient}>
1405
+ {children}
1406
+ </QueryClientProvider>
1407
+ </WagmiProvider>
1408
+ );
1409
+ }
1410
+ `;
1411
+ const WAGMI_CONNECT_BUTTON = `// components/connect-button.tsx
1412
+ // Generated by 'lightnode add wagmi-setup'. Minimal Connect/Disconnect
1413
+ // button using wagmi's useConnect / useAccount hooks. Swap in
1414
+ // RainbowKit's ConnectButton or Reown's <w3m-button /> for a richer UI.
1415
+ "use client";
1416
+
1417
+ import { useAccount, useConnect, useDisconnect, useSwitchChain } from "wagmi";
1418
+
1419
+ const LIGHTCHAIN_IDS = new Set<number>([9200, 8200]);
1420
+
1421
+ function shortAddress(addr: string): string {
1422
+ return \`\${addr.slice(0, 6)}...\${addr.slice(-4)}\`;
1423
+ }
1424
+
1425
+ export function ConnectButton() {
1426
+ const { address, chain, isConnected } = useAccount();
1427
+ const { connect, connectors, isPending } = useConnect();
1428
+ const { disconnect } = useDisconnect();
1429
+ const { switchChain, isPending: switching } = useSwitchChain();
1430
+
1431
+ if (!isConnected) {
1432
+ const connector = connectors[0];
1433
+ return (
1434
+ <button
1435
+ type="button"
1436
+ onClick={() => connect({ connector })}
1437
+ disabled={isPending}
1438
+ style={{ padding: "8px 16px", borderRadius: 8, cursor: "pointer" }}
1439
+ >
1440
+ {isPending ? "Connecting..." : "Connect wallet"}
1441
+ </button>
1442
+ );
1443
+ }
1444
+
1445
+ if (chain && !LIGHTCHAIN_IDS.has(chain.id)) {
1446
+ return (
1447
+ <button
1448
+ type="button"
1449
+ onClick={() => switchChain({ chainId: 9200 })}
1450
+ disabled={switching}
1451
+ style={{ padding: "8px 16px", borderRadius: 8, background: "#fee", cursor: "pointer" }}
1452
+ >
1453
+ {switching ? "Switching..." : "Switch to LightChain"}
1454
+ </button>
1455
+ );
1456
+ }
1457
+
1458
+ return (
1459
+ <button
1460
+ type="button"
1461
+ onClick={() => disconnect()}
1462
+ style={{ padding: "8px 16px", borderRadius: 8, cursor: "pointer", fontFamily: "monospace" }}
1463
+ >
1464
+ {address ? shortAddress(address) : "(unknown)"} ({chain?.name}) - disconnect
1465
+ </button>
1466
+ );
1467
+ }
1468
+ `;
1469
+ export function addWagmiSetup(opts = {}) {
1470
+ const cwd = opts.cwd ?? process.cwd();
1471
+ const network = opts.network ?? "mainnet";
1472
+ const template = opts.template && opts.template !== "auto" ? opts.template : detectTemplate(cwd);
1473
+ const force = !!opts.force;
1474
+ const written = [];
1475
+ written.push(writeFile(path.join(cwd, "lib/wagmi.ts"), WAGMI_CONFIG_FILE, force));
1476
+ written.push(writeFile(path.join(cwd, "app/providers.tsx"), WAGMI_PROVIDERS_FILE, force));
1477
+ written.push(writeFile(path.join(cwd, "components/connect-button.tsx"), WAGMI_CONNECT_BUTTON, force));
1478
+ return {
1479
+ written,
1480
+ install: `npm install wagmi viem @tanstack/react-query`,
1481
+ template,
1482
+ network,
1483
+ };
1484
+ }
1152
1485
  const NEXTJS_JUDGE_ROUTE = `// app/api/judge/route.ts
1153
1486
  // Generated by 'lightnode add judge'. See https://lightnode.app/build
1154
1487
  // The LightChallenge-style evaluator: post evidence + criteria, get a
@@ -1282,6 +1615,7 @@ export function addJudge(opts = {}) {
1282
1615
  const written = [];
1283
1616
  if (template === "nextjs-api") {
1284
1617
  written.push(writeFile(path.join(cwd, "app/api/judge/route.ts"), NEXTJS_JUDGE_ROUTE, force));
1618
+ written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
1285
1619
  }
1286
1620
  else {
1287
1621
  written.push(writeFile(path.join(cwd, "judge.ts"), NODE_JUDGE_SCRIPT, force));
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { LightNode, modelStatsCsv, workerStatsCsv, workerJobsCsv, runInferenceWithKey, runInferenceBatch, Agent, isStalledWorker, workerPreflight, workerWatch, WorkerOperator, isWorkerOpError, BRIDGE_ROUTE, DAO, DAO_ADDRESSES } from "./index.js";
3
- import { addInference, addAnalyticsDashboard, addNftMint, addChat, addChatWeb3, addAgent, addJudge } from "./add.js";
3
+ import { addInference, addAnalyticsDashboard, addNftMint, addChat, addChatWeb3, addAgent, addJudge, addWagmiSetup } from "./add.js";
4
4
  import { createPublicClient, createWalletClient, http, parseEther } from "viem";
5
5
  import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
6
6
  function flag(name) {
@@ -462,7 +462,7 @@ async function main() {
462
462
  const template = flag("--template") ?? "auto";
463
463
  const force = process.argv.includes("--force");
464
464
  const network = (net === "mainnet" ? "mainnet" : "testnet");
465
- const known = ["inference", "chat", "chat-web3", "agent", "judge", "analytics-dashboard", "nft-mint-with-inference"];
465
+ const known = ["inference", "chat", "chat-web3", "wagmi-setup", "agent", "judge", "analytics-dashboard", "nft-mint-with-inference"];
466
466
  if (!known.includes(sub ?? "")) {
467
467
  die(`usage: lightnode add <${known.join("|")}> [--template auto|nextjs-api|hono|node] [--net testnet|mainnet] [--force]`);
468
468
  }
@@ -472,13 +472,15 @@ async function main() {
472
472
  ? addNftMint({ template, network, force })
473
473
  : sub === "chat-web3"
474
474
  ? addChatWeb3({ template, network, force })
475
- : sub === "chat"
476
- ? addChat({ template, network, force })
477
- : sub === "agent"
478
- ? addAgent({ template, network, force })
479
- : sub === "judge"
480
- ? addJudge({ template, network, force })
481
- : addInference({ template, network, force });
475
+ : sub === "wagmi-setup"
476
+ ? addWagmiSetup({ template, network, force })
477
+ : sub === "chat"
478
+ ? addChat({ template, network, force })
479
+ : sub === "agent"
480
+ ? addAgent({ template, network, force })
481
+ : sub === "judge"
482
+ ? addJudge({ template, network, force })
483
+ : addInference({ template, network, force });
482
484
  console.log(`▶ add ${sub} (${result.template} template, default network ${result.network})`);
483
485
  for (const f of result.written) {
484
486
  if (f.skipped)
@@ -493,16 +495,25 @@ async function main() {
493
495
  else {
494
496
  console.log(`\nNext steps (these files were added to your CURRENT folder, not a new project):`);
495
497
  console.log(` 1. ${result.install}`);
496
- if (sub === "chat-web3") {
498
+ if (sub === "wagmi-setup") {
499
+ console.log(` 2. Import Providers in app/layout.tsx and wrap children:`);
500
+ console.log(` import { Providers } from "./providers";`);
501
+ console.log(` // <body><Providers>{children}</Providers></body>`);
502
+ console.log(` 3. Drop <ConnectButton /> anywhere you want a connect UI:`);
503
+ console.log(` import { ConnectButton } from "@/components/connect-button";`);
504
+ console.log(` 4. You can now use any wagmi hook (useAccount, useWalletClient, ...).`);
505
+ console.log(` Wallets on chains other than 9200/8200 will be prompted to switch.`);
506
+ }
507
+ else if (sub === "chat-web3") {
497
508
  // chat-web3 has no PRIVATE_KEY (each visitor pays their own way).
498
509
  const needsWagmi = result.needsWagmi;
499
510
  if (needsWagmi) {
500
- console.log(` 2. Set up wagmi in your app if you have not already.`);
501
- console.log(` See https://wagmi.sh/react/getting-started - wrap your root layout with`);
502
- console.log(` <WagmiProvider config={wagmiConfig}> and add a Connect button using`);
503
- console.log(` useConnect / RainbowKit / Reown AppKit / ConnectKit, whatever you prefer.`);
504
- console.log(` 3. npm run dev, open /chat-web3`);
505
- console.log(` 4. Connect a wallet on LightChain ${result.network === "mainnet" ? "mainnet (chainId 9200)" : "testnet (chainId 8200)"}.`);
511
+ console.log(` 2. Get wagmi wired up with one command:`);
512
+ console.log(` npx lightnode add wagmi-setup`);
513
+ console.log(` (drops lib/wagmi.ts + app/providers.tsx + components/connect-button.tsx)`);
514
+ console.log(` 3. Wrap your layout with <Providers> (see step 2 output) and drop`);
515
+ console.log(` <ConnectButton /> somewhere on the page.`);
516
+ console.log(` 4. npm run dev, open /chat-web3, connect on chainId ${result.network === "mainnet" ? "9200" : "8200"}.`);
506
517
  console.log(` Mainnet llama3-8b costs 0.02 LCAI per turn; testnet is free from https://lightfaucet.ai`);
507
518
  }
508
519
  else {
@@ -510,6 +521,8 @@ async function main() {
510
521
  console.log(` 3. Connect a wallet on LightChain ${result.network === "mainnet" ? "mainnet (chainId 9200)" : "testnet (chainId 8200)"}.`);
511
522
  console.log(` Mainnet llama3-8b costs 0.02 LCAI per turn; testnet is free from https://lightfaucet.ai`);
512
523
  }
524
+ console.log(`\n Note: chat-web3 has NO server-side route, so it scales infinitely on`);
525
+ console.log(` static hosting (Vercel/Netlify/Cloudflare Pages free tier all work).`);
513
526
  }
514
527
  else if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent" || sub === "judge") {
515
528
  console.log(` 2. cp .env.example .env (and put a funded ${result.network} PRIVATE_KEY in it)`);
@@ -521,8 +534,8 @@ async function main() {
521
534
  console.log(` 3. AGENT_INTERVAL_MS=3600000 npx tsx agent.ts # or run under pm2/systemd`);
522
535
  }
523
536
  else if (sub === "chat" && result.template === "nextjs-api") {
524
- console.log(` 3. Make sure /api/inference is mounted too (run: npx lightnode add inference)`);
525
- console.log(` 4. npm run dev, open /chat`);
537
+ console.log(` 3. npm run dev, open /chat`);
538
+ console.log(` (the chat page + /api/inference streaming route are both already wired up)`);
526
539
  }
527
540
  else if (sub === "chat") {
528
541
  console.log(` 3. npx tsx chat-repl.ts (interactive terminal chat)`);
@@ -561,6 +574,13 @@ async function main() {
561
574
  console.log(` 2. npx tsx lightnode-analytics.ts`);
562
575
  }
563
576
  }
577
+ // Hosting warning for server-side commands that ship LIGHTNODE-HOSTING.md.
578
+ if (result.template === "nextjs-api"
579
+ && (sub === "inference" || sub === "chat" || sub === "judge")) {
580
+ console.log(`\n Hosting: a mainnet inference takes 60-90s. Vercel Hobby (free) times`);
581
+ console.log(` out at 10s; Vercel Pro at 60s. See LIGHTNODE-HOSTING.md (in this folder)`);
582
+ console.log(` for hosts that work - Railway / Fly / Render handle long calls fine.`);
583
+ }
564
584
  if (result.network === "testnet") {
565
585
  console.log(`\nNo wallet yet? Make one: npx lightnode wallet new then fund it free below.`);
566
586
  }
package/dist/index.d.ts CHANGED
@@ -134,7 +134,7 @@ export declare class LightNode {
134
134
  * (especially in registry-proxy environments like StackBlitz where lockfiles
135
135
  * may pin an older minor than the local install command suggests).
136
136
  */
137
- export declare const SDK_VERSION = "0.7.15";
137
+ export declare const SDK_VERSION = "0.7.16";
138
138
  export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei, resolveJobTransactions, siweSignIn, siweChallenge, siweVerify, fetchWorkerModels, computeModelId as modelId, estimateJobFee, JOB_REGISTRY_CONSUMER_ABI, consumerGatewayUrl, consumerGatewayHost, GatewayClient, GatewayHttpError, prepareSession, submitPrompt, decryptResponse, generateEcdhKeyPair, crypto, runInference, runInferenceWithKey, runInferenceStream, Conversation, chat, runInferenceBatch, Agent, parseAgentOutput, workerPreflight, workerWatch, Bridge, BRIDGE_ROUTE, HYPERLANE_ROUTER_ABI, ERC20_ABI, addressToBytes32, quoteBridgeFee, bridgeableBalance, bridgeAllowance, approveBridge, bridgeTransfer, DAO, DAO_ADDRESSES, ProposalState, PROPOSAL_STATE_LABEL, VoteSupport, GOVERNOR_ABI, VOTES_ABI, OnchainModelRegistry, AIVM_MODEL_REGISTRY_ABI, BENCHMARK_REGISTRY_ABI, ModelStatus, MODEL_STATUS_LABEL, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError, isStalledWorker, WorkerOperator, WORKER_REGISTRY_ABI, JOB_REGISTRY_OPERATOR_ABI, AI_CONFIG_ABI, JOB_STATE, decodeWorkerError, WorkerOpError, isWorkerOpError, };
139
139
  export type { BearerSource, GatewayClientOptions, SelectSessionResult, PrepareSessionResult, UploadBlobResult, SessionTokenResult } from "./gateway.js";
140
140
  export type { SessionPreparation, RunInferenceArgs, RunInferenceResult, RunInferenceWithKeyArgs, RunInferenceStreamResult } from "./inference.js";
package/dist/index.js CHANGED
@@ -213,7 +213,7 @@ export class LightNode {
213
213
  * (especially in registry-proxy environments like StackBlitz where lockfiles
214
214
  * may pin an older minor than the local install command suggests).
215
215
  */
216
- export const SDK_VERSION = "0.7.15";
216
+ export const SDK_VERSION = "0.7.16";
217
217
  export { NETWORKS, WORKER_REGISTRY, REGISTRY_TOPICS, aggregateModelStats, aggregateWorkerStats, networkAnalytics, modelStatsCsv, workerStatsCsv, workerJobsCsv, fromWei,
218
218
  // v0.7.3 per-job transaction-hash resolver (lifts the upstream
219
219
  // subgraph's "block-only" Job entity to a deep-linkable Job + tx pair).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightnode-sdk",
3
- "version": "0.7.15",
3
+ "version": "0.7.16",
4
4
  "description": "Read-only TypeScript client for LightChain AI: workers, jobs, models, on-chain registration, and per-model network analytics. Independent, community-built (not an official LightChain package).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",