2020117-agent 0.3.4 → 0.3.6

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.js CHANGED
@@ -69,7 +69,7 @@ import { randomBytes } from 'crypto';
69
69
  import { SwarmNode, topicFromKind } from './swarm.js';
70
70
  import { createProcessor } from './processor.js';
71
71
  import { hasApiKey, loadAgentName, registerService, startHeartbeatLoop, getInbox, acceptJob, sendFeedback, submitResult, createJob, getJob, getProfile, reportSession, } from './api.js';
72
- import { generateInvoice } from './clink.js';
72
+ import { initClinkAgent, collectPayment, generateInvoice } from './clink.js';
73
73
  import { readFileSync } from 'fs';
74
74
  import WebSocket from 'ws';
75
75
  // Polyfill global WebSocket for Node.js < 22 (needed by @shocknet/clink-sdk)
@@ -161,7 +161,12 @@ async function main() {
161
161
  console.log(`[${label}] Lightning Address loaded from platform: ${LIGHTNING_ADDRESS}`);
162
162
  }
163
163
  }
164
- // 3. Platform registration + heartbeat
164
+ // 3. Initialize CLINK agent identity (for P2P session debit)
165
+ if (LIGHTNING_ADDRESS) {
166
+ const { pubkey } = initClinkAgent();
167
+ console.log(`[${label}] CLINK: ${LIGHTNING_ADDRESS} (agent pubkey: ${pubkey.slice(0, 16)}...)`);
168
+ }
169
+ // 4. Platform registration + heartbeat
165
170
  await setupPlatform(label);
166
171
  // 5. Async inbox poller
167
172
  startInboxPoller(label);
@@ -336,6 +341,12 @@ async function startSwarmListener(label) {
336
341
  node.send(socket, { type: 'error', id: msg.id, message: 'Provider has no Lightning Address configured' });
337
342
  return;
338
343
  }
344
+ // Negotiate payment method: customer requests "invoice" or "ndebit" (default)
345
+ const paymentMethod = msg.payment_method || (msg.ndebit ? 'ndebit' : 'invoice');
346
+ if (paymentMethod === 'ndebit' && !msg.ndebit) {
347
+ node.send(socket, { type: 'error', id: msg.id, message: 'ndebit payment requires ndebit authorization' });
348
+ return;
349
+ }
339
350
  const satsPerMinute = state.skill?.pricing?.sats_per_minute
340
351
  || Number(process.env.SATS_PER_MINUTE)
341
352
  || msg.sats_per_minute
