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 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 + Cashu micro-payments)
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 + Cashu micro-payments)
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 { waitForPayment, handlePayment, handleStop, streamToCustomer, batchClaim } from './p2p-provider.js';
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 { peekToken } from './cashu.js';
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. Platform registration + heartbeat
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
- // 3. Async inbox poller
173
+ // 4. Async inbox poller
164
174
  startInboxPoller(label);
165
- // 4. P2P swarm listener
175
+ // 5. P2P swarm listener
166
176
  await startSwarmListener(label);
167
- // 5. Graceful shutdown
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 Cashu streaming payments.
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
- tokens: [],
421
+ ndebit: msg.ndebit,
397
422
  totalEarned: 0,
398
423
  startedAt: Date.now(),
399
- lastTickAt: Date.now(),
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
- return;
429
- }
430
- if (msg.type === 'session_tick') {
431
- const session = activeSessions.get(msg.session_id || '');
432
- if (!session) {
433
- node.send(socket, { type: 'error', id: msg.id, message: 'Unknown session' });
434
- return;
435
- }
436
- if (!msg.token) {
437
- node.send(socket, { type: 'error', id: msg.id, message: 'Tick missing token' });
438
- return;
439
- }
440
- try {
441
- const peek = peekToken(msg.token);
442
- session.tokens.push(msg.token);
443
- session.totalEarned += peek.amount;
444
- session.lastTickAt = Date.now();
445
- console.log(`[${label}] Session ${session.sessionId}: tick ${peek.amount} sats (total: ${session.totalEarned})`);
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: msg.id,
449
- session_id: session.sessionId,
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
- tokens: [],
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
- // Wait for first payment
672
- const paid = await waitForPayment(job, msg.id, node, label, PAYMENT_TIMEOUT);
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}: no initial payment, aborting`);
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 — claim any earned tokens immediately
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 — claiming P2P job ${jobId} tokens`);
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
- timeoutMs: PAYMENT_TIMEOUT,
770
+ lightningAddress: LIGHTNING_ADDRESS,
738
771
  label,
739
772
  });
740
- await batchClaim(job.tokens, msg.id, label);
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
- clearInterval(session.timeoutTimer);
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 ---
@@ -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
+ }
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Standalone P2P Customer — connects to a provider, streams results with Cashu payments.
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 Cashu payments.
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
  })) {
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Shared P2P customer protocol — connects to a provider via Hyperswarm,
3
- * negotiates price, pays with Cashu micro-tokens, and streams chunks.
3
+ * negotiates price, authorizes CLINK debit payments, and streams chunks.
4
4
  *
5
- * Extracted from agent.ts delegateP2PStream(), customer.ts, and pipeline.ts runStep()
6
- * to eliminate triple duplication of the same protocol logic.
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, negotiate price, pay with Cashu
35
- * micro-tokens, and yield output chunks as they arrive.
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
  * }