lightnode-sdk 0.7.17 → 0.7.19

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/README.md CHANGED
@@ -464,17 +464,35 @@ PRIVATE_KEY=0x... npx lightnode worker deregister --yes # clear stuck + settl
464
464
 
465
465
  ### Scaffolders (write files into your project)
466
466
 
467
+ Server-paid (you host a backend; your funded wallet pays per call):
468
+
467
469
  ```bash
468
470
  npx lightnode add inference # encrypted inference route or script
469
471
  npx lightnode add chat # chat UI with conversation history
472
+ npx lightnode add judge # pass/fail evaluator route (criteria + evidence)
470
473
  npx lightnode add agent # scheduled inference (Vercel Cron / setInterval)
471
474
  npx lightnode add analytics-dashboard # read-only network + worker analytics page
472
475
  npx lightnode add nft-mint-with-inference # AI-generated NFT metadata with on-chain provenance
473
476
  ```
474
477
 
478
+ User-paid (no backend; each visitor signs + pays from their own wallet):
479
+
480
+ ```bash
481
+ npx lightnode add inference-web3 # one-shot inference UI, wallet-signed
482
+ npx lightnode add chat-web3 # chat UI, wallet-signed (mainnet + testnet aware)
483
+ npx lightnode add judge-web3 # evaluator UI, wallet-signed
484
+ npx lightnode add wagmi-setup # wallet wiring: lib/wagmi + providers + connect button
485
+ ```
486
+
487
+ The `*-web3` scaffolders and `wagmi-setup` write Next.js pages, so run them
488
+ inside a Next.js app (`npx create-next-app@latest .` first if you have none).
489
+
475
490
  All `add` commands accept `--template auto|nextjs-api|hono|node`,
476
491
  `--net testnet|mainnet`, and `--force`.
477
492
 
493
+ > If `add <name>` reports an unknown target, your `npx` cache is serving an
494
+ > older CLI. Force the current release: `npx lightnode-sdk@latest add <name>`.
495
+
478
496
  ## Networks
479
497
 
480
498
  | | Testnet | Mainnet |
package/dist/add.d.ts CHANGED
@@ -70,6 +70,39 @@ export declare function addChatWeb3(opts?: AddOpts): {
70
70
  network: Network;
71
71
  needsWagmi: boolean;
72
72
  };
73
+ /**
74
+ * `lightnode add inference-web3` - the user-pays counterpart to addInference.
75
+ *
76
+ * Browser-only React component. Each visitor signs SIWE + createSession from
77
+ * their own wallet. No backend, no PRIVATE_KEY, no per-call cost for the dev.
78
+ *
79
+ * Fits any one-shot inference UI: classifier, NFT trait generator, content
80
+ * moderation, evaluator. For multi-turn chat use addChatWeb3; for the
81
+ * dedicated judge pattern use addJudgeWeb3.
82
+ */
83
+ export declare function addInferenceWeb3(opts?: AddOpts): {
84
+ written: WrittenFile[];
85
+ install: string;
86
+ template: Template;
87
+ network: Network;
88
+ needsWagmi: boolean;
89
+ };
90
+ /**
91
+ * `lightnode add judge-web3` - the user-pays counterpart to addJudge.
92
+ *
93
+ * The LightChallenge pattern with the user paying: visitor submits criteria
94
+ * + evidence, signs from their own wallet, the structured pass/fail/confidence
95
+ * verdict comes back with on-chain proof. Fit: challenge completion grading,
96
+ * NFT trait verification, content moderation, automated scoring - any flow
97
+ * where the END USER wants a verifiable AI verdict on their own submission.
98
+ */
99
+ export declare function addJudgeWeb3(opts?: AddOpts): {
100
+ written: WrittenFile[];
101
+ install: string;
102
+ template: Template;
103
+ network: Network;
104
+ needsWagmi: boolean;
105
+ };
73
106
  export declare function addWagmiSetup(opts?: AddOpts): {
74
107
  written: WrittenFile[];
75
108
  install: string;
package/dist/add.js CHANGED
@@ -1140,6 +1140,354 @@ export default function ChatWeb3() {
1140
1140
  );
1141
1141
  }