@@ -343,12 +354,14 @@ async function startSwarmListener(label) {
343
354
  const BILLING_INTERVAL_MIN = 10;
344
355
  const debitAmount = satsPerMinute * BILLING_INTERVAL_MIN;
345
356
  const sessionId = randomBytes(8).toString('hex');
346
- console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, billing every ${BILLING_INTERVAL_MIN}min (${debitAmount} sats)`);
357
+ console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, payment=${paymentMethod}, billing every ${BILLING_INTERVAL_MIN}min (${debitAmount} sats)`);
347
358
  const session = {
348
359
  socket,
349
360
  peerId,
350
361
  sessionId,
351
362
  satsPerMinute,
363
+ paymentMethod,
364
+ ndebit: msg.ndebit || '',
352
365
  totalEarned: 0,
353
366
  startedAt: Date.now(),
354
367
  lastDebitAt: Date.now(),
@@ -356,60 +369,134 @@ async function startSwarmListener(label) {
356
369
  timeoutTimer: null,
357
370
  };
358
371
  activeSessions.set(sessionId, session);
359
- // Generate first invoice and send to customer for payment
360
- let firstInvoice;
361
- try {
362
- firstInvoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
363
- }
364
- catch (e) {
365
- console.warn(`[${label}] Session ${sessionId}: invoice error: ${e.message}`);
366
- node.send(socket, { type: 'error', id: msg.id, message: `Invoice error: ${e.message}` });
367
- activeSessions.delete(sessionId);
368
- return;
372
+ if (paymentMethod === 'ndebit') {
373
+ // --- CLINK pull mode: provider debits customer immediately ---
374
+ let firstDebit;
375
+ try {
376
+ firstDebit = await collectPayment({
377
+ ndebit: session.ndebit,
378
+ lightningAddress: LIGHTNING_ADDRESS,
379
+ amountSats: debitAmount,
380
+ });
381
+ }
382
+ catch (e) {
383
+ console.warn(`[${label}] Session ${sessionId}: first debit error: ${e.message}`);
384
+ node.send(socket, { type: 'error', id: msg.id, message: `Payment error: ${e.message}` });
385
+ activeSessions.delete(sessionId);
386
+ return;
387
+ }
388
+ if (!firstDebit.ok) {
389
+ console.warn(`[${label}] Session ${sessionId}: first debit failed: ${firstDebit.error}`);
390
+ node.send(socket, { type: 'error', id: msg.id, message: `Payment failed: ${firstDebit.error}` });
391
+ activeSessions.delete(sessionId);
392
+ return;
393
+ }
394
+ session.totalEarned += debitAmount;
395
+ session.lastDebitAt = Date.now();
396
+ console.log(`[${label}] Session ${sessionId}: first ${BILLING_INTERVAL_MIN}min paid (${debitAmount} sats)`);
397
+ node.send(socket, {
398
+ type: 'session_ack',
399
+ id: msg.id,
400
+ session_id: sessionId,
401
+ sats_per_minute: satsPerMinute,
402
+ payment_method: 'ndebit',
403
+ });
404
+ node.send(socket, {
405
+ type: 'session_tick_ack',
406
+ id: sessionId,
407
+ session_id: sessionId,
408
+ amount: debitAmount,
409
+ balance: msg.budget ? msg.budget - session.totalEarned : undefined,
410
+ });
411
+ // Recurring debit every 10 minutes
412
+ session.debitTimer = setInterval(async () => {
413
+ let debit;
414
+ try {
415
+ debit = await collectPayment({
416
+ ndebit: session.ndebit,
417
+ lightningAddress: LIGHTNING_ADDRESS,
418
+ amountSats: debitAmount,
419
+ });
420
+ }
421
+ catch (e) {
422
+ console.log(`[${label}] Session ${sessionId}: debit error (${e.message}) — ending session`);
423
+ endSession(node, session, label);
424
+ return;
425
+ }
426
+ if (!debit.ok) {
427
+ console.log(`[${label}] Session ${sessionId}: debit failed (${debit.error}) — ending session`);
428
+ endSession(node, session, label);
429
+ return;
430
+ }
431
+ session.totalEarned += debitAmount;
432
+ session.lastDebitAt = Date.now();
433
+ console.log(`[${label}] Session ${sessionId}: debit OK (+${debitAmount}, total: ${session.totalEarned} sats)`);
434
+ node.send(socket, {
435
+ type: 'session_tick_ack',
436
+ id: sessionId,
437
+ session_id: sessionId,
438
+ amount: debitAmount,
439
+ balance: msg.budget ? msg.budget - session.totalEarned : undefined,
440
+ });
441
+ }, BILLING_INTERVAL_MIN * 60_000);
369
442
  }
370
- // Accept session first, then request payment
371
- node.send(socket, {
372
- type: 'session_ack',
373
- id: msg.id,
374
- session_id: sessionId,
375
- sats_per_minute: satsPerMinute,
376
- });
377
- // Send invoice to customer — wait for session_tick_ack with preimage
378
- const tickId = randomBytes(4).toString('hex');
379
- node.send(socket, {
380
- type: 'session_tick',
381
- id: tickId,
382
- session_id: sessionId,
383
- bolt11: firstInvoice,
384
- amount: debitAmount,
385
- });
386
- console.log(`[${label}] Session ${sessionId}: waiting for first payment (${debitAmount} sats)`);
387
- // Billing every 10 minutes — send invoice, wait for customer to pay
388
- session.debitTimer = setInterval(async () => {
389
- let invoice;
443
+ else {
444
+ // --- Invoice push mode: provider sends bolt11, customer pays ---
445
+ node.send(socket, {
446
+ type: 'session_ack',
447
+ id: msg.id,
448
+ session_id: sessionId,
449
+ sats_per_minute: satsPerMinute,
450
+ payment_method: 'invoice',
451
+ });
452
+ // Generate and send first invoice
453
+ let firstInvoice;
390
454
  try {
391
- invoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
455
+ firstInvoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
392
456
  }
393
457
  catch (e) {
394
- console.log(`[${label}] Session ${sessionId}: invoice error (${e.message}) — ending session`);
395
- endSession(node, session, label);
458
+ console.warn(`[${label}] Session ${sessionId}: invoice error: ${e.message}`);
459
+ node.send(socket, { type: 'error', id: msg.id, message: `Invoice error: ${e.message}` });
460
+ activeSessions.delete(sessionId);
396
461
  return;
397
462
  }
398
- const tid = randomBytes(4).toString('hex');
463
+ const tickId = randomBytes(4).toString('hex');
399
464
  node.send(socket, {
400
465
  type: 'session_tick',
401
- id: tid,
466
+ id: tickId,
402
467
  session_id: sessionId,
403
- bolt11: invoice,
468
+ bolt11: firstInvoice,
404
469
  amount: debitAmount,
405
470
  });
406
- console.log(`[${label}] Session ${sessionId}: sent invoice for ${debitAmount} sats`);
407
- }, BILLING_INTERVAL_MIN * 60_000);
471
+ console.log(`[${label}] Session ${sessionId}: sent first invoice (${debitAmount} sats)`);
472
+ // Recurring invoices every 10 minutes
473
+ session.debitTimer = setInterval(async () => {
474
+ let invoice;
475
+ try {
476
+ invoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
477
+ }
478
+ catch (e) {
479
+ console.log(`[${label}] Session ${sessionId}: invoice error (${e.message}) — ending session`);
480
+ endSession(node, session, label);
481
+ return;
482
+ }
483
+ const tid = randomBytes(4).toString('hex');
484
+ node.send(socket, {
485
+ type: 'session_tick',
486
+ id: tid,
487
+ session_id: sessionId,
488
+ bolt11: invoice,
489
+ amount: debitAmount,
490
+ });
491
+ console.log(`[${label}] Session ${sessionId}: sent invoice for ${debitAmount} sats`);
492
+ }, BILLING_INTERVAL_MIN * 60_000);
493
+ }
408
494
  return;
409
495
  }
410
- if (msg.type === 'session_tick_ack') {
496
+ // Handle invoice payment confirmation (invoice mode only)
497
+ if (msg.type === 'session_tick_ack' && msg.preimage) {
411
498
  const session = activeSessions.get(msg.session_id || '');
412
- if (!session)
499
+ if (!session || session.paymentMethod !== 'invoice')
413
500
  return;
414
501
  const amount = msg.amount || 0;
415
502
  session.totalEarned += amount;
package/dist/session.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * P2P Session Client — "rent" a provider's service over Hyperswarm with
4
- * per-minute wallet payments.
4
+ * per-minute payments (CLINK ndebit or Lightning invoice).
5
5
  *
6
6
  * Features:
7
- * - Customer pays provider's invoices via built-in wallet
7
+ * - Payment negotiation: ndebit (provider pulls) or invoice (customer pushes)
8
8
  * - HTTP proxy server for browser-based access to provider APIs
9
9
  * - Interactive CLI REPL (generate, status, skill, help, quit)
10
10
  *
package/dist/session.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * P2P Session Client — "rent" a provider's service over Hyperswarm with
4
- * per-minute wallet payments.
4
+ * per-minute payments (CLINK ndebit or Lightning invoice).
5
5
  *
6
6
  * Features:
7
- * - Customer pays provider's invoices via built-in wallet
7
+ * - Payment negotiation: ndebit (provider pulls) or invoice (customer pushes)
8
8
  * - HTTP proxy server for browser-based access to provider APIs
9
9
  * - Interactive CLI REPL (generate, status, skill, help, quit)
10
10
  *
@@ -39,21 +39,25 @@ for (const arg of process.argv.slice(2)) {
39
39
  break;
40
40
  case '--ndebit':
41
41
  process.env.CLINK_NDEBIT = val;
42
- break; // legacy, ignored
42
+ break;
43
43
  }
44
44
  }
45
45
  import { SwarmNode, topicFromKind } from './swarm.js';
46
46
  import { queryProviderSkill } from './p2p-customer.js';
47
- import { walletPayInvoice, walletGetBalance } from './api.js';
47
+ import { loadNdebit, walletPayInvoice, walletGetBalance, hasApiKey } from './api.js';
48
48
  import { randomBytes } from 'crypto';
49
49
  import { createServer } from 'http';
50
50
  import { createInterface } from 'readline';
51
51
  import { mkdirSync, writeFileSync } from 'fs';
52
52
  import { WebSocketServer, WebSocket as WsWebSocket } from 'ws';
53
+ // Polyfill global WebSocket for Node.js < 22 (needed by @shocknet/clink-sdk)
54
+ if (!globalThis.WebSocket)
55
+ globalThis.WebSocket = WsWebSocket;
53
56
  // --- Config ---
54
57
  const KIND = Number(process.env.DVM_KIND) || 5200;
55
58
  const BUDGET = Number(process.env.BUDGET_SATS) || 500;
56
59
  const PORT = Number(process.env.SESSION_PORT) || 8080;
60
+ const NDEBIT = process.env.CLINK_NDEBIT || loadNdebit() || '';
57
61
  const TICK_INTERVAL_MS = 60_000;
58
62
  const HTTP_TIMEOUT_MS = 60_000;
59
63
  const state = {
@@ -159,7 +163,7 @@ function setupMessageHandler() {
159
163
  break;
160
164
  }
161
165
  case 'session_tick': {
162
- // Provider sent an invoice for the next billing period — pay it
166
+ // Invoice mode: provider sent a bolt11 invoice — pay it
163
167
  if (msg.bolt11 && msg.amount !== undefined) {
164
168
  const amount = msg.amount;
165
169
  if (state.totalSpent + amount > BUDGET) {
@@ -188,7 +192,7 @@ function setupMessageHandler() {
188
192
  break;
189
193
  }
190
194
  case 'session_tick_ack': {
191
- // Legacy: provider-pull model notification (backward compat)
195
+ // Ndebit mode: provider debited our wallet and is reporting the result
192
196
  if (msg.amount !== undefined) {
193
197
  state.totalSpent += msg.amount;
194
198
  }
@@ -663,13 +667,28 @@ async function main() {
663
667
  state.satsPerMinute = satsPerMinute;
664
668
  log(`Pricing: ${satsPerMinute} sats/min`);
665
669
  log(`Budget: ${BUDGET} sats (~${Math.floor(BUDGET / satsPerMinute)} min)`);
666
- // 4. Check wallet balance
667
- const balance = await walletGetBalance();
668
- if (balance < BUDGET) {
669
- warn(`Wallet balance (${balance} sats) is less than budget (${BUDGET} sats).`);
670
- warn(`Fund your wallet: curl -X POST ${process.env.API_2020117_URL || 'https://2020117.xyz'}/api/wallet/invoice -H "Authorization: Bearer ..." -d '{"amount_sats":${BUDGET}}'`);
670
+ // 4. Negotiate payment method: ndebit (CLINK pull) or invoice (wallet push)
671
+ let paymentMethod;
672
+ if (NDEBIT) {
673
+ paymentMethod = 'ndebit';
674
+ log(`Payment: CLINK ndebit (provider pulls per-minute)`);
675
+ }
676
+ else if (hasApiKey()) {
677
+ paymentMethod = 'invoice';
678
+ const balance = await walletGetBalance();
679
+ if (balance < BUDGET) {
680
+ warn(`Wallet balance (${balance} sats) is less than budget (${BUDGET} sats).`);
681
+ warn(`Fund your wallet: POST ${process.env.API_2020117_URL || 'https://2020117.xyz'}/api/wallet/invoice`);
682
+ }
683
+ log(`Payment: invoice (wallet pays provider, balance: ${balance} sats)`);
684
+ }
685
+ else {
686
+ warn('No payment method available.');
687
+ warn(' Option 1: --ndebit=ndebit1... (CLINK wallet authorization)');
688
+ warn(' Option 2: --agent=NAME (built-in wallet via API key)');
689
+ await node.destroy();
690
+ process.exit(1);
671
691
  }
672
- log(`Wallet balance: ${balance} sats`);
673
692
  // 5. Send session_start, wait for session_ack
674
693
  const startId = randomBytes(4).toString('hex');
675
694
  const ackResp = await sendAndWait({
@@ -677,6 +696,8 @@ async function main() {
677
696
  id: startId,
678
697
  budget: BUDGET,
679
698
  sats_per_minute: satsPerMinute,
699
+ payment_method: paymentMethod,
700
+ ...(paymentMethod === 'ndebit' ? { ndebit: NDEBIT } : {}),
680
701
  }, 15_000);
681
702
  if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
682
703
  warn(`Unexpected response: ${ackResp.type}`);
@@ -691,7 +712,7 @@ async function main() {
691
712
  log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
692
713
  }
693
714
  log(`Session started: ${state.sessionId}`);
694
- log(`Billing: ${state.satsPerMinute} sats/min (wallet → provider invoice)`);
715
+ log(`Billing: ${state.satsPerMinute} sats/min via ${paymentMethod}`);
695
716
  // 6. Start HTTP proxy
696
717
  try {
697
718
  await startHttpProxy();
package/dist/swarm.d.ts CHANGED
@@ -46,6 +46,7 @@ export interface SwarmMessage {
46
46
  balance?: number;
47
47
  duration_s?: number;
48
48
  ndebit?: string;
49
+ payment_method?: 'ndebit' | 'invoice';
49
50
  bolt11?: string;
50
51
  preimage?: string;
51
52
  method?: string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.3.4",
4
- "description": "2020117 agent runtime — API polling + Hyperswarm P2P + wallet Lightning payments",
3
+ "version": "0.3.6",
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",