lightnode-sdk 0.7.15 → 0.7.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,157 @@ function detectTemplate(cwd) {
31
31
  return "hono";
32
32
  return "node";
33
33
  }
34
+ const HOSTING_GUIDE = `# Hosting your LightChain AI app
35
+
36
+ A single LightChain mainnet inference takes **60-90 seconds** (the workers
37
+ run the model, attest the result on-chain, and return the answer). Anywhere
38
+ that puts a short timeout on your request will fail with a generic timeout
39
+ error. So the question is just: how do you give your route enough time to
40
+ finish?
41
+
42
+ If you used \`lightnode add chat-web3\`, skip this file - that path has NO
43
+ server-side route, the visitor's own browser does the wait.
44
+
45
+ ## The clean answer: run it yourself
46
+
47
+ This template ships with a **Dockerfile + docker-compose.yml** so you can
48
+ run the entire stack on your own machine, your own VPS, or anywhere Docker
49
+ runs. Long-running Node processes have no timeout. The result:
50
+
51
+ \`\`\`bash
52
+ docker compose up --build
53
+ # → http://localhost:3000 ready, no signup, no per-call cost beyond your
54
+ # LCAI fee, no platform vendor lock-in.
55
+ \`\`\`
56
+
57
+ That's the recommended path. You own the box, you own the keys, you own
58
+ the uptime. Costs as much as the VPS does - $5/mo on Hetzner gets you a
59
+ 2-core machine that handles plenty of traffic.
60
+
61
+ ### Where to run that container
62
+
63
+ | Where | Cost | Notes |
64
+ |--------------------------|-----------------------|-------|
65
+ | Your laptop / home server | free | Perfect for dev + small personal projects. Expose via Cloudflare Tunnel or Tailscale Funnel if you want a public URL. |
66
+ | Hetzner CX22 | ~€4/mo | 2 CPU, 4 GB RAM. Generous bandwidth. EU-based. |
67
+ | DigitalOcean droplet | $4/mo | 1 CPU, 512 MB. Bumps to $6 for 1 GB. |
68
+ | OVH VPS | ~€3/mo | Cheap, EU. |
69
+ | AWS Lightsail | $5/mo | 1 CPU, 1 GB. AWS billing if you want it. |
70
+ | Your existing k8s | $0 marginal | Just \`docker push\` and \`kubectl apply\`. |
71
+ | Fly.io | free tier + $0-5/mo | Their Docker-native platform, cleanest UX of the paid options. |
72
+ | Railway | $5/mo | Same idea. No timeout, easy deploys. |
73
+ | Render | $7/mo | Same idea. |
74
+ | Google Cloud Run | pay-per-request | Scales to zero. Watch the 60-minute request limit. |
75
+
76
+ ## When you'd pick a managed platform instead
77
+
78
+ | Platform | Trade-off |
79
+ |---------------------|-----------|
80
+ | Vercel Pro ($20/mo) | If you're already deploying your Next.js app on Vercel and don't want to split infra. The 60s function cap is tight for mainnet (70-80s calls cut it close); rely on streaming to keep the connection warm. **Hobby tier (free) does NOT work** - 10s cap, every call times out. |
81
+ | Netlify | 26s sync function cap is too tight. Use Netlify's "background functions" (15min) and adapt the route to write the result to KV / a webhook. More work. |
82
+ | Cloudflare Workers | 30s on free, unbounded with Durable Objects. The WebSocket relay setup is more involved than a plain Node server. |
83
+
84
+ The free-tier serverless platforms (Vercel Hobby, Netlify free, Cloudflare
85
+ free) **all fail** at the 60-90s mark. There's no way around that on those
86
+ plans short of upgrading. If you're not paying anyway, self-host - it's
87
+ strictly cheaper and faster than a $20/mo plan.
88
+
89
+ ## What I'd actually pick
90
+
91
+ - **First time trying this out**: \`docker compose up\` on your laptop. Free,
92
+ works in 30 seconds, real end-to-end test of your code.
93
+ - **Going to production with users**: same Dockerfile on a $5/mo Hetzner or
94
+ Fly VM. You're done; it'll handle plenty of traffic.
95
+ - **You already have a Next.js app on Vercel**: upgrade to Pro and keep your
96
+ build pipeline. The streaming route works under their 60s cap for most
97
+ mainnet calls.
98
+ - **You're building a Web3 dApp**: re-run \`lightnode add chat-web3\`. No
99
+ backend, no LCAI cost for you - each user pays their own way.
100
+
101
+ ## Why the request is slow at all
102
+
103
+ LightChain inference is not a synchronous LLM call. Each request:
104
+ 1. Negotiates an ECDH-encrypted session with a worker (off-chain).
105
+ 2. Sends a \`createSession\` tx on-chain (one confirmation).
106
+ 3. Uploads the encrypted prompt blob to the gateway.
107
+ 4. Sends a \`submitJob\` tx on-chain (one confirmation).
108
+ 5. Waits for the worker to run inference + post the encrypted result.
109
+ 6. Decrypts the result client-side.
110
+
111
+ Steps 2, 4, and 5 are the slow part - each waits for a block confirmation
112
+ and worker pickup. The protocol's verifiable-AI guarantee comes from doing
113
+ all of this on-chain instead of just hitting an OpenAI-style API, which is
114
+ the reason the call takes 60-90s instead of 1-2s. There is no way to
115
+ shortcut this on the SDK side; the host just has to allow long-running
116
+ processes - which is exactly what a plain Node server (or Docker container)
117
+ already does for free.
118
+ `;
119
+ /**
120
+ * Dockerfile that builds your Next.js app and runs it as a long-running
121
+ * Node server. There is no function timeout on a plain server, so a 60-90s
122
+ * mainnet inference just works. Multi-stage build keeps the runtime image
123
+ * around 200 MB.
124
+ */
125
+ const NEXTJS_DOCKERFILE = `# Generated by 'lightnode add chat' (or 'add inference' / 'add judge').
126
+ # Build a Next.js production image; run with 'docker compose up --build'.
127
+ FROM node:20-alpine AS deps
128
+ WORKDIR /app
129
+ COPY package.json package-lock.json* ./
130
+ RUN npm ci --omit=optional
131
+
132
+ FROM node:20-alpine AS builder
133
+ WORKDIR /app
134
+ COPY --from=deps /app/node_modules ./node_modules
135
+ COPY . .
136
+ ENV NEXT_TELEMETRY_DISABLED=1
137
+ RUN npm run build
138
+
139
+ FROM node:20-alpine AS runner
140
+ WORKDIR /app
141
+ ENV NODE_ENV=production
142
+ ENV NEXT_TELEMETRY_DISABLED=1
143
+ COPY --from=builder /app/public ./public
144
+ COPY --from=builder /app/.next ./.next
145
+ COPY --from=builder /app/node_modules ./node_modules
146
+ COPY --from=builder /app/package.json ./package.json
147
+ EXPOSE 3000
148
+ # Long-running Node process - no function timeout to fight. Mainnet inference
149
+ # calls (60-90s) complete normally because the process just stays up.
150
+ CMD ["npm", "start"]
151
+ `;
152
+ /**
153
+ * docker-compose.yml. The 'env_file' line wires .env from the project root
154
+ * into the container at runtime, so the same PRIVATE_KEY a 'npm run dev'
155
+ * session uses also flows to the container build.
156
+ */
157
+ const NEXTJS_DOCKER_COMPOSE = `# Generated by 'lightnode add chat' (or 'add inference' / 'add judge').
158
+ # Quick start: docker compose up --build
159
+ # (then visit http://localhost:3000)
160
+ services:
161
+ app:
162
+ build: .
163
+ image: lightnode-app
164
+ container_name: lightnode-app
165
+ ports:
166
+ - "3000:3000"
167
+ # PRIVATE_KEY, NETWORK, MODEL are read from .env at runtime.
168
+ # Make sure .env exists in the same folder as this file (cp .env.example .env).
169
+ env_file:
170
+ - .env
171
+ restart: unless-stopped
172
+ `;
173
+ const DOCKERIGNORE = `# Generated by 'lightnode add chat' (or 'add inference' / 'add judge').
174
+ .git
175
+ .gitignore
176
+ node_modules
177
+ .next
178
+ .env
179
+ .env.local
180
+ .env.*.local
181
+ LIGHTNODE-HOSTING.md
182
+ README.md
183
+ *.log
184
+ `;
34
185
  const ENV_EXAMPLE = (net) => `# Funded private key. Testnet works free (faucet at https://lightfaucet.ai).
35
186
  PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
36
187
 
@@ -182,6 +333,10 @@ export function addInference(opts = {}) {
182
333
  const written = [];
183
334
  if (template === "nextjs-api") {
184
335
  written.push(writeFile(path.join(cwd, "app/api/inference/route.ts"), NEXTJS_ROUTE, force));
336
+ written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
337
+ written.push(writeFile(path.join(cwd, "Dockerfile"), NEXTJS_DOCKERFILE, force));
338
+ written.push(writeFile(path.join(cwd, "docker-compose.yml"), NEXTJS_DOCKER_COMPOSE, force));
339
+ written.push(writeFile(path.join(cwd, ".dockerignore"), DOCKERIGNORE, force));
185
340
  }
186
341
  else if (template === "hono") {
187
342
  written.push(writeFile(path.join(cwd, "lightchain-inference.ts"), HONO_HANDLER, force));
@@ -590,20 +745,47 @@ export default function Chat() {
590
745
  setTurns(next);
591
746
  setDraft("");
592
747
  setBusy(true);
748
+ // Reserve the assistant bubble immediately so tokens can stream into it.
749
+ setTurns([...next, { role: "assistant", text: "" }]);
593
750
  try {
594
- // Concatenate prior turns so the model has the conversation context.
595
751
  const prompt =
596
752
  next.map((t) => (t.role === "user" ? \`User: \${t.text}\` : \`Assistant: \${t.text}\`)).join("\\n") +
597
753
  "\\nAssistant:";
598
754
  const r = await fetch("/api/inference", {
599
755
  method: "POST",
600
756
  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 }]);
757
+ body: JSON.stringify({ prompt, stream: true }),
758
+ });
759
+ if (!r.ok || !r.body) {
760
+ const err = await r.text().catch(() => "");
761
+ throw new Error(\`route returned \${r.status}: \${err.slice(0, 200)}\`);
762
+ }
763
+ const reader = r.body.getReader();
764
+ const decoder = new TextDecoder();
765
+ let assembled = "";
766
+ let txs: Record<string, string> | undefined;
767
+ while (true) {
768
+ const { done, value } = await reader.read();
769
+ if (done) break;
770
+ const text = decoder.decode(value, { stream: true });
771
+ // The route ends the stream with one line of JSON on a new \\u0000
772
+ // delimiter (a NUL byte we put between the streamed prose and the
773
+ // metadata). Anything before is body; anything after is metadata.
774
+ const nulIdx = text.indexOf("\\u0000");
775
+ if (nulIdx === -1) {
776
+ assembled += text;
777
+ setTurns((prev) => prev.map((t, i) => i === prev.length - 1 ? { ...t, text: assembled } : t));
778
+ } else {
779
+ assembled += text.slice(0, nulIdx);
780
+ try { txs = JSON.parse(text.slice(nulIdx + 1)) as Record<string, string>; } catch { /* ignore */ }
781
+ setTurns((prev) => prev.map((t, i) => i === prev.length - 1 ? { ...t, text: assembled, txs } : t));
782
+ }
783
+ }
605
784
  } catch (e) {
606
- setTurns([...next, { role: "assistant", text: \`(error: \${(e as Error).message})\` }]);
785
+ // Replace the empty assistant bubble with the error.
786
+ setTurns((prev) => prev.map((t, i) =>
787
+ i === prev.length - 1 ? { ...t, text: \`(error: \${(e as Error).message})\` } : t
788
+ ));
607
789
  } finally {
608
790
  setBusy(false);
609
791
  }
@@ -664,13 +846,99 @@ export default function Chat() {
664
846
  </button>
665
847
  </div>
666
848
  <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.
849
+ Your server signs every call with the PRIVATE_KEY in <code>.env</code>. Cost per turn is paid from that wallet.
850
+ See <code>LIGHTNODE-HOSTING.md</code> for picking a host that handles 60-90s function calls.
669
851
  </p>
670
852
  </main>
671
853
  );
672
854
  }
673
855
  `;