1142
1142
  `;
1143
+ const NEXTJS_INFERENCE_WEB3_PAGE = `// app/inference-web3/page.tsx
1144
+ // Generated by 'lightnode add inference-web3'. User-pays one-shot inference:
1145
+ // each visitor's wallet signs SIWE + createSession + submitJob. Your app
1146
+ // holds zero funds and the answer comes back with on-chain proof anyone
1147
+ // can verify.
1148
+ //
1149
+ // Prereqs:
1150
+ // - wagmi configured (npx lightnode add wagmi-setup if you don't have it)
1151
+ // - the connected wallet has LCAI on the chain it's on
1152
+ // (mainnet 9200 or testnet 8200). Mainnet llama3-8b is 0.02 LCAI per call.
1153
+ "use client";
1154
+
1155
+ import { useEffect, useState } from "react";
1156
+ import { useAccount, useWalletClient, usePublicClient } from "wagmi";
1157
+ import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
1158
+
1159
+ type Result = {
1160
+ answer: string;
1161
+ worker: \`0x\${string}\`;
1162
+ jobId: string;
1163
+ submitJob: \`0x\${string}\`;
1164
+ jobCompleted: \`0x\${string}\` | null;
1165
+ elapsedMs: number;
1166
+ };
1167
+
1168
+ const MODEL = "llama3-8b";
1169
+
1170
+ export default function InferenceWeb3() {
1171
+ const { address, chain } = useAccount();
1172
+ const network: "mainnet" | "testnet" | null =
1173
+ chain?.id === 9200 ? "mainnet" : chain?.id === 8200 ? "testnet" : null;
1174
+ const { data: walletClient } = useWalletClient({ chainId: chain?.id });
1175
+ const publicClient = usePublicClient({ chainId: chain?.id });
1176
+
1177
+ const [system, setSystem] = useState("You are a concise assistant. Reply in one or two short sentences.");
1178
+ const [prompt, setPrompt] = useState("Reply with the single word OK.");
1179
+ const [busy, setBusy] = useState(false);
1180
+ const [busyStage, setBusyStage] = useState("");
1181
+ const [result, setResult] = useState<Result | null>(null);
1182
+ const [err, setErr] = useState<string | null>(null);
1183
+ const [feeLcai, setFeeLcai] = useState<number | null>(null);
1184
+
1185
+ useEffect(() => {
1186
+ if (!network) { setFeeLcai(null); return; }
1187
+ let cancelled = false;
1188
+ estimateJobFee(NETWORKS[network], MODEL).then(
1189
+ (fee) => { if (!cancelled) setFeeLcai(fee); },
1190
+ () => { if (!cancelled) setFeeLcai(null); },
1191
+ );
1192
+ return () => { cancelled = true; };
1193
+ }, [network]);
1194
+
1195
+ async function run() {
1196
+ if (!walletClient || !publicClient || !address || !network) {
1197
+ setErr("Connect a wallet on LightChain mainnet (9200) or testnet (8200) first.");
1198
+ return;
1199
+ }
1200
+ if (!prompt.trim()) return;
1201
+ setBusy(true);
1202
+ setErr(null);
1203
+ setResult(null);
1204
+ const t0 = Date.now();
1205
+ try {
1206
+ setBusyStage("Sign in with your wallet (SIWE)...");
1207
+ const session = await siweSignIn(walletClient as unknown as Parameters<typeof siweSignIn>[0], network);
1208
+
1209
+ setBusyStage("Approve the createSession transaction in your wallet...");
1210
+ const gateway = new GatewayClient({ network, bearer: session.bearer });
1211
+ const composed = system.trim() ? \`\${system.trim()}\\n\\n\${prompt}\` : prompt;
1212
+ const r = await runInference({
1213
+ prompt: composed,
1214
+ gateway,
1215
+ wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
1216
+ publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
1217
+ network: NETWORKS[network],
1218
+ model: MODEL,
1219
+ jobCompletedTimeoutMs: 120_000,
1220
+ maxRetries: 1,
1221
+ });
1222
+ setResult({
1223
+ answer: r.answer,
1224
+ worker: r.worker,
1225
+ jobId: r.jobId?.toString() ?? "",
1226
+ submitJob: r.txs?.submitJob,
1227
+ jobCompleted: r.txs?.jobCompleted ?? null,
1228
+ elapsedMs: Date.now() - t0,
1229
+ });
1230
+ } catch (e) {
1231
+ const msg = (e as Error).message ?? "inference failed";
1232
+ setErr(
1233
+ /user rejected|user denied|reject/i.test(msg) ? "You rejected the wallet popup. Try again."
1234
+ : /insufficient funds|insufficient balance/i.test(msg)
1235
+ ? \`Your wallet has no \${network === "mainnet" ? "LCAI" : "testnet LCAI"}. Top it up before sending.\`
1236
+ : msg.split("\\n")[0]
1237
+ );
1238
+ } finally {
1239
+ setBusy(false);
1240
+ setBusyStage("");
1241
+ }
1242
+ }
1243
+
1244
+ return (
1245
+ <main style={{ maxWidth: 720, margin: "32px auto", padding: 16, fontFamily: "system-ui" }}>
1246
+ <h1>Inference (user-pays)</h1>
1247
+ <p style={{ color: "#666" }}>
1248
+ Signs one encrypted inference from your wallet on{" "}
1249
+ <code>{network ?? "(connect a wallet)"}</code>. Fee:{" "}
1250
+ <code>{feeLcai != null ? \`\${feeLcai} LCAI\` : "(fetching)"}</code> per call plus a small gas amount.
1251
+ </p>
1252
+ {!address && (
1253
+ <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0" }}>
1254
+ Connect a wallet to run inference. Drop &lt;ConnectButton /&gt; here or wherever your app exposes one.
1255
+ </div>
1256
+ )}
1257
+
1258
+ <label style={{ display: "block", margin: "12px 0" }}>
1259
+ <div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 4 }}>System prompt</div>
1260
+ <textarea value={system} onChange={(e) => setSystem(e.target.value)} rows={2}
1261
+ style={{ width: "100%", padding: 8, fontFamily: "monospace", fontSize: 12 }} />
1262
+ </label>
1263
+ <label style={{ display: "block", margin: "12px 0" }}>
1264
+ <div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 4 }}>Prompt</div>
1265
+ <textarea value={prompt} onChange={(e) => setPrompt(e.target.value)} rows={5}
1266
+ style={{ width: "100%", padding: 8, fontFamily: "monospace", fontSize: 12 }} />
1267
+ </label>
1268
+ <button type="button" onClick={() => run()} disabled={busy || !prompt.trim() || !address || !network}
1269
+ style={{ padding: "8px 16px", borderRadius: 8, cursor: busy ? "wait" : "pointer" }}>
1270
+ {busy ? (busyStage || "Running...") : "Run inference"}
1271
+ </button>
1272
+
1273
+ {err && (
1274
+ <p style={{ marginTop: 12, padding: "8px 12px", border: "1px solid #f5c2c7", background: "#f8d7da", color: "#842029", borderRadius: 6 }}>
1275
+ {err}
1276
+ </p>
1277
+ )}
1278
+
1279
+ {result && (
1280
+ <div style={{ marginTop: 16, padding: 16, border: "1px solid #ddd", borderRadius: 8 }}>
1281
+ <div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 8 }}>Answer</div>
1282
+ <pre style={{ whiteSpace: "pre-wrap", margin: 0, fontFamily: "inherit" }}>{result.answer}</pre>
1283
+ <div style={{ marginTop: 12, fontSize: 12, color: "#666", display: "flex", gap: 12, flexWrap: "wrap" }}>
1284
+ <span>elapsed {Math.round(result.elapsedMs / 1000)}s</span>
1285
+ <span>job #{result.jobId}</span>
1286
+ <a href={\`https://\${network}.lightscan.app/address/\${result.worker}\`} target="_blank" rel="noopener noreferrer">worker</a>
1287
+ <a href={\`https://\${network}.lightscan.app/tx/\${result.submitJob}\`} target="_blank" rel="noopener noreferrer">submitJob</a>
1288
+ {result.jobCompleted && (
1289
+ <a href={\`https://\${network}.lightscan.app/tx/\${result.jobCompleted}\`} target="_blank" rel="noopener noreferrer">jobCompleted</a>
1290
+ )}
1291
+ </div>
1292
+ </div>
1293
+ )}
1294
+ </main>
1295
+ );
1296
+ }
1297
+ `;
1298
+ const NEXTJS_JUDGE_WEB3_PAGE = `// app/judge-web3/page.tsx
1299
+ // Generated by 'lightnode add judge-web3'. The LightChallenge pattern with
1300
+ // the user paying: visitor submits criteria + evidence, signs from their own
1301
+ // wallet, the verdict comes back with on-chain proof. Pattern fits any
1302
+ // platform where users want a verifiable AI verdict on their own submission
1303
+ // (challenge completion, NFT trait grading, content moderation, etc.).
1304
+ //
1305
+ // Prereqs:
1306
+ // - wagmi configured (npx lightnode add wagmi-setup if you don't have it)
1307
+ // - the connected wallet has LCAI on the chain it's on
1308
+ "use client";
1309
+
1310
+ import { useEffect, useState } from "react";
1311
+ import { useAccount, useWalletClient, usePublicClient } from "wagmi";
1312
+ import { siweSignIn, GatewayClient, runInference, estimateJobFee, NETWORKS } from "lightnode-sdk";
1313
+
1314
+ type Verdict = {
1315
+ passed: boolean;
1316
+ confidence: number;
1317
+ reason: string;
1318
+ };
1319
+
1320
+ type Result = {
1321
+ verdict: Verdict | null;
1322
+ raw: string;
1323
+ worker: \`0x\${string}\`;
1324
+ jobId: string;
1325
+ submitJob: \`0x\${string}\`;
1326
+ jobCompleted: \`0x\${string}\` | null;
1327
+ };
1328
+
1329
+ const MODEL = "llama3-8b";
1330
+
1331
+ export default function JudgeWeb3() {
1332
+ const { address, chain } = useAccount();
1333
+ const network: "mainnet" | "testnet" | null =
1334
+ chain?.id === 9200 ? "mainnet" : chain?.id === 8200 ? "testnet" : null;
1335
+ const { data: walletClient } = useWalletClient({ chainId: chain?.id });
1336
+ const publicClient = usePublicClient({ chainId: chain?.id });
1337
+
1338
+ const [criteria, setCriteria] = useState("Run a mile under 8 minutes");
1339
+ const [evidence, setEvidence] = useState('{"distance_km": 1.61, "time_minutes": 7.4}');
1340
+ const [busy, setBusy] = useState(false);
1341
+ const [busyStage, setBusyStage] = useState("");
1342
+ const [result, setResult] = useState<Result | null>(null);
1343
+ const [err, setErr] = useState<string | null>(null);
1344
+ const [feeLcai, setFeeLcai] = useState<number | null>(null);
1345
+
1346
+ useEffect(() => {
1347
+ if (!network) { setFeeLcai(null); return; }
1348
+ let cancelled = false;
1349
+ estimateJobFee(NETWORKS[network], MODEL).then(
1350
+ (fee) => { if (!cancelled) setFeeLcai(fee); },
1351
+ () => { if (!cancelled) setFeeLcai(null); },
1352
+ );
1353
+ return () => { cancelled = true; };
1354
+ }, [network]);
1355
+
1356
+ /** Parse the verdict defensively; fall back to the first {...} block. */
1357
+ function parseVerdict(answer: string): Verdict | null {
1358
+ try { return JSON.parse(answer) as Verdict; } catch { /* try regex */ }
1359
+ const m = answer.match(/\\{[\\s\\S]*\\}/);
1360
+ if (m) {
1361
+ try { return JSON.parse(m[0]) as Verdict; } catch { /* keep null */ }
1362
+ }
1363
+ return null;
1364
+ }
1365
+
1366
+ async function run() {
1367
+ if (!walletClient || !publicClient || !address || !network) {
1368
+ setErr("Connect a wallet on LightChain mainnet (9200) or testnet (8200) first.");
1369
+ return;
1370
+ }
1371
+ if (!criteria.trim() || !evidence.trim()) return;
1372
+ // Validate evidence JSON before paying.
1373
+ try { JSON.parse(evidence); } catch {
1374
+ setErr("Evidence is not valid JSON. Fix it and try again.");
1375
+ return;
1376
+ }
1377
+ setBusy(true);
1378
+ setErr(null);
1379
+ setResult(null);
1380
+ try {
1381
+ setBusyStage("Sign in with your wallet (SIWE)...");
1382
+ const session = await siweSignIn(walletClient as unknown as Parameters<typeof siweSignIn>[0], network);
1383
+
1384
+ setBusyStage("Approve the createSession transaction in your wallet...");
1385
+ const gateway = new GatewayClient({ network, bearer: session.bearer });
1386
+ const prompt = \`Criteria: \${criteria.trim()}
1387
+
1388
+ Evidence: \${evidence.trim()}
1389
+
1390
+ Reply with STRICT JSON only, matching: { "passed": boolean, "confidence": 0-1, "reason": string }\`;
1391
+
1392
+ const r = await runInference({
1393
+ prompt: \`You are a careful judge. Reply with STRICT JSON only, no prose.\\n\\n\${prompt}\`,
1394
+ gateway,
1395
+ wallet: walletClient as unknown as Parameters<typeof runInference>[0]["wallet"],
1396
+ publicClient: publicClient as unknown as Parameters<typeof runInference>[0]["publicClient"],
1397
+ network: NETWORKS[network],
1398
+ model: MODEL,
1399
+ jobCompletedTimeoutMs: 120_000,
1400
+ maxRetries: 1,
1401
+ });
1402
+ setResult({
1403
+ verdict: parseVerdict(r.answer),
1404
+ raw: r.answer,
1405
+ worker: r.worker,
1406
+ jobId: r.jobId?.toString() ?? "",
1407
+ submitJob: r.txs?.submitJob,
1408
+ jobCompleted: r.txs?.jobCompleted ?? null,
1409
+ });
1410
+ } catch (e) {
1411
+ const msg = (e as Error).message ?? "judge failed";
1412
+ setErr(
1413
+ /user rejected|user denied|reject/i.test(msg) ? "You rejected the wallet popup. Try again."
1414
+ : /insufficient funds|insufficient balance/i.test(msg)
1415
+ ? \`Your wallet has no \${network === "mainnet" ? "LCAI" : "testnet LCAI"}. Top it up before submitting.\`
1416
+ : msg.split("\\n")[0]
1417
+ );
1418
+ } finally {
1419
+ setBusy(false);
1420
+ setBusyStage("");
1421
+ }
1422
+ }
1423
+
1424
+ return (
1425
+ <main style={{ maxWidth: 720, margin: "32px auto", padding: 16, fontFamily: "system-ui" }}>
1426
+ <h1>AI Judge (user-pays)</h1>
1427
+ <p style={{ color: "#666" }}>
1428
+ Each submission signs one inference from your wallet on{" "}
1429
+ <code>{network ?? "(connect a wallet)"}</code>. Cost:{" "}
1430
+ <code>{feeLcai != null ? \`\${feeLcai} LCAI\` : "(fetching)"}</code> plus gas. Verdict comes back with on-chain proof.
1431
+ </p>
1432
+ {!address && (
1433
+ <div style={{ border: "1px solid #ddd", borderRadius: 8, padding: 12, margin: "12px 0" }}>
1434
+ Connect a wallet to submit. Drop &lt;ConnectButton /&gt; here or wherever your app exposes one.
1435
+ </div>
1436
+ )}
1437
+
1438
+ <label style={{ display: "block", margin: "12px 0" }}>
1439
+ <div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 4 }}>Criteria</div>
1440
+ <textarea value={criteria} onChange={(e) => setCriteria(e.target.value)} rows={2}
1441
+ style={{ width: "100%", padding: 8, fontFamily: "monospace", fontSize: 12 }} />
1442
+ </label>
1443
+ <label style={{ display: "block", margin: "12px 0" }}>
1444
+ <div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 4 }}>Evidence (JSON)</div>
1445
+ <textarea value={evidence} onChange={(e) => setEvidence(e.target.value)} rows={5}
1446
+ style={{ width: "100%", padding: 8, fontFamily: "monospace", fontSize: 12 }} />
1447
+ </label>
1448
+ <button type="button" onClick={() => run()} disabled={busy || !criteria.trim() || !evidence.trim() || !address || !network}
1449
+ style={{ padding: "8px 16px", borderRadius: 8, cursor: busy ? "wait" : "pointer" }}>
1450
+ {busy ? (busyStage || "Judging...") : "Get AI verdict"}
1451
+ </button>
1452
+
1453
+ {err && (
1454
+ <p style={{ marginTop: 12, padding: "8px 12px", border: "1px solid #f5c2c7", background: "#f8d7da", color: "#842029", borderRadius: 6 }}>
1455
+ {err}
1456
+ </p>
1457
+ )}
1458
+
1459
+ {result && (
1460
+ <div style={{ marginTop: 16, padding: 16, border: "1px solid #ddd", borderRadius: 8 }}>
1461
+ <div style={{ fontSize: 11, textTransform: "uppercase", color: "#888", marginBottom: 8 }}>Verdict</div>
1462
+ {result.verdict ? (
1463
+ <div>
1464
+ <div style={{ fontSize: 24, fontWeight: 600, color: result.verdict.passed ? "#2e7d32" : "#c62828" }}>
1465
+ {result.verdict.passed ? "PASSED" : "FAILED"}
1466
+ <span style={{ marginLeft: 12, fontSize: 14, color: "#666" }}>
1467
+ confidence {Math.round(result.verdict.confidence * 100)}%
1468
+ </span>
1469
+ </div>
1470
+ <p style={{ marginTop: 8, color: "#444" }}>{result.verdict.reason}</p>
1471
+ </div>
1472
+ ) : (
1473
+ <pre style={{ whiteSpace: "pre-wrap", margin: 0, fontFamily: "monospace", fontSize: 12, color: "#666" }}>
1474
+ {result.raw}
1475
+ </pre>
1476
+ )}
1477
+ <div style={{ marginTop: 12, fontSize: 12, color: "#666", display: "flex", gap: 12, flexWrap: "wrap" }}>
1478
+ <span>job #{result.jobId}</span>
1479
+ <a href={\`https://\${network}.lightscan.app/address/\${result.worker}\`} target="_blank" rel="noopener noreferrer">worker</a>
1480
+ <a href={\`https://\${network}.lightscan.app/tx/\${result.submitJob}\`} target="_blank" rel="noopener noreferrer">submitJob</a>
1481
+ {result.jobCompleted && (
1482
+ <a href={\`https://\${network}.lightscan.app/tx/\${result.jobCompleted}\`} target="_blank" rel="noopener noreferrer">jobCompleted</a>
1483
+ )}
1484
+ </div>
1485
+ </div>
1486
+ )}
1487
+ </main>
1488
+ );
1489
+ }
1490
+ `;
1143
1491
  const NODE_CHAT_REPL = `// chat-repl.ts
1144
1492
  // Generated by 'lightnode add chat'. Interactive chat REPL in your terminal.
1145
1493
  // npm install lightnode-sdk viem ws
@@ -1426,6 +1774,59 @@ export function addChatWeb3(opts = {}) {
1426
1774
  needsWagmi: !hasWagmi,
1427
1775
  };
1428
1776
  }
1777
+ /**
1778
+ * `lightnode add inference-web3` - the user-pays counterpart to addInference.
1779
+ *
1780
+ * Browser-only React component. Each visitor signs SIWE + createSession from
1781
+ * their own wallet. No backend, no PRIVATE_KEY, no per-call cost for the dev.
1782
+ *
1783
+ * Fits any one-shot inference UI: classifier, NFT trait generator, content
1784
+ * moderation, evaluator. For multi-turn chat use addChatWeb3; for the
1785
+ * dedicated judge pattern use addJudgeWeb3.
1786
+ */
1787
+ export function addInferenceWeb3(opts = {}) {
1788
+ const cwd = opts.cwd ?? process.cwd();
1789
+ const network = opts.network ?? "mainnet";
1790
+ const detected = detectTemplate(cwd);
1791
+ const template = opts.template && opts.template !== "auto" ? opts.template : detected;
1792
+ const force = !!opts.force;
1793
+ const written = [];
1794
+ const pkg = readPackageJson(cwd);
1795
+ const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
1796
+ const hasWagmi = Boolean(deps["wagmi"]);
1797
+ written.push(writeFile(path.join(cwd, "app/inference-web3/page.tsx"), NEXTJS_INFERENCE_WEB3_PAGE, force));
1798
+ return {
1799
+ written,
1800
+ install: `npm install lightnode-sdk viem` + (hasWagmi ? "" : " wagmi @tanstack/react-query"),
1801
+ template, network, needsWagmi: !hasWagmi,
1802
+ };
1803
+ }
1804
+ /**
1805
+ * `lightnode add judge-web3` - the user-pays counterpart to addJudge.
1806
+ *
1807
+ * The LightChallenge pattern with the user paying: visitor submits criteria
1808
+ * + evidence, signs from their own wallet, the structured pass/fail/confidence
1809
+ * verdict comes back with on-chain proof. Fit: challenge completion grading,
1810
+ * NFT trait verification, content moderation, automated scoring - any flow
1811
+ * where the END USER wants a verifiable AI verdict on their own submission.
1812
+ */
1813
+ export function addJudgeWeb3(opts = {}) {
1814
+ const cwd = opts.cwd ?? process.cwd();
1815
+ const network = opts.network ?? "mainnet";
1816
+ const detected = detectTemplate(cwd);
1817
+ const template = opts.template && opts.template !== "auto" ? opts.template : detected;
1818
+ const force = !!opts.force;
1819
+ const written = [];
1820
+ const pkg = readPackageJson(cwd);
1821
+ const deps = { ...(pkg?.dependencies ?? {}), ...(pkg?.devDependencies ?? {}) };
1822
+ const hasWagmi = Boolean(deps["wagmi"]);
1823
+ written.push(writeFile(path.join(cwd, "app/judge-web3/page.tsx"), NEXTJS_JUDGE_WEB3_PAGE, force));
1824
+ return {
1825
+ written,
1826
+ install: `npm install lightnode-sdk viem` + (hasWagmi ? "" : " wagmi @tanstack/react-query"),
1827
+ template, network, needsWagmi: !hasWagmi,
1828
+ };
1829
+ }
1429
1830
  const WAGMI_CONFIG_FILE = `// lib/wagmi.ts
1430
1831
  // Generated by 'lightnode add wagmi-setup'. Minimal wagmi setup for
1431
1832
  // LightChain mainnet (9200) + testnet (8200). Use this as a starting
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, addWagmiSetup } from "./add.js";
3
+ import { addInference, addInferenceWeb3, addJudgeWeb3, 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) {
@@ -62,13 +62,20 @@ Ecosystem (read-only):
62
62
  dao addresses print LCAI Governor + Timelock + Treasury addresses
63
63
  dao config print voting delay / period / threshold (live read)
64
64
 
65
- Scaffold templates into the current project:
66
- add inference end-to-end encrypted inference route/script
67
- add chat chat-style UI with conversation history
68
- add agent scheduled/loop inference (cron-style)
69
- add analytics-dashboard read-only network + worker analytics page
70
- add nft-mint-with-inference AI-generated NFT metadata (provenance on-chain)
71
- (all add commands: [--template auto|nextjs-api|hono|node] [--force])
65
+ Scaffold templates into the current project (run inside a Next.js app):
66
+ Server-paid (you host a backend; your funded wallet pays per call):
67
+ add inference end-to-end encrypted inference route/script
68
+ add chat chat-style UI with conversation history
69
+ add judge pass/fail evaluator route (criteria + evidence)
70
+ add agent scheduled/loop inference (cron-style)
71
+ add analytics-dashboard read-only network + worker analytics page
72
+ add nft-mint-with-inference AI-generated NFT metadata (provenance on-chain)
73
+ User-paid (no backend; each visitor signs + pays from their own wallet):
74
+ add inference-web3 one-shot inference UI, wallet-signed
75
+ add chat-web3 chat UI, wallet-signed (mainnet + testnet aware)
76
+ add judge-web3 evaluator UI, wallet-signed
77
+ add wagmi-setup wallet wiring: lib/wagmi + providers + connect button
78
+ (all add commands: [--template auto|nextjs-api|hono|node] [--net testnet|mainnet] [--force])
72
79
 
73
80
  To scaffold a new project instead, run: npm create lightnode-app my-app`;
74
81
  function pickKey() {
@@ -462,25 +469,31 @@ async function main() {
462
469
  const template = flag("--template") ?? "auto";
463
470
  const force = process.argv.includes("--force");
464
471
  const network = (net === "mainnet" ? "mainnet" : "testnet");
465
- const known = ["inference", "chat", "chat-web3", "wagmi-setup", "agent", "judge", "analytics-dashboard", "nft-mint-with-inference"];
472
+ const known = ["inference", "inference-web3", "chat", "chat-web3", "judge", "judge-web3", "wagmi-setup", "agent", "analytics-dashboard", "nft-mint-with-inference"];
466
473
  if (!known.includes(sub ?? "")) {
467
- die(`usage: lightnode add <${known.join("|")}> [--template auto|nextjs-api|hono|node] [--net testnet|mainnet] [--force]`);
468
- }
469
- const result = sub === "analytics-dashboard"
470
- ? addAnalyticsDashboard({ template, network, force })
471
- : sub === "nft-mint-with-inference"
472
- ? addNftMint({ template, network, force })
473
- : sub === "chat-web3"
474
- ? addChatWeb3({ 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 });
474
+ const lines = [
475
+ `usage: lightnode add <${known.join("|")}> [--template auto|nextjs-api|hono|node] [--net testnet|mainnet] [--force]`,
476
+ ];
477
+ // If the requested name is valid in a newer release but missing here,
478
+ // the user is almost certainly running a stale npx-cached CLI. Point
479
+ // them at @latest rather than letting them think the command is gone.
480
+ if (sub) {
481
+ lines.push("");
482
+ lines.push(`unknown add target "${sub}". If you expected this to work, you may be on an`);
483
+ lines.push(`older cached CLI. Force the current version: npx lightnode-sdk@latest add ${sub}`);
484
+ }
485
+ die(lines.join("\n"));
486
+ }
487
+ const result = sub === "analytics-dashboard" ? addAnalyticsDashboard({ template, network, force })
488
+ : sub === "nft-mint-with-inference" ? addNftMint({ template, network, force })
489
+ : sub === "chat-web3" ? addChatWeb3({ template, network, force })
490
+ : sub === "inference-web3" ? addInferenceWeb3({ template, network, force })
491
+ : sub === "judge-web3" ? addJudgeWeb3({ template, network, force })
492
+ : sub === "wagmi-setup" ? addWagmiSetup({ template, network, force })
493
+ : sub === "chat" ? addChat({ template, network, force })
494
+ : sub === "agent" ? addAgent({ template, network, force })
495
+ : sub === "judge" ? addJudge({ template, network, force })
496
+ : addInference({ template, network, force });
484
497
  console.log(`▶ add ${sub} (${result.template} template, default network ${result.network})`);
485
498
  for (const f of result.written) {
486
499
  if (f.skipped)
@@ -493,6 +506,16 @@ async function main() {
493
506
  console.log("\nNothing to do - all target files already exist. Pass --force to overwrite.");
494
507
  }
495
508
  else {
509
+ // The *-web3 pages and wagmi-setup are Next.js React files. If no
510
+ // Next.js app was detected (e.g. an empty folder), nothing can render
511
+ // what we just wrote - surface that before the numbered steps so the
512
+ // user scaffolds an app first instead of chasing a non-running page.
513
+ const isNextOnly = sub === "chat-web3" || sub === "inference-web3" || sub === "judge-web3" || sub === "wagmi-setup";
514
+ if (isNextOnly && result.template !== "nextjs-api") {
515
+ console.log(`\nNo Next.js app detected in this folder. ${sub} is a Next.js page, so`);
516
+ console.log(`create one here first, then re-run this command:`);
517
+ console.log(` npx create-next-app@latest .`);
518
+ }
496
519
  console.log(`\nNext steps (these files were added to your CURRENT folder, not a new project):`);
497
520
  console.log(` 1. ${result.install}`);
498
521
  if (sub === "wagmi-setup") {
@@ -504,24 +527,26 @@ async function main() {
504
527
  console.log(` 4. You can now use any wagmi hook (useAccount, useWalletClient, ...).`);
505
528
  console.log(` Wallets on chains other than 9200/8200 will be prompted to switch.`);
506
529
  }
507
- else if (sub === "chat-web3") {
508
- // chat-web3 has no PRIVATE_KEY (each visitor pays their own way).
530
+ else if (sub === "chat-web3" || sub === "inference-web3" || sub === "judge-web3") {
531
+ // *-web3 variants have no PRIVATE_KEY (each visitor pays their own way).
509
532
  const needsWagmi = result.needsWagmi;
533
+ const route = sub === "chat-web3" ? "/chat-web3"
534
+ : sub === "inference-web3" ? "/inference-web3"
535
+ : "/judge-web3";
510
536
  if (needsWagmi) {
511
537
  console.log(` 2. Get wagmi wired up with one command:`);
512
538
  console.log(` npx lightnode add wagmi-setup`);
513
539
  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"}.`);
