@0xopenseeddev/sdk 0.1.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 ADDED
@@ -0,0 +1,294 @@
1
+ # @0xopenseeddev/sdk
2
+
3
+ Programmatic SDK for the **OpenSeed** network.
4
+ Spin up seller nodes, define AI offerings, and orchestrate a local peer network — all from JavaScript.
5
+
6
+ ---
7
+
8
+ ## Installation
9
+
10
+ The SDK is a workspace package and is available automatically within the monorepo.
11
+ Import directly from source:
12
+
13
+ ```js
14
+ import { Network, SellerNode, Offering } from './packages/sdk/src/index.js';
15
+ ```
16
+
17
+ Or, if published externally:
18
+
19
+ ```sh
20
+ npm install @0xopenseeddev/sdk
21
+ ```
22
+
23
+ ---
24
+
25
+ ## Quick Start
26
+
27
+ ```js
28
+ import { Network, Offering } from '@0xopenseeddev/sdk';
29
+
30
+ const net = new Network({
31
+ apiKey: process.env.OPENROUTER_API_KEY,
32
+ registryUrl: 'http://localhost:9000'
33
+ });
34
+
35
+ net
36
+ .addSeller({
37
+ port: 8401,
38
+ peerId: 'my-node-001',
39
+ offerings: [
40
+ new Offering('inference')
41
+ .name('Llama 3 8B')
42
+ .description('Fast open-source model for everyday tasks.')
43
+ .services(['llama-3-8b'])
44
+ .pricing({ inputUsdPerMillion: 0.05, outputUsdPerMillion: 0.05 })
45
+ .upstreamModel('meta-llama/llama-3-8b-instruct:free')
46
+ ]
47
+ });
48
+
49
+ await net.start();
50
+ // SIGINT / SIGTERM are handled automatically — Ctrl+C shuts everything down cleanly.
51
+ ```
52
+
53
+ ---
54
+
55
+ ## API Reference
56
+
57
+ ### `Offering`
58
+
59
+ Fluent builder for a single capability advertised by a seller node.
60
+
61
+ ```js
62
+ new Offering(capability)
63
+ ```
64
+
65
+ | Argument | Type | Description |
66
+ |---|---|---|
67
+ | `capability` | `'inference' \| 'agent' \| 'tool'` | Capability type |
68
+
69
+ **Chain methods** (all return `this`):
70
+
71
+ | Method | Argument | Description |
72
+ |---|---|---|
73
+ | `.name(str)` | `string` | Human-readable offering name |
74
+ | `.description(str)` | `string` | Short description shown to buyers |
75
+ | `.services(ids)` | `string[]` | Model/service IDs buyers request (e.g. `['gpt-4o-mini']`) |
76
+ | `.pricing(p)` | `object` | Pricing in USD per 1M tokens (see below) |
77
+ | `.upstreamModel(str)` | `string` | Model name forwarded upstream (e.g. `'openai/gpt-4o-mini'`) |
78
+
79
+ **Pricing object:**
80
+
81
+ ```js
82
+ {
83
+ inputUsdPerMillion: 0.15, // required
84
+ cachedInputUsdPerMillion: 0.075, // optional, defaults to inputUsdPerMillion
85
+ outputUsdPerMillion: 0.60 // required
86
+ }
87
+ ```
88
+
89
+ > `.toJSON()` validates all required fields and throws if anything is missing.
90
+
91
+ **Example:**
92
+
93
+ ```js
94
+ new Offering('agent')
95
+ .name('Code Master')
96
+ .description('Best-in-class coding agent.')
97
+ .services(['claude-3-5-sonnet'])
98
+ .pricing({ inputUsdPerMillion: 3.00, cachedInputUsdPerMillion: 1.50, outputUsdPerMillion: 15.00 })
99
+ .upstreamModel('anthropic/claude-3-5-sonnet')
100
+ ```
101
+
102
+ ---
103
+
104
+ ### `SellerNode`
105
+
106
+ Wraps a forked seller process. Extends `EventEmitter`.
107
+
108
+ ```js
109
+ new SellerNode(opts)
110
+ ```
111
+
112
+ **Options:**
113
+
114
+ | Option | Type | Required | Description |
115
+ |---|---|---|---|
116
+ | `peerId` | `string` | ✅ | Unique peer identifier |
117
+ | `port` | `number` | ✅ | HTTP port to listen on |
118
+ | `offerings` | `Offering[]` | ✅ | At least one `Offering` instance |
119
+ | `endpoint` | `string` | — | Public URL buyers connect to (default: `http://localhost:<port>`) |
120
+ | `merchantAddress` | `string` | — | On-chain address for payment receipts |
121
+ | `registryUrl` | `string` | — | Registry base URL (default: `http://localhost:9000`) |
122
+ | `apiKey` | `string` | — | Upstream AI provider key |
123
+ | `upstreamBaseUrl` | `string` | — | Upstream base URL (default: OpenRouter) |
124
+ | `sellerPrivateKey` | `string` | — | Seller private key for on-chain settlement |
125
+ | `contractAddress` | `string` | — | AntseedDeposits contract address |
126
+ | `chainName` | `string` | — | `'base'` \| `'base-sepolia'` \| `'localhost'` |
127
+ | `rpcUrl` | `string` | — | RPC endpoint |
128
+ | `sellerPath` | `string` | — | Override path to seller entry point |
129
+ | `env` | `object` | — | Extra env vars passed to the child process |
130
+
131
+ **Methods:**
132
+
133
+ ```js
134
+ await node.start() // fork the process; resolves once forked
135
+ await node.stop() // send SIGTERM and wait for exit
136
+ node.info() // { peerId, port, endpoint, running, offerings[] }
137
+ ```
138
+
139
+ **Events:**
140
+
141
+ ```js
142
+ node.on('started', ({ peerId, port }) => { /* process is up and listening */ })
143
+ node.on('error', (err) => { /* fork failed */ })
144
+ node.on('exit', ({ peerId, code, signal }) => { /* process exited */ })
145
+ ```
146
+
147
+ **Example:**
148
+
149
+ ```js
150
+ import { SellerNode, Offering } from '@0xopenseeddev/sdk';
151
+
152
+ const node = new SellerNode({
153
+ peerId: 'my-node-001',
154
+ port: 8401,
155
+ apiKey: process.env.OPENROUTER_API_KEY,
156
+ offerings: [
157
+ new Offering('inference')
158
+ .name('GPT-4o Mini')
159
+ .services(['gpt-4o-mini'])
160
+ .pricing({ inputUsdPerMillion: 0.15, outputUsdPerMillion: 0.60 })
161
+ .upstreamModel('openai/gpt-4o-mini')
162
+ ]
163
+ });
164
+
165
+ node.on('started', () => console.log('Node is ready!'));
166
+ node.on('exit', ({ code }) => console.log('Exited with code', code));
167
+
168
+ await node.start();
169
+
170
+ // Later:
171
+ await node.stop();
172
+ ```
173
+
174
+ ---
175
+
176
+ ### `Network`
177
+
178
+ Orchestrates multiple `SellerNode` instances with shared defaults.
179
+ Extends `EventEmitter`.
180
+
181
+ ```js
182
+ new Network(defaults?)
183
+ ```
184
+
185
+ **Defaults (applied to every node added via `.addSeller()`):**
186
+
187
+ | Option | Type | Description |
188
+ |---|---|---|
189
+ | `apiKey` | `string` | Upstream AI provider key |
190
+ | `upstreamBaseUrl` | `string` | Upstream base URL |
191
+ | `registryUrl` | `string` | Registry base URL |
192
+ | `merchantAddress` | `string` | Shared merchant address |
193
+ | `env` | `object` | Extra env vars (merged with per-node env) |
194
+
195
+ **Methods:**
196
+
197
+ ```js
198
+ net.addSeller(opts) // same opts as SellerNode; returns Network (chainable)
199
+ await net.start() // start all nodes concurrently; hooks SIGINT/SIGTERM
200
+ await net.stop() // stop all running nodes concurrently
201
+ net.list() // array of node.info() for each node
202
+ net.size // number of nodes
203
+ ```
204
+
205
+ **Events:**
206
+
207
+ ```js
208
+ net.on('node:started', ({ peerId, port }) => { })
209
+ net.on('node:error', ({ peerId, err }) => { })
210
+ net.on('node:exit', ({ peerId, code }) => { })
211
+ ```
212
+
213
+ **Example — multi-node simulation:**
214
+
215
+ ```js
216
+ import { Network, Offering } from '@0xopenseeddev/sdk';
217
+
218
+ const net = new Network({
219
+ apiKey: process.env.OPENROUTER_KEY,
220
+ registryUrl: 'http://localhost:9000',
221
+ merchantAddress: '0xYourAddress'
222
+ });
223
+
224
+ net
225
+ .addSeller({
226
+ port: 8401,
227
+ peerId: 'peer-llama-001',
228
+ offerings: [
229
+ new Offering('inference')
230
+ .name('Llama 3 8B')
231
+ .services(['llama-3-8b'])
232
+ .pricing({ inputUsdPerMillion: 0.05, outputUsdPerMillion: 0.05 })
233
+ .upstreamModel('meta-llama/llama-3-8b-instruct:free')
234
+ ]
235
+ })
236
+ .addSeller({
237
+ port: 8402,
238
+ peerId: 'peer-gemini-002',
239
+ offerings: [
240
+ new Offering('inference')
241
+ .name('Gemini 2.5 Flash')
242
+ .services(['gemini-2.5-flash'])
243
+ .pricing({ inputUsdPerMillion: 0.27, outputUsdPerMillion: 0.27 })
244
+ .upstreamModel('google/gemini-2.5-flash')
245
+ ]
246
+ });
247
+
248
+ await net.start();
249
+ // Ctrl+C → graceful shutdown of all nodes
250
+ ```
251
+
252
+ ---
253
+
254
+ ## Environment Variables
255
+
256
+ All environment variables from the parent process are inherited by child nodes.
257
+ The SDK additionally injects the following:
258
+
259
+ | Variable | Source |
260
+ |---|---|
261
+ | `PEER_ID` | `opts.peerId` |
262
+ | `SELLER_PORT` | `opts.port` |
263
+ | `SELLER_ENDPOINT` | `opts.endpoint` |
264
+ | `REGISTRY_URL` | `opts.registryUrl` |
265
+ | `OFFERINGS` | serialised JSON of offerings |
266
+ | `MERCHANT_ADDRESS` | `opts.merchantAddress` |
267
+ | `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` | `opts.apiKey` |
268
+ | `UPSTREAM_BASE_URL` | `opts.upstreamBaseUrl` |
269
+ | `SELLER_PRIVATE_KEY` | `opts.sellerPrivateKey` |
270
+ | `CONTRACT_ADDRESS` | `opts.contractAddress` |
271
+ | `CHAIN_NAME` | `opts.chainName` |
272
+ | `RPC_URL` | `opts.rpcUrl` |
273
+
274
+ ---
275
+
276
+ ## Package Layout
277
+
278
+ ```
279
+ packages/sdk/
280
+ ├── src/
281
+ │ ├── index.js ← barrel export
282
+ │ ├── offering.js ← Offering builder
283
+ │ ├── seller.js ← SellerNode (fork wrapper)
284
+ │ └── network.js ← Network (multi-node orchestrator)
285
+ └── package.json
286
+ ```
287
+
288
+ ---
289
+
290
+ ## See Also
291
+
292
+ - [`simulate.js`](../../simulate.js) — live example using the SDK to boot a 3-peer local network
293
+ - [`packages/seller`](../seller) — the seller node implementation the SDK forks
294
+ - [`packages/cli`](../cli) — CLI that also uses the seller node programmatically
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@0xopenseeddev/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Programmatic SDK for spawning and managing OpenSeed seller nodes",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./seller": "./src/seller.js",
10
+ "./network": "./src/network.js"
11
+ },
12
+ "scripts": {
13
+ "build": "echo 'sdk has no build step'"
14
+ },
15
+ "keywords": ["openseed", "sdk", "p2p", "ai"],
16
+ "license": "ISC"
17
+ }
package/src/index.js ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @0xopenseeddev/sdk — Programmatic SDK for the OpenSeed network.
3
+ *
4
+ * Public API:
5
+ * - Network → orchestrate multiple seller nodes
6
+ * - SellerNode → spawn a single seller node
7
+ * - Offering → fluent builder for offering definitions
8
+ */
9
+
10
+ export { Network } from './network.js';
11
+ export { SellerNode } from './seller.js';
12
+ export { Offering } from './offering.js';
package/src/network.js ADDED
@@ -0,0 +1,112 @@
1
+ import { EventEmitter } from 'events';
2
+ import { SellerNode } from './seller.js';
3
+ import { Offering } from './offering.js';
4
+
5
+ /**
6
+ * Network — manages a group of SellerNode instances.
7
+ *
8
+ * Usage:
9
+ * const net = new Network({ apiKey: '...', registryUrl: '...' });
10
+ * net.addSeller({ peerId: '...', port: 8401, offerings: [...] });
11
+ * net.addSeller({ ... });
12
+ * await net.start();
13
+ * // later:
14
+ * await net.stop();
15
+ *
16
+ * @extends EventEmitter
17
+ */
18
+ export class Network extends EventEmitter {
19
+ /**
20
+ * @param {object} [defaults] - default values applied to every SellerNode in this network
21
+ * @param {string} [defaults.apiKey] - upstream AI provider API key
22
+ * @param {string} [defaults.upstreamBaseUrl] - upstream base URL
23
+ * @param {string} [defaults.registryUrl] - registry URL
24
+ * @param {string} [defaults.merchantAddress] - shared merchant address
25
+ * @param {object} [defaults.env] - extra env vars for every node
26
+ */
27
+ constructor(defaults = {}) {
28
+ super();
29
+ this._defaults = defaults;
30
+ /** @type {SellerNode[]} */
31
+ this._nodes = [];
32
+ }
33
+
34
+ /**
35
+ * Add a seller node to the network.
36
+ *
37
+ * @param {object} opts - same options as SellerNode constructor;
38
+ * defaults from the Network constructor are merged in (node opts win).
39
+ * @returns {Network} this (chainable)
40
+ */
41
+ addSeller(opts) {
42
+ const merged = {
43
+ ...this._defaults,
44
+ ...opts,
45
+ env: { ...(this._defaults.env ?? {}), ...(opts.env ?? {}) }
46
+ };
47
+ this._nodes.push(new SellerNode(merged));
48
+ return this;
49
+ }
50
+
51
+ /**
52
+ * Start all nodes concurrently.
53
+ * @returns {Promise<Network>} this
54
+ */
55
+ async start() {
56
+ console.log(`[sdk] Starting network with ${this._nodes.length} seller node(s)…`);
57
+
58
+ await Promise.all(
59
+ this._nodes.map(node =>
60
+ node
61
+ .start()
62
+ .then(() => {
63
+ node.on('started', (info) => {
64
+ console.log(`[sdk] ✅ ${info.peerId} ready on :${info.port}`);
65
+ if (info.firstService) {
66
+ console.log(`[sdk] 💬 Chat: http://localhost:3000/?peer=${info.peerId}&service=${info.firstService}`);
67
+ }
68
+ this.emit('node:started', info);
69
+ });
70
+ node.on('error', (err) => {
71
+ console.error(`[sdk] ❌ ${node.peerId} error:`, err.message);
72
+ this.emit('node:error', { peerId: node.peerId, err });
73
+ });
74
+ node.on('exit', (info) => {
75
+ console.log(`[sdk] 🔴 ${info.peerId} exited (code ${info.code})`);
76
+ this.emit('node:exit', info);
77
+ });
78
+ })
79
+ )
80
+ );
81
+
82
+ // Gracefully handle SIGINT / SIGTERM at the network level
83
+ const shutdown = async () => {
84
+ console.log('\n[sdk] Shutting down network…');
85
+ await this.stop();
86
+ process.exit(0);
87
+ };
88
+ process.once('SIGINT', shutdown);
89
+ process.once('SIGTERM', shutdown);
90
+
91
+ return this;
92
+ }
93
+
94
+ /**
95
+ * Stop all running nodes concurrently.
96
+ * @returns {Promise<void>}
97
+ */
98
+ async stop() {
99
+ await Promise.all(this._nodes.filter(n => n.running).map(n => n.stop()));
100
+ }
101
+
102
+ /**
103
+ * Return info for every node.
104
+ * @returns {object[]}
105
+ */
106
+ list() {
107
+ return this._nodes.map(n => n.info());
108
+ }
109
+
110
+ /** Number of nodes in this network */
111
+ get size() { return this._nodes.length; }
112
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Offering builder — describes one capability a seller node advertises.
3
+ *
4
+ * Usage:
5
+ * new Offering('inference')
6
+ * .name('Llama 3 8B')
7
+ * .description('...')
8
+ * .services(['llama-3-8b'])
9
+ * .pricing({ inputUsdPerMillion: 0.05, outputUsdPerMillion: 0.05 })
10
+ * .upstreamModel('meta-llama/llama-3-8b-instruct:free')
11
+ */
12
+ export class Offering {
13
+ /**
14
+ * @param {'inference' | 'agent' | 'tool'} capability
15
+ */
16
+ constructor(capability) {
17
+ this._data = {
18
+ capability,
19
+ name: '',
20
+ description: '',
21
+ services: [],
22
+ pricing: {
23
+ inputUsdPerMillion: 0,
24
+ cachedInputUsdPerMillion: 0,
25
+ outputUsdPerMillion: 0
26
+ },
27
+ upstreamModel: ''
28
+ };
29
+ }
30
+
31
+ /** @param {string} name - human-readable offering name */
32
+ name(name) {
33
+ this._data.name = name;
34
+ return this;
35
+ }
36
+
37
+ /** @param {string} desc */
38
+ description(desc) {
39
+ this._data.description = desc;
40
+ return this;
41
+ }
42
+
43
+ /**
44
+ * @param {string[]} serviceIds - model/service IDs buyers will request
45
+ */
46
+ services(serviceIds) {
47
+ this._data.services = Array.isArray(serviceIds) ? serviceIds : [serviceIds];
48
+ return this;
49
+ }
50
+
51
+ /**
52
+ * @param {{ inputUsdPerMillion: number, cachedInputUsdPerMillion?: number, outputUsdPerMillion: number }} p
53
+ */
54
+ pricing(p) {
55
+ this._data.pricing = {
56
+ inputUsdPerMillion: p.inputUsdPerMillion ?? 0,
57
+ cachedInputUsdPerMillion: p.cachedInputUsdPerMillion ?? p.inputUsdPerMillion ?? 0,
58
+ outputUsdPerMillion: p.outputUsdPerMillion ?? 0
59
+ };
60
+ return this;
61
+ }
62
+
63
+ /**
64
+ * @param {string} model - model name to send upstream (e.g. 'openai/gpt-4o-mini')
65
+ */
66
+ upstreamModel(model) {
67
+ this._data.upstreamModel = model;
68
+ return this;
69
+ }
70
+
71
+ /** @returns {object} plain JSON-serialisable object */
72
+ toJSON() {
73
+ if (!this._data.name) throw new Error('Offering is missing a name');
74
+ if (!this._data.services.length) throw new Error(`Offering "${this._data.name}" has no services`);
75
+ if (!this._data.upstreamModel) throw new Error(`Offering "${this._data.name}" is missing upstreamModel`);
76
+ return { ...this._data };
77
+ }
78
+ }
package/src/seller.js ADDED
@@ -0,0 +1,154 @@
1
+ import { fork } from 'child_process';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, resolve } from 'path';
4
+ import { EventEmitter } from 'events';
5
+ import { Offering } from './offering.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ // Default path to the seller entry point (packages/seller/src/index.js)
11
+ const DEFAULT_SELLER_PATH = resolve(__dirname, '../../seller/src/index.js');
12
+
13
+ /**
14
+ * SellerNode — wraps a forked seller process.
15
+ *
16
+ * Lifecycle:
17
+ * node.start() → forks the process, registers with registry
18
+ * node.stop() → sends SIGTERM (graceful deregister + close)
19
+ * node.on('started' | 'error' | 'exit') → event hooks
20
+ *
21
+ * @extends EventEmitter
22
+ */
23
+ export class SellerNode extends EventEmitter {
24
+ /**
25
+ * @param {object} opts
26
+ * @param {string} opts.peerId - unique peer identifier
27
+ * @param {number} opts.port - HTTP port to listen on
28
+ * @param {string} [opts.endpoint] - public endpoint URL (defaults to http://localhost:<port>)
29
+ * @param {string} [opts.merchantAddress]- on-chain address for payment receipts
30
+ * @param {Offering[]} opts.offerings - list of Offering instances
31
+ * @param {string} [opts.registryUrl] - registry base URL (default: http://localhost:9000)
32
+ * @param {string} [opts.apiKey] - upstream AI provider key
33
+ * @param {string} [opts.upstreamBaseUrl]- upstream base URL (default: openrouter)
34
+ * @param {string} [opts.sellerPrivateKey] - seller private key for on-chain settlement
35
+ * @param {string} [opts.contractAddress] - AntseedDeposits contract address
36
+ * @param {string} [opts.chainName] - 'base' | 'base-sepolia' | 'localhost'
37
+ * @param {string} [opts.rpcUrl] - RPC endpoint
38
+ * @param {string} [opts.sellerPath] - override the seller entry point (for testing)
39
+ * @param {object} [opts.env] - additional environment variables to pass
40
+ */
41
+ constructor(opts = {}) {
42
+ super();
43
+
44
+ if (!opts.peerId) throw new Error('SellerNode: opts.peerId is required');
45
+ if (!opts.port) throw new Error('SellerNode: opts.port is required');
46
+ if (!opts.offerings?.length) throw new Error('SellerNode: at least one offering is required');
47
+
48
+ this.peerId = opts.peerId;
49
+ this.port = opts.port;
50
+ this.endpoint = opts.endpoint ?? `http://127.0.0.1:${opts.port}`;
51
+ this.merchantAddress = opts.merchantAddress ?? '';
52
+ this.registryUrl = opts.registryUrl ?? 'http://localhost:9000';
53
+ this.sellerPath = opts.sellerPath ?? DEFAULT_SELLER_PATH;
54
+
55
+ // Serialise offerings (calls toJSON() which validates each one)
56
+ this._offerings = opts.offerings.map(o => o instanceof Offering ? o.toJSON() : o);
57
+
58
+ this._env = {
59
+ ...(opts.env ?? {}),
60
+ PEER_ID: this.peerId,
61
+ SELLER_PORT: String(this.port),
62
+ SELLER_ENDPOINT: this.endpoint,
63
+ REGISTRY_URL: this.registryUrl,
64
+ OFFERINGS: JSON.stringify(this._offerings),
65
+ ...(opts.merchantAddress && { MERCHANT_ADDRESS: opts.merchantAddress }),
66
+ ...(opts.apiKey && { OPENAI_API_KEY: opts.apiKey, ANTHROPIC_API_KEY: opts.apiKey }),
67
+ ...(opts.upstreamBaseUrl && { UPSTREAM_BASE_URL: opts.upstreamBaseUrl }),
68
+ ...(opts.sellerPrivateKey && { SELLER_PRIVATE_KEY: opts.sellerPrivateKey }),
69
+ ...(opts.contractAddress && { CONTRACT_ADDRESS: opts.contractAddress }),
70
+ ...(opts.chainName && { CHAIN_NAME: opts.chainName }),
71
+ ...(opts.rpcUrl && { RPC_URL: opts.rpcUrl })
72
+ };
73
+
74
+ /** @type {import('child_process').ChildProcess | null} */
75
+ this._child = null;
76
+ this.running = false;
77
+ }
78
+
79
+ /**
80
+ * Fork the seller process and start the node.
81
+ * Resolves once the process has been forked (not necessarily listening yet).
82
+ * Listen for the 'started' event for a best-effort "ready" signal.
83
+ *
84
+ * @returns {Promise<SellerNode>} this
85
+ */
86
+ async start() {
87
+ if (this.running) {
88
+ throw new Error(`SellerNode ${this.peerId} is already running`);
89
+ }
90
+
91
+ const env = { ...process.env, ...this._env };
92
+
93
+ this._child = fork(this.sellerPath, [], { env, stdio: 'pipe' });
94
+ this.running = true;
95
+
96
+ // Pipe stdout/stderr and detect "listening" banner
97
+ this._child.stdout?.on('data', (chunk) => {
98
+ const text = chunk.toString();
99
+ process.stdout.write(`[${this.peerId.slice(0, 20)}] ${text}`);
100
+ if (text.includes('🐜 OpenSeed Seller')) {
101
+ this.emit('started', {
102
+ peerId: this.peerId,
103
+ port: this.port,
104
+ firstService: this._offerings[0]?.services[0]
105
+ });
106
+ }
107
+ });
108
+
109
+ this._child.stderr?.on('data', (chunk) => {
110
+ process.stderr.write(`[${this.peerId.slice(0, 20)}][ERR] ${chunk}`);
111
+ });
112
+
113
+ this._child.on('error', (err) => {
114
+ this.running = false;
115
+ this.emit('error', err);
116
+ });
117
+
118
+ this._child.on('exit', (code, signal) => {
119
+ this.running = false;
120
+ this._child = null;
121
+ this.emit('exit', { peerId: this.peerId, code, signal });
122
+ });
123
+
124
+ return this;
125
+ }
126
+
127
+ /**
128
+ * Gracefully stop the seller node (sends SIGTERM).
129
+ * @returns {Promise<void>} resolves once the process exits
130
+ */
131
+ stop() {
132
+ return new Promise((resolve) => {
133
+ if (!this._child || !this.running) {
134
+ resolve();
135
+ return;
136
+ }
137
+ this.once('exit', () => resolve());
138
+ this._child.kill('SIGTERM');
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Summary info for logging / dashboards.
144
+ */
145
+ info() {
146
+ return {
147
+ peerId: this.peerId,
148
+ port: this.port,
149
+ endpoint: this.endpoint,
150
+ running: this.running,
151
+ offerings: this._offerings.map(o => o.name)
152
+ };
153
+ }
154
+ }