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 +285 -178
- package/dist/agent.d.ts +122 -0
- package/dist/agent.js +176 -0
- package/dist/batch.d.ts +88 -0
- package/dist/batch.js +102 -0
- package/dist/bridge.js +1 -1
- package/dist/cli.js +16 -2
- package/dist/dao.d.ts +32 -7
- package/dist/dao.js +17 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.js +7 -1
- package/dist/inference.d.ts +12 -0
- package/dist/inference.js +8 -2
- package/dist/networks.js +9 -0
- package/dist/types.d.ts +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,231 +1,338 @@
|
|
|
1
1
|
# lightnode-sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/lightnode-sdk)
|
|
4
|
+
[](LICENSE)
|
|
8
5
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
+
LightChain's own docs list official SDKs as "soon"; this fills the gap. Not
|
|
17
|
+
affiliated with LightChain.
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
import { LightNode } from "lightnode-sdk";
|
|
19
|
+
## Five-line "hello world"
|
|
23
20
|
|
|
24
|
-
|
|
21
|
+
```ts
|
|
22
|
+
import { runInferenceWithKey } from "lightnode-sdk";
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
//
|
|
32
|
-
|
|
30
|
+
console.log(answer); // the decrypted reply
|
|
31
|
+
console.log(txs.createSession); // on-chain receipts
|
|
32
|
+
```
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
79
|
+
Plus the bare-metal aggregators (`aggregateModelStats`, `aggregateWorkerStats`,
|
|
80
|
+
`networkAnalytics`) and CSV exporters (`modelStatsCsv`, `workerStatsCsv`,
|
|
81
|
+
`workerJobsCsv`) for reporting / dashboards.
|
|
96
82
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 {
|
|
104
|
-
import
|
|
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
|
|
107
|
-
const
|
|
108
|
-
const {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
###
|
|
125
|
+
### DAO SDK (new in 0.5.0)
|
|
124
126
|
|
|
125
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
153
|
+
Voting params (live-read via `dao.config()`): ~1 day delay, ~14 day period,
|
|
154
|
+
140k LCAI threshold, 3% quorum.
|
|
140
155
|
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
199
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
+
### Scaffolders (write files into your project)
|
|
216
261
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
| |
|
|
224
|
-
|
|
225
|
-
|
|
|
226
|
-
|
|
|
227
|
-
|
|
|
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.
|
package/dist/agent.d.ts
ADDED
|
@@ -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 {};
|