856
+ /**
857
+ * Streaming inference route, paired with NEXTJS_CHAT_PAGE.
858
+ *
859
+ * Why a second route: the plain NEXTJS_ROUTE returns JSON in one shot,
860
+ * which is fine for one-off calls but means the chat UI shows nothing for
861
+ * 60-90s. This streams decrypted chunks as they arrive (via
862
+ * runInferenceStream), so the user sees tokens land live and the host
863
+ * keeps the connection warm. After the last chunk we append a NUL byte
864
+ * (\\u0000) and a JSON line with the tx hashes so the client can render
865
+ * the on-chain receipt under the assistant bubble.
866
+ */
867
+ const NEXTJS_INFERENCE_STREAM_ROUTE = `// app/api/inference/route.ts
868
+ // Generated by 'lightnode add chat'. Streaming inference route - tokens
869
+ // arrive live as the model produces them, then a NUL byte separates them
870
+ // from a final JSON line with the on-chain tx hashes.
871
+ //
872
+ // Pass { stream: true } in the body to get streaming. Without that flag
873
+ // the same route still returns one-shot JSON, which is fine for non-chat
874
+ // integrations.
875
+ //
876
+ // Mainnet 8b inference takes 60-90s. Vercel Hobby caps function execution
877
+ // at 10s and will time out. See LIGHTNODE-HOSTING.md for hosts that work.
878
+ import { NextResponse } from "next/server";
879
+ import { runInferenceWithKey, runInferenceStream } from "lightnode-sdk";
880
+
881
+ export const runtime = "nodejs";
882
+ export const dynamic = "force-dynamic";
883
+ export const maxDuration = 120;
884
+
885
+ const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
886
+ const MODEL = process.env.MODEL ?? "llama3-8b";
887
+
888
+ export async function POST(req: Request) {
889
+ if (!process.env.PRIVATE_KEY?.startsWith("0x")) {
890
+ return NextResponse.json({ error: "PRIVATE_KEY not configured" }, { status: 500 });
891
+ }
892
+ const body = (await req.json().catch(() => ({}))) as { prompt?: string; system?: string; stream?: boolean };
893
+ const prompt = body.prompt?.trim();
894
+ if (!prompt) return NextResponse.json({ error: "prompt is required" }, { status: 400 });
895
+
896
+ const args = {
897
+ network: NETWORK,
898
+ privateKey: process.env.PRIVATE_KEY as \`0x\${string}\`,
899
+ model: MODEL,
900
+ system: body.system?.trim() || undefined,
901
+ prompt,
902
+ };
903
+
904
+ // One-shot JSON path: same shape as the original 'add inference' route.
905
+ if (!body.stream) {
906
+ try {
907
+ const { answer, worker, txs, jobId } = await runInferenceWithKey(args);
908
+ return NextResponse.json({
909
+ answer, worker, jobId: jobId.toString(),
910
+ txs: { createSession: txs.createSession, submitJob: txs.submitJob, jobCompleted: txs.jobCompleted },
911
+ });
912
+ } catch (e) {
913
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
914
+ }
915
+ }
916
+
917
+ // Streaming path: tokens, NUL byte, then tx metadata as JSON.
918
+ const stream = runInferenceStream(args);
919
+ const encoder = new TextEncoder();
920
+ const body$ = new ReadableStream({
921
+ async start(controller) {
922
+ try {
923
+ for await (const chunk of stream) controller.enqueue(encoder.encode(chunk));
924
+ const { worker, txs, jobId } = await stream.done;
925
+ const meta = JSON.stringify({
926
+ worker, jobId: jobId.toString(),
927
+ createSession: txs.createSession, submitJob: txs.submitJob, jobCompleted: txs.jobCompleted,
928
+ });
929
+ controller.enqueue(encoder.encode("\\u0000" + meta));
930
+ controller.close();
931
+ } catch (e) {
932
+ controller.enqueue(encoder.encode(\`\\u0000{"error":\${JSON.stringify((e as Error).message)}}\`));
933
+ controller.close();
934
+ }
935
+ },
936
+ });
937
+ return new Response(body$, {
938
+ headers: { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" },
939
+ });
940
+ }
941
+ `;
674
942
  const NEXTJS_CHAT_WEB3_PAGE = `// app/chat-web3/page.tsx
675
943
  // Generated by 'lightnode add chat-web3'. User-pays chat: each visitor's own
676
944
  // wallet signs SIWE + createSession per turn. Your app holds zero funds.
@@ -1101,7 +1369,16 @@ export function addChat(opts = {}) {
1101
1369
  const force = !!opts.force;
1102
1370
  const written = [];
1103
1371
  if (template === "nextjs-api") {
1372
+ // 'add chat' is self-contained: it writes the chat page, the streaming
1373
+ // inference route, the hosting guide, AND the Docker setup so the dev
1374
+ // can run the whole stack locally (or anywhere Docker runs) with one
1375
+ // command. No external host signup, no function-timeout fights.
1104
1376
  written.push(writeFile(path.join(cwd, "app/chat/page.tsx"), NEXTJS_CHAT_PAGE, force));
1377
+ written.push(writeFile(path.join(cwd, "app/api/inference/route.ts"), NEXTJS_INFERENCE_STREAM_ROUTE, force));
1378
+ written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
1379
+ written.push(writeFile(path.join(cwd, "Dockerfile"), NEXTJS_DOCKERFILE, force));
1380
+ written.push(writeFile(path.join(cwd, "docker-compose.yml"), NEXTJS_DOCKER_COMPOSE, force));
1381
+ written.push(writeFile(path.join(cwd, ".dockerignore"), DOCKERIGNORE, force));
1105
1382
  }
1106
1383
  else {
1107
1384
  written.push(writeFile(path.join(cwd, "chat-repl.ts"), NODE_CHAT_REPL, force));
@@ -1149,6 +1426,151 @@ export function addChatWeb3(opts = {}) {
1149
1426
  needsWagmi: !hasWagmi,
1150
1427
  };
1151
1428
  }
1429
+ const WAGMI_CONFIG_FILE = `// lib/wagmi.ts
1430
+ // Generated by 'lightnode add wagmi-setup'. Minimal wagmi setup for
1431
+ // LightChain mainnet (9200) + testnet (8200). Use this as a starting
1432
+ // point; swap in RainbowKit / Reown AppKit / ConnectKit if you want a
1433
+ // richer connect UI.
1434
+ import { createConfig, http } from "wagmi";
1435
+ import { injected } from "wagmi/connectors";
1436
+ import type { Chain } from "viem";
1437
+
1438
+ export const lightchainMainnet: Chain = {
1439
+ id: 9200,
1440
+ name: "LightChain",
1441
+ nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 },
1442
+ rpcUrls: { default: { http: ["https://rpc.mainnet.lightchain.ai"] } },
1443
+ blockExplorers: { default: { name: "Lightscan", url: "https://mainnet.lightscan.app" } },
1444
+ };
1445
+
1446
+ export const lightchainTestnet: Chain = {
1447
+ id: 8200,
1448
+ name: "LightChain Testnet",
1449
+ nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 },
1450
+ rpcUrls: { default: { http: ["https://rpc.testnet.lightchain.ai"] } },
1451
+ blockExplorers: { default: { name: "Lightscan", url: "https://testnet.lightscan.app" } },
1452
+ };
1453
+
1454
+ export const wagmiConfig = createConfig({
1455
+ chains: [lightchainMainnet, lightchainTestnet],
1456
+ // \`injected\` covers MetaMask, Rabby, OKX, Phantom EVM, and any browser
1457
+ // wallet that follows EIP-1193. Add walletConnect() / coinbaseWallet()
1458
+ // here if you want explicit support for those.
1459
+ connectors: [injected()],
1460
+ transports: {
1461
+ [lightchainMainnet.id]: http(),
1462
+ [lightchainTestnet.id]: http(),
1463
+ },
1464
+ });
1465
+ `;
1466
+ const WAGMI_PROVIDERS_FILE = `// app/providers.tsx
1467
+ // Generated by 'lightnode add wagmi-setup'. Wraps the app with the
1468
+ // wagmi + react-query providers needed for any wagmi hook to work.
1469
+ //
1470
+ // Import this from your root layout:
1471
+ //
1472
+ // // app/layout.tsx
1473
+ // import { Providers } from "./providers";
1474
+ // export default function RootLayout({ children }: { children: React.ReactNode }) {
1475
+ // return (
1476
+ // <html lang="en">
1477
+ // <body><Providers>{children}</Providers></body>
1478
+ // </html>
1479
+ // );
1480
+ // }
1481
+ "use client";
1482
+
1483
+ import { WagmiProvider } from "wagmi";
1484
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
1485
+ import { wagmiConfig } from "@/lib/wagmi";
1486
+ import type { ReactNode } from "react";
1487
+
1488
+ const queryClient = new QueryClient();
1489
+
1490
+ export function Providers({ children }: { children: ReactNode }) {
1491
+ return (
1492
+ <WagmiProvider config={wagmiConfig}>
1493
+ <QueryClientProvider client={queryClient}>
1494
+ {children}
1495
+ </QueryClientProvider>
1496
+ </WagmiProvider>
1497
+ );
1498
+ }
1499
+ `;
1500
+ const WAGMI_CONNECT_BUTTON = `// components/connect-button.tsx
1501
+ // Generated by 'lightnode add wagmi-setup'. Minimal Connect/Disconnect
1502
+ // button using wagmi's useConnect / useAccount hooks. Swap in
1503
+ // RainbowKit's ConnectButton or Reown's <w3m-button /> for a richer UI.
1504
+ "use client";
1505
+
1506
+ import { useAccount, useConnect, useDisconnect, useSwitchChain } from "wagmi";
1507
+
1508
+ const LIGHTCHAIN_IDS = new Set<number>([9200, 8200]);
1509
+
1510
+ function shortAddress(addr: string): string {
1511
+ return \`\${addr.slice(0, 6)}...\${addr.slice(-4)}\`;
1512
+ }
1513
+
1514
+ export function ConnectButton() {
1515
+ const { address, chain, isConnected } = useAccount();
1516
+ const { connect, connectors, isPending } = useConnect();
1517
+ const { disconnect } = useDisconnect();
1518
+ const { switchChain, isPending: switching } = useSwitchChain();
1519
+
1520
+ if (!isConnected) {
1521
+ const connector = connectors[0];
1522
+ return (
1523
+ <button
1524
+ type="button"
1525
+ onClick={() => connect({ connector })}
1526
+ disabled={isPending}
1527
+ style={{ padding: "8px 16px", borderRadius: 8, cursor: "pointer" }}
1528
+ >
1529
+ {isPending ? "Connecting..." : "Connect wallet"}
1530
+ </button>
1531
+ );
1532
+ }
1533
+
1534
+ if (chain && !LIGHTCHAIN_IDS.has(chain.id)) {
1535
+ return (
1536
+ <button
1537
+ type="button"
1538
+ onClick={() => switchChain({ chainId: 9200 })}
1539
+ disabled={switching}
1540
+ style={{ padding: "8px 16px", borderRadius: 8, background: "#fee", cursor: "pointer" }}
1541
+ >
1542
+ {switching ? "Switching..." : "Switch to LightChain"}
1543
+ </button>
1544
+ );
1545
+ }
1546
+
1547
+ return (
1548
+ <button
1549
+ type="button"
1550
+ onClick={() => disconnect()}
1551
+ style={{ padding: "8px 16px", borderRadius: 8, cursor: "pointer", fontFamily: "monospace" }}
1552
+ >
1553
+ {address ? shortAddress(address) : "(unknown)"} ({chain?.name}) - disconnect
1554
+ </button>
1555
+ );
1556
+ }
1557
+ `;
1558
+ export function addWagmiSetup(opts = {}) {
1559
+ const cwd = opts.cwd ?? process.cwd();
1560
+ const network = opts.network ?? "mainnet";
1561
+ const template = opts.template && opts.template !== "auto" ? opts.template : detectTemplate(cwd);
1562
+ const force = !!opts.force;
1563
+ const written = [];
1564
+ written.push(writeFile(path.join(cwd, "lib/wagmi.ts"), WAGMI_CONFIG_FILE, force));
1565
+ written.push(writeFile(path.join(cwd, "app/providers.tsx"), WAGMI_PROVIDERS_FILE, force));
1566
+ written.push(writeFile(path.join(cwd, "components/connect-button.tsx"), WAGMI_CONNECT_BUTTON, force));
1567
+ return {
1568
+ written,
1569
+ install: `npm install wagmi viem @tanstack/react-query`,
1570
+ template,
1571
+ network,
1572
+ };
1573
+ }
1152
1574
  const NEXTJS_JUDGE_ROUTE = `// app/api/judge/route.ts
1153
1575
  // Generated by 'lightnode add judge'. See https://lightnode.app/build
1154
1576
  // The LightChallenge-style evaluator: post evidence + criteria, get a
@@ -1282,6 +1704,10 @@ export function addJudge(opts = {}) {
1282
1704
  const written = [];
1283
1705
  if (template === "nextjs-api") {
1284
1706
  written.push(writeFile(path.join(cwd, "app/api/judge/route.ts"), NEXTJS_JUDGE_ROUTE, force));
1707
+ written.push(writeFile(path.join(cwd, "LIGHTNODE-HOSTING.md"), HOSTING_GUIDE, force));
1708
+ written.push(writeFile(path.join(cwd, "Dockerfile"), NEXTJS_DOCKERFILE, force));
1709
+ written.push(writeFile(path.join(cwd, "docker-compose.yml"), NEXTJS_DOCKER_COMPOSE, force));
1710
+ written.push(writeFile(path.join(cwd, ".dockerignore"), DOCKERIGNORE, force));
1285
1711
  }
1286
1712
  else {
1287
1713
  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,14 +534,18 @@ 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. Pick one:`);
