2020117-agent 0.3.5 → 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 { initClinkAgent, collectPayment } 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)
@@ -341,8 +341,10 @@ async function startSwarmListener(label) {
341
341
  node.send(socket, { type: 'error', id: msg.id, message: 'Provider has no Lightning Address configured' });
342
342
  return;
343
343
  }
344
- if (!msg.ndebit) {
345
- node.send(socket, { type: 'error', id: msg.id, message: 'session_start requires ndebit authorization' });
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' });
346
348
  return;
347
349
  }
348
350
  const satsPerMinute = state.skill?.pricing?.sats_per_minute
@@ -352,13 +354,14 @@ async function startSwarmListener(label) {
352
354
  const BILLING_INTERVAL_MIN = 10;
353
355
  const debitAmount = satsPerMinute * BILLING_INTERVAL_MIN;
354
356
  const sessionId = randomBytes(8).toString('hex');
355
- 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)`);
356
358
  const session = {
357
359
  socket,
358
360
  peerId,
359
361
  sessionId,
360
362
  satsPerMinute,
361
- ndebit: msg.ndebit,
363
+ paymentMethod,
364
+ ndebit: msg.ndebit || '',
362
365
  totalEarned: 0,
363
366
  startedAt: Date.now(),
364
367
  lastDebitAt: Date.now(),
@@ -366,68 +369,38 @@ async function startSwarmListener(label) {
366
369
  timeoutTimer: null,
367
370
  };
368
371
  activeSessions.set(sessionId, session);
369
- // Debit first 10 minutes immediately (prepaid model)
370
- let firstDebit;
371
- try {
372
- firstDebit = await collectPayment({
373
- ndebit: session.ndebit,
374
- lightningAddress: LIGHTNING_ADDRESS,
375
- amountSats: debitAmount,
376
- });
377
- }
378
- catch (e) {
379
- console.warn(`[${label}] Session ${sessionId}: first debit error: ${e.message}`);
380
- node.send(socket, { type: 'error', id: msg.id, message: `Payment error: ${e.message}` });
381
- activeSessions.delete(sessionId);
382
- return;
383
- }
384
- if (!firstDebit.ok) {
385
- console.warn(`[${label}] Session ${sessionId}: first debit failed: ${firstDebit.error}`);
386
- node.send(socket, { type: 'error', id: msg.id, message: `Payment failed: ${firstDebit.error}` });
387
- activeSessions.delete(sessionId);
388
- return;
389
- }
390
- session.totalEarned += debitAmount;
391
- session.lastDebitAt = Date.now();
392
- console.log(`[${label}] Session ${sessionId}: first ${BILLING_INTERVAL_MIN}min paid (${debitAmount} sats)`);
393
- node.send(socket, {
394
- type: 'session_ack',
395
- id: msg.id,
396
- session_id: sessionId,
397
- sats_per_minute: satsPerMinute,
398
- });
399
- // Notify customer of the debit
400
- node.send(socket, {
401
- type: 'session_tick_ack',
402
- id: sessionId,
403
- session_id: sessionId,
404
- amount: debitAmount,
405
- balance: msg.budget ? msg.budget - session.totalEarned : undefined,
406
- });
407
- // Debit every 10 minutes
408
- session.debitTimer = setInterval(async () => {
409
- let debit;
372
+ if (paymentMethod === 'ndebit') {
373
+ // --- CLINK pull mode: provider debits customer immediately ---
374
+ let firstDebit;
410
375
  try {
411
- debit = await collectPayment({
376
+ firstDebit = await collectPayment({
412
377
  ndebit: session.ndebit,
413
378
  lightningAddress: LIGHTNING_ADDRESS,
414
379
  amountSats: debitAmount,
415
380
  });
416
381
  }
417
382
  catch (e) {
418
- console.log(`[${label}] Session ${sessionId}: debit error (${e.message}) — ending session`);
419
- endSession(node, session, label);
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);
420
386
  return;
421
387
  }
422
- if (!debit.ok) {
423
- console.log(`[${label}] Session ${sessionId}: debit failed (${debit.error}) — ending session`);
424
- endSession(node, session, label);
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);
425
392
  return;
426
393
  }
427
394
  session.totalEarned += debitAmount;
428
395
  session.lastDebitAt = Date.now();
429
- console.log(`[${label}] Session ${sessionId}: debit OK (+${debitAmount}, total: ${session.totalEarned} sats)`);
430
- // Notify customer
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
+ });
431
404
  node.send(socket, {
432
405
  type: 'session_tick_ack',
433
406
  id: sessionId,
@@ -435,7 +408,100 @@ async function startSwarmListener(label) {
435
408
  amount: debitAmount,
436
409
  balance: msg.budget ? msg.budget - session.totalEarned : undefined,
437
410
  });
438
- }, BILLING_INTERVAL_MIN * 60_000);
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);
442
+ }
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;
454
+ try {
455
+ firstInvoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
456
+ }
457
+ catch (e) {
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);
461
+ return;
462
+ }
463
+ const tickId = randomBytes(4).toString('hex');
464
+ node.send(socket, {
465
+ type: 'session_tick',
466
+ id: tickId,
467
+ session_id: sessionId,
468
+ bolt11: firstInvoice,
469
+ amount: debitAmount,
470
+ });
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
+ }
494
+ return;
495
+ }
496
+ // Handle invoice payment confirmation (invoice mode only)
497
+ if (msg.type === 'session_tick_ack' && msg.preimage) {
498
+ const session = activeSessions.get(msg.session_id || '');
499
+ if (!session || session.paymentMethod !== 'invoice')
500
+ return;
501
+ const amount = msg.amount || 0;
502
+ session.totalEarned += amount;
503
+ session.lastDebitAt = Date.now();
504
+ console.log(`[${label}] Session ${session.sessionId}: payment received (+${amount}, total: ${session.totalEarned} sats)`);
439
505
  return;
440
506
  }
441
507
  if (msg.type === 'session_end') {
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 CLINK debit payments.
4
+ * per-minute payments (CLINK ndebit or Lightning invoice).
5
5
  *
6
6
  * Features:
7
- * - Provider pulls per-minute payments via CLINK debit (ndebit authorization)
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 CLINK debit payments.
4
+ * per-minute payments (CLINK ndebit or Lightning invoice).
5
5
  *
6
6
  * Features:
7
- * - Provider pulls per-minute payments via CLINK debit (ndebit authorization)
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
  *
@@ -44,7 +44,7 @@ for (const arg of process.argv.slice(2)) {
44
44
  }
45
45
  import { SwarmNode, topicFromKind } from './swarm.js';
46
46
  import { queryProviderSkill } from './p2p-customer.js';
47
- import { loadNdebit } 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';
@@ -112,7 +112,7 @@ function sendAndWait(msg, timeoutMs) {
112
112
  }
113
113
  // --- 6. Message handler ---
114
114
  function setupMessageHandler() {
115
- state.node.on('message', (msg) => {
115
+ state.node.on('message', async (msg) => {
116
116
  // Handle chunked HTTP responses — reassemble before resolving
117
117
  if (msg.type === 'http_response' && msg.chunk_total && msg.chunk_total > 1) {
118
118
  const id = msg.id;
@@ -162,8 +162,37 @@ function setupMessageHandler() {
162
162
  cleanup();
163
163
  break;
164
164
  }
165
+ case 'session_tick': {
166
+ // Invoice mode: provider sent a bolt11 invoice — pay it
167
+ if (msg.bolt11 && msg.amount !== undefined) {
168
+ const amount = msg.amount;
169
+ if (state.totalSpent + amount > BUDGET) {
170
+ log(`Budget exhausted (need ${amount}, remaining ${remainingSats()}) — ending session`);
171
+ endSession();
172
+ break;
173
+ }
174
+ log(`Paying invoice: ${amount} sats...`);
175
+ const payResult = await walletPayInvoice(msg.bolt11);
176
+ if (payResult.ok) {
177
+ state.totalSpent += amount;
178
+ log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
179
+ state.node.send(state.socket, {
180
+ type: 'session_tick_ack',
181
+ id: msg.id,
182
+ session_id: state.sessionId,
183
+ preimage: payResult.preimage,
184
+ amount,
185
+ });
186
+ }
187
+ else {
188
+ warn(`Payment failed: ${payResult.error} — ending session`);
189
+ endSession();
190
+ }
191
+ }
192
+ break;
193
+ }
165
194
  case 'session_tick_ack': {
166
- // Provider debited our wallet and is reporting the result
195
+ // Ndebit mode: provider debited our wallet and is reporting the result
167
196
  if (msg.amount !== undefined) {
168
197
  state.totalSpent += msg.amount;
169
198
  }
@@ -638,22 +667,37 @@ async function main() {
638
667
  state.satsPerMinute = satsPerMinute;
639
668
  log(`Pricing: ${satsPerMinute} sats/min`);
640
669
  log(`Budget: ${BUDGET} sats (~${Math.floor(BUDGET / satsPerMinute)} min)`);
641
- // 4. Verify CLINK ndebit authorization
642
- if (!NDEBIT) {
643
- warn('No ndebit authorization. Provide --ndebit=ndebit1... or set CLINK_NDEBIT env var.');
644
- warn('Get your ndebit from your CLINK-compatible wallet (e.g. ShockWallet → Connections).');
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)');
645
689
  await node.destroy();
646
690
  process.exit(1);
647
691
  }
648
- log(`Payment: CLINK debit (provider pulls per-minute)`);
649
- // 5. Send session_start with ndebit, wait for session_ack
692
+ // 5. Send session_start, wait for session_ack
650
693
  const startId = randomBytes(4).toString('hex');
651
694
  const ackResp = await sendAndWait({
652
695
  type: 'session_start',
653
696
  id: startId,
654
697
  budget: BUDGET,
655
698
  sats_per_minute: satsPerMinute,
656
- ndebit: NDEBIT,
699
+ payment_method: paymentMethod,
700
+ ...(paymentMethod === 'ndebit' ? { ndebit: NDEBIT } : {}),
657
701
  }, 15_000);
658
702
  if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
659
703
  warn(`Unexpected response: ${ackResp.type}`);
@@ -668,7 +712,7 @@ async function main() {
668
712
  log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
669
713
  }
670
714
  log(`Session started: ${state.sessionId}`);
671
- log(`Provider will debit ${state.satsPerMinute} sats/min via CLINK`);
715
+ log(`Billing: ${state.satsPerMinute} sats/min via ${paymentMethod}`);
672
716
  // 6. Start HTTP proxy
673
717
  try {
674
718
  await startHttpProxy();
package/dist/swarm.d.ts CHANGED
@@ -46,6 +46,9 @@ export interface SwarmMessage {
46
46
  balance?: number;
47
47
  duration_s?: number;
48
48
  ndebit?: string;
49
+ payment_method?: 'ndebit' | 'invoice';
50
+ bolt11?: string;
51
+ preimage?: string;
49
52
  method?: string;
50
53
  path?: string;
51
54
  headers?: Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "2020117 agent runtime — API polling + Hyperswarm P2P + CLINK Lightning payments",
5
5
  "type": "module",
6
6
  "bin": {