technocore-compute 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.
@@ -0,0 +1,308 @@
1
+ /**
2
+ * ComputeExchange - P2P compute marketplace for AI agents
3
+ *
4
+ * Manages provider registration, compute requests, job matching,
5
+ * and tclk payment coordination.
6
+ */
7
+
8
+ import type {
9
+ ComputeProvider,
10
+ ComputeRequest,
11
+ ComputeJob,
12
+ ComputeType,
13
+ ExchangeStats,
14
+ JobStatus,
15
+ } from './types.js';
16
+
17
+ export interface ExchangeConfig {
18
+ /** technocore.chat base URL */
19
+ technocoreBaseUrl?: string;
20
+ /** Room prefix for compute exchange */
21
+ roomPrefix?: string;
22
+ /** How often to refresh providers (ms) */
23
+ refreshIntervalMs?: number;
24
+ /** Data directory for persistence */
25
+ dataDir?: string;
26
+ }
27
+
28
+ export class ComputeExchange {
29
+ private providers: Map<string, ComputeProvider> = new Map();
30
+ private requests: Map<string, ComputeRequest> = new Map();
31
+ private jobs: Map<string, ComputeJob> = new Map();
32
+ private config: Required<ExchangeConfig>;
33
+
34
+ constructor(config: ExchangeConfig = {}) {
35
+ this.config = {
36
+ technocoreBaseUrl: config.technocoreBaseUrl || 'https://technocore.chat',
37
+ roomPrefix: config.roomPrefix || 'compute',
38
+ refreshIntervalMs: config.refreshIntervalMs || 30_000,
39
+ dataDir: config.dataDir || './compute-data',
40
+ };
41
+ }
42
+
43
+ // ── Provider Management ──────────────────────────────────────────────────
44
+
45
+ /** Register a compute provider */
46
+ registerProvider(provider: ComputeProvider): void {
47
+ this.providers.set(provider.did, {
48
+ ...provider,
49
+ registeredAt: provider.registeredAt || Date.now(),
50
+ lastHeartbeat: provider.lastHeartbeat || Date.now(),
51
+ available: provider.available !== undefined ? provider.available : true,
52
+ rating: provider.rating || 0,
53
+ jobsCompleted: provider.jobsCompleted || 0,
54
+ });
55
+ }
56
+
57
+ /** Update provider heartbeat */
58
+ heartbeat(did: string): void {
59
+ const provider = this.providers.get(did);
60
+ if (provider) {
61
+ provider.lastHeartbeat = Date.now();
62
+ provider.available = true;
63
+ }
64
+ }
65
+
66
+ /** Set provider availability */
67
+ setAvailable(did: string, available: boolean): void {
68
+ const provider = this.providers.get(did);
69
+ if (provider) {
70
+ provider.available = available;
71
+ }
72
+ }
73
+
74
+ /** Remove stale providers (no heartbeat in 5 minutes) */
75
+ pruneStaleProviders(maxAgeMs = 300_000): number {
76
+ const now = Date.now();
77
+ let pruned = 0;
78
+ for (const [did, provider] of this.providers) {
79
+ if (now - provider.lastHeartbeat > maxAgeMs) {
80
+ this.providers.delete(did);
81
+ pruned++;
82
+ }
83
+ }
84
+ return pruned;
85
+ }
86
+
87
+ /** Get a provider by DID */
88
+ getProvider(did: string): ComputeProvider | undefined {
89
+ return this.providers.get(did);
90
+ }
91
+
92
+ /** List all providers */
93
+ listProviders(): ComputeProvider[] {
94
+ return Array.from(this.providers.values());
95
+ }
96
+
97
+ /** Find providers matching requirements */
98
+ findProviders(requirements: {
99
+ hardware?: ComputeType;
100
+ minVramGB?: number;
101
+ minCpuCores?: number;
102
+ minRamGB?: number;
103
+ maxPricePerHour?: string;
104
+ }): ComputeProvider[] {
105
+ return this.listProviders().filter(p => {
106
+ if (!p.available) return false;
107
+ if (requirements.hardware && requirements.hardware !== 'any' && p.hardware !== requirements.hardware) return false;
108
+ if (requirements.minVramGB && (!p.vramGB || p.vramGB < requirements.minVramGB)) return false;
109
+ if (requirements.minCpuCores && (!p.cpuCores || p.cpuCores < requirements.minCpuCores)) return false;
110
+ if (requirements.minRamGB && (!p.ramGB || p.ramGB < requirements.minRamGB)) return false;
111
+ if (requirements.maxPricePerHour) {
112
+ const price = BigInt(p.pricePerHour);
113
+ const max = BigInt(requirements.maxPricePerHour);
114
+ if (price > max) return false;
115
+ }
116
+ return true;
117
+ });
118
+ }
119
+
120
+ // ── Request Management ───────────────────────────────────────────────────
121
+
122
+ /** Create a compute request */
123
+ createRequest(request: Omit<ComputeRequest, 'id' | 'createdAt' | 'expiresAt'>): ComputeRequest {
124
+ const id = `req-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
125
+ const fullRequest: ComputeRequest = {
126
+ ...request,
127
+ id,
128
+ createdAt: Date.now(),
129
+ expiresAt: Date.now() + (request.durationSec * 1000 || 3_600_000),
130
+ };
131
+ this.requests.set(id, fullRequest);
132
+ return fullRequest;
133
+ }
134
+
135
+ /** Get a request by ID */
136
+ getRequest(id: string): ComputeRequest | undefined {
137
+ return this.requests.get(id);
138
+ }
139
+
140
+ /** List all pending requests */
141
+ listPendingRequests(): ComputeRequest[] {
142
+ const now = Date.now();
143
+ return Array.from(this.requests.values()).filter(r => r.expiresAt > now);
144
+ }
145
+
146
+ /** Remove expired requests */
147
+ pruneExpiredRequests(): number {
148
+ const now = Date.now();
149
+ let pruned = 0;
150
+ for (const [id, request] of this.requests) {
151
+ if (request.expiresAt <= now) {
152
+ this.requests.delete(id);
153
+ pruned++;
154
+ }
155
+ }
156
+ return pruned;
157
+ }
158
+
159
+ // ── Job Matching ─────────────────────────────────────────────────────────
160
+
161
+ /** Match a request with the best available provider */
162
+ matchRequest(requestId: string): ComputeJob | null {
163
+ const request = this.requests.get(requestId);
164
+ if (!request) return null;
165
+
166
+ const providers = this.findProviders({
167
+ hardware: request.hardware,
168
+ minVramGB: request.minVramGB,
169
+ minCpuCores: request.minCpuCores,
170
+ minRamGB: request.minRamGB,
171
+ maxPricePerHour: request.maxPricePerHour,
172
+ });
173
+
174
+ if (providers.length === 0) return null;
175
+
176
+ // Sort by price (cheapest first), then by rating (highest first)
177
+ providers.sort((a, b) => {
178
+ const priceDiff = BigInt(a.pricePerHour) - BigInt(b.pricePerHour);
179
+ if (priceDiff !== 0n) return priceDiff < 0n ? -1 : 1;
180
+ return b.rating - a.rating;
181
+ });
182
+
183
+ const bestProvider = providers[0];
184
+ return this.createJob(request, bestProvider);
185
+ }
186
+
187
+ /** Create a job from a request and provider */
188
+ createJob(request: ComputeRequest, provider: ComputeProvider): ComputeJob {
189
+ const jobId = `job-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
190
+ const hours = request.durationSec / 3600;
191
+ const cost = BigInt(provider.pricePerHour) * BigInt(Math.ceil(hours * 10)) / 10n;
192
+
193
+ const job: ComputeJob = {
194
+ id: jobId,
195
+ providerDid: provider.did,
196
+ consumerDid: request.consumerDid,
197
+ request,
198
+ provider,
199
+ status: 'matched',
200
+ totalCost: cost.toString(),
201
+ room: `${this.config.roomPrefix}-${jobId.slice(4, 12)}`,
202
+ createdAt: Date.now(),
203
+ };
204
+
205
+ this.jobs.set(jobId, job);
206
+ return job;
207
+ }
208
+
209
+ /** Accept a job (provider confirms) */
210
+ acceptJob(jobId: string): boolean {
211
+ const job = this.jobs.get(jobId);
212
+ if (!job || job.status !== 'matched') return false;
213
+ job.status = 'locked';
214
+ return true;
215
+ }
216
+
217
+ /** Start a job */
218
+ startJob(jobId: string): boolean {
219
+ const job = this.jobs.get(jobId);
220
+ if (!job || job.status !== 'locked') return false;
221
+ job.status = 'running';
222
+ job.startedAt = Date.now();
223
+ return true;
224
+ }
225
+
226
+ /** Complete a job */
227
+ completeJob(jobId: string): boolean {
228
+ const job = this.jobs.get(jobId);
229
+ if (!job || job.status !== 'running') return false;
230
+ job.status = 'completed';
231
+ job.completedAt = Date.now();
232
+
233
+ // Update provider stats
234
+ const provider = this.providers.get(job.providerDid);
235
+ if (provider) {
236
+ provider.jobsCompleted++;
237
+ }
238
+
239
+ return true;
240
+ }
241
+
242
+ /** Fail a job */
243
+ failJob(jobId: string, _reason?: string): boolean {
244
+ const job = this.jobs.get(jobId);
245
+ if (!job) return false;
246
+ job.status = 'failed';
247
+ return true;
248
+ }
249
+
250
+ /** Refund a job */
251
+ refundJob(jobId: string): boolean {
252
+ const job = this.jobs.get(jobId);
253
+ if (!job) return false;
254
+ job.status = 'refunded';
255
+ return true;
256
+ }
257
+
258
+ /** Get a job by ID */
259
+ getJob(id: string): ComputeJob | undefined {
260
+ return this.jobs.get(id);
261
+ }
262
+
263
+ /** List all jobs */
264
+ listJobs(status?: JobStatus): ComputeJob[] {
265
+ const jobs = Array.from(this.jobs.values());
266
+ return status ? jobs.filter(j => j.status === status) : jobs;
267
+ }
268
+
269
+ // ── Stats ────────────────────────────────────────────────────────────────
270
+
271
+ /** Get exchange statistics */
272
+ getStats(): ExchangeStats {
273
+ const allJobs = Array.from(this.jobs.values());
274
+ const allProviders = Array.from(this.providers.values());
275
+ return {
276
+ totalProviders: allProviders.length,
277
+ availableProviders: allProviders.filter(p => p.available).length,
278
+ totalJobs: allJobs.length,
279
+ activeJobs: allJobs.filter(j => ['matched', 'locked', 'running'].includes(j.status)).length,
280
+ completedJobs: allJobs.filter(j => j.status === 'completed').length,
281
+ totalVolume: allJobs
282
+ .filter(j => j.status === 'completed')
283
+ .reduce((sum, j) => sum + BigInt(j.totalCost), 0n)
284
+ .toString(),
285
+ };
286
+ }
287
+
288
+ // ── Serialization ────────────────────────────────────────────────────────
289
+
290
+ /** Export all data as JSON */
291
+ toJSON(): string {
292
+ return JSON.stringify({
293
+ providers: Array.from(this.providers.entries()),
294
+ requests: Array.from(this.requests.entries()),
295
+ jobs: Array.from(this.jobs.entries()),
296
+ });
297
+ }
298
+
299
+ /** Import data from JSON */
300
+ fromJSON(json: string): void {
301
+ try {
302
+ const data = JSON.parse(json);
303
+ if (data.providers) this.providers = new Map(data.providers);
304
+ if (data.requests) this.requests = new Map(data.requests);
305
+ if (data.jobs) this.jobs = new Map(data.jobs);
306
+ } catch {}
307
+ }
308
+ }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * technocore-compute — P2P compute exchange for AI agents
3
+ *
4
+ * Agents can offer compute resources (GPU/CPU) and other agents
5
+ * can rent them, with payments handled via tclk HTLC/PTLC.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { ComputeExchange } from 'technocore-compute';
10
+ *
11
+ * const exchange = new ComputeExchange();
12
+ *
13
+ * // Register as a provider
14
+ * exchange.registerProvider({
15
+ * did: 'did:key:z6Mk...',
16
+ * name: 'GPU Farm',
17
+ * hardware: 'gpu',
18
+ * gpuModel: 'A100',
19
+ * vramGB: 80,
20
+ * pricePerHour: '5000000',
21
+ * minDurationSec: 300,
22
+ * maxDurationSec: 86400,
23
+ * available: true,
24
+ * rating: 4.8,
25
+ * jobsCompleted: 42,
26
+ * room: 'compute-gpu-farm',
27
+ * registeredAt: Date.now(),
28
+ * lastHeartbeat: Date.now(),
29
+ * });
30
+ *
31
+ * // Find providers
32
+ * const providers = exchange.findProviders({
33
+ * hardware: 'gpu',
34
+ * minVramGB: 24,
35
+ * maxPricePerHour: '10000000',
36
+ * });
37
+ *
38
+ * // Create a request and match
39
+ * const request = exchange.createRequest({
40
+ * consumerDid: 'did:key:z6Mk...',
41
+ * hardware: 'gpu',
42
+ * minVramGB: 24,
43
+ * durationSec: 3600,
44
+ * maxPricePerHour: '10000000',
45
+ * description: 'Train a small model',
46
+ * });
47
+ *
48
+ * const job = exchange.matchRequest(request.id);
49
+ * ```
50
+ */
51
+
52
+ export { ComputeExchange } from './exchange.js';
53
+ export type { ExchangeConfig } from './exchange.js';
54
+ export { ComputeServer } from './server.js';
55
+ export type { ServerConfig } from './server.js';
56
+ export type {
57
+ ComputeProvider,
58
+ ComputeRequest,
59
+ ComputeJob,
60
+ ComputeType,
61
+ JobStatus,
62
+ ExchangeStats,
63
+ } from './types.js';
package/src/server.ts ADDED
@@ -0,0 +1,234 @@
1
+ /**
2
+ * HTTP API server for the Compute Exchange
3
+ *
4
+ * Provides REST endpoints for providers to list compute,
5
+ * consumers to request compute, and job management.
6
+ */
7
+
8
+ import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
9
+ import { ComputeExchange, type ExchangeConfig } from './exchange.js';
10
+ import type { ComputeProvider, ComputeRequest } from './types.js';
11
+
12
+ export interface ServerConfig extends ExchangeConfig {
13
+ port?: number;
14
+ host?: string;
15
+ }
16
+
17
+ export class ComputeServer {
18
+ private exchange: ComputeExchange;
19
+ private server: ReturnType<typeof createServer> | null = null;
20
+ private config: Required<ServerConfig>;
21
+
22
+ constructor(config: ServerConfig = {}) {
23
+ this.config = {
24
+ port: config.port || 4010,
25
+ host: config.host || '0.0.0.0',
26
+ technocoreBaseUrl: config.technocoreBaseUrl || 'https://technocore.chat',
27
+ roomPrefix: config.roomPrefix || 'compute',
28
+ refreshIntervalMs: config.refreshIntervalMs || 30_000,
29
+ dataDir: config.dataDir || './compute-data',
30
+ };
31
+ this.exchange = new ComputeExchange(this.config);
32
+ }
33
+
34
+ /** Get the exchange instance */
35
+ getExchange(): ComputeExchange {
36
+ return this.exchange;
37
+ }
38
+
39
+ /** Start the server */
40
+ start(): Promise<void> {
41
+ return new Promise((resolve) => {
42
+ this.server = createServer((req, res) => this.handleRequest(req, res));
43
+ this.server.listen(this.config.port, this.config.host, () => {
44
+ console.log(`Compute Exchange API running on http://${this.config.host}:${this.config.port}`);
45
+ resolve();
46
+ });
47
+ });
48
+ }
49
+
50
+ /** Stop the server */
51
+ stop(): Promise<void> {
52
+ return new Promise((resolve) => {
53
+ if (this.server) {
54
+ this.server.close(() => resolve());
55
+ } else {
56
+ resolve();
57
+ }
58
+ });
59
+ }
60
+
61
+ private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {
62
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
63
+ const path = url.pathname;
64
+ const method = req.method || 'GET';
65
+
66
+ // CORS headers
67
+ res.setHeader('Access-Control-Allow-Origin', '*');
68
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
69
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
70
+
71
+ if (method === 'OPTIONS') {
72
+ res.writeHead(204);
73
+ res.end();
74
+ return;
75
+ }
76
+
77
+ try {
78
+ // Routes
79
+ if (path === '/stats' && method === 'GET') {
80
+ return this.json(res, this.exchange.getStats());
81
+ }
82
+
83
+ if (path === '/providers' && method === 'GET') {
84
+ return this.json(res, { providers: this.exchange.listProviders() });
85
+ }
86
+
87
+ if (path === '/providers' && method === 'POST') {
88
+ const body = await this.readBody(req);
89
+ const provider: ComputeProvider = {
90
+ did: body.did,
91
+ name: body.name || 'Unnamed Provider',
92
+ hardware: body.hardware || 'gpu',
93
+ gpuModel: body.gpuModel,
94
+ vramGB: body.vramGB,
95
+ cpuCores: body.cpuCores,
96
+ ramGB: body.ramGB,
97
+ storageGB: body.storageGB,
98
+ pricePerHour: body.pricePerHour || '1000000',
99
+ minDurationSec: body.minDurationSec || 300,
100
+ maxDurationSec: body.maxDurationSec || 86400,
101
+ available: true,
102
+ rating: 0,
103
+ jobsCompleted: 0,
104
+ room: body.room || 'compute-general',
105
+ registeredAt: Date.now(),
106
+ lastHeartbeat: Date.now(),
107
+ };
108
+ this.exchange.registerProvider(provider);
109
+ return this.json(res, { success: true, provider });
110
+ }
111
+
112
+ if (path.startsWith('/providers/') && method === 'PUT') {
113
+ const did = path.split('/')[2];
114
+ if (path.endsWith('/heartbeat')) {
115
+ this.exchange.heartbeat(did);
116
+ return this.json(res, { success: true });
117
+ }
118
+ if (path.endsWith('/available')) {
119
+ const body = await this.readBody(req);
120
+ this.exchange.setAvailable(did, body.available);
121
+ return this.json(res, { success: true });
122
+ }
123
+ }
124
+
125
+ if (path === '/requests' && method === 'GET') {
126
+ return this.json(res, { requests: this.exchange.listPendingRequests() });
127
+ }
128
+
129
+ if (path === '/requests' && method === 'POST') {
130
+ const body = await this.readBody(req);
131
+ const request = this.exchange.createRequest({
132
+ consumerDid: body.consumerDid,
133
+ hardware: body.hardware || 'gpu',
134
+ minGpuModel: body.minGpuModel,
135
+ minVramGB: body.minVramGB,
136
+ minCpuCores: body.minCpuCores,
137
+ minRamGB: body.minRamGB,
138
+ durationSec: body.durationSec || 3600,
139
+ maxPricePerHour: body.maxPricePerHour || '5000000',
140
+ description: body.description || '',
141
+ });
142
+ return this.json(res, { success: true, request });
143
+ }
144
+
145
+ if (path.startsWith('/requests/') && path.endsWith('/match') && method === 'POST') {
146
+ const requestId = path.split('/')[2];
147
+ const job = this.exchange.matchRequest(requestId);
148
+ if (!job) {
149
+ return this.json(res, { error: 'No matching provider found' }, 404);
150
+ }
151
+ return this.json(res, { success: true, job });
152
+ }
153
+
154
+ if (path === '/jobs' && method === 'GET') {
155
+ const status = url.searchParams.get('status') as any;
156
+ return this.json(res, { jobs: this.exchange.listJobs(status || undefined) });
157
+ }
158
+
159
+ if (path.startsWith('/jobs/') && method === 'GET') {
160
+ const jobId = path.split('/')[2];
161
+ const job = this.exchange.getJob(jobId);
162
+ if (!job) return this.json(res, { error: 'Job not found' }, 404);
163
+ return this.json(res, { job });
164
+ }
165
+
166
+ if (path.startsWith('/jobs/') && path.endsWith('/accept') && method === 'POST') {
167
+ const jobId = path.split('/')[2];
168
+ const ok = this.exchange.acceptJob(jobId);
169
+ return this.json(res, { success: ok });
170
+ }
171
+
172
+ if (path.startsWith('/jobs/') && path.endsWith('/start') && method === 'POST') {
173
+ const jobId = path.split('/')[2];
174
+ const ok = this.exchange.startJob(jobId);
175
+ return this.json(res, { success: ok });
176
+ }
177
+
178
+ if (path.startsWith('/jobs/') && path.endsWith('/complete') && method === 'POST') {
179
+ const jobId = path.split('/')[2];
180
+ const ok = this.exchange.completeJob(jobId);
181
+ return this.json(res, { success: ok });
182
+ }
183
+
184
+ if (path.startsWith('/jobs/') && path.endsWith('/fail') && method === 'POST') {
185
+ const jobId = path.split('/')[2];
186
+ const ok = this.exchange.failJob(jobId);
187
+ return this.json(res, { success: ok });
188
+ }
189
+
190
+ if (path.startsWith('/jobs/') && path.endsWith('/refund') && method === 'POST') {
191
+ const jobId = path.split('/')[2];
192
+ const ok = this.exchange.refundJob(jobId);
193
+ return this.json(res, { success: ok });
194
+ }
195
+
196
+ if (path === '/find' && method === 'POST') {
197
+ const body = await this.readBody(req);
198
+ const providers = this.exchange.findProviders({
199
+ hardware: body.hardware,
200
+ minVramGB: body.minVramGB,
201
+ minCpuCores: body.minCpuCores,
202
+ minRamGB: body.minRamGB,
203
+ maxPricePerHour: body.maxPricePerHour,
204
+ });
205
+ return this.json(res, { providers });
206
+ }
207
+
208
+ // 404
209
+ this.json(res, { error: 'Not found', path }, 404);
210
+ } catch (err: any) {
211
+ this.json(res, { error: err.message }, 500);
212
+ }
213
+ }
214
+
215
+ private json(res: ServerResponse, data: any, status = 200): void {
216
+ res.writeHead(status, { 'Content-Type': 'application/json' });
217
+ res.end(JSON.stringify(data, null, 2));
218
+ }
219
+
220
+ private readBody(req: IncomingMessage): Promise<any> {
221
+ return new Promise((resolve, reject) => {
222
+ let body = '';
223
+ req.on('data', chunk => body += chunk);
224
+ req.on('end', () => {
225
+ try {
226
+ resolve(JSON.parse(body));
227
+ } catch {
228
+ resolve({});
229
+ }
230
+ });
231
+ req.on('error', reject);
232
+ });
233
+ }
234
+ }