538
+ console.log(` a) docker compose up --build # run the whole stack yourself, no timeout`);
539
+ console.log(` b) npm run dev # local dev only`);
540
+ console.log(` Open http://localhost:3000/chat`);
526
541
  }
527
542
  else if (sub === "chat") {
528
543
  console.log(` 3. npx tsx chat-repl.ts (interactive terminal chat)`);
529
544
  }
530
545
  else if (sub === "judge" && result.template === "nextjs-api") {
531
- console.log(` 3. npm run dev`);
546
+ console.log(` 3. Pick one:`);
547
+ console.log(` a) docker compose up --build # run the whole stack yourself, no timeout`);
548
+ console.log(` b) npm run dev # local dev only`);
532
549
  console.log(` 4. curl -X POST localhost:3000/api/judge -H 'content-type: application/json' \\\\`);
533
550
  console.log(` -d '{"criteria":"Run a mile under 8 minutes","evidence":{"time_minutes":7.4,"distance_km":1.61}}'`);
534
551
  }
@@ -540,7 +557,10 @@ async function main() {
540
557
  console.log(` 4. npm run dev, open /nft-mint`);
541
558
  }
542
559
  else if (result.template === "nextjs-api") {
543
- console.log(` 3. npm run dev (then POST /api/inference)`);
560
+ console.log(` 3. Pick one:`);
561
+ console.log(` a) docker compose up --build # run the whole stack yourself, no timeout`);
562
+ console.log(` b) npm run dev # local dev only`);
563
+ console.log(` POST http://localhost:3000/api/inference {"prompt":"hello"}`);
544
564
  }
