lightnode-sdk 0.5.0 → 0.6.0

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
@@ -1,231 +1,338 @@
1
1
  # lightnode-sdk
2
2
 
3
- TypeScript client for **LightChain AI**: read workers, jobs, models, on-chain
4
- registration and per-model analytics, **and run encrypted inference end to end**
5
- (prepare session, submit prompt, decrypt the streamed response). The SDK is
6
- non-custodial - it never holds your private key; on-chain calls are signed by
7
- your wallet via viem. Single peer dep: `viem`.
3
+ [![npm](https://img.shields.io/npm/v/lightnode-sdk?color=7064e9)](https://www.npmjs.com/package/lightnode-sdk)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-7064e9.svg)](LICENSE)
8
5
 
9
- > Independent, community-built. Not an official LightChain package.
10
- > Live-verified end-to-end on both **mainnet** (chain 9200) and **testnet** (chain 8200)
11
- > with real LCAI - example transactions in the "Submitting inference" section below.
12
-
13
- ## Install
6
+ **The community SDK for LightChain AI.** Encrypted on-chain inference, network
7
+ analytics, multi-turn chat, an Ethereum bridge wrapper, an LCAI Governor
8
+ client, an on-chain model registry reader, worker preflight + watch, and a
9
+ bundled `lightnode` CLI. Non-custodial. Pure JS (works in Node 18+, browsers,
10
+ StackBlitz, Cloudflare Workers, Bun). Single peer dep: `viem`.
14
11
 
15
12
  ```bash
16
13
  npm install lightnode-sdk viem
17
14
  ```
18
15
 
19
- ## Usage
16
+ LightChain's own docs list official SDKs as "soon"; this fills the gap. Not
17
+ affiliated with LightChain.
20
18
 
21
- ```ts
22
- import { LightNode } from "lightnode-sdk";
19
+ ## Five-line "hello world"
23
20
 
24
- const ln = new LightNode("mainnet"); // or "testnet"
21
+ ```ts
22
+ import { runInferenceWithKey } from "lightnode-sdk";
25
23
 
26
- // One worker
27
- const worker = await ln.getWorker("0x6781...6e0f");
28
- const jobs = await ln.getWorkerJobs("0x6781...6e0f", 20);
29
- const earnings = await ln.getEarningsLcai("0x6781...6e0f"); // whole LCAI
24
+ const { answer, txs } = await runInferenceWithKey({
25
+ network: "testnet", // or "mainnet"
26
+ privateKey: process.env.PRIVATE_KEY as `0x${string}`,
27
+ prompt: "Reply with a one-sentence fun fact about the ocean.",
28
+ });
30
29
 
31
- // On-chain truth (independent of the indexer, which can lag a re-register)
32
- const registered = await ln.isRegistered("0x6781...6e0f"); // true | false | null
30
+ console.log(answer); // the decrypted reply
31
+ console.log(txs.createSession); // on-chain receipts
32
+ ```
33
33
 
34
- // Network-wide
35
- const stats = await ln.getNetworkStats(); // { total, active, jobsCompleted, totalEarnedLcai, models }
36
- const models = await ln.getModels(); // [{ name, fee, max_output_tokens, ... }]
37
- const perModel = await ln.getModelStats(); // completion rate, p50/p95 latency, incomplete, earnings
38
- const perWorker = await ln.getWorkerStats(); // per-worker reliability, busiest first
39
- const rollup = await ln.getNetworkAnalytics(); // overall completion / jobs / incomplete / earnings
34
+ In the browser this works as-is. In Node, `ws` is auto-detected if installed,
35
+ so you don't need to pass a WebSocket explicitly.
40
36
 
41
- // Inference cost
42
- const fee = await ln.estimateFee("llama3-8b"); // whole LCAI per job (on-chain calculateJobFee)
43
- const id = ln.modelId("llama3-8b"); // keccak256 model id
37
+ ## What's in the SDK
44
38
 
45
- // CSV export (same exporters the LightNode dashboard uses)
46
- import { workerJobsCsv, modelStatsCsv, workerStatsCsv } from "lightnode-sdk";
47
- const csv = workerJobsCsv(await ln.getWorkerJobs("0x6781...6e0f", 100));
48
- ```
39
+ ### Inference (paid)
49
40
 
50
- ## API
51
-
52
- | Method | Returns |
53
- | --- | --- |
54
- | `getWorker(address)` | `Worker \| null` |
55
- | `getWorkerJobs(address, first?)` | `Job[]` |
56
- | `getEarningsLcai(address)` | `number` |
57
- | `isRegistered(address)` | `boolean \| null` (read from chain events) |
58
- | `getModels()` | `ModelInfo[]` |
59
- | `getWorkers(first?)` | `Worker[]` |
60
- | `getNetworkStats()` | `NetworkStats` |
61
- | `getModelStats(sample?)` | `ModelStat[]` |
62
- | `getWorkerStats(sample?, limit?)` | `WorkerStat[]` (reliability) |
63
- | `getNetworkAnalytics(sample?)` | `NetworkAnalytics` |
64
- | `estimateFee(modelTag)` | `number` (LCAI per job) |
65
- | `modelId(tag)` | `0x${string}` |
66
-
67
- Also exported: `NETWORKS`, `WORKER_REGISTRY`, `REGISTRY_TOPICS`, `aggregateModelStats`,
68
- `aggregateWorkerStats`, `networkAnalytics`, `modelStatsCsv`, `workerStatsCsv`,
69
- `workerJobsCsv`, `JOB_REGISTRY_CONSUMER_ABI`, `consumerGatewayUrl`, `fromWei`, and all
70
- the types.
41
+ | API | Use when |
42
+ |---|---|
43
+ | **`runInferenceWithKey({ network, privateKey, prompt, ... })`** | One call from a wallet. The SDK builds viem clients, runs SIWE, encrypts, signs, decrypts. ~5 lines total. |
44
+ | **`runInference({ gateway, wallet, publicClient, network, prompt, ... })`** | You already have viem clients + a SIWE JWT. Same internals, no setup duplication. The /playground uses this with a Reown wallet. |
45
+ | **`runInferenceStream({ network, privateKey, prompt, ... })`** | Modern `AsyncIterable<string>` of chunks plus a `done` promise for the final receipt. `for await (const chunk of stream) ...` |
46
+ | **`Conversation` / `chat({ network, privateKey })`** | Multi-turn chat helper. Keeps history client-side; one encrypted inference per `.send()`. Optional `system` prompt, `maxHistoryTurns` cap. |
47
+ | **`prepareSession`, `submitPrompt`, `decryptResponse`** | Lowest-level: drive the protocol step by step. Build custom retry, batching, multi-turn-with-session-reuse on top. |
71
48
 
72
- ## CLI
49
+ All four high-level entry points share:
50
+ - Auto-retry on `StalledWorkerError` (default 2 retries, configurable).
51
+ - Auto-resolve `globalThis.WebSocket` in browsers, dynamic-import `ws` in Node.
52
+ - Streaming via `onChunk(piece, totalSoFar)` callback.
53
+ - Byte-perfect crypto vs LightChain's reference client (ECDH P-256 + raw
54
+ 32-byte shared secret + AES-256-GCM, `@noble/curves` and `@noble/ciphers`
55
+ under the hood).
73
56
 
74
- ```bash
75
- npx lightnode network --net testnet # network summary
76
- npx lightnode models # registered models + fees
77
- npx lightnode worker 0x6781…6e0f # one worker (on-chain + recent jobs)
78
- npx lightnode jobs 0x6781…6e0f --csv # one worker's job history (table or CSV)
79
- npx lightnode registered 0x6781…6e0f # true | false | null
80
- npx lightnode fee llama3-8b # on-chain job fee
81
- npx lightnode analytics --csv # per-model performance (CSV)
82
- npx lightnode reliability --csv # per-worker reliability (CSV)
83
-
84
- # Patch an existing project (auto-detects Next.js, Hono, or Node):
85
- npx lightnode add inference # encrypted inference route/script
86
- npx lightnode add chat # chat-style UI with conversation history
87
- npx lightnode add analytics-dashboard # read-only network + worker analytics page
88
- npx lightnode add nft-mint-with-inference # AI-generated NFT metadata with on-chain provenance
89
- # All `add` commands accept [--template auto|nextjs-api|hono|node] [--net testnet|mainnet] [--force]
57
+ ### Read-only `LightNode` client (free, no key)
90
58
 
91
- # Or scaffold a brand-new project:
92
- npm create lightnode-app my-app
59
+ ```ts
60
+ import { LightNode } from "lightnode-sdk";
61
+ const ln = new LightNode("mainnet"); // or "testnet" or a custom NetworkConfig
62
+
63
+ await ln.getNetworkStats(); // totals + active count + earnings
64
+ await ln.getModels(); // ModelInfo[] (name, fee, max tokens)
65
+ await ln.getWorkers(200); // Worker[], busiest first
66
+ await ln.getWorker("0x..."); // one worker record (or null)
67
+ await ln.getWorkerJobs("0x...", 20); // recent jobs for one worker
68
+ await ln.getModelStats(1000); // per-model completion / p50 / p95
69
+ await ln.getWorkerStats(1000, 25); // per-worker reliability
70
+ await ln.getNetworkAnalytics(1000); // network-wide rollup
71
+ await ln.isRegistered("0x..."); // chain-truth registration (no indexer lag)
72
+ await ln.getEarningsLcai("0x..."); // settled earnings in LCAI
73
+ await ln.estimateFee("llama3-8b"); // live per-job fee from AIConfig
74
+ await ln.modelId("llama3-8b"); // keccak256 of the model tag
75
+ await ln.getJobStatus(1234n); // category + refundable flag (new in 0.5.0)
76
+ ln.gateway({ bearer }); // pre-configured GatewayClient
93
77
  ```
94
78
 
95
- ## Submitting inference
79
+ Plus the bare-metal aggregators (`aggregateModelStats`, `aggregateWorkerStats`,
80
+ `networkAnalytics`) and CSV exporters (`modelStatsCsv`, `workerStatsCsv`,
81
+ `workerJobsCsv`) for reporting / dashboards.
96
82
 
97
- **Easy mode (`runInference` v0.4+):** one async call drives the whole protocol —
98
- SIWE → prepareSession → on-chain createSession → relay WS → encrypt + upload prompt
99
- on-chain submitJob decrypt streamed chunks wait for `JobCompleted` →
100
- return the assembled answer + three tx hashes. Built-in retry on `StalledWorkerError`.
83
+ ### Bridge SDK (new in 0.5.0)
84
+
85
+ Typed wrapper around the LightChain Hyperlane Warp Route bridge.
101
86
 
102
87
  ```ts
103
- import { LightNode, GatewayClient, runInference } from "lightnode-sdk";
104
- import WS from "ws"; // omit in the browser
88
+ import { Bridge, BRIDGE_ROUTE } from "lightnode-sdk";
89
+ import { createPublicClient, createWalletClient, http, parseEther } from "viem";
90
+ import { privateKeyToAccount } from "viem/accounts";
105
91
 
106
- const ln = new LightNode("testnet");
107
- const gateway = new GatewayClient({ network: "testnet", bearer: await getJwt() });
108
- const { answer, txs } = await runInference({
109
- prompt: "Reply with a one-sentence fun fact about the ocean.",
110
- gateway, wallet, publicClient, network: ln.network,
111
- WebSocket: WS,
112
- onChunk: (chunk) => process.stdout.write(chunk), // live streaming
113
- maxRetries: 2, // auto-retry on stall
92
+ const account = privateKeyToAccount(process.env.PRIVATE_KEY!);
93
+ const ethPub = createPublicClient({ transport: http(BRIDGE_ROUTE.ethereum.rpc) });
94
+ const ethWal = createWalletClient({ account, transport: http(BRIDGE_ROUTE.ethereum.rpc) });
95
+
96
+ const bridge = new Bridge(ethPub, ethWal);
97
+
98
+ // Quote the Hyperlane gas payment for one message
99
+ const fee = await bridge.quoteFee("ethereum", "lightchain-mainnet");
100
+
101
+ // One-time ERC-20 approval (MaxUint256 by default)
102
+ await bridge.approve();
103
+
104
+ // Send 100 LCAI to your own address on LightChain mainnet
105
+ await bridge.transfer({
106
+ from: "ethereum",
107
+ to: "lightchain-mainnet",
108
+ amount: parseEther("100"),
109
+ recipient: account.address,
110
+ fee,
114
111
  });
115
- console.log("\n", txs); // { createSession, submitJob, jobCompleted }
116
112
  ```
117
113
 
118
- The lower-level helpers (`prepareSession`, `submitPrompt`, `decryptResponse`,
119
- the typed errors `StalledWorkerError` / `OnChainRevertError` / `GatewayAuthError`
120
- / `RelayTokenTimeoutError`) stay exported for builders who want a different
121
- retry policy or to reuse a session across prompts.
114
+ For the reverse direction, wire `BRIDGE_ROUTE["lightchain-mainnet"].rpc`
115
+ instead and `from: "lightchain-mainnet"`. The SDK attaches native LCAI as
116
+ value automatically.
117
+
118
+ Confirmed addresses (baked in):
119
+ | Side | Role | Address |
120
+ |------|------|---------|
121
+ | Ethereum | HypERC20Collateral | `0x01f80bb8e78e79881E8Ec7832fB6C2c59f64e353` |
122
+ | Ethereum | LCAI ERC-20 | `0x9cA8530CA349c966Fe9ef903Df17a75B8A778927` |
123
+ | LightChain | HypNative | `0xEc7096A3116EE769457C939617375Ec1785AA6f1` |
122
124
 
123
- ### Manual mode (the full surface)
125
+ ### DAO SDK (new in 0.5.0)
124
126
 
125
- `v0.3+` ships the encrypted inference-submit flow end to end. Wire-compatible with
126
- the reference client [`lcai-chat-v2`](https://github.com/lightchain-protocol/lcai-chat-v2)
127
- (same ECDH-P256 + AES-256-GCM, same gateway endpoints, same `JobRegistry` calls).
128
- **Live-verified** with real LCAI on both networks before this release:
127
+ OpenZeppelin Governor v5 wrapper for the LCAIGovernor on Ethereum mainnet.
129
128
 
130
- | Network | Tx | Decrypted model output (excerpt) |
131
- | --- | --- | --- |
132
- | testnet (8200) | createSession `0x77686f3f…ef2bc587` · submitJob `0xba9d48c4…293b2bd96` | "Did you know that the deepest part of the ocean, the Mariana Trench, is so deep that if you were to drop Mount Everest into it, its peak would still be more than 1 mile underwater?!" |
133
- | mainnet (9200) | createSession `0xf091957f…57d4a6ca` · submitJob `0x6ff44a4a…79846bb89` | "Did you know there is a type of jellyfish called the 'Upside-Down Jellyfish' that actually swims on its back, using its tentacles to catch prey and defend itself from predators?" |
129
+ ```ts
130
+ import { DAO, VoteSupport, PROPOSAL_STATE_LABEL } from "lightnode-sdk";
131
+
132
+ // Read
133
+ const dao = new DAO(publicClient, "ethereum");
134
+ const cfg = await dao.config(); // delay / period / threshold
135
+ const p = await dao.proposal(12345n); // state + votes + key blocks
136
+ console.log(p.stateLabel); // "active" | "queued" | ...
137
+
138
+ // Write (needs wallet)
139
+ const daoRW = new DAO(publicClient, "ethereum", walletClient);
140
+ await daoRW.castVote(12345n, VoteSupport.For, "I support this");
141
+ await daoRW.propose({ targets, values, calldatas, description });
142
+ await daoRW.queue({ targets, values, calldatas, descriptionHash });
143
+ await daoRW.execute({ targets, values, calldatas, descriptionHash });
144
+ ```
134
145
 
135
- The pieces that talk to the chain (`createSession` / `submitJob`) are signed by
136
- **your** wallet via viem; the SDK only prepares the data, does the crypto, and
137
- talks to the consumer gateway.
146
+ Confirmed Ethereum addresses (baked in):
147
+ - LCAIGovernor `0x6dfa413B5900a1a7947BC75E68AbBA093cB2492d`
148
+ - LCAITimeLock `0xbE1c37F8C4DA77dD06F4A8AC5098Ec70273093d7`
149
+ - LCAIBallots (IVotes) `0x75F3D01c4D960FE986A598B7954A3b786B29cE49`
150
+ - LCAI ERC-20 `0x9cA8530CA349c966Fe9ef903Df17a75B8A778927`
151
+ - LCAITreasury `0x07A716a551E5f4CA7D6C71Da9dF1cb1429Dba826`
138
152
 
139
- ### Auth (your responsibility)
153
+ Voting params (live-read via `dao.config()`): ~1 day delay, ~14 day period,
154
+ 140k LCAI threshold, 3% quorum.
140
155
 
141
- The gateway requires a bearer JWT obtained via the consumer-api's SIWE sign-in.
142
- The SDK does **not** bundle SIWE - hand the SDK either a fixed token or a
143
- `() => Promise<string>` thunk that refreshes it on demand.
156
+ ### On-chain Model Registry reader (new in 0.5.0)
144
157
 
145
- ### End-to-end (sketch)
158
+ Typed reader for `AIVMModelRegistry` + `BenchmarkRegistry`. Since LightChain
159
+ has not published a public deployment address, you pass yours explicitly:
146
160
 
147
161
  ```ts
148
- import {
149
- LightNode,
150
- prepareSession,
151
- submitPrompt,
152
- decryptResponse,
153
- JOB_REGISTRY_CONSUMER_ABI,
154
- } from "lightnode-sdk";
155
- import { createWalletClient, http, parseAbi } from "viem";
156
- import { privateKeyToAccount } from "viem/accounts";
162
+ import { OnchainModelRegistry, MODEL_STATUS_LABEL } from "lightnode-sdk";
157
163
 
158
- const ln = new LightNode("testnet");
159
- const gateway = ln.gateway({ bearer: () => mySiweJwt() });
160
-
161
- // 1) Prepare the session: the gateway picks a worker, we wrap a fresh session
162
- // key for the worker (and the disputer, if returned) and get the dispatcher
163
- // signature authorising createSession.
164
- const { sessionKey, createSessionArgs } = await prepareSession(gateway, "llama3-8b");
165
-
166
- // 2) Call createSession ON-CHAIN with the prepared args. You sign with your
167
- // wallet; the SDK ships the ABI but never custodies the key.
168
- const wallet = createWalletClient({ account: privateKeyToAccount("0x..."), transport: http(ln.network.rpc) });
169
- const abi = parseAbi(JOB_REGISTRY_CONSUMER_ABI);
170
- const sessionTx = await wallet.writeContract({
171
- address: ln.network.jobRegistry as `0x${string}`,
172
- abi,
173
- functionName: "createSession",
174
- args: [
175
- createSessionArgs.modelId,
176
- createSessionArgs.worker,
177
- createSessionArgs.encWorkerKey,
178
- createSessionArgs.encDisputerKey,
179
- createSessionArgs.dispatcherSignature,
180
- createSessionArgs.expiry,
181
- ],
164
+ const reader = new OnchainModelRegistry({
165
+ publicClient,
166
+ registry: "0x...", // AIVMModelRegistry deployment
167
+ benchmarks: "0x...", // optional, only for benchmark methods
182
168
  });
183
- // Wait for the receipt and pull the sessionId out of the SessionCreated event.
184
-
185
- // 3) Encrypt + upload your prompt. Returns the EIP-4844 blob hash.
186
- const blobHash = await submitPrompt(gateway, sessionKey, "write a haiku about LCAI");
187
-
188
- // 4) Submit the job on-chain, paying the fee:
189
- const feeLcai = await ln.estimateFee("llama3-8b");
190
- await wallet.writeContract({
191
- address: ln.network.jobRegistry as `0x${string}`,
192
- abi,
193
- functionName: "submitJob",
194
- args: [sessionId, blobHash],
195
- value: BigInt(Math.round(feeLcai * 1e18)),
169
+
170
+ const baseIds = await reader.getBaseModelIds();
171
+ const variantIds = await reader.getAllVariants();
172
+ const variant = await reader.getVariant("...");
173
+ const policy = await reader.getAccessPolicy("..."); // tier: "free" | "paywalled" | "ticket-gated"
174
+ const variants = await reader.getVariantsForBaseModel(baseId);
175
+ ```
176
+
177
+ Surfaces the full ABI for both contracts plus a builder-friendly `tier`
178
+ heuristic derived from the raw `AccessPolicyConfig`.
179
+
180
+ ### Worker preflight + watch (new in 0.5.0)
181
+
182
+ Remote operational SDK for the worker network. No SSH, no Docker. Works from
183
+ any machine with a funded wallet (preflight) or no key at all (watch).
184
+
185
+ ```ts
186
+ import { workerPreflight, workerWatch, LightNode } from "lightnode-sdk";
187
+
188
+ // One real test inference. Returns verdict, elapsed time, on-chain receipts.
189
+ const r = await workerPreflight({
190
+ network: "testnet",
191
+ privateKey: process.env.PRIVATE_KEY!,
192
+ model: "llama3-8b",
193
+ deadlineMs: 60_000,
196
194
  });
195
+ console.log(r.verdict); // "ok" | "over-deadline" | "stalled" | "failed"
196
+ console.log(r.summary); // human one-liner
197
+ console.log(r.txs); // createSession + submitJob + jobCompleted
198
+
199
+ // Watch a worker's on-chain + indexer state. AsyncIterable of events.
200
+ const ln = new LightNode("mainnet");
201
+ const handle = workerWatch(ln, "0xWorker...", { intervalMs: 30_000 });
202
+ for await (const event of handle.events) {
203
+ console.log(event.kind); // "snapshot" | "registered" | "went-stale" | "back-online" | "jobs-completed" | "earnings-up"
204
+ console.log(event.state); // { registered, lastSeenSecsAgo, jobsCompleted, earningsLcai, ... }
205
+ }
206
+ ```
207
+
208
+ ### Typed errors
197
209
 
198
- // 5) Watch JobCompleted (or read the response blob via the relay), then decrypt:
199
- const answer = await decryptResponse(sessionKey, responseCiphertextFromRelay);
210
+ ```ts
211
+ import { isStalledWorker, StalledWorkerError, OnChainRevertError, RelayTokenTimeoutError, GatewayAuthError } from "lightnode-sdk";
212
+
213
+ try {
214
+ await runInferenceWithKey({ ... });
215
+ } catch (e) {
216
+ if (isStalledWorker(e)) { /* worker never produced an answer; protocol refunds */ }
217
+ // ...
218
+ }
200
219
  ```
201
220
 
202
- ### What's exported (v0.3)
221
+ | Error | When |
222
+ |---|---|
223
+ | `StalledWorkerError` | Worker ack'd then went silent. After `maxRetries`, raised. Protocol refunds. |
224
+ | `OnChainRevertError` | `createSession` or `submitJob` reverted. Includes the tx hash. |
225
+ | `RelayTokenTimeoutError` | Gateway dispatcher never issued the relay JWT (transient). |
226
+ | `GatewayAuthError` | SIWE handshake or JWT issue. Re-auth and retry. |
227
+
228
+ ## CLI
229
+
230
+ `lightnode` is bundled. Read-only commands work anywhere; chat / wallet / preflight need `PRIVATE_KEY`.
231
+
232
+ ### Read-only (no key)
233
+
234
+ ```bash
235
+ npx lightnode network # network summary JSON
236
+ npx lightnode models # registered models + fees
237
+ npx lightnode worker 0x... # one worker + 5 recent jobs
238
+ npx lightnode jobs 0x... --csv # job history
239
+ npx lightnode registered 0x... # true | false | null (chain truth)
240
+ npx lightnode fee llama3-8b # per-job LCAI fee
241
+ npx lightnode analytics --csv # per-model performance
242
+ npx lightnode reliability --csv # per-worker reliability
243
+ npx lightnode job 1234 # job status + refundable flag
244
+ npx lightnode worker watch 0x... --interval 30 # JSON event per state change
245
+ npx lightnode bridge addresses # bridge route
246
+ npx lightnode dao addresses # LCAI Governor addresses
247
+ npx lightnode dao config # live voting delay / period / threshold
248
+ ```
203
249
 
204
- - `prepareSession(gateway, modelTag)` - select + wrap + prepare (steps 1+2).
205
- - `submitPrompt(gateway, sessionKey, prompt)` - encrypt + upload (step 3).
206
- - `decryptResponse(sessionKey, ciphertext)` - decrypt the worker's reply (step 5).
207
- - `GatewayClient` + `consumerGatewayUrl(net)` - typed HTTP client.
208
- - `crypto.*` - the wire-compatible primitives (`encrypt`, `decrypt`,
209
- `encryptSessionKey`, `decryptSessionKey`, `generateEcdhKeyPair`,
210
- `generateSessionKey`, hex/base64/utf8 helpers).
211
- - `JOB_REGISTRY_CONSUMER_ABI` + `estimateJobFee` + `modelId` - on-chain primitives.
250
+ ### Need PRIVATE_KEY
212
251
 
213
- A managed REST alternative (API-key) also exists at `https://chat2.lightchain.ai/api/v1` for builders who'd rather skip running their own gateway/SIWE auth.
252
+ ```bash
253
+ PRIVATE_KEY=0x... npx lightnode chat "Write me a haiku about LightChain"
254
+ PRIVATE_KEY=0x... npx lightnode wallet address
255
+ PRIVATE_KEY=0x... npx lightnode wallet balance --net testnet
256
+ npx lightnode wallet new # generates a fresh key
257
+ PRIVATE_KEY=0x... npx lightnode worker preflight --net testnet
258
+ ```
214
259
 
215
- ## Why `isRegistered` reads the chain
260
+ ### Scaffolders (write files into your project)
216
261
 
217
- The public indexer can report a registered worker as `deregistered` after a
218
- deregister -> re-register cycle. `isRegistered` instead reads the WorkerRegistry's
219
- join/exit events directly and returns the latest, so it is correct for any worker.
262
+ ```bash
263
+ npx lightnode add inference # encrypted inference route or script
264
+ npx lightnode add chat # chat UI with conversation history
265
+ npx lightnode add agent # scheduled inference (Vercel Cron / setInterval)
266
+ npx lightnode add analytics-dashboard # read-only network + worker analytics page
267
+ npx lightnode add nft-mint-with-inference # AI-generated NFT metadata with on-chain provenance
268
+ ```
269
+
270
+ All `add` commands accept `--template auto|nextjs-api|hono|node`,
271
+ `--net testnet|mainnet`, and `--force`.
220
272
 
221
273
  ## Networks
222
274
 
223
- | | mainnet | testnet |
224
- | --- | --- | --- |
225
- | chainId | 9200 | 8200 |
226
- | min stake | 50,000 LCAI | 5,000 LCAI |
227
- | WorkerRegistry | `0x…1002` (genesis predeploy) | same |
275
+ | | Testnet | Mainnet |
276
+ |---|---|---|
277
+ | Chain ID | 8200 | 9200 |
278
+ | RPC | `https://rpc.testnet.lightchain.ai` | `https://rpc.mainnet.lightchain.ai` |
279
+ | Explorer | <https://testnet.lightscan.app> | <https://mainnet.lightscan.app> |
280
+ | Faucet | <https://lightfaucet.ai> (~2 LCAI / IP / day) | n/a (bridge from Ethereum) |
281
+ | Inference cost | free | ~0.022 LCAI per call |
282
+ | Worker stake | 5,000 LCAI | 50,000 LCAI |
283
+
284
+ ## Examples
285
+
286
+ Tiny standalone repo: <https://github.com/marinom2/lightnode-examples>.
287
+ Eight runnable examples covering every SDK module:
288
+
289
+ - `quickstart-inference/` (30-line one-shot)
290
+ - `multi-turn-chat/` (interactive REPL)
291
+ - `nextjs-api-route/` (drop-in App Router route)
292
+ - `hono-server/` (any-Node microservice)
293
+ - `bridge-transfer/` (LCAI bridge in both directions)
294
+ - `dao-vote/` (read + vote LCAI Governor)
295
+ - `worker-preflight/` (one real test inference + watch)
296
+ - `model-registry-read/` (AIVMModelRegistry reader)
297
+
298
+ Open any of them in StackBlitz in about 5 seconds:
299
+
300
+ ```
301
+ https://stackblitz.com/github/marinom2/lightnode-examples/tree/main/quickstart-inference
302
+ ```
303
+
304
+ ## Non-custodial
305
+
306
+ - The SDK never holds your key. Every on-chain call is signed via viem in
307
+ your process.
308
+ - End-to-end encryption: your prompt is encrypted to the worker's ECDH pubkey
309
+ before it leaves your machine. The gateway, the relay, and any third party
310
+ in the path see only ciphertext.
311
+ - The session key is ephemeral (32 random bytes per session). Never persisted.
312
+ - Browser bundles work too: noble-backed crypto, no Web Crypto algorithm
313
+ dependency, no Node-only imports.
314
+
315
+ ## Compatibility
316
+
317
+ | Runtime | Status |
318
+ |---|---|
319
+ | Node 18+ | Tested; `ws` auto-detected. |
320
+ | Modern browsers | Works via `globalThis.WebSocket`. The /playground uses it. |
321
+ | StackBlitz / Bolt WebContainer | Works since 0.4.8 (noble crypto, lightnode.app CORS proxy). |
322
+ | Cloudflare Workers / Bun | Works. Pass a `WebSocket` ctor if the runtime lacks one. |
323
+
324
+ ## Provenance
325
+
326
+ The protocol surface (consumer gateway, relay, JobRegistry ABI, crypto
327
+ layout) is built against
328
+ [LightChain's reference client](https://github.com/lightchain-protocol/lcai-chat-v2)
329
+ and cert-transparency host enumeration. Crypto is byte-perfect vs the
330
+ reference (`@noble/curves` for P-256, `@noble/ciphers` for AES-256-GCM).
331
+
332
+ If LightChain ships official SDKs that supersede this one, we'll archive the
333
+ inference path and keep the analytics + bridge + DAO + preflight modules.
228
334
 
229
335
  ## License
230
336
 
231
- MIT
337
+ MIT. Independent, community-built. Not affiliated with or endorsed by the
338
+ LightChain team.
@@ -0,0 +1,122 @@
1
+ import { type RunInferenceWithKeyArgs } from "./inference.js";
2
+ /**
3
+ * One tool the agent can call. The model decides when by emitting a tool
4
+ * call block; the handler returns plain JSON which is threaded back into
5
+ * the next turn as an observation.
6
+ *
7
+ * The handler MUST return JSON-serializable data so the model can read
8
+ * it back. Use `String()` to stringify primitives, `JSON.stringify`-able
9
+ * objects otherwise.
10
+ */
11
+ export interface AgentTool {
12
+ /** Short snake_case name. The model uses this exact string in tool calls. */
13
+ name: string;
14
+ /** One-line description shown to the model so it knows when to use the tool. */
15
+ description: string;
16
+ /**
17
+ * Argument schema, very informal: { argName: "string description" }. The
18
+ * model is told to fill in matching JSON values; the SDK does not enforce
19
+ * types beyond JSON parsing.
20
+ */
21
+ args: Record<string, string>;
22
+ /** Run the tool. Return JSON-serializable data (string, number, object, etc.). */
23
+ handler: (args: Record<string, unknown>) => Promise<unknown> | unknown;
24
+ }
25
+ /** One step in the agent's reasoning loop. */
26
+ export type AgentStep = {
27
+ kind: "thought";
28
+ text: string;
29
+ } | {
30
+ kind: "tool_call";
31
+ name: string;
32
+ args: Record<string, unknown>;
33
+ result: unknown;
34
+ } | {
35
+ kind: "tool_error";
36
+ name: string;
37
+ args: Record<string, unknown>;
38
+ error: string;
39
+ } | {
40
+ kind: "answer";
41
+ text: string;
42
+ };
43
+ export interface AgentRunResult {
44
+ /** The model's final answer. May be empty if maxIterations was hit. */
45
+ answer: string;
46
+ /** Every step taken (thoughts + tool calls + final answer). Useful for debugging + UI. */
47
+ steps: AgentStep[];
48
+ /** Total inferences fired. Each iteration is one inference, so this is also iteration count. */
49
+ iterations: number;
50
+ /** True if the loop bailed because it ran out of iterations without an `answer` block. */
51
+ hitLimit: boolean;
52
+ }
53
+ export interface AgentOptions extends Omit<RunInferenceWithKeyArgs, "prompt" | "system"> {
54
+ /** Human-readable goal / persona. Becomes the BASE system prompt; the tool harness is appended automatically. */
55
+ system?: string;
56
+ /** Tools the model is allowed to call. */
57
+ tools: ReadonlyArray<AgentTool>;
58
+ /** Hard cap on reasoning steps. Default 5. */
59
+ maxIterations?: number;
60
+ /** Called after every step (thought / tool call / answer). Useful for live UI. */
61
+ onStep?: (step: AgentStep) => void;
62
+ }
63
+ /**
64
+ * ReAct-style agent on top of `runInferenceWithKey`. Each iteration:
65
+ * 1. The SDK sends a system prompt that lists tools + the JSON tool-call
66
+ * format to the model, plus the running transcript.
67
+ * 2. The model emits either a tool call (`<tool>name {"k":"v"}</tool>`)
68
+ * or a final answer (`<answer>...</answer>`).
69
+ * 3. The SDK parses, runs the tool, threads the result back, repeats.
70
+ *
71
+ * Designed for smaller open models (llama3-8b). The protocol is simple
72
+ * string markers, not native function calling, so it works on any model
73
+ * the LightChain network exposes.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import { Agent } from "lightnode-sdk";
78
+ *
79
+ * const agent = new Agent({
80
+ * network: "testnet",
81
+ * privateKey: process.env.PRIVATE_KEY!,
82
+ * model: "llama3-8b",
83
+ * system: "You are a careful research assistant.",
84
+ * tools: [
85
+ * {
86
+ * name: "add",
87
+ * description: "Add two integers and return the sum.",
88
+ * args: { a: "first integer", b: "second integer" },
89
+ * handler: ({ a, b }) => Number(a) + Number(b),
90
+ * },
91
+ * ],
92
+ * maxIterations: 3,
93
+ * });
94
+ *
95
+ * const { answer, steps } = await agent.run("What is 17 + 25?");
96
+ * console.log(answer); // "42"
97
+ * ```
98
+ */
99
+ export declare class Agent {
100
+ private readonly opts;
101
+ constructor(opts: AgentOptions);
102
+ run(userMessage: string): Promise<AgentRunResult>;
103
+ private buildSystemPrompt;
104
+ }
105
+ type ParsedOutput = {
106
+ kind: "tool_call";
107
+ name: string;
108
+ args: Record<string, unknown>;
109
+ } | {
110
+ kind: "answer";
111
+ text: string;
112
+ } | {
113
+ kind: "thought";
114
+ text: string;
115
+ };
116
+ /**
117
+ * Parse the model's raw output into either a tool call, a final answer, or
118
+ * a thought (everything else). Tolerant of whitespace and the model
119
+ * forgetting to close a tag. Visible for testing.
120
+ */
121
+ export declare function parseAgentOutput(raw: string): ParsedOutput;
122
+ export {};