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 +18 -0
- package/dist/add.d.ts +33 -0
- package/dist/add.js +401 -0
- package/dist/cli.js +60 -35
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
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 <ConnectButton /> 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 <ConnectButton /> 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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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", "
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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
|
-
//
|
|
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>
|
|
515
|
-
console.log(`
|
|
516
|
-
console.log(`
|
|
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
|
|
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
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|