545
565
  else if (result.template === "hono") {
546
566
  console.log(` 3. wire inferenceHandler into your Hono app, then start it`);
@@ -561,6 +581,17 @@ async function main() {
561
581
  console.log(` 2. npx tsx lightnode-analytics.ts`);
562
582
  }
563
583
  }
584
+ // Hosting note: the Docker setup we shipped is the recommended path.
585
+ // The managed platforms (Vercel etc.) are the fallback if a builder is
586
+ // already committed to one.
587
+ if (result.template === "nextjs-api"
588
+ && (sub === "inference" || sub === "chat" || sub === "judge")) {
589
+ console.log(`\n Hosting: a mainnet inference takes 60-90s. The Dockerfile + docker-compose.yml`);
590
+ console.log(` we just dropped run a long-running Node server with no timeout - that's the`);
591
+ console.log(` recommended path (your laptop, a $5/mo VPS, anywhere Docker runs).`);
592
+ console.log(` Don't use Vercel Hobby (10s cap, every call times out). Vercel Pro works at`);
593
+ console.log(` 60s if you'd rather stay on Vercel. See LIGHTNODE-HOSTING.md for the full table.`);
594
+ }
564
595
  if (result.network === "testnet") {
565
596
  console.log(`\nNo wallet yet? Make one: npx lightnode wallet new then fund it free below.`);
566
597
  }
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.17";
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.17";
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.17",
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",