@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 +294 -0
- package/package.json +17 -0
- package/src/index.js +12 -0
- package/src/network.js +112 -0
- package/src/offering.js +78 -0
- package/src/seller.js +154 -0
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
|
+
}
|
package/src/offering.js
ADDED
|
@@ -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
|
+
}
|