517
- console.log(` Mainnet llama3-8b costs 0.02 LCAI per turn; testnet is free from https://lightfaucet.ai`);
540
+ console.log(` 3. Wrap your layout with <Providers> and drop <ConnectButton /> on the page.`);
541
+ console.log(` 4. npm run dev, open ${route}, connect on chainId ${result.network === "mainnet" ? "9200" : "8200"}.`);
542
+ console.log(` Mainnet llama3-8b is 0.02 LCAI per call; testnet is free from https://lightfaucet.ai`);
518
543
  }
519
544
  else {
520
- console.log(` 2. npm run dev, open /chat-web3`);
545
+ console.log(` 2. npm run dev, open ${route}`);
521
546
  console.log(` 3. Connect a wallet on LightChain ${result.network === "mainnet" ? "mainnet (chainId 9200)" : "testnet (chainId 8200)"}.`);
522
- console.log(` Mainnet llama3-8b costs 0.02 LCAI per turn; testnet is free from https://lightfaucet.ai`);
547
+ console.log(` Mainnet llama3-8b is 0.02 LCAI per call; testnet is free from https://lightfaucet.ai`);
523
548
  }
524
- console.log(`\n Note: chat-web3 has NO server-side route, so it scales infinitely on`);
549
+ console.log(`\n Note: ${sub} has NO server-side route, so it scales infinitely on`);
525
550
  console.log(` static hosting (Vercel/Netlify/Cloudflare Pages free tier all work).`);
526
551
  }
527
552
  else if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent" || sub === "judge") {
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.17";
137
+ export declare const SDK_VERSION = "0.7.18";
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.17";
216
+ export const SDK_VERSION = "0.7.18";
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.17",
3
+ "version": "0.7.19",
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",