2020117-agent 0.1.15 → 0.2.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/dist/agent.d.ts +1 -1
- package/dist/agent.js +109 -73
- package/dist/clink.d.ts +42 -0
- package/dist/clink.js +87 -0
- package/dist/customer.d.ts +2 -2
- package/dist/customer.js +13 -4
- package/dist/p2p-customer.d.ts +9 -5
- package/dist/p2p-customer.js +20 -48
- package/dist/p2p-provider.d.ts +24 -30
- package/dist/p2p-provider.js +36 -96
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +28 -5
- package/dist/provider.js +44 -11
- package/dist/session.d.ts +2 -2
- package/dist/session.js +24 -94
- package/dist/swarm.d.ts +1 -0
- package/package.json +4 -4
package/dist/agent.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Unified Agent Runtime — runs as a long-lived daemon that handles both:
|
|
4
4
|
* 1. Async platform tasks (inbox polling → accept → Ollama → submit result)
|
|
5
|
-
* 2. Real-time P2P streaming (Hyperswarm +
|
|
5
|
+
* 2. Real-time P2P streaming (Hyperswarm + CLINK debit payments)
|
|
6
6
|
*
|
|
7
7
|
* Both channels share a single capacity counter so the agent never overloads.
|
|
8
8
|
*
|
package/dist/agent.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Unified Agent Runtime — runs as a long-lived daemon that handles both:
|
|
4
4
|
* 1. Async platform tasks (inbox polling → accept → Ollama → submit result)
|
|
5
|
-
* 2. Real-time P2P streaming (Hyperswarm +
|
|
5
|
+
* 2. Real-time P2P streaming (Hyperswarm + CLINK debit payments)
|
|
6
6
|
*
|
|
7
7
|
* Both channels share a single capacity counter so the agent never overloads.
|
|
8
8
|
*
|
|
@@ -66,15 +66,18 @@ for (const arg of process.argv.slice(2)) {
|
|
|
66
66
|
case '--skill':
|
|
67
67
|
process.env.SKILL_FILE = val;
|
|
68
68
|
break;
|
|
69
|
+
case '--lightning-address':
|
|
70
|
+
process.env.LIGHTNING_ADDRESS = val;
|
|
71
|
+
break;
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
74
|
import { randomBytes } from 'crypto';
|
|
72
75
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
73
|
-
import {
|
|
76
|
+
import { collectP2PPayment, handleStop, streamToCustomer } from './p2p-provider.js';
|
|
74
77
|
import { streamFromProvider } from './p2p-customer.js';
|
|
75
78
|
import { createProcessor } from './processor.js';
|
|
76
79
|
import { hasApiKey, loadAgentName, registerService, startHeartbeatLoop, getInbox, acceptJob, sendFeedback, submitResult, createJob, getJob, } from './api.js';
|
|
77
|
-
import {
|
|
80
|
+
import { initClinkAgent, collectPayment } from './clink.js';
|
|
78
81
|
import { readFileSync } from 'fs';
|
|
79
82
|
import WebSocket from 'ws';
|
|
80
83
|
// --- Config from env ---
|
|
@@ -84,6 +87,8 @@ const POLL_INTERVAL = Number(process.env.POLL_INTERVAL) || 30_000;
|
|
|
84
87
|
const SATS_PER_CHUNK = Number(process.env.SATS_PER_CHUNK) || 1;
|
|
85
88
|
const CHUNKS_PER_PAYMENT = Number(process.env.CHUNKS_PER_PAYMENT) || 10;
|
|
86
89
|
const PAYMENT_TIMEOUT = Number(process.env.PAYMENT_TIMEOUT) || 30_000;
|
|
90
|
+
// --- CLINK payment config ---
|
|
91
|
+
const LIGHTNING_ADDRESS = process.env.LIGHTNING_ADDRESS || '';
|
|
87
92
|
// --- Sub-task delegation config ---
|
|
88
93
|
const SUB_KIND = process.env.SUB_KIND ? Number(process.env.SUB_KIND) : null;
|
|
89
94
|
const SUB_BUDGET = Number(process.env.SUB_BUDGET) || 50;
|
|
@@ -158,13 +163,18 @@ async function main() {
|
|
|
158
163
|
if (state.skill) {
|
|
159
164
|
console.log(`[${label}] Skill: ${state.skill.name} v${state.skill.version} (${state.skill.features.join(', ')})`);
|
|
160
165
|
}
|
|
161
|
-
// 2.
|
|
166
|
+
// 2. Initialize CLINK agent identity (for P2P session debit)
|
|
167
|
+
if (LIGHTNING_ADDRESS) {
|
|
168
|
+
const { pubkey } = initClinkAgent();
|
|
169
|
+
console.log(`[${label}] CLINK: ${LIGHTNING_ADDRESS} (agent pubkey: ${pubkey.slice(0, 16)}...)`);
|
|
170
|
+
}
|
|
171
|
+
// 3. Platform registration + heartbeat
|
|
162
172
|
await setupPlatform(label);
|
|
163
|
-
//
|
|
173
|
+
// 4. Async inbox poller
|
|
164
174
|
startInboxPoller(label);
|
|
165
|
-
//
|
|
175
|
+
// 5. P2P swarm listener
|
|
166
176
|
await startSwarmListener(label);
|
|
167
|
-
//
|
|
177
|
+
// 6. Graceful shutdown
|
|
168
178
|
setupShutdown(label);
|
|
169
179
|
console.log(`[${label}] Agent ready — async + P2P channels active\n`);
|
|
170
180
|
}
|
|
@@ -290,14 +300,18 @@ async function processAsyncJob(label, inboxJobId, input, params) {
|
|
|
290
300
|
}
|
|
291
301
|
// --- Sub-task delegation ---
|
|
292
302
|
/**
|
|
293
|
-
* Delegate a sub-task via Hyperswarm P2P with
|
|
303
|
+
* Delegate a sub-task via Hyperswarm P2P with CLINK debit payments.
|
|
294
304
|
* Thin wrapper around the shared streamFromProvider() module.
|
|
295
305
|
*/
|
|
296
306
|
async function* delegateP2PStream(kind, input, budgetSats) {
|
|
307
|
+
const ndebit = process.env.CLINK_NDEBIT || '';
|
|
308
|
+
if (!ndebit)
|
|
309
|
+
throw new Error('Pipeline sub-delegation requires CLINK_NDEBIT env var (--ndebit)');
|
|
297
310
|
yield* streamFromProvider({
|
|
298
311
|
kind,
|
|
299
312
|
input,
|
|
300
313
|
budgetSats,
|
|
314
|
+
ndebit,
|
|
301
315
|
maxSatsPerChunk: MAX_SATS_PER_CHUNK,
|
|
302
316
|
label: 'sub-p2p',
|
|
303
317
|
});
|
|
@@ -382,77 +396,89 @@ async function startSwarmListener(label) {
|
|
|
382
396
|
}
|
|
383
397
|
// --- Session protocol ---
|
|
384
398
|
if (msg.type === 'session_start') {
|
|
399
|
+
if (!LIGHTNING_ADDRESS) {
|
|
400
|
+
console.warn(`[${label}] Session rejected: no --lightning-address configured`);
|
|
401
|
+
node.send(socket, { type: 'error', id: msg.id, message: 'Provider has no Lightning Address configured' });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (!msg.ndebit) {
|
|
405
|
+
node.send(socket, { type: 'error', id: msg.id, message: 'session_start requires ndebit authorization' });
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
385
408
|
const satsPerMinute = state.skill?.pricing?.sats_per_minute
|
|
386
409
|
|| Number(process.env.SATS_PER_MINUTE)
|
|
387
410
|
|| msg.sats_per_minute
|
|
388
411
|
|| 10;
|
|
412
|
+
const BILLING_INTERVAL_MIN = 10;
|
|
413
|
+
const debitAmount = satsPerMinute * BILLING_INTERVAL_MIN;
|
|
389
414
|
const sessionId = randomBytes(8).toString('hex');
|
|
390
|
-
console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min`);
|
|
415
|
+
console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, billing every ${BILLING_INTERVAL_MIN}min (${debitAmount} sats)`);
|
|
391
416
|
const session = {
|
|
392
417
|
socket,
|
|
393
418
|
peerId,
|
|
394
419
|
sessionId,
|
|
395
420
|
satsPerMinute,
|
|
396
|
-
|
|
421
|
+
ndebit: msg.ndebit,
|
|
397
422
|
totalEarned: 0,
|
|
398
423
|
startedAt: Date.now(),
|
|
399
|
-
|
|
424
|
+
lastDebitAt: Date.now(),
|
|
425
|
+
debitTimer: null,
|
|
400
426
|
timeoutTimer: null,
|
|
401
427
|
};
|
|
402
|
-
// Dynamic timeout based on tick value: if a tick covers N minutes of service,
|
|
403
|
-
// allow N minutes + 2 min grace before timing out. Updates on each tick.
|
|
404
|
-
const baseTimeoutMs = satsPerMinute > 0
|
|
405
|
-
? Math.max(120_000, Math.round((1 / satsPerMinute) * 60_000) + 120_000)
|
|
406
|
-
: 120_000;
|
|
407
|
-
session.timeoutTimer = setInterval(() => {
|
|
408
|
-
// Recalculate timeout based on last tick amount
|
|
409
|
-
const lastTickAmount = session.totalEarned > 0
|
|
410
|
-
? Math.max(1, session.tokens.length > 0 ? peekToken(session.tokens[session.tokens.length - 1]).amount : 1)
|
|
411
|
-
: 1;
|
|
412
|
-
const tickCoverageMs = satsPerMinute > 0
|
|
413
|
-
? Math.round((lastTickAmount / satsPerMinute) * 60_000) + 120_000
|
|
414
|
-
: baseTimeoutMs;
|
|
415
|
-
const elapsed = Date.now() - session.lastTickAt;
|
|
416
|
-
if (elapsed > tickCoverageMs) {
|
|
417
|
-
console.log(`[${label}] Session ${sessionId}: timeout (no tick for ${Math.round(elapsed / 1000)}s, limit ${Math.round(tickCoverageMs / 1000)}s)`);
|
|
418
|
-
endSession(node, session, label);
|
|
419
|
-
}
|
|
420
|
-
}, 30_000);
|
|
421
428
|
activeSessions.set(sessionId, session);
|
|
429
|
+
// Debit first 10 minutes immediately (prepaid model)
|
|
430
|
+
const firstDebit = await collectPayment({
|
|
431
|
+
ndebit: session.ndebit,
|
|
432
|
+
lightningAddress: LIGHTNING_ADDRESS,
|
|
433
|
+
amountSats: debitAmount,
|
|
434
|
+
});
|
|
435
|
+
if (!firstDebit.ok) {
|
|
436
|
+
console.warn(`[${label}] Session ${sessionId}: first debit failed: ${firstDebit.error}`);
|
|
437
|
+
node.send(socket, { type: 'error', id: msg.id, message: `Payment failed: ${firstDebit.error}` });
|
|
438
|
+
activeSessions.delete(sessionId);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
session.totalEarned += debitAmount;
|
|
442
|
+
session.lastDebitAt = Date.now();
|
|
443
|
+
console.log(`[${label}] Session ${sessionId}: first ${BILLING_INTERVAL_MIN}min paid (${debitAmount} sats)`);
|
|
422
444
|
node.send(socket, {
|
|
423
445
|
type: 'session_ack',
|
|
424
446
|
id: msg.id,
|
|
425
447
|
session_id: sessionId,
|
|
426
448
|
sats_per_minute: satsPerMinute,
|
|
427
449
|
});
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
450
|
+
// Notify customer of the debit
|
|
451
|
+
node.send(socket, {
|
|
452
|
+
type: 'session_tick_ack',
|
|
453
|
+
id: sessionId,
|
|
454
|
+
session_id: sessionId,
|
|
455
|
+
amount: debitAmount,
|
|
456
|
+
balance: msg.budget ? msg.budget - session.totalEarned : undefined,
|
|
457
|
+
});
|
|
458
|
+
// Debit every 10 minutes
|
|
459
|
+
session.debitTimer = setInterval(async () => {
|
|
460
|
+
const debit = await collectPayment({
|
|
461
|
+
ndebit: session.ndebit,
|
|
462
|
+
lightningAddress: LIGHTNING_ADDRESS,
|
|
463
|
+
amountSats: debitAmount,
|
|
464
|
+
});
|
|
465
|
+
if (!debit.ok) {
|
|
466
|
+
console.log(`[${label}] Session ${sessionId}: debit failed (${debit.error}) — ending session`);
|
|
467
|
+
endSession(node, session, label);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
session.totalEarned += debitAmount;
|
|
471
|
+
session.lastDebitAt = Date.now();
|
|
472
|
+
console.log(`[${label}] Session ${sessionId}: debit OK (+${debitAmount}, total: ${session.totalEarned} sats)`);
|
|
473
|
+
// Notify customer
|
|
446
474
|
node.send(socket, {
|
|
447
475
|
type: 'session_tick_ack',
|
|
448
|
-
id:
|
|
449
|
-
session_id:
|
|
476
|
+
id: sessionId,
|
|
477
|
+
session_id: sessionId,
|
|
478
|
+
amount: debitAmount,
|
|
450
479
|
balance: msg.budget ? msg.budget - session.totalEarned : undefined,
|
|
451
480
|
});
|
|
452
|
-
}
|
|
453
|
-
catch (e) {
|
|
454
|
-
node.send(socket, { type: 'error', id: msg.id, message: `Tick payment failed: ${e.message}` });
|
|
455
|
-
}
|
|
481
|
+
}, BILLING_INTERVAL_MIN * 60_000);
|
|
456
482
|
return;
|
|
457
483
|
}
|
|
458
484
|
if (msg.type === 'session_end') {
|
|
@@ -640,6 +666,14 @@ async function startSwarmListener(label) {
|
|
|
640
666
|
}
|
|
641
667
|
if (msg.type === 'request') {
|
|
642
668
|
console.log(`[${label}] P2P job ${msg.id} from ${tag}: "${(msg.input || '').slice(0, 60)}..."`);
|
|
669
|
+
if (!LIGHTNING_ADDRESS) {
|
|
670
|
+
node.send(socket, { type: 'error', id: msg.id, message: 'Provider has no Lightning Address configured' });
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (!msg.ndebit) {
|
|
674
|
+
node.send(socket, { type: 'error', id: msg.id, message: 'Request requires ndebit authorization' });
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
643
677
|
if (!acquireSlot()) {
|
|
644
678
|
node.send(socket, {
|
|
645
679
|
type: 'error',
|
|
@@ -655,10 +689,9 @@ async function startSwarmListener(label) {
|
|
|
655
689
|
const job = {
|
|
656
690
|
socket,
|
|
657
691
|
credit: 0,
|
|
658
|
-
|
|
692
|
+
ndebit: msg.ndebit,
|
|
659
693
|
totalEarned: 0,
|
|
660
694
|
stopped: false,
|
|
661
|
-
paymentResolve: null,
|
|
662
695
|
};
|
|
663
696
|
p2pJobs.set(msg.id, job);
|
|
664
697
|
// Send offer
|
|
@@ -668,10 +701,16 @@ async function startSwarmListener(label) {
|
|
|
668
701
|
sats_per_chunk: SATS_PER_CHUNK,
|
|
669
702
|
chunks_per_payment: CHUNKS_PER_PAYMENT,
|
|
670
703
|
});
|
|
671
|
-
//
|
|
672
|
-
const paid = await
|
|
704
|
+
// Debit first payment cycle via CLINK
|
|
705
|
+
const paid = await collectP2PPayment({
|
|
706
|
+
job, node, jobId: msg.id,
|
|
707
|
+
satsPerChunk: SATS_PER_CHUNK,
|
|
708
|
+
chunksPerPayment: CHUNKS_PER_PAYMENT,
|
|
709
|
+
lightningAddress: LIGHTNING_ADDRESS,
|
|
710
|
+
label,
|
|
711
|
+
});
|
|
673
712
|
if (!paid) {
|
|
674
|
-
console.log(`[${label}] P2P job ${msg.id}:
|
|
713
|
+
console.log(`[${label}] P2P job ${msg.id}: first debit failed, aborting`);
|
|
675
714
|
p2pJobs.delete(msg.id);
|
|
676
715
|
releaseSlot();
|
|
677
716
|
return;
|
|
@@ -680,18 +719,13 @@ async function startSwarmListener(label) {
|
|
|
680
719
|
node.send(socket, { type: 'accepted', id: msg.id });
|
|
681
720
|
await runP2PGeneration(node, job, msg, label);
|
|
682
721
|
}
|
|
683
|
-
if (msg.type === 'payment') {
|
|
684
|
-
const job = p2pJobs.get(msg.id);
|
|
685
|
-
if (job)
|
|
686
|
-
handlePayment(node, socket, msg, job, SATS_PER_CHUNK, label);
|
|
687
|
-
}
|
|
688
722
|
if (msg.type === 'stop') {
|
|
689
723
|
const job = p2pJobs.get(msg.id);
|
|
690
724
|
if (job)
|
|
691
725
|
handleStop(job, msg.id, label);
|
|
692
726
|
}
|
|
693
727
|
});
|
|
694
|
-
// Handle customer disconnect —
|
|
728
|
+
// Handle customer disconnect — payments already settled via CLINK
|
|
695
729
|
node.on('peer-leave', (peerId) => {
|
|
696
730
|
const tag = peerId.slice(0, 8);
|
|
697
731
|
// Find and end all sessions for this peer
|
|
@@ -704,9 +738,8 @@ async function startSwarmListener(label) {
|
|
|
704
738
|
// Clean up any P2P streaming jobs for this peer
|
|
705
739
|
for (const [jobId, job] of p2pJobs) {
|
|
706
740
|
if (job.socket?.remotePublicKey?.toString('hex') === peerId) {
|
|
707
|
-
console.log(`[${label}] Peer ${tag} disconnected —
|
|
741
|
+
console.log(`[${label}] Peer ${tag} disconnected — P2P job ${jobId} (${job.totalEarned} sats earned)`);
|
|
708
742
|
job.stopped = true;
|
|
709
|
-
batchClaim(job.tokens, jobId, label);
|
|
710
743
|
p2pJobs.delete(jobId);
|
|
711
744
|
releaseSlot();
|
|
712
745
|
}
|
|
@@ -734,10 +767,10 @@ async function runP2PGeneration(node, job, msg, label) {
|
|
|
734
767
|
source,
|
|
735
768
|
satsPerChunk: SATS_PER_CHUNK,
|
|
736
769
|
chunksPerPayment: CHUNKS_PER_PAYMENT,
|
|
737
|
-
|
|
770
|
+
lightningAddress: LIGHTNING_ADDRESS,
|
|
738
771
|
label,
|
|
739
772
|
});
|
|
740
|
-
|
|
773
|
+
// No batch claim needed — CLINK payments settle instantly via Lightning
|
|
741
774
|
p2pJobs.delete(msg.id);
|
|
742
775
|
releaseSlot();
|
|
743
776
|
}
|
|
@@ -751,8 +784,13 @@ function findSessionBySocket(socket) {
|
|
|
751
784
|
}
|
|
752
785
|
function endSession(node, session, label) {
|
|
753
786
|
const durationS = Math.round((Date.now() - session.startedAt) / 1000);
|
|
787
|
+
// Stop debit timer
|
|
788
|
+
if (session.debitTimer) {
|
|
789
|
+
clearInterval(session.debitTimer);
|
|
790
|
+
session.debitTimer = null;
|
|
791
|
+
}
|
|
754
792
|
if (session.timeoutTimer) {
|
|
755
|
-
|
|
793
|
+
clearTimeout(session.timeoutTimer);
|
|
756
794
|
session.timeoutTimer = null;
|
|
757
795
|
}
|
|
758
796
|
// Close all backend WebSockets for this peer
|
|
@@ -778,11 +816,9 @@ function endSession(node, session, label) {
|
|
|
778
816
|
// Socket may already be closed (peer disconnect)
|
|
779
817
|
}
|
|
780
818
|
console.log(`[${label}] Session ${session.sessionId} ended: ${session.totalEarned} sats, ${durationS}s`);
|
|
781
|
-
// Update P2P lifetime counters
|
|
819
|
+
// Update P2P lifetime counters — no batch claim needed with CLINK (payments settled instantly)
|
|
782
820
|
state.p2pSessionsCompleted++;
|
|
783
821
|
state.p2pTotalEarnedSats += session.totalEarned;
|
|
784
|
-
// Batch claim tokens
|
|
785
|
-
batchClaim(session.tokens, session.sessionId, label);
|
|
786
822
|
activeSessions.delete(session.sessionId);
|
|
787
823
|
}
|
|
788
824
|
// --- 5. Graceful shutdown ---
|
package/dist/clink.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLINK payment utilities — replaces Cashu for P2P payments
|
|
3
|
+
*
|
|
4
|
+
* Provider uses ndebit to pull payments from customer's wallet.
|
|
5
|
+
* Invoice generation via LNURL-pay from provider's own Lightning Address.
|
|
6
|
+
*/
|
|
7
|
+
export declare function initClinkAgent(): {
|
|
8
|
+
privateKey: Uint8Array;
|
|
9
|
+
pubkey: string;
|
|
10
|
+
};
|
|
11
|
+
export interface DebitResult {
|
|
12
|
+
ok: boolean;
|
|
13
|
+
preimage?: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Provider calls this to debit customer's wallet via CLINK protocol.
|
|
18
|
+
* Sends a Kind 21002 event to the customer's wallet service via Nostr relay.
|
|
19
|
+
*/
|
|
20
|
+
export declare function debitCustomer(opts: {
|
|
21
|
+
ndebit: string;
|
|
22
|
+
bolt11: string;
|
|
23
|
+
timeoutSeconds?: number;
|
|
24
|
+
}): Promise<DebitResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Resolve a Lightning Address to a bolt11 invoice via LNURL-pay protocol.
|
|
27
|
+
* The provider calls this on their OWN Lightning Address to generate
|
|
28
|
+
* an invoice that pays themselves.
|
|
29
|
+
*
|
|
30
|
+
* Flow: address → .well-known/lnurlp → callback?amount= → bolt11
|
|
31
|
+
*/
|
|
32
|
+
export declare function generateInvoice(lightningAddress: string, amountSats: number): Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Full payment cycle: generate invoice from provider's Lightning Address,
|
|
35
|
+
* then debit customer's wallet via CLINK.
|
|
36
|
+
*/
|
|
37
|
+
export declare function collectPayment(opts: {
|
|
38
|
+
ndebit: string;
|
|
39
|
+
lightningAddress: string;
|
|
40
|
+
amountSats: number;
|
|
41
|
+
timeoutSeconds?: number;
|
|
42
|
+
}): Promise<DebitResult>;
|
package/dist/clink.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLINK payment utilities — replaces Cashu for P2P payments
|
|
3
|
+
*
|
|
4
|
+
* Provider uses ndebit to pull payments from customer's wallet.
|
|
5
|
+
* Invoice generation via LNURL-pay from provider's own Lightning Address.
|
|
6
|
+
*/
|
|
7
|
+
import { ClinkSDK, decodeBech32, generateSecretKey, getPublicKey, newNdebitPaymentRequest } from '@shocknet/clink-sdk';
|
|
8
|
+
// --- Agent identity ---
|
|
9
|
+
let agentKey = null;
|
|
10
|
+
let agentPubkey = null;
|
|
11
|
+
export function initClinkAgent() {
|
|
12
|
+
agentKey = generateSecretKey();
|
|
13
|
+
agentPubkey = getPublicKey(agentKey);
|
|
14
|
+
console.log(`[clink] Agent identity: ${agentPubkey.slice(0, 16)}...`);
|
|
15
|
+
return { privateKey: agentKey, pubkey: agentPubkey };
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Provider calls this to debit customer's wallet via CLINK protocol.
|
|
19
|
+
* Sends a Kind 21002 event to the customer's wallet service via Nostr relay.
|
|
20
|
+
*/
|
|
21
|
+
export async function debitCustomer(opts) {
|
|
22
|
+
if (!agentKey)
|
|
23
|
+
throw new Error('CLINK agent not initialized — call initClinkAgent() first');
|
|
24
|
+
const decoded = decodeBech32(opts.ndebit);
|
|
25
|
+
if (decoded.type !== 'ndebit')
|
|
26
|
+
throw new Error(`Invalid ndebit string (got type: ${decoded.type})`);
|
|
27
|
+
const sdk = new ClinkSDK({
|
|
28
|
+
privateKey: agentKey,
|
|
29
|
+
relays: [decoded.data.relay],
|
|
30
|
+
toPubKey: decoded.data.pubkey,
|
|
31
|
+
defaultTimeoutSeconds: opts.timeoutSeconds ?? 30,
|
|
32
|
+
});
|
|
33
|
+
const result = await sdk.Ndebit(newNdebitPaymentRequest(opts.bolt11, undefined, decoded.data.pointer));
|
|
34
|
+
if (result.res === 'ok') {
|
|
35
|
+
return { ok: true, preimage: result.preimage };
|
|
36
|
+
}
|
|
37
|
+
return { ok: false, error: result.error || 'Debit rejected' };
|
|
38
|
+
}
|
|
39
|
+
// --- Invoice generation via LNURL-pay ---
|
|
40
|
+
/**
|
|
41
|
+
* Resolve a Lightning Address to a bolt11 invoice via LNURL-pay protocol.
|
|
42
|
+
* The provider calls this on their OWN Lightning Address to generate
|
|
43
|
+
* an invoice that pays themselves.
|
|
44
|
+
*
|
|
45
|
+
* Flow: address → .well-known/lnurlp → callback?amount= → bolt11
|
|
46
|
+
*/
|
|
47
|
+
export async function generateInvoice(lightningAddress, amountSats) {
|
|
48
|
+
const [user, domain] = lightningAddress.split('@');
|
|
49
|
+
if (!user || !domain)
|
|
50
|
+
throw new Error(`Invalid Lightning Address: ${lightningAddress}`);
|
|
51
|
+
// Step 1: Fetch LNURL-pay metadata
|
|
52
|
+
const metaUrl = `https://${domain}/.well-known/lnurlp/${user}`;
|
|
53
|
+
const metaResp = await fetch(metaUrl);
|
|
54
|
+
if (!metaResp.ok)
|
|
55
|
+
throw new Error(`LNURL fetch failed: ${metaResp.status} from ${metaUrl}`);
|
|
56
|
+
const meta = await metaResp.json();
|
|
57
|
+
if (meta.tag !== 'payRequest')
|
|
58
|
+
throw new Error(`Not a LNURL-pay endpoint (tag: ${meta.tag})`);
|
|
59
|
+
const amountMsats = amountSats * 1000;
|
|
60
|
+
if (amountMsats < meta.minSendable)
|
|
61
|
+
throw new Error(`Amount ${amountSats} sats below min ${meta.minSendable / 1000} sats`);
|
|
62
|
+
if (amountMsats > meta.maxSendable)
|
|
63
|
+
throw new Error(`Amount ${amountSats} sats above max ${meta.maxSendable / 1000} sats`);
|
|
64
|
+
// Step 2: Request invoice from callback
|
|
65
|
+
const sep = meta.callback.includes('?') ? '&' : '?';
|
|
66
|
+
const invoiceUrl = `${meta.callback}${sep}amount=${amountMsats}`;
|
|
67
|
+
const invoiceResp = await fetch(invoiceUrl);
|
|
68
|
+
if (!invoiceResp.ok)
|
|
69
|
+
throw new Error(`Invoice request failed: ${invoiceResp.status}`);
|
|
70
|
+
const invoiceData = await invoiceResp.json();
|
|
71
|
+
if (!invoiceData.pr)
|
|
72
|
+
throw new Error(`No invoice returned: ${invoiceData.reason || 'unknown error'}`);
|
|
73
|
+
return invoiceData.pr;
|
|
74
|
+
}
|
|
75
|
+
// --- Combined: generate invoice + debit ---
|
|
76
|
+
/**
|
|
77
|
+
* Full payment cycle: generate invoice from provider's Lightning Address,
|
|
78
|
+
* then debit customer's wallet via CLINK.
|
|
79
|
+
*/
|
|
80
|
+
export async function collectPayment(opts) {
|
|
81
|
+
const bolt11 = await generateInvoice(opts.lightningAddress, opts.amountSats);
|
|
82
|
+
return debitCustomer({
|
|
83
|
+
ndebit: opts.ndebit,
|
|
84
|
+
bolt11,
|
|
85
|
+
timeoutSeconds: opts.timeoutSeconds,
|
|
86
|
+
});
|
|
87
|
+
}
|
package/dist/customer.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Standalone P2P Customer — connects to a provider, streams results with
|
|
3
|
+
* Standalone P2P Customer — connects to a provider, streams results with CLINK debit payments.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* 2020117-customer --kind=5100 --budget=50 "Explain quantum computing"
|
|
6
|
+
* 2020117-customer --kind=5100 --budget=50 --ndebit=ndebit1... "Explain quantum computing"
|
|
7
7
|
*/
|
|
8
8
|
export {};
|
package/dist/customer.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Standalone P2P Customer — connects to a provider, streams results with
|
|
3
|
+
* Standalone P2P Customer — connects to a provider, streams results with CLINK debit payments.
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
|
-
* 2020117-customer --kind=5100 --budget=50 "Explain quantum computing"
|
|
6
|
+
* 2020117-customer --kind=5100 --budget=50 --ndebit=ndebit1... "Explain quantum computing"
|
|
7
7
|
*/
|
|
8
8
|
// --- CLI args → env (before any imports) ---
|
|
9
9
|
for (const arg of process.argv.slice(2)) {
|
|
@@ -24,26 +24,35 @@ for (const arg of process.argv.slice(2)) {
|
|
|
24
24
|
case '--max-price':
|
|
25
25
|
process.env.MAX_SATS_PER_CHUNK = val;
|
|
26
26
|
break;
|
|
27
|
+
case '--ndebit':
|
|
28
|
+
process.env.CLINK_NDEBIT = val;
|
|
29
|
+
break;
|
|
27
30
|
}
|
|
28
31
|
}
|
|
29
32
|
import { streamFromProvider } from './p2p-customer.js';
|
|
30
33
|
const KIND = Number(process.env.DVM_KIND) || 5100;
|
|
31
34
|
const BUDGET_SATS = Number(process.env.BUDGET_SATS) || 100;
|
|
32
35
|
const MAX_SATS_PER_CHUNK = Number(process.env.MAX_SATS_PER_CHUNK) || 5;
|
|
36
|
+
const NDEBIT = process.env.CLINK_NDEBIT || '';
|
|
33
37
|
async function main() {
|
|
34
38
|
const prompt = process.argv.slice(2).filter(a => !a.startsWith('--')).join(' ');
|
|
35
39
|
if (!prompt) {
|
|
36
|
-
console.error('Usage: 2020117-customer --kind=5100 --budget=50 "your prompt here"');
|
|
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)');
|
|
37
45
|
process.exit(1);
|
|
38
46
|
}
|
|
39
47
|
console.log(`[customer] Prompt: "${prompt.slice(0, 60)}..."`);
|
|
40
48
|
console.log(`[customer] Budget: ${BUDGET_SATS} sats, max price: ${MAX_SATS_PER_CHUNK} sat/chunk`);
|
|
41
|
-
// Stream from provider (handles connection, negotiation, payments internally)
|
|
49
|
+
// Stream from provider (handles connection, negotiation, CLINK payments internally)
|
|
42
50
|
let output = '';
|
|
43
51
|
for await (const chunk of streamFromProvider({
|
|
44
52
|
kind: KIND,
|
|
45
53
|
input: prompt,
|
|
46
54
|
budgetSats: BUDGET_SATS,
|
|
55
|
+
ndebit: NDEBIT,
|
|
47
56
|
maxSatsPerChunk: MAX_SATS_PER_CHUNK,
|
|
48
57
|
label: 'customer',
|
|
49
58
|
})) {
|
package/dist/p2p-customer.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared P2P customer protocol — connects to a provider via Hyperswarm,
|
|
3
|
-
* negotiates price,
|
|
3
|
+
* negotiates price, authorizes CLINK debit payments, and streams chunks.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The customer sends an ndebit authorization with the request. The provider
|
|
6
|
+
* pulls payments directly from the customer's wallet via CLINK debit —
|
|
7
|
+
* the customer does not send payment messages.
|
|
7
8
|
*
|
|
8
9
|
* Exports:
|
|
9
10
|
* - P2PStreamOptions — config interface
|
|
@@ -21,6 +22,8 @@ export interface P2PStreamOptions {
|
|
|
21
22
|
input: string;
|
|
22
23
|
/** Total budget in sats for this session */
|
|
23
24
|
budgetSats: number;
|
|
25
|
+
/** Customer's ndebit1... authorization for CLINK debit payments */
|
|
26
|
+
ndebit: string;
|
|
24
27
|
/** Maximum acceptable price per chunk in sats (default: 5) */
|
|
25
28
|
maxSatsPerChunk?: number;
|
|
26
29
|
/** Overall timeout in milliseconds (default: 120_000) */
|
|
@@ -31,8 +34,8 @@ export interface P2PStreamOptions {
|
|
|
31
34
|
params?: Record<string, unknown>;
|
|
32
35
|
}
|
|
33
36
|
/**
|
|
34
|
-
* Connect to a provider via Hyperswarm,
|
|
35
|
-
*
|
|
37
|
+
* Connect to a provider via Hyperswarm, authorize CLINK debit payments,
|
|
38
|
+
* and yield output chunks as they arrive.
|
|
36
39
|
*
|
|
37
40
|
* Creates and destroys its own temporary SwarmNode — callers do not need
|
|
38
41
|
* to manage any swarm state.
|
|
@@ -43,6 +46,7 @@ export interface P2PStreamOptions {
|
|
|
43
46
|
* kind: 5100,
|
|
44
47
|
* input: 'Explain quantum computing',
|
|
45
48
|
* budgetSats: 50,
|
|
49
|
+
* ndebit: 'ndebit1...',
|
|
46
50
|
* })) {
|
|
47
51
|
* process.stdout.write(chunk)
|
|
48
52
|
* }
|