lightnode-sdk 0.7.12 → 0.7.14

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
@@ -48,6 +48,12 @@ export declare function addChat(opts?: AddOpts): {
48
48
  template: Template;
49
49
  network: Network;
50
50
  };
51
+ export declare function addJudge(opts?: AddOpts): {
52
+ written: WrittenFile[];
53
+ install: string;
54
+ template: Template;
55
+ network: Network;
56
+ };
51
57
  export declare function addNftMint(opts?: AddOpts): {
52
58
  written: WrittenFile[];
53
59
  install: string;
package/dist/add.js CHANGED
@@ -39,109 +39,48 @@ MODEL=llama3-8b
39
39
  `;
40
40
  const NEXTJS_ROUTE = `// app/api/inference/route.ts
41
41
  // Generated by 'lightnode add inference'. See https://lightnode.app/build
42
+ // Uses runInferenceWithKey: one call, full encrypted-inference flow.
42
43
  import { NextResponse } from "next/server";
43
- import WS from "ws";
44
- import { createPublicClient, createWalletClient, http, parseAbi, parseAbiItem, parseEther, type Log } from "viem";
45
- import { privateKeyToAccount } from "viem/accounts";
46
- import {
47
- LightNode, prepareSession, submitPrompt, decryptResponse,
48
- estimateJobFee, consumerGatewayUrl, JOB_REGISTRY_CONSUMER_ABI,
49
- GatewayClient, type NetworkId,
50
- } from "lightnode-sdk";
44
+ import { runInferenceWithKey } from "lightnode-sdk";
51
45
 
52
46
  export const runtime = "nodejs";
53
47
  export const dynamic = "force-dynamic";
48
+ // Mainnet 8b takes 60-90s under load; give the function room to finish.
54
49
  export const maxDuration = 120;
55
50
 
56
- const NETWORK = (process.env.NETWORK ?? "testnet") as NetworkId;
51
+ const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
57
52
  const MODEL = process.env.MODEL ?? "llama3-8b";
58
53
 
59
54
  export async function POST(req: Request) {
60
55
  if (!process.env.PRIVATE_KEY?.startsWith("0x")) {
61
56
  return NextResponse.json({ error: "PRIVATE_KEY not configured" }, { status: 500 });
62
57
  }
63
- const body = (await req.json().catch(() => ({}))) as { prompt?: string };
58
+ const body = (await req.json().catch(() => ({}))) as { prompt?: string; system?: string };
64
59
  const prompt = body.prompt?.trim();
65
60
  if (!prompt) return NextResponse.json({ error: "prompt is required" }, { status: 400 });
66
61
 
67
- const ln = new LightNode(NETWORK);
68
- const cfg = ln.network;
69
- const acct = privateKeyToAccount(process.env.PRIVATE_KEY as \`0x\${string}\`);
70
- const chain = { id: cfg.chainId, name: cfg.label, nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 }, rpcUrls: { default: { http: [cfg.rpc] } } };
71
- const pub = createPublicClient({ transport: http(cfg.rpc), chain });
72
- const wal = createWalletClient({ account: acct, transport: http(cfg.rpc), chain });
73
- const abi = parseAbi(JOB_REGISTRY_CONSUMER_ABI);
74
-
75
- const ch = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/challenge?address=\${acct.address}\`)).json() as { message?: string };
76
- if (!ch.message) return NextResponse.json({ error: "auth challenge failed" }, { status: 502 });
77
- const sig = await wal.signMessage({ message: ch.message });
78
- const verify = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/verify\`, {
79
- method: "POST", headers: { "Content-Type": "application/json" },
80
- body: JSON.stringify({ message: ch.message, signature: sig }),
81
- })).json() as { token?: string };
82
- if (!verify.token) return NextResponse.json({ error: "auth verify failed" }, { status: 502 });
83
-
84
- const gateway = new GatewayClient({ network: NETWORK, bearer: verify.token });
85
- const { sessionKey, createSessionArgs } = await prepareSession(gateway, MODEL);
86
- const fee = await estimateJobFee(cfg, MODEL);
87
- const createTx = await wal.writeContract({
88
- address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "createSession",
89
- args: [createSessionArgs.paramsHash, createSessionArgs.worker, createSessionArgs.encWorkerKey, createSessionArgs.ephemeralPubKey, createSessionArgs.initState, createSessionArgs.expiry],
90
- gas: 1_000_000n,
91
- });
92
- const createReceipt = await pub.waitForTransactionReceipt({ hash: createTx });
93
- const sessionCreated = parseAbiItem("event SessionCreated(uint256 indexed sessionId, address indexed user, bytes32 indexed paramsHash, address worker, bytes encWorkerKey, bytes ephemeralPubKey)");
94
- const sessionLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: sessionCreated, blockHash: createReceipt.blockHash })).find((l) => l.transactionHash === createTx);
95
- const sessionId = sessionLog?.args.sessionId;
96
- if (!sessionId) return NextResponse.json({ error: "SessionCreated missing" }, { status: 500 });
97
-
98
- let relayToken: string | undefined;
99
- for (let i = 0; i < 30 && !relayToken; i++) {
100
- const r = await gateway.getSessionToken(Number(sessionId));
101
- if ("token" in r && r.token) relayToken = r.token; else await new Promise((res) => setTimeout(res, 1000));
102
- }
103
- if (!relayToken) return NextResponse.json({ error: "relay token never became ready" }, { status: 504 });
104
- const ws = new WS(\`wss://relay.\${NETWORK}.lightchain.ai/ws?token=\${encodeURIComponent(relayToken)}\`);
105
- const chunks: string[] = [];
106
- await new Promise<void>((res, rej) => { ws.once("open", () => res()); ws.once("error", rej); });
107
- ws.on("message", async (data: Buffer) => {
108
- let f: { type?: string; payload?: string };
109
- try { f = JSON.parse(data.toString("utf8")); } catch { return; }
110
- if (!f.payload) return;
111
- if (f.type === "chunk") { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
112
- else if (f.type === "complete" && chunks.length === 0) { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
113
- });
114
-
115
- const promptHash = await submitPrompt(gateway, sessionKey, prompt);
116
- const submitTx = await wal.writeContract({
117
- address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "submitJob",
118
- args: [sessionId, promptHash], value: parseEther(String(fee)), gas: 500_000n,
119
- });
120
- const submitReceipt = await pub.waitForTransactionReceipt({ hash: submitTx });
121
- const jobSubmitted = parseAbiItem("event JobSubmitted(uint256 indexed jobId, uint256 indexed sessionId, address worker)");
122
- const jobLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobSubmitted, blockHash: submitReceipt.blockHash })).find((l) => l.transactionHash === submitTx);
123
- const jobId = jobLog?.args.jobId;
124
- if (!jobId) return NextResponse.json({ error: "JobSubmitted missing" }, { status: 500 });
125
-
126
- const jobCompleted = parseAbiItem("event JobCompleted(uint256 indexed jobId, address indexed worker, bytes32 responseHash, bytes32 ciphertextHash)");
127
- const deadline = Date.now() + 90_000;
128
- let completed: Log | null = null;
129
- while (!completed && Date.now() < deadline) {
130
- await new Promise((res) => setTimeout(res, 3000));
131
- const logs = await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobCompleted, args: { jobId }, fromBlock: submitReceipt.blockNumber });
132
- if (logs.length) completed = logs[0] as Log;
62
+ try {
63
+ const { answer, worker, txs, jobId } = await runInferenceWithKey({
64
+ network: NETWORK,
65
+ privateKey: process.env.PRIVATE_KEY as \`0x\${string}\`,
66
+ model: MODEL,
67
+ // Optional system prompt - leave undefined for raw user prompts.
68
+ system: body.system?.trim() || undefined,
69
+ prompt,
70
+ });
71
+ return NextResponse.json({
72
+ answer,
73
+ worker,
74
+ jobId: jobId.toString(),
75
+ txs: {
76
+ createSession: txs.createSession,
77
+ submitJob: txs.submitJob,
78
+ jobCompleted: txs.jobCompleted,
79
+ },
80
+ });
81
+ } catch (e) {
82
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
133
83
  }
134
- await new Promise((res) => setTimeout(res, 4000));
135
- ws.close();
136
- if (!completed) return NextResponse.json({ error: "worker stalled", txs: { createSession: createTx, submitJob: submitTx } }, { status: 504 });
137
-
138
- return NextResponse.json({
139
- answer: chunks.join(""),
140
- txs: { createSession: createTx, submitJob: submitTx, jobCompleted: completed.transactionHash },
141
- sessionId: sessionId.toString(),
142
- jobId: jobId.toString(),
143
- worker: createSessionArgs.worker,
144
- });
145
84
  }
146
85
  `;
147
86
  const HONO_HANDLER = `// lightchain-inference.ts
@@ -153,179 +92,65 @@ const HONO_HANDLER = `// lightchain-inference.ts
153
92
  // const app = new Hono();
154
93
  // app.post("/inference", inferenceHandler);
155
94
  //
156
- import WS from "ws";
157
- import { createPublicClient, createWalletClient, http, parseAbi, parseAbiItem, parseEther, type Log } from "viem";
158
- import { privateKeyToAccount } from "viem/accounts";
159
95
  import type { Context } from "hono";
160
- import {
161
- LightNode, prepareSession, submitPrompt, decryptResponse,
162
- estimateJobFee, consumerGatewayUrl, JOB_REGISTRY_CONSUMER_ABI,
163
- GatewayClient, type NetworkId,
164
- } from "lightnode-sdk";
96
+ import { runInferenceWithKey } from "lightnode-sdk";
165
97
 
166
- const NETWORK = (process.env.NETWORK ?? "testnet") as NetworkId;
98
+ const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
167
99
  const MODEL = process.env.MODEL ?? "llama3-8b";
168
100
 
169
101
  export async function inferenceHandler(c: Context) {
170
102
  if (!process.env.PRIVATE_KEY?.startsWith("0x")) return c.json({ error: "PRIVATE_KEY not set" }, 500);
171
- const body = await c.req.json().catch(() => ({} as { prompt?: string }));
103
+ const body = (await c.req.json().catch(() => ({}))) as { prompt?: string; system?: string };
172
104
  const prompt = body.prompt?.trim();
173
105
  if (!prompt) return c.json({ error: "prompt is required" }, 400);
174
106
 
175
- const ln = new LightNode(NETWORK);
176
- const cfg = ln.network;
177
- const acct = privateKeyToAccount(process.env.PRIVATE_KEY as \`0x\${string}\`);
178
- const chain = { id: cfg.chainId, name: cfg.label, nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 }, rpcUrls: { default: { http: [cfg.rpc] } } };
179
- const pub = createPublicClient({ transport: http(cfg.rpc), chain });
180
- const wal = createWalletClient({ account: acct, transport: http(cfg.rpc), chain });
181
- const abi = parseAbi(JOB_REGISTRY_CONSUMER_ABI);
182
-
183
- const ch = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/challenge?address=\${acct.address}\`)).json() as { message?: string };
184
- if (!ch.message) return c.json({ error: "auth challenge failed" }, 502);
185
- const sig = await wal.signMessage({ message: ch.message });
186
- const verify = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/verify\`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: ch.message, signature: sig }) })).json() as { token?: string };
187
- if (!verify.token) return c.json({ error: "auth verify failed" }, 502);
188
- const gateway = new GatewayClient({ network: NETWORK, bearer: verify.token });
189
-
190
- const { sessionKey, createSessionArgs } = await prepareSession(gateway, MODEL);
191
- const fee = await estimateJobFee(cfg, MODEL);
192
- const createTx = await wal.writeContract({
193
- address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "createSession",
194
- args: [createSessionArgs.paramsHash, createSessionArgs.worker, createSessionArgs.encWorkerKey, createSessionArgs.ephemeralPubKey, createSessionArgs.initState, createSessionArgs.expiry],
195
- gas: 1_000_000n,
196
- });
197
- const createReceipt = await pub.waitForTransactionReceipt({ hash: createTx });
198
- const sessionCreated = parseAbiItem("event SessionCreated(uint256 indexed sessionId, address indexed user, bytes32 indexed paramsHash, address worker, bytes encWorkerKey, bytes ephemeralPubKey)");
199
- const sessionLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: sessionCreated, blockHash: createReceipt.blockHash })).find((l) => l.transactionHash === createTx);
200
- const sessionId = sessionLog?.args.sessionId;
201
- if (!sessionId) return c.json({ error: "SessionCreated missing" }, 500);
202
-
203
- let relayToken: string | undefined;
204
- for (let i = 0; i < 30 && !relayToken; i++) { const r = await gateway.getSessionToken(Number(sessionId)); if ("token" in r && r.token) relayToken = r.token; else await new Promise((res) => setTimeout(res, 1000)); }
205
- if (!relayToken) return c.json({ error: "relay token never became ready" }, 504);
206
- const ws = new WS(\`wss://relay.\${NETWORK}.lightchain.ai/ws?token=\${encodeURIComponent(relayToken)}\`);
207
- const chunks: string[] = [];
208
- await new Promise<void>((res, rej) => { ws.once("open", () => res()); ws.once("error", rej); });
209
- ws.on("message", async (data: Buffer) => {
210
- let f: { type?: string; payload?: string };
211
- try { f = JSON.parse(data.toString("utf8")); } catch { return; }
212
- if (!f.payload) return;
213
- if (f.type === "chunk") { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
214
- else if (f.type === "complete" && chunks.length === 0) { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
215
- });
216
-
217
- const promptHash = await submitPrompt(gateway, sessionKey, prompt);
218
- const submitTx = await wal.writeContract({
219
- address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "submitJob",
220
- args: [sessionId, promptHash], value: parseEther(String(fee)), gas: 500_000n,
221
- });
222
- const submitReceipt = await pub.waitForTransactionReceipt({ hash: submitTx });
223
- const jobSubmitted = parseAbiItem("event JobSubmitted(uint256 indexed jobId, uint256 indexed sessionId, address worker)");
224
- const jobLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobSubmitted, blockHash: submitReceipt.blockHash })).find((l) => l.transactionHash === submitTx);
225
- const jobId = jobLog?.args.jobId;
226
- if (!jobId) return c.json({ error: "JobSubmitted missing" }, 500);
227
-
228
- const jobCompleted = parseAbiItem("event JobCompleted(uint256 indexed jobId, address indexed worker, bytes32 responseHash, bytes32 ciphertextHash)");
229
- const deadline = Date.now() + 90_000;
230
- let completed: Log | null = null;
231
- while (!completed && Date.now() < deadline) {
232
- await new Promise((res) => setTimeout(res, 3000));
233
- const logs = await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobCompleted, args: { jobId }, fromBlock: submitReceipt.blockNumber });
234
- if (logs.length) completed = logs[0] as Log;
107
+ try {
108
+ const { answer, worker, txs, jobId } = await runInferenceWithKey({
109
+ network: NETWORK,
110
+ privateKey: process.env.PRIVATE_KEY as \`0x\${string}\`,
111
+ model: MODEL,
112
+ system: body.system?.trim() || undefined,
113
+ prompt,
114
+ });
115
+ return c.json({
116
+ answer,
117
+ worker,
118
+ jobId: jobId.toString(),
119
+ txs: {
120
+ createSession: txs.createSession,
121
+ submitJob: txs.submitJob,
122
+ jobCompleted: txs.jobCompleted,
123
+ },
124
+ });
125
+ } catch (e) {
126
+ return c.json({ error: (e as Error).message }, 500);
235
127
  }
236
- await new Promise((res) => setTimeout(res, 4000));
237
- ws.close();
238
- if (!completed) return c.json({ error: "worker stalled", txs: { createSession: createTx, submitJob: submitTx } }, 504);
239
- return c.json({ answer: chunks.join(""), txs: { createSession: createTx, submitJob: submitTx, jobCompleted: completed.transactionHash }, sessionId: sessionId.toString(), jobId: jobId.toString(), worker: createSessionArgs.worker });
240
128
  }
241
129
  `;
242
130
  const NODE_SCRIPT = `// lightchain-inference.ts
243
131
  // Generated by 'lightnode add inference'. Run with: tsx lightchain-inference.ts "your prompt"
244
- import WS from "ws";
245
- import { createPublicClient, createWalletClient, http, parseAbi, parseAbiItem, parseEther, type Log } from "viem";
246
- import { privateKeyToAccount } from "viem/accounts";
247
- import {
248
- LightNode, prepareSession, submitPrompt, decryptResponse,
249
- estimateJobFee, consumerGatewayUrl, JOB_REGISTRY_CONSUMER_ABI,
250
- GatewayClient, type NetworkId,
251
- } from "lightnode-sdk";
132
+ import { runInferenceWithKey, LightNode } from "lightnode-sdk";
252
133
 
253
- const NETWORK = (process.env.NETWORK ?? "testnet") as NetworkId;
134
+ const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
254
135
  const MODEL = process.env.MODEL ?? "llama3-8b";
255
136
  const PROMPT = process.argv.slice(2).join(" ").trim() || "Reply with a one-sentence fun fact.";
256
137
  const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\${string}\` | undefined;
257
- if (!PRIVATE_KEY?.startsWith("0x") || PRIVATE_KEY.length !== 66) { console.error("set PRIVATE_KEY in .env"); process.exit(1); }
138
+ if (!PRIVATE_KEY) { console.error("PRIVATE_KEY not set. Put one in .env (testnet faucet: https://lightfaucet.ai)"); process.exit(1); }
139
+ const KEY = PRIVATE_KEY as \`0x\${string}\`;
258
140
 
259
141
  const ln = new LightNode(NETWORK);
260
- const cfg = ln.network;
261
- const acct = privateKeyToAccount(PRIVATE_KEY);
262
- const chain = { id: cfg.chainId, name: cfg.label, nativeCurrency: { name: "LCAI", symbol: "LCAI", decimals: 18 }, rpcUrls: { default: { http: [cfg.rpc] } } };
263
- const pub = createPublicClient({ transport: http(cfg.rpc), chain });
264
- const wal = createWalletClient({ account: acct, transport: http(cfg.rpc), chain });
265
- const abi = parseAbi(JOB_REGISTRY_CONSUMER_ABI);
266
-
267
- const ch = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/challenge?address=\${acct.address}\`)).json() as { message?: string };
268
- if (!ch.message) throw new Error("auth challenge failed");
269
- const sig = await wal.signMessage({ message: ch.message });
270
- const verify = await (await fetch(\`\${consumerGatewayUrl(NETWORK)}/api/auth/verify\`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: ch.message, signature: sig }) })).json() as { token?: string };
271
- if (!verify.token) throw new Error("auth verify failed");
272
- const gateway = new GatewayClient({ network: NETWORK, bearer: verify.token });
273
-
274
- const { sessionKey, createSessionArgs } = await prepareSession(gateway, MODEL);
275
- const fee = await estimateJobFee(cfg, MODEL);
276
- const createTx = await wal.writeContract({
277
- address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "createSession",
278
- args: [createSessionArgs.paramsHash, createSessionArgs.worker, createSessionArgs.encWorkerKey, createSessionArgs.ephemeralPubKey, createSessionArgs.initState, createSessionArgs.expiry],
279
- gas: 1_000_000n,
280
- });
281
- const createReceipt = await pub.waitForTransactionReceipt({ hash: createTx });
282
- const sessionCreated = parseAbiItem("event SessionCreated(uint256 indexed sessionId, address indexed user, bytes32 indexed paramsHash, address worker, bytes encWorkerKey, bytes ephemeralPubKey)");
283
- const sessionLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: sessionCreated, blockHash: createReceipt.blockHash })).find((l) => l.transactionHash === createTx);
284
- if (!sessionLog?.args.sessionId) throw new Error("SessionCreated missing");
285
- const sessionId = sessionLog.args.sessionId;
286
-
287
- let relayToken: string | undefined;
288
- for (let i = 0; i < 30 && !relayToken; i++) {
289
- const r = await gateway.getSessionToken(Number(sessionId));
290
- if ("token" in r && r.token) relayToken = r.token; else await new Promise((res) => setTimeout(res, 1000));
291
- }
292
- if (!relayToken) throw new Error("relay token never became ready");
293
- const ws = new WS(\`wss://relay.\${NETWORK}.lightchain.ai/ws?token=\${encodeURIComponent(relayToken)}\`);
294
- const chunks: string[] = [];
295
- await new Promise<void>((res, rej) => { ws.once("open", () => res()); ws.once("error", rej); });
296
- ws.on("message", async (data: Buffer) => {
297
- let f: { type?: string; payload?: string };
298
- try { f = JSON.parse(data.toString("utf8")); } catch { return; }
299
- if (!f.payload) return;
300
- if (f.type === "chunk") { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
301
- else if (f.type === "complete" && chunks.length === 0) { try { chunks.push(await decryptResponse(sessionKey, f.payload)); } catch {} }
142
+ const { answer, worker, txs, jobId } = await runInferenceWithKey({
143
+ network: NETWORK,
144
+ privateKey: KEY,
145
+ model: MODEL,
146
+ prompt: PROMPT,
302
147
  });
303
148
 
304
- const promptHash = await submitPrompt(gateway, sessionKey, PROMPT);
305
- const submitTx = await wal.writeContract({
306
- address: cfg.jobRegistry as \`0x\${string}\`, abi, functionName: "submitJob",
307
- args: [sessionId, promptHash], value: parseEther(String(fee)), gas: 500_000n,
308
- });
309
- const submitReceipt = await pub.waitForTransactionReceipt({ hash: submitTx });
310
- const jobSubmitted = parseAbiItem("event JobSubmitted(uint256 indexed jobId, uint256 indexed sessionId, address worker)");
311
- const jobLog = (await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobSubmitted, blockHash: submitReceipt.blockHash })).find((l) => l.transactionHash === submitTx);
312
- const jobId = jobLog?.args.jobId;
313
- if (!jobId) throw new Error("JobSubmitted missing");
314
-
315
- const jobCompleted = parseAbiItem("event JobCompleted(uint256 indexed jobId, address indexed worker, bytes32 responseHash, bytes32 ciphertextHash)");
316
- const deadline = Date.now() + 90_000;
317
- let completed: Log | null = null;
318
- while (!completed && Date.now() < deadline) {
319
- await new Promise((res) => setTimeout(res, 3000));
320
- const logs = await pub.getLogs({ address: cfg.jobRegistry as \`0x\${string}\`, event: jobCompleted, args: { jobId }, fromBlock: submitReceipt.blockNumber });
321
- if (logs.length) completed = logs[0] as Log;
322
- }
323
- if (!completed) { console.error("worker stalled - re-run for a different worker"); process.exit(1); }
324
- await new Promise((res) => setTimeout(res, 4000));
325
- ws.close();
326
- console.log("\\n=== ANSWER ===\\n" + chunks.join("") + "\\n");
327
- console.log("createSession:", createTx, "\\nsubmitJob: ", submitTx, "\\njobCompleted: ", completed.transactionHash);
328
- process.exit(0);
149
+ console.log("\\nanswer :", answer);
150
+ console.log("job id :", jobId.toString());
151
+ console.log("worker :", worker);
152
+ console.log("submitJob tx :", ln.explorerTxUrl(txs.submitJob));
153
+ if (txs.jobCompleted) console.log("completed tx :", ln.explorerTxUrl(txs.jobCompleted));
329
154
  `;
330
155
  function writeFile(abs, contents, force) {
331
156
  const rel = path.relative(process.cwd(), abs) || abs;
@@ -1083,6 +908,146 @@ export function addChat(opts = {}) {
1083
908
  written.push(writeFile(path.join(cwd, ".env.example"), ENV_EXAMPLE(network), force));
1084
909
  return { written, install: `npm install ${depsNeeded(template).join(" ")}`, template, network };
1085
910
  }
911
+ const NEXTJS_JUDGE_ROUTE = `// app/api/judge/route.ts
912
+ // Generated by 'lightnode add judge'. See https://lightnode.app/build
913
+ // The LightChallenge-style evaluator: post evidence + criteria, get a
914
+ // pass/fail verdict + confidence + reason, plus an on-chain receipt
915
+ // (submitJob + jobCompleted tx) anyone can verify.
916
+ import { NextResponse } from "next/server";
917
+ import { runInferenceWithKey } from "lightnode-sdk";
918
+
919
+ export const runtime = "nodejs";
920
+ export const dynamic = "force-dynamic";
921
+ export const maxDuration = 120;
922
+
923
+ const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
924
+ const MODEL = process.env.MODEL ?? "llama3-8b";
925
+
926
+ interface JudgeRequest {
927
+ criteria: string; // what the user has to satisfy
928
+ evidence: unknown; // anything serialisable (the proof to grade)
929
+ // Optional: extend the rubric the model uses. Defaults to passed/confidence/reason.
930
+ schema?: string;
931
+ }
932
+
933
+ interface Verdict {
934
+ passed: boolean;
935
+ confidence: number;
936
+ reason: string;
937
+ }
938
+
939
+ const DEFAULT_SCHEMA = '{ "passed": boolean, "confidence": 0-1, "reason": string }';
940
+
941
+ export async function POST(req: Request) {
942
+ if (!process.env.PRIVATE_KEY?.startsWith("0x")) {
943
+ return NextResponse.json({ error: "PRIVATE_KEY not configured" }, { status: 500 });
944
+ }
945
+ const body = (await req.json().catch(() => ({}))) as Partial<JudgeRequest>;
946
+ if (!body.criteria?.trim()) return NextResponse.json({ error: "criteria is required" }, { status: 400 });
947
+ if (body.evidence === undefined) return NextResponse.json({ error: "evidence is required" }, { status: 400 });
948
+
949
+ const schema = body.schema?.trim() || DEFAULT_SCHEMA;
950
+ const prompt = \`Criteria: \${body.criteria.trim()}
951
+
952
+ Evidence: \${JSON.stringify(body.evidence)}
953
+
954
+ Reply with STRICT JSON only, matching: \${schema}\`;
955
+
956
+ try {
957
+ const { answer, worker, txs, jobId } = await runInferenceWithKey({
958
+ network: NETWORK,
959
+ privateKey: process.env.PRIVATE_KEY as \`0x\${string}\`,
960
+ model: MODEL,
961
+ system: "You are a careful judge. Reply with STRICT JSON only, no prose.",
962
+ prompt,
963
+ });
964
+ // Parse the verdict defensively. If the model adds prose, extract the
965
+ // first {...} block. Surface the raw answer either way so callers can
966
+ // audit when the parser falls back.
967
+ let verdict: Verdict | null = null;
968
+ try {
969
+ verdict = JSON.parse(answer) as Verdict;
970
+ } catch {
971
+ const m = answer.match(/\{[\s\\S]*\}/);
972
+ if (m) {
973
+ try { verdict = JSON.parse(m[0]) as Verdict; } catch { /* keep null */ }
974
+ }
975
+ }
976
+ return NextResponse.json({
977
+ verdict,
978
+ raw: answer,
979
+ worker,
980
+ jobId: jobId.toString(),
981
+ txs: {
982
+ createSession: txs.createSession,
983
+ submitJob: txs.submitJob,
984
+ jobCompleted: txs.jobCompleted,
985
+ },
986
+ });
987
+ } catch (e) {
988
+ return NextResponse.json({ error: (e as Error).message }, { status: 500 });
989
+ }
990
+ }
991
+ `;
992
+ const NODE_JUDGE_SCRIPT = `// judge.ts
993
+ // Generated by 'lightnode add judge'. Run with: tsx judge.ts '<criteria>' '<evidence>'
994
+ // Example:
995
+ // tsx judge.ts 'Run a mile under 8 minutes' '{"distance_km":1.61,"time_minutes":7.4}'
996
+ import { runInferenceWithKey, LightNode } from "lightnode-sdk";
997
+
998
+ const NETWORK = (process.env.NETWORK ?? "testnet") as "mainnet" | "testnet";
999
+ const MODEL = process.env.MODEL ?? "llama3-8b";
1000
+ const PRIVATE_KEY = process.env.PRIVATE_KEY as \`0x\${string}\` | undefined;
1001
+ if (!PRIVATE_KEY) { console.error("PRIVATE_KEY not set. Put one in .env (testnet faucet: https://lightfaucet.ai)"); process.exit(1); }
1002
+ const KEY = PRIVATE_KEY as \`0x\${string}\`;
1003
+
1004
+ const [criteria, evidenceJson] = process.argv.slice(2);
1005
+ if (!criteria || !evidenceJson) {
1006
+ console.error("usage: tsx judge.ts '<criteria>' '<evidence-json>'");
1007
+ process.exit(2);
1008
+ }
1009
+ const evidence = JSON.parse(evidenceJson);
1010
+
1011
+ const ln = new LightNode(NETWORK);
1012
+ const { answer, worker, txs, jobId } = await runInferenceWithKey({
1013
+ network: NETWORK,
1014
+ privateKey: KEY,
1015
+ model: MODEL,
1016
+ system: "You are a careful judge. Reply with STRICT JSON only, no prose.",
1017
+ prompt: \`Criteria: \${criteria}
1018
+
1019
+ Evidence: \${JSON.stringify(evidence)}
1020
+
1021
+ Reply with STRICT JSON only: { "passed": boolean, "confidence": 0-1, "reason": string }\`,
1022
+ });
1023
+
1024
+ console.log("\\nraw answer :", answer);
1025
+ let verdict: { passed: boolean; confidence: number; reason: string } | null = null;
1026
+ try { verdict = JSON.parse(answer); } catch {
1027
+ const m = answer.match(/\\\{[\\\s\\S]*\\\}/);
1028
+ if (m) { try { verdict = JSON.parse(m[0]); } catch { /* keep null */ } }
1029
+ }
1030
+ console.log("verdict :", verdict);
1031
+ console.log("job id :", jobId.toString());
1032
+ console.log("worker :", worker);
1033
+ console.log("submitJob tx:", ln.explorerTxUrl(txs.submitJob));
1034
+ if (txs.jobCompleted) console.log("completed tx:", ln.explorerTxUrl(txs.jobCompleted));
1035
+ `;
1036
+ export function addJudge(opts = {}) {
1037
+ const cwd = opts.cwd ?? process.cwd();
1038
+ const network = opts.network ?? "testnet";
1039
+ const template = opts.template && opts.template !== "auto" ? opts.template : detectTemplate(cwd);
1040
+ const force = !!opts.force;
1041
+ const written = [];
1042
+ if (template === "nextjs-api") {
1043
+ written.push(writeFile(path.join(cwd, "app/api/judge/route.ts"), NEXTJS_JUDGE_ROUTE, force));
1044
+ }
1045
+ else {
1046
+ written.push(writeFile(path.join(cwd, "judge.ts"), NODE_JUDGE_SCRIPT, force));
1047
+ }
1048
+ written.push(writeFile(path.join(cwd, ".env.example"), ENV_EXAMPLE(network), force));
1049
+ return { written, install: `npm install ${depsNeeded(template).join(" ")}`, template, network };
1050
+ }
1086
1051
  export function addNftMint(opts = {}) {
1087
1052
  const cwd = opts.cwd ?? process.cwd();
1088
1053
  const network = opts.network ?? "testnet";
package/dist/auth.js CHANGED
@@ -152,10 +152,18 @@ export async function siweSignIn(walletClient, network, opts = {}) {
152
152
  throw new Error("siweSignIn: walletClient has no account; pass `address` explicitly");
153
153
  }
154
154
  const { message } = await siweChallenge(cfg, address, { signal: opts.signal, baseUrl: opts.baseUrl });
155
- // viem's signMessage requires `account` even when one is set on the
156
- // client; passing it explicitly works with both wagmi and bare viem.
155
+ // Pass the FULL account object when the walletClient has one (viem's
156
+ // privateKeyToAccount returns a "local" account whose signMessage signs
157
+ // off-RPC with the secret material). Passing only the address would make
158
+ // viem fall back to chain.personal_sign, which LightChain RPC does not
159
+ // expose -> -32601 MethodNotFoundRpcError.
160
+ //
161
+ // For wagmi-injected wallets the account is just `{ address }`; viem then
162
+ // dispatches to the provider (MetaMask / WalletConnect / etc.) which
163
+ // does its own personal_sign over the EIP-1193 channel.
164
+ const accountForSign = walletClient.account ?? address;
157
165
  const signature = await walletClient.signMessage({
158
- account: walletClient.account?.address ?? address,
166
+ account: accountForSign,
159
167
  message,
160
168
  });
161
169
  const verified = await siweVerify(cfg, { message, signature }, { signal: opts.signal, baseUrl: opts.baseUrl });
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, addAgent } from "./add.js";
3
+ import { addInference, addAnalyticsDashboard, addNftMint, addChat, addAgent, addJudge } 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", "agent", "analytics-dashboard", "nft-mint-with-inference"];
465
+ const known = ["inference", "chat", "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
  }
@@ -474,7 +474,9 @@ async function main() {
474
474
  ? addChat({ template, network, force })
475
475
  : sub === "agent"
476
476
  ? addAgent({ template, network, force })
477
- : addInference({ template, network, force });
477
+ : sub === "judge"
478
+ ? addJudge({ template, network, force })
479
+ : addInference({ template, network, force });
478
480
  console.log(`▶ add ${sub} (${result.template} template, default network ${result.network})`);
479
481
  for (const f of result.written) {
480
482
  if (f.skipped)
@@ -489,7 +491,7 @@ async function main() {
489
491
  else {
490
492
  console.log(`\nNext steps (these files were added to your CURRENT folder, not a new project):`);
491
493
  console.log(` 1. ${result.install}`);
492
- if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent") {
494
+ if (sub === "nft-mint-with-inference" || sub === "inference" || sub === "chat" || sub === "agent" || sub === "judge") {
493
495
  console.log(` 2. cp .env.example .env (and put a funded ${result.network} PRIVATE_KEY in it)`);
494
496
  if (sub === "agent" && result.template === "nextjs-api") {
495
497
  console.log(` 3. Set CRON_SECRET in your Vercel env vars + edit AGENT_TASK in .env`);
@@ -505,6 +507,14 @@ async function main() {
505
507
  else if (sub === "chat") {
506
508
  console.log(` 3. npx tsx chat-repl.ts (interactive terminal chat)`);
507
509
  }
510
+ else if (sub === "judge" && result.template === "nextjs-api") {
511
+ console.log(` 3. npm run dev`);
512
+ console.log(` 4. curl -X POST localhost:3000/api/judge -H 'content-type: application/json' \\\\`);
513
+ console.log(` -d '{"criteria":"Run a mile under 8 minutes","evidence":{"time_minutes":7.4,"distance_km":1.61}}'`);
514
+ }
515
+ else if (sub === "judge") {
516
+ console.log(` 3. npx tsx judge.ts 'Run a mile under 8 minutes' '{"time_minutes":7.4,"distance_km":1.61}'`);
517
+ }
508
518
  else if (sub === "nft-mint-with-inference" && result.template === "nextjs-api") {
509
519
  console.log(` 3. Make sure /api/inference is mounted too (run: npx lightnode add inference)`);
510
520
  console.log(` 4. npm run dev, open /nft-mint`);
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.12";
137
+ export declare const SDK_VERSION = "0.7.14";
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.12";
216
+ export const SDK_VERSION = "0.7.14";
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.12",
3
+ "version": "0.7.14",
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",