2020117-agent 0.2.5 → 0.3.1
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 +14 -15
- package/dist/agent.d.ts +3 -3
- package/dist/agent.js +14 -167
- package/dist/api.d.ts +9 -0
- package/dist/api.js +16 -0
- package/dist/clink.d.ts +1 -1
- package/dist/clink.js +1 -1
- package/dist/p2p-customer.d.ts +1 -1
- package/dist/p2p-customer.js +1 -1
- package/package.json +2 -6
- package/dist/cashu.d.ts +0 -41
- package/dist/cashu.js +0 -112
- package/dist/customer.d.ts +0 -8
- package/dist/customer.js +0 -64
- package/dist/p2p-provider.d.ts +0 -62
- package/dist/p2p-provider.js +0 -105
- package/dist/pipeline.d.ts +0 -8
- package/dist/pipeline.js +0 -106
- package/dist/provider.d.ts +0 -6
- package/dist/provider.js +0 -110
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# 2020117-agent
|
|
2
2
|
|
|
3
|
-
Decentralized AI agent runtime for the [2020117](https://2020117.xyz) network. Connects your agent to the DVM compute marketplace via API polling + P2P Hyperswarm, with Lightning
|
|
3
|
+
Decentralized AI agent runtime for the [2020117](https://2020117.xyz) network. Connects your agent to the DVM compute marketplace via API polling + P2P Hyperswarm, with CLINK Lightning payments.
|
|
4
4
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
@@ -14,8 +14,8 @@ npx 2020117-agent --kind=5302 --processor=exec:./translate.sh
|
|
|
14
14
|
# Run as provider (HTTP backend)
|
|
15
15
|
npx 2020117-agent --kind=5200 --processor=http://localhost:7860 --models=sdxl-lightning,sd3.5-turbo
|
|
16
16
|
|
|
17
|
-
# P2P
|
|
18
|
-
npx 2020117-
|
|
17
|
+
# P2P session — rent an agent by the minute
|
|
18
|
+
npx 2020117-session --kind=5200 --budget=500 --port=8080
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
## Setup
|
|
@@ -47,10 +47,8 @@ npx 2020117-agent --agent=my-agent --kind=5100
|
|
|
47
47
|
|
|
48
48
|
| Command | Description |
|
|
49
49
|
|---------|-------------|
|
|
50
|
-
| `2020117-agent` | Unified agent (API polling + P2P listening) |
|
|
51
|
-
| `2020117-
|
|
52
|
-
| `2020117-provider` | P2P-only provider |
|
|
53
|
-
| `2020117-pipeline` | Multi-step pipeline agent |
|
|
50
|
+
| `2020117-agent` | Unified agent (API polling + P2P session listening) |
|
|
51
|
+
| `2020117-session` | P2P session client (CLI REPL + HTTP proxy) |
|
|
54
52
|
|
|
55
53
|
## CLI Parameters
|
|
56
54
|
|
|
@@ -64,10 +62,11 @@ npx 2020117-agent --agent=my-agent --kind=5100
|
|
|
64
62
|
| `--max-jobs` | `MAX_JOBS` | Max concurrent jobs (default: 3) |
|
|
65
63
|
| `--api-key` | `API_2020117_KEY` | API key (overrides `.2020117_keys`) |
|
|
66
64
|
| `--api-url` | `API_2020117_URL` | API base URL |
|
|
67
|
-
| `--sub-kind` | `SUB_KIND` | Sub-task kind (enables pipeline) |
|
|
68
|
-
| `--sub-channel` | `SUB_CHANNEL` | Sub-task channel: `p2p` or `api` |
|
|
69
|
-
| `--budget` | `SUB_BUDGET` | P2P sub-task budget in sats |
|
|
65
|
+
| `--sub-kind` | `SUB_KIND` | Sub-task kind (enables pipeline via API) |
|
|
70
66
|
| `--skill` | `SKILL_FILE` | Path to skill JSON file describing agent capabilities |
|
|
67
|
+
| `--port` | `SESSION_PORT` | Session HTTP proxy port (default: 8080) |
|
|
68
|
+
| `--provider` | `PROVIDER_PUBKEY` | Target provider public key |
|
|
69
|
+
| `--lightning-address` | `LIGHTNING_ADDRESS` | Provider's Lightning Address (auto-fetched from platform if not set) |
|
|
71
70
|
|
|
72
71
|
Environment variables also work: `AGENT=my-agent DVM_KIND=5100 2020117-agent`
|
|
73
72
|
|
|
@@ -85,7 +84,7 @@ Environment variables also work: `AGENT=my-agent DVM_KIND=5100 2020117-agent`
|
|
|
85
84
|
```js
|
|
86
85
|
import { createProcessor } from '2020117-agent/processor'
|
|
87
86
|
import { SwarmNode } from '2020117-agent/swarm'
|
|
88
|
-
import {
|
|
87
|
+
import { collectPayment } from '2020117-agent/clink'
|
|
89
88
|
import { hasApiKey, registerService } from '2020117-agent/api'
|
|
90
89
|
```
|
|
91
90
|
|
|
@@ -99,14 +98,14 @@ import { hasApiKey, registerService } from '2020117-agent/api'
|
|
|
99
98
|
(heartbeat, │ (inbox → accept → │
|
|
100
99
|
inbox, result) │ process → result) │
|
|
101
100
|
│ │
|
|
102
|
-
Hyperswarm DHT ◄──┤ P2P
|
|
103
|
-
(encrypted TCP) │ (
|
|
104
|
-
│ result)
|
|
101
|
+
Hyperswarm DHT ◄──┤ P2P Sessions │──► CLINK Payments
|
|
102
|
+
(encrypted TCP) │ (session → HTTP │ (ndebit via Lightning)
|
|
103
|
+
│ tunnel → result) │
|
|
105
104
|
└─────────────────────┘
|
|
106
105
|
```
|
|
107
106
|
|
|
108
107
|
- **API channel**: Polls platform inbox, accepts jobs, submits results. Lightning payments on completion.
|
|
109
|
-
- **P2P channel**: Listens on Hyperswarm DHT topic `SHA256("2020117-dvm-kind-{kind}")`.
|
|
108
|
+
- **P2P channel**: Listens on Hyperswarm DHT topic `SHA256("2020117-dvm-kind-{kind}")`. Interactive sessions with CLINK per-minute billing.
|
|
110
109
|
- Both channels share a single capacity counter — the agent never overloads.
|
|
111
110
|
|
|
112
111
|
## Development
|
package/dist/agent.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Unified Agent Runtime — runs as a long-lived daemon that handles
|
|
4
|
-
* 1. Async platform tasks (inbox polling → accept →
|
|
5
|
-
* 2.
|
|
3
|
+
* Unified Agent Runtime — runs as a long-lived daemon that handles:
|
|
4
|
+
* 1. Async platform tasks (inbox polling → accept → process → submit result)
|
|
5
|
+
* 2. P2P sessions (Hyperswarm + CLINK per-minute billing)
|
|
6
6
|
*
|
|
7
7
|
* Both channels share a single capacity counter so the agent never overloads.
|
|
8
8
|
*
|
package/dist/agent.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Unified Agent Runtime — runs as a long-lived daemon that handles
|
|
4
|
-
* 1. Async platform tasks (inbox polling → accept →
|
|
5
|
-
* 2.
|
|
3
|
+
* Unified Agent Runtime — runs as a long-lived daemon that handles:
|
|
4
|
+
* 1. Async platform tasks (inbox polling → accept → process → submit result)
|
|
5
|
+
* 2. P2P sessions (Hyperswarm + CLINK per-minute billing)
|
|
6
6
|
*
|
|
7
7
|
* Both channels share a single capacity counter so the agent never overloads.
|
|
8
8
|
*
|
|
@@ -42,18 +42,12 @@ for (const arg of process.argv.slice(2)) {
|
|
|
42
42
|
case '--sub-kind':
|
|
43
43
|
process.env.SUB_KIND = val;
|
|
44
44
|
break;
|
|
45
|
-
case '--sub-channel':
|
|
46
|
-
process.env.SUB_CHANNEL = val;
|
|
47
|
-
break;
|
|
48
45
|
case '--sub-provider':
|
|
49
46
|
process.env.SUB_PROVIDER = val;
|
|
50
47
|
break;
|
|
51
48
|
case '--sub-bid':
|
|
52
49
|
process.env.SUB_BID = val;
|
|
53
50
|
break;
|
|
54
|
-
case '--budget':
|
|
55
|
-
process.env.SUB_BUDGET = val;
|
|
56
|
-
break;
|
|
57
51
|
case '--api-key':
|
|
58
52
|
process.env.API_2020117_KEY = val;
|
|
59
53
|
break;
|
|
@@ -73,10 +67,8 @@ for (const arg of process.argv.slice(2)) {
|
|
|
73
67
|
}
|
|
74
68
|
import { randomBytes } from 'crypto';
|
|
75
69
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
76
|
-
import { collectP2PPayment, handleStop, streamToCustomer } from './p2p-provider.js';
|
|
77
|
-
import { streamFromProvider } from './p2p-customer.js';
|
|
78
70
|
import { createProcessor } from './processor.js';
|
|
79
|
-
import { hasApiKey, loadAgentName, registerService, startHeartbeatLoop, getInbox, acceptJob, sendFeedback, submitResult, createJob, getJob, getProfile, } from './api.js';
|
|
71
|
+
import { hasApiKey, loadAgentName, registerService, startHeartbeatLoop, getInbox, acceptJob, sendFeedback, submitResult, createJob, getJob, getProfile, reportSession, } from './api.js';
|
|
80
72
|
import { initClinkAgent, collectPayment } from './clink.js';
|
|
81
73
|
import { readFileSync } from 'fs';
|
|
82
74
|
import WebSocket from 'ws';
|
|
@@ -86,18 +78,13 @@ const MAX_CONCURRENT = Number(process.env.MAX_JOBS) || 3;
|
|
|
86
78
|
const POLL_INTERVAL = Number(process.env.POLL_INTERVAL) || 30_000;
|
|
87
79
|
const SATS_PER_CHUNK = Number(process.env.SATS_PER_CHUNK) || 1;
|
|
88
80
|
const CHUNKS_PER_PAYMENT = Number(process.env.CHUNKS_PER_PAYMENT) || 10;
|
|
89
|
-
const PAYMENT_TIMEOUT = Number(process.env.PAYMENT_TIMEOUT) || 30_000;
|
|
90
81
|
// --- CLINK payment config ---
|
|
91
82
|
let LIGHTNING_ADDRESS = process.env.LIGHTNING_ADDRESS || '';
|
|
92
83
|
// --- Sub-task delegation config ---
|
|
93
84
|
const SUB_KIND = process.env.SUB_KIND ? Number(process.env.SUB_KIND) : null;
|
|
94
|
-
const SUB_BUDGET = Number(process.env.SUB_BUDGET) || 50;
|
|
95
|
-
const SUB_CHANNEL = process.env.SUB_CHANNEL || 'p2p';
|
|
96
85
|
const SUB_PROVIDER = process.env.SUB_PROVIDER || undefined;
|
|
97
86
|
const SUB_BID = Number(process.env.SUB_BID) || 100;
|
|
98
|
-
const MAX_SATS_PER_CHUNK = Number(process.env.MAX_SATS_PER_CHUNK) || 5;
|
|
99
87
|
const MIN_BID_SATS = Number(process.env.MIN_BID_SATS) || SATS_PER_CHUNK * CHUNKS_PER_PAYMENT; // default = pricing per job
|
|
100
|
-
const SUB_BATCH_SIZE = Number(process.env.SUB_BATCH_SIZE) || 500; // chars to accumulate before local processing
|
|
101
88
|
// --- Skill file loading ---
|
|
102
89
|
function loadSkill() {
|
|
103
90
|
const skillPath = process.env.SKILL_FILE;
|
|
@@ -153,7 +140,7 @@ async function main() {
|
|
|
153
140
|
state.processor = await createProcessor();
|
|
154
141
|
console.log(`[${label}] kind=${KIND} processor=${state.processor.name} maxJobs=${MAX_CONCURRENT}`);
|
|
155
142
|
if (SUB_KIND) {
|
|
156
|
-
console.log(`[${label}] Pipeline: sub-task kind=${SUB_KIND}
|
|
143
|
+
console.log(`[${label}] Pipeline: sub-task kind=${SUB_KIND} (bid=${SUB_BID}${SUB_PROVIDER ? `, provider=${SUB_PROVIDER}` : ''})`);
|
|
157
144
|
}
|
|
158
145
|
else if (state.processor.name === 'none') {
|
|
159
146
|
console.warn(`[${label}] WARNING: processor=none without SUB_KIND — generate() will pass through input as-is`);
|
|
@@ -266,23 +253,13 @@ async function processAsyncJob(label, inboxJobId, input, params) {
|
|
|
266
253
|
console.log(`[${label}] Job ${providerJobId}: processing "${input.slice(0, 60)}..."`);
|
|
267
254
|
await sendFeedback(providerJobId, 'processing');
|
|
268
255
|
let result;
|
|
269
|
-
// Pipeline: delegate sub-task then process locally
|
|
256
|
+
// Pipeline: delegate sub-task via API then process locally
|
|
270
257
|
if (SUB_KIND) {
|
|
271
|
-
console.log(`[${label}] Job ${providerJobId}: delegating to kind ${SUB_KIND}
|
|
258
|
+
console.log(`[${label}] Job ${providerJobId}: delegating to kind ${SUB_KIND}...`);
|
|
272
259
|
try {
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
console.log(`[${label}] Job ${providerJobId}: sub-task returned ${subResult.length} chars`);
|
|
277
|
-
result = await state.processor.generate({ input: subResult, params });
|
|
278
|
-
}
|
|
279
|
-
else {
|
|
280
|
-
// P2P delegation — stream-collect from sub-provider, batch-translate
|
|
281
|
-
result = '';
|
|
282
|
-
for await (const chunk of pipelineStream(SUB_KIND, input, SUB_BUDGET)) {
|
|
283
|
-
result += chunk;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
260
|
+
const subResult = await delegateAPI(SUB_KIND, input, SUB_BID, SUB_PROVIDER);
|
|
261
|
+
console.log(`[${label}] Job ${providerJobId}: sub-task returned ${subResult.length} chars`);
|
|
262
|
+
result = await state.processor.generate({ input: subResult, params });
|
|
286
263
|
}
|
|
287
264
|
catch (e) {
|
|
288
265
|
console.error(`[${label}] Job ${providerJobId}: sub-task failed: ${e.message}, using original input`);
|
|
@@ -307,50 +284,6 @@ async function processAsyncJob(label, inboxJobId, input, params) {
|
|
|
307
284
|
}
|
|
308
285
|
}
|
|
309
286
|
// --- Sub-task delegation ---
|
|
310
|
-
/**
|
|
311
|
-
* Delegate a sub-task via Hyperswarm P2P with CLINK debit payments.
|
|
312
|
-
* Thin wrapper around the shared streamFromProvider() module.
|
|
313
|
-
*/
|
|
314
|
-
async function* delegateP2PStream(kind, input, budgetSats) {
|
|
315
|
-
const ndebit = process.env.CLINK_NDEBIT || '';
|
|
316
|
-
if (!ndebit)
|
|
317
|
-
throw new Error('Pipeline sub-delegation requires CLINK_NDEBIT env var (--ndebit)');
|
|
318
|
-
yield* streamFromProvider({
|
|
319
|
-
kind,
|
|
320
|
-
input,
|
|
321
|
-
budgetSats,
|
|
322
|
-
ndebit,
|
|
323
|
-
maxSatsPerChunk: MAX_SATS_PER_CHUNK,
|
|
324
|
-
label: 'sub-p2p',
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
/**
|
|
328
|
-
* Streaming pipeline: delegates to a sub-provider via P2P, accumulates
|
|
329
|
-
* chunks into batches, translates each batch locally via streaming Ollama,
|
|
330
|
-
* and yields the translated tokens.
|
|
331
|
-
*
|
|
332
|
-
* Flow: sub-provider streams → batch → Ollama stream-translate → yield tokens
|
|
333
|
-
*/
|
|
334
|
-
async function* pipelineStream(kind, input, budgetSats) {
|
|
335
|
-
let batch = '';
|
|
336
|
-
async function* translateBatch(text) {
|
|
337
|
-
for await (const token of state.processor.generateStream({ input: text })) {
|
|
338
|
-
yield token;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
for await (const chunk of delegateP2PStream(kind, input, budgetSats)) {
|
|
342
|
-
batch += chunk;
|
|
343
|
-
// When batch is big enough, translate and stream out
|
|
344
|
-
if (batch.length >= SUB_BATCH_SIZE) {
|
|
345
|
-
yield* translateBatch(batch);
|
|
346
|
-
batch = '';
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
// Translate remaining text
|
|
350
|
-
if (batch.length > 0) {
|
|
351
|
-
yield* translateBatch(batch);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
287
|
/**
|
|
355
288
|
* Delegate a sub-task via platform API. Creates a job, then polls until
|
|
356
289
|
* the result is available (max 120s).
|
|
@@ -382,17 +315,13 @@ async function delegateAPI(kind, input, bidSats, provider) {
|
|
|
382
315
|
}
|
|
383
316
|
throw new Error(`Sub-task ${jobId} timed out after 120s`);
|
|
384
317
|
}
|
|
385
|
-
// --- 4. P2P Swarm Listener ---
|
|
386
|
-
const p2pJobs = new Map();
|
|
387
318
|
const activeSessions = new Map();
|
|
388
319
|
// Backend WebSocket connections for WS tunnel (keyed by ws_id)
|
|
389
320
|
const backendWebSockets = new Map();
|
|
390
321
|
async function startSwarmListener(label) {
|
|
391
322
|
const node = new SwarmNode();
|
|
392
323
|
state.swarmNode = node;
|
|
393
|
-
const satsPerPayment = SATS_PER_CHUNK * CHUNKS_PER_PAYMENT;
|
|
394
324
|
const topic = topicFromKind(KIND);
|
|
395
|
-
console.log(`[${label}] P2P: ${SATS_PER_CHUNK} sat/chunk, ${CHUNKS_PER_PAYMENT} chunks/payment (${satsPerPayment} sats/cycle)`);
|
|
396
325
|
console.log(`[${label}] Joining swarm topic for kind ${KIND}`);
|
|
397
326
|
await node.listen(topic);
|
|
398
327
|
console.log(`[${label}] P2P listening for customers...`);
|
|
@@ -672,65 +601,6 @@ async function startSwarmListener(label) {
|
|
|
672
601
|
}
|
|
673
602
|
return;
|
|
674
603
|
}
|
|
675
|
-
if (msg.type === 'request') {
|
|
676
|
-
console.log(`[${label}] P2P job ${msg.id} from ${tag}: "${(msg.input || '').slice(0, 60)}..."`);
|
|
677
|
-
if (!LIGHTNING_ADDRESS) {
|
|
678
|
-
node.send(socket, { type: 'error', id: msg.id, message: 'Provider has no Lightning Address configured' });
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
if (!msg.ndebit) {
|
|
682
|
-
node.send(socket, { type: 'error', id: msg.id, message: 'Request requires ndebit authorization' });
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
if (!acquireSlot()) {
|
|
686
|
-
node.send(socket, {
|
|
687
|
-
type: 'error',
|
|
688
|
-
id: msg.id,
|
|
689
|
-
message: `No capacity (${state.activeJobs}/${MAX_CONCURRENT} slots used)`,
|
|
690
|
-
});
|
|
691
|
-
console.log(`[${label}] P2P job ${msg.id}: rejected (no capacity)`);
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
if (msg.budget !== undefined) {
|
|
695
|
-
console.log(`[${label}] Customer budget: ${msg.budget} sats`);
|
|
696
|
-
}
|
|
697
|
-
const job = {
|
|
698
|
-
socket,
|
|
699
|
-
credit: CHUNKS_PER_PAYMENT, // start with free credit; debit after delivery
|
|
700
|
-
ndebit: msg.ndebit,
|
|
701
|
-
totalEarned: 0,
|
|
702
|
-
stopped: false,
|
|
703
|
-
};
|
|
704
|
-
p2pJobs.set(msg.id, job);
|
|
705
|
-
// Send offer (price quote — no payment yet)
|
|
706
|
-
node.send(socket, {
|
|
707
|
-
type: 'offer',
|
|
708
|
-
id: msg.id,
|
|
709
|
-
sats_per_chunk: SATS_PER_CHUNK,
|
|
710
|
-
chunks_per_payment: CHUNKS_PER_PAYMENT,
|
|
711
|
-
});
|
|
712
|
-
// Generate first, pay after delivery
|
|
713
|
-
node.send(socket, { type: 'accepted', id: msg.id });
|
|
714
|
-
await runP2PGeneration(node, job, msg, label);
|
|
715
|
-
// Debit after result delivered
|
|
716
|
-
if (!job.stopped) {
|
|
717
|
-
const paid = await collectP2PPayment({
|
|
718
|
-
job, node, jobId: msg.id,
|
|
719
|
-
satsPerChunk: SATS_PER_CHUNK,
|
|
720
|
-
chunksPerPayment: CHUNKS_PER_PAYMENT,
|
|
721
|
-
lightningAddress: LIGHTNING_ADDRESS,
|
|
722
|
-
label,
|
|
723
|
-
});
|
|
724
|
-
if (!paid) {
|
|
725
|
-
console.log(`[${label}] P2P job ${msg.id}: post-delivery debit failed`);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
if (msg.type === 'stop') {
|
|
730
|
-
const job = p2pJobs.get(msg.id);
|
|
731
|
-
if (job)
|
|
732
|
-
handleStop(job, msg.id, label);
|
|
733
|
-
}
|
|
734
604
|
});
|
|
735
605
|
// Handle customer disconnect — payments already settled via CLINK
|
|
736
606
|
node.on('peer-leave', (peerId) => {
|
|
@@ -742,15 +612,6 @@ async function startSwarmListener(label) {
|
|
|
742
612
|
endSession(node, session, label);
|
|
743
613
|
}
|
|
744
614
|
}
|
|
745
|
-
// Clean up any P2P streaming jobs for this peer
|
|
746
|
-
for (const [jobId, job] of p2pJobs) {
|
|
747
|
-
if (job.socket?.remotePublicKey?.toString('hex') === peerId) {
|
|
748
|
-
console.log(`[${label}] Peer ${tag} disconnected — P2P job ${jobId} (${job.totalEarned} sats earned)`);
|
|
749
|
-
job.stopped = true;
|
|
750
|
-
p2pJobs.delete(jobId);
|
|
751
|
-
releaseSlot();
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
615
|
// Clean up backend WebSockets for this peer
|
|
755
616
|
for (const [wsId, entry] of backendWebSockets) {
|
|
756
617
|
if (entry.peerId === peerId) {
|
|
@@ -763,24 +624,6 @@ async function startSwarmListener(label) {
|
|
|
763
624
|
}
|
|
764
625
|
});
|
|
765
626
|
}
|
|
766
|
-
async function runP2PGeneration(node, job, msg, label) {
|
|
767
|
-
const source = SUB_KIND
|
|
768
|
-
? pipelineStream(SUB_KIND, msg.input || '', SUB_BUDGET)
|
|
769
|
-
: state.processor.generateStream({ input: msg.input || '', params: msg.params });
|
|
770
|
-
await streamToCustomer({
|
|
771
|
-
node,
|
|
772
|
-
job,
|
|
773
|
-
jobId: msg.id,
|
|
774
|
-
source,
|
|
775
|
-
satsPerChunk: SATS_PER_CHUNK,
|
|
776
|
-
chunksPerPayment: CHUNKS_PER_PAYMENT,
|
|
777
|
-
lightningAddress: LIGHTNING_ADDRESS,
|
|
778
|
-
label,
|
|
779
|
-
});
|
|
780
|
-
// No batch claim needed — CLINK payments settle instantly via Lightning
|
|
781
|
-
p2pJobs.delete(msg.id);
|
|
782
|
-
releaseSlot();
|
|
783
|
-
}
|
|
784
627
|
// --- Session helpers ---
|
|
785
628
|
function findSessionBySocket(socket) {
|
|
786
629
|
for (const session of activeSessions.values()) {
|
|
@@ -826,6 +669,10 @@ function endSession(node, session, label) {
|
|
|
826
669
|
// Update P2P lifetime counters — no batch claim needed with CLINK (payments settled instantly)
|
|
827
670
|
state.p2pSessionsCompleted++;
|
|
828
671
|
state.p2pTotalEarnedSats += session.totalEarned;
|
|
672
|
+
// Report session to platform activity feed (best-effort, no content exposed)
|
|
673
|
+
if (hasApiKey()) {
|
|
674
|
+
reportSession({ kind: KIND, durationS, totalSats: session.totalEarned });
|
|
675
|
+
}
|
|
829
676
|
activeSessions.delete(session.sessionId);
|
|
830
677
|
}
|
|
831
678
|
// --- 5. Graceful shutdown ---
|
package/dist/api.d.ts
CHANGED
|
@@ -100,3 +100,12 @@ export declare function proxyDebit(opts: {
|
|
|
100
100
|
lightningAddress: string;
|
|
101
101
|
amountSats: number;
|
|
102
102
|
}): Promise<ProxyDebitResult | null>;
|
|
103
|
+
/**
|
|
104
|
+
* Report a completed P2P session to the platform for the activity feed.
|
|
105
|
+
* Content stays private — only metadata (kind, duration, sats) is sent.
|
|
106
|
+
*/
|
|
107
|
+
export declare function reportSession(opts: {
|
|
108
|
+
kind: number;
|
|
109
|
+
durationS: number;
|
|
110
|
+
totalSats: number;
|
|
111
|
+
}): Promise<void>;
|
package/dist/api.js
CHANGED
|
@@ -282,3 +282,19 @@ export async function proxyDebit(opts) {
|
|
|
282
282
|
amount_sats: opts.amountSats,
|
|
283
283
|
});
|
|
284
284
|
}
|
|
285
|
+
/**
|
|
286
|
+
* Report a completed P2P session to the platform for the activity feed.
|
|
287
|
+
* Content stays private — only metadata (kind, duration, sats) is sent.
|
|
288
|
+
*/
|
|
289
|
+
export async function reportSession(opts) {
|
|
290
|
+
try {
|
|
291
|
+
await apiPost('/api/dvm/session-report', {
|
|
292
|
+
kind: opts.kind,
|
|
293
|
+
duration_s: opts.durationS,
|
|
294
|
+
total_sats: opts.totalSats,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Best-effort — don't break session cleanup
|
|
299
|
+
}
|
|
300
|
+
}
|
package/dist/clink.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLINK payment utilities —
|
|
2
|
+
* CLINK payment utilities — debit-based Lightning payments for P2P sessions
|
|
3
3
|
*
|
|
4
4
|
* Provider uses ndebit to pull payments from customer's wallet.
|
|
5
5
|
* Invoice generation via LNURL-pay from provider's own Lightning Address.
|
package/dist/clink.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLINK payment utilities —
|
|
2
|
+
* CLINK payment utilities — debit-based Lightning payments for P2P sessions
|
|
3
3
|
*
|
|
4
4
|
* Provider uses ndebit to pull payments from customer's wallet.
|
|
5
5
|
* Invoice generation via LNURL-pay from provider's own Lightning Address.
|
package/dist/p2p-customer.d.ts
CHANGED
package/dist/p2p-customer.js
CHANGED
|
@@ -35,7 +35,7 @@ import { randomBytes } from 'crypto';
|
|
|
35
35
|
export async function* streamFromProvider(opts) {
|
|
36
36
|
const { kind, input, budgetSats, ndebit, maxSatsPerChunk = 5, timeoutMs = 120_000, label = 'p2p', params, } = opts;
|
|
37
37
|
if (!ndebit) {
|
|
38
|
-
throw new Error('ndebit authorization required for P2P
|
|
38
|
+
throw new Error('ndebit authorization required for P2P connection');
|
|
39
39
|
}
|
|
40
40
|
const jobId = randomBytes(8).toString('hex');
|
|
41
41
|
const tag = `${label}-${jobId.slice(0, 8)}`;
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "2020117-agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "2020117 agent runtime — API polling + Hyperswarm P2P + CLINK Lightning payments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"2020117-agent": "./dist/agent.js",
|
|
8
|
-
"2020117-pipeline": "./dist/pipeline.js",
|
|
9
8
|
"2020117-session": "./dist/session.js"
|
|
10
9
|
},
|
|
11
10
|
"files": [
|
|
@@ -15,18 +14,15 @@
|
|
|
15
14
|
"./processor": "./dist/processor.js",
|
|
16
15
|
"./swarm": "./dist/swarm.js",
|
|
17
16
|
"./clink": "./dist/clink.js",
|
|
18
|
-
"./api": "./dist/api.js"
|
|
19
|
-
"./p2p-provider": "./dist/p2p-provider.js"
|
|
17
|
+
"./api": "./dist/api.js"
|
|
20
18
|
},
|
|
21
19
|
"scripts": {
|
|
22
20
|
"agent": "node dist/agent.js",
|
|
23
|
-
"pipeline": "node dist/pipeline.js",
|
|
24
21
|
"session": "node dist/session.js",
|
|
25
22
|
"build": "tsc",
|
|
26
23
|
"prepublishOnly": "tsc",
|
|
27
24
|
"typecheck": "tsc --noEmit",
|
|
28
25
|
"dev:agent": "npx tsx src/agent.ts",
|
|
29
|
-
"dev:pipeline": "npx tsx src/pipeline.ts",
|
|
30
26
|
"dev:session": "npx tsx src/session.ts"
|
|
31
27
|
},
|
|
32
28
|
"dependencies": {
|
package/dist/cashu.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cashu eCash helper — mint, send, receive, melt tokens
|
|
3
|
-
*
|
|
4
|
-
* Uses testnut.cashu.space (fake sats) for PoC.
|
|
5
|
-
* Switch to mint.coinos.io for production.
|
|
6
|
-
*/
|
|
7
|
-
import { Mint, Wallet } from '@cashu/cashu-ts';
|
|
8
|
-
export declare function createWallet(mintUrl?: string): {
|
|
9
|
-
mint: Mint;
|
|
10
|
-
wallet: Wallet;
|
|
11
|
-
};
|
|
12
|
-
/**
|
|
13
|
-
* Mint new tokens (testnut mints fake tokens without real Lightning payment)
|
|
14
|
-
*/
|
|
15
|
-
export declare function mintTokens(amount: number, mintUrl?: string): Promise<{
|
|
16
|
-
token: string;
|
|
17
|
-
proofs: import("@cashu/cashu-ts").Proof[];
|
|
18
|
-
}>;
|
|
19
|
-
/**
|
|
20
|
-
* Verify and receive a Cashu token — swaps proofs with the mint to prevent double-spend
|
|
21
|
-
*/
|
|
22
|
-
export declare function receiveToken(tokenStr: string): Promise<{
|
|
23
|
-
proofs: import("@cashu/cashu-ts").Proof[];
|
|
24
|
-
amount: number;
|
|
25
|
-
mintUrl: string;
|
|
26
|
-
}>;
|
|
27
|
-
/**
|
|
28
|
-
* Get the total amount of a token without claiming it
|
|
29
|
-
*/
|
|
30
|
-
export declare function peekToken(tokenStr: string): {
|
|
31
|
-
amount: number;
|
|
32
|
-
mint: string;
|
|
33
|
-
proofCount: number;
|
|
34
|
-
};
|
|
35
|
-
/**
|
|
36
|
-
* Split a large token into multiple micro-tokens of a given amount.
|
|
37
|
-
* Used for streaming payments: customer pre-splits budget into per-payment chunks.
|
|
38
|
-
*
|
|
39
|
-
* Example: splitTokens(token_100sats, 10) → 10 tokens of 10 sats each
|
|
40
|
-
*/
|
|
41
|
-
export declare function splitTokens(tokenStr: string, perAmount: number): Promise<string[]>;
|
package/dist/cashu.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cashu eCash helper — mint, send, receive, melt tokens
|
|
3
|
-
*
|
|
4
|
-
* Uses testnut.cashu.space (fake sats) for PoC.
|
|
5
|
-
* Switch to mint.coinos.io for production.
|
|
6
|
-
*/
|
|
7
|
-
import { Mint, Wallet, getEncodedToken, getDecodedToken, MintQuoteState } from '@cashu/cashu-ts';
|
|
8
|
-
const DEFAULT_MINT_URL = process.env.CASHU_MINT_URL || 'https://nofee.testnut.cashu.space';
|
|
9
|
-
export function createWallet(mintUrl = DEFAULT_MINT_URL) {
|
|
10
|
-
const mint = new Mint(mintUrl);
|
|
11
|
-
const wallet = new Wallet(mint);
|
|
12
|
-
return { mint, wallet };
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Mint new tokens (testnut mints fake tokens without real Lightning payment)
|
|
16
|
-
*/
|
|
17
|
-
export async function mintTokens(amount, mintUrl = DEFAULT_MINT_URL) {
|
|
18
|
-
const { wallet } = createWallet(mintUrl);
|
|
19
|
-
await wallet.loadMint();
|
|
20
|
-
// Request a mint quote
|
|
21
|
-
const quote = await wallet.createMintQuote(amount);
|
|
22
|
-
console.log(`[cashu] Mint quote created: ${quote.quote}`);
|
|
23
|
-
// For testnut, quotes are auto-paid. For real mints, you'd pay the Lightning invoice.
|
|
24
|
-
// Poll until quote is paid
|
|
25
|
-
let state = quote.state;
|
|
26
|
-
for (let i = 0; i < 30 && state !== MintQuoteState.PAID; i++) {
|
|
27
|
-
await sleep(1000);
|
|
28
|
-
const check = await wallet.checkMintQuote(quote.quote);
|
|
29
|
-
state = check.state;
|
|
30
|
-
}
|
|
31
|
-
if (state !== MintQuoteState.PAID) {
|
|
32
|
-
throw new Error(`Mint quote not paid after 30s (state: ${state}). For real mints, pay invoice: ${quote.request}`);
|
|
33
|
-
}
|
|
34
|
-
// Mint the tokens (v3.5 returns Proof[] directly)
|
|
35
|
-
const proofs = await wallet.mintProofs(amount, quote.quote);
|
|
36
|
-
console.log(`[cashu] Minted ${amount} sats (${proofs.length} proofs)`);
|
|
37
|
-
// Encode as a portable token string
|
|
38
|
-
const token = getEncodedToken({ mint: mintUrl, proofs });
|
|
39
|
-
return { token, proofs };
|
|
40
|
-
}
|
|
41
|
-
/**
|
|
42
|
-
* Verify and receive a Cashu token — swaps proofs with the mint to prevent double-spend
|
|
43
|
-
*/
|
|
44
|
-
export async function receiveToken(tokenStr) {
|
|
45
|
-
const decoded = getDecodedToken(tokenStr);
|
|
46
|
-
const mintUrl = decoded.mint;
|
|
47
|
-
if (!mintUrl)
|
|
48
|
-
throw new Error('Token has no mint URL');
|
|
49
|
-
const { wallet } = createWallet(mintUrl);
|
|
50
|
-
await wallet.loadMint();
|
|
51
|
-
// Swap proofs with the mint (this claims them, preventing re-use)
|
|
52
|
-
const proofs = await wallet.receive(tokenStr);
|
|
53
|
-
const total = proofs.reduce((sum, p) => sum + p.amount, 0);
|
|
54
|
-
console.log(`[cashu] Received ${total} sats from token (${proofs.length} proofs)`);
|
|
55
|
-
return { proofs, amount: total, mintUrl };
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Get the total amount of a token without claiming it
|
|
59
|
-
*/
|
|
60
|
-
export function peekToken(tokenStr) {
|
|
61
|
-
const decoded = getDecodedToken(tokenStr);
|
|
62
|
-
const total = decoded.proofs.reduce((sum, p) => sum + p.amount, 0);
|
|
63
|
-
return { amount: total, mint: decoded.mint, proofCount: decoded.proofs.length };
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Split a large token into multiple micro-tokens of a given amount.
|
|
67
|
-
* Used for streaming payments: customer pre-splits budget into per-payment chunks.
|
|
68
|
-
*
|
|
69
|
-
* Example: splitTokens(token_100sats, 10) → 10 tokens of 10 sats each
|
|
70
|
-
*/
|
|
71
|
-
export async function splitTokens(tokenStr, perAmount) {
|
|
72
|
-
const decoded = getDecodedToken(tokenStr);
|
|
73
|
-
const mintUrl = decoded.mint;
|
|
74
|
-
if (!mintUrl)
|
|
75
|
-
throw new Error('Token has no mint URL');
|
|
76
|
-
const { wallet } = createWallet(mintUrl);
|
|
77
|
-
await wallet.loadMint();
|
|
78
|
-
const microTokens = [];
|
|
79
|
-
let remaining = decoded.proofs;
|
|
80
|
-
while (true) {
|
|
81
|
-
const total = remaining.reduce((sum, p) => sum + p.amount, 0);
|
|
82
|
-
if (total < perAmount)
|
|
83
|
-
break;
|
|
84
|
-
let retries = 5;
|
|
85
|
-
while (retries > 0) {
|
|
86
|
-
try {
|
|
87
|
-
const { send, keep: kept } = await wallet.send(perAmount, remaining);
|
|
88
|
-
microTokens.push(getEncodedToken({ mint: mintUrl, proofs: send }));
|
|
89
|
-
remaining = kept;
|
|
90
|
-
break;
|
|
91
|
-
}
|
|
92
|
-
catch (e) {
|
|
93
|
-
if (retries > 1 && /rate.limit/i.test(e.message || '')) {
|
|
94
|
-
const delay = (6 - retries) * 2000; // 2s, 4s, 6s, 8s
|
|
95
|
-
console.log(`[cashu] Rate limited, waiting ${delay / 1000}s... (${retries - 1} retries left)`);
|
|
96
|
-
await sleep(delay);
|
|
97
|
-
retries--;
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
throw e;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
if (remaining.length === 0)
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
console.log(`[cashu] Split into ${microTokens.length} micro-tokens of ${perAmount} sats each`);
|
|
108
|
-
return microTokens;
|
|
109
|
-
}
|
|
110
|
-
function sleep(ms) {
|
|
111
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
112
|
-
}
|
package/dist/customer.d.ts
DELETED
package/dist/customer.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Standalone P2P Customer — connects to a provider, streams results with CLINK debit payments.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* 2020117-customer --kind=5100 --budget=50 --ndebit=ndebit1... "Explain quantum computing"
|
|
7
|
-
*/
|
|
8
|
-
// --- CLI args → env (before any imports) ---
|
|
9
|
-
for (const arg of process.argv.slice(2)) {
|
|
10
|
-
if (!arg.startsWith('--'))
|
|
11
|
-
continue;
|
|
12
|
-
const eq = arg.indexOf('=');
|
|
13
|
-
if (eq === -1)
|
|
14
|
-
continue;
|
|
15
|
-
const key = arg.slice(0, eq);
|
|
16
|
-
const val = arg.slice(eq + 1);
|
|
17
|
-
switch (key) {
|
|
18
|
-
case '--kind':
|
|
19
|
-
process.env.DVM_KIND = val;
|
|
20
|
-
break;
|
|
21
|
-
case '--budget':
|
|
22
|
-
process.env.BUDGET_SATS = val;
|
|
23
|
-
break;
|
|
24
|
-
case '--max-price':
|
|
25
|
-
process.env.MAX_SATS_PER_CHUNK = val;
|
|
26
|
-
break;
|
|
27
|
-
case '--ndebit':
|
|
28
|
-
process.env.CLINK_NDEBIT = val;
|
|
29
|
-
break;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
import { streamFromProvider } from './p2p-customer.js';
|
|
33
|
-
const KIND = Number(process.env.DVM_KIND) || 5100;
|
|
34
|
-
const BUDGET_SATS = Number(process.env.BUDGET_SATS) || 100;
|
|
35
|
-
const MAX_SATS_PER_CHUNK = Number(process.env.MAX_SATS_PER_CHUNK) || 5;
|
|
36
|
-
const NDEBIT = process.env.CLINK_NDEBIT || '';
|
|
37
|
-
async function main() {
|
|
38
|
-
const prompt = process.argv.slice(2).filter(a => !a.startsWith('--')).join(' ');
|
|
39
|
-
if (!prompt) {
|
|
40
|
-
console.error('Usage: 2020117-customer --kind=5100 --budget=50 --ndebit=ndebit1... "your prompt here"');
|
|
41
|
-
process.exit(1);
|
|
42
|
-
}
|
|
43
|
-
if (!NDEBIT) {
|
|
44
|
-
console.error('[customer] Error: --ndebit=ndebit1... required (CLINK debit authorization)');
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
console.log(`[customer] Prompt: "${prompt.slice(0, 60)}..."`);
|
|
48
|
-
console.log(`[customer] Budget: ${BUDGET_SATS} sats, max price: ${MAX_SATS_PER_CHUNK} sat/chunk`);
|
|
49
|
-
// Stream from provider (handles connection, negotiation, CLINK payments internally)
|
|
50
|
-
let output = '';
|
|
51
|
-
for await (const chunk of streamFromProvider({
|
|
52
|
-
kind: KIND,
|
|
53
|
-
input: prompt,
|
|
54
|
-
budgetSats: BUDGET_SATS,
|
|
55
|
-
ndebit: NDEBIT,
|
|
56
|
-
maxSatsPerChunk: MAX_SATS_PER_CHUNK,
|
|
57
|
-
label: 'customer',
|
|
58
|
-
})) {
|
|
59
|
-
process.stdout.write(chunk);
|
|
60
|
-
output += chunk;
|
|
61
|
-
}
|
|
62
|
-
console.log(`\n[customer] Done (${output.length} chars)`);
|
|
63
|
-
}
|
|
64
|
-
main().catch(err => { console.error('[customer] Fatal:', err.message || err); process.exit(1); });
|
package/dist/p2p-provider.d.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared P2P provider protocol — reusable building blocks for provider-side
|
|
3
|
-
* streaming with credit-based flow control and CLINK debit payments.
|
|
4
|
-
*
|
|
5
|
-
* Provider actively pulls payments from customer's wallet via CLINK ndebit,
|
|
6
|
-
* generating invoices from their own Lightning Address via LNURL-pay.
|
|
7
|
-
*
|
|
8
|
-
* Consumers: agent.ts, provider.ts
|
|
9
|
-
*/
|
|
10
|
-
import { SwarmNode } from './swarm.js';
|
|
11
|
-
/** Per-connection job state for P2P streaming */
|
|
12
|
-
export interface P2PJobState {
|
|
13
|
-
socket: any;
|
|
14
|
-
credit: number;
|
|
15
|
-
ndebit: string;
|
|
16
|
-
totalEarned: number;
|
|
17
|
-
stopped: boolean;
|
|
18
|
-
}
|
|
19
|
-
/** Options for {@link streamToCustomer} */
|
|
20
|
-
export interface StreamOptions {
|
|
21
|
-
node: SwarmNode;
|
|
22
|
-
job: P2PJobState;
|
|
23
|
-
jobId: string;
|
|
24
|
-
source: AsyncIterable<string>;
|
|
25
|
-
satsPerChunk: number;
|
|
26
|
-
chunksPerPayment: number;
|
|
27
|
-
lightningAddress: string;
|
|
28
|
-
label: string;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Provider pulls a payment cycle from the customer's wallet via CLINK debit.
|
|
32
|
-
*
|
|
33
|
-
* Generates an invoice from the provider's Lightning Address, then sends a
|
|
34
|
-
* debit request to the customer's wallet service via Nostr relay.
|
|
35
|
-
*
|
|
36
|
-
* Returns true if payment succeeded and credit was added.
|
|
37
|
-
*/
|
|
38
|
-
export declare function collectP2PPayment(opts: {
|
|
39
|
-
job: P2PJobState;
|
|
40
|
-
node: SwarmNode;
|
|
41
|
-
jobId: string;
|
|
42
|
-
satsPerChunk: number;
|
|
43
|
-
chunksPerPayment: number;
|
|
44
|
-
lightningAddress: string;
|
|
45
|
-
label: string;
|
|
46
|
-
}): Promise<boolean>;
|
|
47
|
-
/**
|
|
48
|
-
* Handle an incoming `stop` message: mark the job as stopped.
|
|
49
|
-
*/
|
|
50
|
-
export declare function handleStop(job: P2PJobState, jobId: string, label: string): void;
|
|
51
|
-
/**
|
|
52
|
-
* Stream chunks from an async source to the customer with credit-based
|
|
53
|
-
* flow control. When credit runs out, the provider debits the customer's
|
|
54
|
-
* wallet via CLINK before continuing.
|
|
55
|
-
*
|
|
56
|
-
* Returns the concatenated full output string.
|
|
57
|
-
*
|
|
58
|
-
* **Cleanup note**: this function does NOT delete the job from any map or
|
|
59
|
-
* release any capacity slot — callers are responsible for post-stream
|
|
60
|
-
* cleanup.
|
|
61
|
-
*/
|
|
62
|
-
export declare function streamToCustomer(opts: StreamOptions): Promise<string>;
|
package/dist/p2p-provider.js
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared P2P provider protocol — reusable building blocks for provider-side
|
|
3
|
-
* streaming with credit-based flow control and CLINK debit payments.
|
|
4
|
-
*
|
|
5
|
-
* Provider actively pulls payments from customer's wallet via CLINK ndebit,
|
|
6
|
-
* generating invoices from their own Lightning Address via LNURL-pay.
|
|
7
|
-
*
|
|
8
|
-
* Consumers: agent.ts, provider.ts
|
|
9
|
-
*/
|
|
10
|
-
import { collectPayment } from './clink.js';
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Payment helpers
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
/**
|
|
15
|
-
* Provider pulls a payment cycle from the customer's wallet via CLINK debit.
|
|
16
|
-
*
|
|
17
|
-
* Generates an invoice from the provider's Lightning Address, then sends a
|
|
18
|
-
* debit request to the customer's wallet service via Nostr relay.
|
|
19
|
-
*
|
|
20
|
-
* Returns true if payment succeeded and credit was added.
|
|
21
|
-
*/
|
|
22
|
-
export async function collectP2PPayment(opts) {
|
|
23
|
-
const { job, node, jobId, satsPerChunk, chunksPerPayment, lightningAddress, label } = opts;
|
|
24
|
-
const amount = satsPerChunk * chunksPerPayment;
|
|
25
|
-
try {
|
|
26
|
-
const result = await collectPayment({
|
|
27
|
-
ndebit: job.ndebit,
|
|
28
|
-
lightningAddress,
|
|
29
|
-
amountSats: amount,
|
|
30
|
-
});
|
|
31
|
-
if (!result.ok) {
|
|
32
|
-
console.log(`[${label}] P2P job ${jobId}: debit failed: ${result.error}`);
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
35
|
-
job.credit += chunksPerPayment;
|
|
36
|
-
job.totalEarned += amount;
|
|
37
|
-
console.log(`[${label}] P2P job ${jobId}: debit OK +${amount} sats → +${chunksPerPayment} chunks (credit: ${job.credit}, total: ${job.totalEarned} sats)`);
|
|
38
|
-
node.send(job.socket, { type: 'payment_ack', id: jobId, amount });
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
catch (e) {
|
|
42
|
-
console.log(`[${label}] P2P job ${jobId}: debit error: ${e.message}`);
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Handle an incoming `stop` message: mark the job as stopped.
|
|
48
|
-
*/
|
|
49
|
-
export function handleStop(job, jobId, label) {
|
|
50
|
-
console.log(`[${label}] P2P job ${jobId}: customer requested stop`);
|
|
51
|
-
job.stopped = true;
|
|
52
|
-
}
|
|
53
|
-
// ---------------------------------------------------------------------------
|
|
54
|
-
// Streaming
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
/**
|
|
57
|
-
* Stream chunks from an async source to the customer with credit-based
|
|
58
|
-
* flow control. When credit runs out, the provider debits the customer's
|
|
59
|
-
* wallet via CLINK before continuing.
|
|
60
|
-
*
|
|
61
|
-
* Returns the concatenated full output string.
|
|
62
|
-
*
|
|
63
|
-
* **Cleanup note**: this function does NOT delete the job from any map or
|
|
64
|
-
* release any capacity slot — callers are responsible for post-stream
|
|
65
|
-
* cleanup.
|
|
66
|
-
*/
|
|
67
|
-
export async function streamToCustomer(opts) {
|
|
68
|
-
const { node, job, jobId, source, satsPerChunk, chunksPerPayment, lightningAddress, label } = opts;
|
|
69
|
-
let fullOutput = '';
|
|
70
|
-
try {
|
|
71
|
-
for await (const chunk of source) {
|
|
72
|
-
if (job.stopped) {
|
|
73
|
-
console.log(`[${label}] P2P job ${jobId}: stopped by customer`);
|
|
74
|
-
break;
|
|
75
|
-
}
|
|
76
|
-
if (job.credit <= 0) {
|
|
77
|
-
// Pull next payment from customer's wallet
|
|
78
|
-
console.log(`[${label}] P2P job ${jobId}: credit exhausted, debiting customer...`);
|
|
79
|
-
const paid = await collectP2PPayment({
|
|
80
|
-
job, node, jobId, satsPerChunk, chunksPerPayment, lightningAddress, label,
|
|
81
|
-
});
|
|
82
|
-
if (!paid || job.stopped) {
|
|
83
|
-
console.log(`[${label}] P2P job ${jobId}: ending (paid=${paid}, stopped=${job.stopped})`);
|
|
84
|
-
break;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
fullOutput += chunk;
|
|
88
|
-
node.send(job.socket, { type: 'chunk', id: jobId, data: chunk });
|
|
89
|
-
job.credit--;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
catch (e) {
|
|
93
|
-
console.error(`[${label}] P2P job ${jobId} generation error: ${e.message}`);
|
|
94
|
-
node.send(job.socket, { type: 'error', id: jobId, message: e.message });
|
|
95
|
-
}
|
|
96
|
-
// Send final result
|
|
97
|
-
node.send(job.socket, {
|
|
98
|
-
type: 'result',
|
|
99
|
-
id: jobId,
|
|
100
|
-
output: fullOutput,
|
|
101
|
-
total_sats: job.totalEarned,
|
|
102
|
-
});
|
|
103
|
-
console.log(`[${label}] P2P job ${jobId} completed (${fullOutput.length} chars, ${job.totalEarned} sats earned)`);
|
|
104
|
-
return fullOutput;
|
|
105
|
-
}
|
package/dist/pipeline.d.ts
DELETED
package/dist/pipeline.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Pipeline — chain multiple P2P providers in sequence.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* BUDGET_SATS=100 CLINK_NDEBIT=ndebit1... TARGET_LANG=Chinese npm run pipeline "Write a short poem"
|
|
7
|
-
*/
|
|
8
|
-
// --- CLI args → env (before any imports) ---
|
|
9
|
-
for (const arg of process.argv.slice(2)) {
|
|
10
|
-
if (!arg.startsWith('--'))
|
|
11
|
-
continue;
|
|
12
|
-
const eq = arg.indexOf('=');
|
|
13
|
-
if (eq === -1)
|
|
14
|
-
continue;
|
|
15
|
-
const key = arg.slice(0, eq);
|
|
16
|
-
const val = arg.slice(eq + 1);
|
|
17
|
-
switch (key) {
|
|
18
|
-
case '--ndebit':
|
|
19
|
-
process.env.CLINK_NDEBIT = val;
|
|
20
|
-
break;
|
|
21
|
-
case '--budget':
|
|
22
|
-
process.env.BUDGET_SATS = val;
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
import { streamFromProvider } from './p2p-customer.js';
|
|
27
|
-
import { getOnlineProviders } from './api.js';
|
|
28
|
-
const BUDGET_SATS = Number(process.env.BUDGET_SATS) || 100;
|
|
29
|
-
const MAX_SATS_PER_CHUNK = Number(process.env.MAX_SATS_PER_CHUNK) || 5;
|
|
30
|
-
const NDEBIT = process.env.CLINK_NDEBIT || '';
|
|
31
|
-
const GEN_KIND = Number(process.env.GEN_KIND) || 5100;
|
|
32
|
-
const TRANS_KIND = Number(process.env.TRANS_KIND) || 5302;
|
|
33
|
-
const TARGET_LANG = process.env.TARGET_LANG || 'Chinese';
|
|
34
|
-
async function showProviders(kind, label) {
|
|
35
|
-
try {
|
|
36
|
-
const agents = await getOnlineProviders(kind);
|
|
37
|
-
if (agents.length === 0) {
|
|
38
|
-
console.log(`[${label}] No providers online for kind ${kind}`);
|
|
39
|
-
}
|
|
40
|
-
else {
|
|
41
|
-
console.log(`[${label}] ${agents.length} provider(s) online for kind ${kind}:`);
|
|
42
|
-
for (const a of agents) {
|
|
43
|
-
const cap = a.capacity !== undefined ? `, capacity: ${a.capacity}` : '';
|
|
44
|
-
const price = a.pricing ? `, pricing: ${JSON.stringify(a.pricing)}` : '';
|
|
45
|
-
console.log(`[${label}] - ${a.username || a.user_id} (${a.status}${cap}${price})`);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
console.log(`[${label}] Could not query platform`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
async function collectStream(opts) {
|
|
54
|
-
let output = '';
|
|
55
|
-
for await (const chunk of streamFromProvider(opts)) {
|
|
56
|
-
process.stdout.write(chunk);
|
|
57
|
-
output += chunk;
|
|
58
|
-
}
|
|
59
|
-
return output;
|
|
60
|
-
}
|
|
61
|
-
async function main() {
|
|
62
|
-
const prompt = process.argv.slice(2).filter(a => !a.startsWith('--')).join(' ');
|
|
63
|
-
if (!prompt) {
|
|
64
|
-
console.error('Usage: 2020117-pipeline --ndebit=ndebit1... --budget=100 "your prompt"');
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
|
-
if (!NDEBIT) {
|
|
68
|
-
console.error('[pipeline] Error: --ndebit=ndebit1... required (CLINK debit authorization)');
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
console.log('='.repeat(60));
|
|
72
|
-
console.log(`Pipeline: generate (kind ${GEN_KIND}) → translate to ${TARGET_LANG} (kind ${TRANS_KIND})`);
|
|
73
|
-
console.log(`Total budget: ${BUDGET_SATS} sats`);
|
|
74
|
-
console.log('='.repeat(60));
|
|
75
|
-
const genBudget = Math.ceil(BUDGET_SATS * 0.6);
|
|
76
|
-
const transBudget = BUDGET_SATS - genBudget;
|
|
77
|
-
// Phase 1: Text Generation
|
|
78
|
-
console.log(`\n${'─'.repeat(60)}`);
|
|
79
|
-
console.log(`Phase 1: Text Generation (budget: ${genBudget} sats)`);
|
|
80
|
-
console.log('─'.repeat(60));
|
|
81
|
-
await showProviders(GEN_KIND, 'gen');
|
|
82
|
-
const generated = await collectStream({
|
|
83
|
-
kind: GEN_KIND, input: prompt, budgetSats: genBudget, ndebit: NDEBIT,
|
|
84
|
-
maxSatsPerChunk: MAX_SATS_PER_CHUNK, label: 'gen',
|
|
85
|
-
});
|
|
86
|
-
if (!generated.trim()) {
|
|
87
|
-
console.error('\n[pipeline] Phase 1 produced no output, aborting');
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
// Phase 2: Translation
|
|
91
|
-
console.log(`\n${'─'.repeat(60)}`);
|
|
92
|
-
console.log(`Phase 2: Translation to ${TARGET_LANG} (budget: ${transBudget} sats)`);
|
|
93
|
-
console.log('─'.repeat(60));
|
|
94
|
-
await showProviders(TRANS_KIND, 'trans');
|
|
95
|
-
const translated = await collectStream({
|
|
96
|
-
kind: TRANS_KIND, input: `Translate the following text to ${TARGET_LANG}:\n\n${generated}`,
|
|
97
|
-
budgetSats: transBudget, ndebit: NDEBIT, maxSatsPerChunk: MAX_SATS_PER_CHUNK, label: 'trans',
|
|
98
|
-
});
|
|
99
|
-
// Summary
|
|
100
|
-
console.log(`\n${'='.repeat(60)}`);
|
|
101
|
-
console.log('Pipeline complete!');
|
|
102
|
-
console.log('='.repeat(60));
|
|
103
|
-
console.log(`\nGenerated (${generated.length} chars):\n${generated}`);
|
|
104
|
-
console.log(`\nTranslated (${translated.length} chars):\n${translated}`);
|
|
105
|
-
}
|
|
106
|
-
main().catch(err => { console.error('[pipeline] Fatal:', err.message || err); process.exit(1); });
|
package/dist/provider.d.ts
DELETED
package/dist/provider.js
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Standalone Provider daemon — thin wrapper around shared P2P provider protocol.
|
|
4
|
-
* For most use cases, prefer `2020117-agent` which handles both API + P2P.
|
|
5
|
-
*/
|
|
6
|
-
// --- CLI args → env (before any imports) ---
|
|
7
|
-
for (const arg of process.argv.slice(2)) {
|
|
8
|
-
if (!arg.startsWith('--'))
|
|
9
|
-
continue;
|
|
10
|
-
const eq = arg.indexOf('=');
|
|
11
|
-
if (eq === -1)
|
|
12
|
-
continue;
|
|
13
|
-
const key = arg.slice(0, eq);
|
|
14
|
-
const val = arg.slice(eq + 1);
|
|
15
|
-
switch (key) {
|
|
16
|
-
case '--kind':
|
|
17
|
-
process.env.DVM_KIND = val;
|
|
18
|
-
break;
|
|
19
|
-
case '--lightning-address':
|
|
20
|
-
process.env.LIGHTNING_ADDRESS = val;
|
|
21
|
-
break;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
25
|
-
import { createProcessor } from './processor.js';
|
|
26
|
-
import { hasApiKey, registerService, startHeartbeatLoop } from './api.js';
|
|
27
|
-
import { initClinkAgent } from './clink.js';
|
|
28
|
-
import { collectP2PPayment, handleStop, streamToCustomer } from './p2p-provider.js';
|
|
29
|
-
const KIND = Number(process.env.DVM_KIND) || 5100;
|
|
30
|
-
const SATS_PER_CHUNK = Number(process.env.SATS_PER_CHUNK) || 1;
|
|
31
|
-
const CHUNKS_PER_PAYMENT = Number(process.env.CHUNKS_PER_PAYMENT) || 10;
|
|
32
|
-
const LIGHTNING_ADDRESS = process.env.LIGHTNING_ADDRESS || '';
|
|
33
|
-
const jobs = new Map();
|
|
34
|
-
async function main() {
|
|
35
|
-
if (!LIGHTNING_ADDRESS) {
|
|
36
|
-
console.error('[provider] Error: --lightning-address=you@wallet.com required');
|
|
37
|
-
process.exit(1);
|
|
38
|
-
}
|
|
39
|
-
const processor = await createProcessor();
|
|
40
|
-
await processor.verify();
|
|
41
|
-
console.log(`[provider] Processor "${processor.name}" verified`);
|
|
42
|
-
// Initialize CLINK agent identity
|
|
43
|
-
const { pubkey } = initClinkAgent();
|
|
44
|
-
console.log(`[provider] CLINK: ${LIGHTNING_ADDRESS} (agent pubkey: ${pubkey.slice(0, 16)}...)`);
|
|
45
|
-
// Platform registration
|
|
46
|
-
let stopHeartbeat = null;
|
|
47
|
-
if (hasApiKey()) {
|
|
48
|
-
console.log('[provider] Registering on platform...');
|
|
49
|
-
await registerService({ kind: KIND, satsPerChunk: SATS_PER_CHUNK, chunksPerPayment: CHUNKS_PER_PAYMENT, model: processor.name });
|
|
50
|
-
stopHeartbeat = startHeartbeatLoop();
|
|
51
|
-
}
|
|
52
|
-
else {
|
|
53
|
-
console.log('[provider] No API key — P2P-only mode');
|
|
54
|
-
}
|
|
55
|
-
const satsPerPayment = SATS_PER_CHUNK * CHUNKS_PER_PAYMENT;
|
|
56
|
-
const node = new SwarmNode();
|
|
57
|
-
const topic = topicFromKind(KIND);
|
|
58
|
-
console.log(`[provider] Streaming payment: ${SATS_PER_CHUNK} sat/chunk, ${CHUNKS_PER_PAYMENT} chunks/payment (${satsPerPayment} sats/cycle)`);
|
|
59
|
-
await node.listen(topic);
|
|
60
|
-
console.log(`[provider] Listening for customers...\n`);
|
|
61
|
-
node.on('message', async (msg, socket, peerId) => {
|
|
62
|
-
const tag = peerId.slice(0, 8);
|
|
63
|
-
if (msg.type === 'request') {
|
|
64
|
-
console.log(`[provider] Job ${msg.id} from ${tag}: "${(msg.input || '').slice(0, 60)}..."`);
|
|
65
|
-
if (!msg.ndebit) {
|
|
66
|
-
node.send(socket, { type: 'error', id: msg.id, message: 'Request requires ndebit authorization' });
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
if (msg.budget !== undefined) {
|
|
70
|
-
console.log(`[provider] Customer budget: ${msg.budget} sats`);
|
|
71
|
-
}
|
|
72
|
-
const job = {
|
|
73
|
-
socket, credit: 0, ndebit: msg.ndebit, totalEarned: 0, stopped: false,
|
|
74
|
-
};
|
|
75
|
-
jobs.set(msg.id, job);
|
|
76
|
-
node.send(socket, { type: 'offer', id: msg.id, sats_per_chunk: SATS_PER_CHUNK, chunks_per_payment: CHUNKS_PER_PAYMENT });
|
|
77
|
-
// Debit first payment cycle via CLINK
|
|
78
|
-
const paid = await collectP2PPayment({
|
|
79
|
-
job, node, jobId: msg.id,
|
|
80
|
-
satsPerChunk: SATS_PER_CHUNK, chunksPerPayment: CHUNKS_PER_PAYMENT,
|
|
81
|
-
lightningAddress: LIGHTNING_ADDRESS, label: 'provider',
|
|
82
|
-
});
|
|
83
|
-
if (!paid) {
|
|
84
|
-
jobs.delete(msg.id);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
node.send(socket, { type: 'accepted', id: msg.id });
|
|
88
|
-
const source = processor.generateStream({ input: msg.input || '', params: msg.params });
|
|
89
|
-
await streamToCustomer({
|
|
90
|
-
node, job, jobId: msg.id, source,
|
|
91
|
-
satsPerChunk: SATS_PER_CHUNK, chunksPerPayment: CHUNKS_PER_PAYMENT,
|
|
92
|
-
lightningAddress: LIGHTNING_ADDRESS, label: 'provider',
|
|
93
|
-
});
|
|
94
|
-
jobs.delete(msg.id);
|
|
95
|
-
}
|
|
96
|
-
if (msg.type === 'stop') {
|
|
97
|
-
const job = jobs.get(msg.id);
|
|
98
|
-
if (job)
|
|
99
|
-
handleStop(job, msg.id, 'provider');
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
process.on('SIGINT', async () => {
|
|
103
|
-
console.log('\n[provider] Shutting down...');
|
|
104
|
-
if (stopHeartbeat)
|
|
105
|
-
stopHeartbeat();
|
|
106
|
-
await node.destroy();
|
|
107
|
-
process.exit(0);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
main().catch(err => { console.error('[provider] Fatal:', err); process.exit(1); });
|