2020117-agent 0.3.4 → 0.3.5

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 } 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,10 @@ 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
+ if (!msg.ndebit) {
345
+ node.send(socket, { type: 'error', id: msg.id, message: 'session_start requires ndebit authorization' });
346
+ return;
347
+ }
339
348
  const satsPerMinute = state.skill?.pricing?.sats_per_minute
340
349
  || Number(process.env.SATS_PER_MINUTE)
341
350
  || msg.sats_per_minute
@@ -349,6 +358,7 @@ async function startSwarmListener(label) {
349
358
  peerId,
350
359
  sessionId,
351
360
  satsPerMinute,
361
+ ndebit: msg.ndebit,
352
362
  totalEarned: 0,
353
363
  startedAt: Date.now(),
354
364
  lastDebitAt: Date.now(),
@@ -356,67 +366,78 @@ async function startSwarmListener(label) {
356
366
  timeoutTimer: null,
357
367
  };
358
368
  activeSessions.set(sessionId, session);
359
- // Generate first invoice and send to customer for payment
360
- let firstInvoice;
369
+ // Debit first 10 minutes immediately (prepaid model)
370
+ let firstDebit;
361
371
  try {
362
- firstInvoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
372
+ firstDebit = await collectPayment({
373
+ ndebit: session.ndebit,
374
+ lightningAddress: LIGHTNING_ADDRESS,
375
+ amountSats: debitAmount,
376
+ });
363
377
  }
364
378
  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}` });
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}` });
367
381
  activeSessions.delete(sessionId);
368
382
  return;
369
383
  }
370
- // Accept session first, then request payment
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)`);
371
393
  node.send(socket, {
372
394
  type: 'session_ack',
373
395
  id: msg.id,
374
396
  session_id: sessionId,
375
397
  sats_per_minute: satsPerMinute,
376
398
  });
377
- // Send invoice to customer wait for session_tick_ack with preimage
378
- const tickId = randomBytes(4).toString('hex');
399
+ // Notify customer of the debit
379
400
  node.send(socket, {
380
- type: 'session_tick',
381
- id: tickId,
401
+ type: 'session_tick_ack',
402
+ id: sessionId,
382
403
  session_id: sessionId,
383
- bolt11: firstInvoice,
384
404
  amount: debitAmount,
405
+ balance: msg.budget ? msg.budget - session.totalEarned : undefined,
385
406
  });
386
- console.log(`[${label}] Session ${sessionId}: waiting for first payment (${debitAmount} sats)`);
387
- // Billing every 10 minutes — send invoice, wait for customer to pay
407
+ // Debit every 10 minutes
388
408
  session.debitTimer = setInterval(async () => {
389
- let invoice;
409
+ let debit;
390
410
  try {
391
- invoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
411
+ debit = await collectPayment({
412
+ ndebit: session.ndebit,
413
+ lightningAddress: LIGHTNING_ADDRESS,
414
+ amountSats: debitAmount,
415
+ });
392
416
  }
393
417
  catch (e) {
394
- console.log(`[${label}] Session ${sessionId}: invoice error (${e.message}) — ending session`);
418
+ console.log(`[${label}] Session ${sessionId}: debit error (${e.message}) — ending session`);
395
419
  endSession(node, session, label);
396
420
  return;
397
421
  }
398
- const tid = randomBytes(4).toString('hex');
422
+ if (!debit.ok) {
423
+ console.log(`[${label}] Session ${sessionId}: debit failed (${debit.error}) — ending session`);
424
+ endSession(node, session, label);
425
+ return;
426
+ }
427
+ session.totalEarned += debitAmount;
428
+ session.lastDebitAt = Date.now();
429
+ console.log(`[${label}] Session ${sessionId}: debit OK (+${debitAmount}, total: ${session.totalEarned} sats)`);
430
+ // Notify customer
399
431
  node.send(socket, {
400
- type: 'session_tick',
401
- id: tid,
432
+ type: 'session_tick_ack',
433
+ id: sessionId,
402
434
  session_id: sessionId,
403
- bolt11: invoice,
404
435
  amount: debitAmount,
436
+ balance: msg.budget ? msg.budget - session.totalEarned : undefined,
405
437
  });
406
- console.log(`[${label}] Session ${sessionId}: sent invoice for ${debitAmount} sats`);
407
438
  }, BILLING_INTERVAL_MIN * 60_000);
408
439
  return;
409
440
  }
410
- if (msg.type === 'session_tick_ack') {
411
- const session = activeSessions.get(msg.session_id || '');
412
- if (!session)
413
- return;
414
- const amount = msg.amount || 0;
415
- session.totalEarned += amount;
416
- session.lastDebitAt = Date.now();
417
- console.log(`[${label}] Session ${session.sessionId}: payment received (+${amount}, total: ${session.totalEarned} sats)`);
418
- return;
419
- }
420
441
  if (msg.type === 'session_end') {
421
442
  const session = activeSessions.get(msg.session_id || '');
422
443
  if (!session)
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 CLINK debit payments.
5
5
  *
6
6
  * Features:
7
- * - Customer pays provider's invoices via built-in wallet
7
+ * - Provider pulls per-minute payments via CLINK debit (ndebit authorization)
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 CLINK debit payments.
5
5
  *
6
6
  * Features:
7
- * - Customer pays provider's invoices via built-in wallet
7
+ * - Provider pulls per-minute payments via CLINK debit (ndebit authorization)
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 } 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 = {
@@ -108,7 +112,7 @@ function sendAndWait(msg, timeoutMs) {
108
112
  }
109
113
  // --- 6. Message handler ---
110
114
  function setupMessageHandler() {
111
- state.node.on('message', async (msg) => {
115
+ state.node.on('message', (msg) => {
112
116
  // Handle chunked HTTP responses — reassemble before resolving
113
117
  if (msg.type === 'http_response' && msg.chunk_total && msg.chunk_total > 1) {
114
118
  const id = msg.id;
@@ -158,37 +162,8 @@ function setupMessageHandler() {
158
162
  cleanup();
159
163
  break;
160
164
  }
161
- case 'session_tick': {
162
- // Provider sent an invoice for the next billing period — pay it
163
- if (msg.bolt11 && msg.amount !== undefined) {
164
- const amount = msg.amount;
165
- if (state.totalSpent + amount > BUDGET) {
166
- log(`Budget exhausted (need ${amount}, remaining ${remainingSats()}) — ending session`);
167
- endSession();
168
- break;
169
- }
170
- log(`Paying invoice: ${amount} sats...`);
171
- const payResult = await walletPayInvoice(msg.bolt11);
172
- if (payResult.ok) {
173
- state.totalSpent += amount;
174
- log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
175
- state.node.send(state.socket, {
176
- type: 'session_tick_ack',
177
- id: msg.id,
178
- session_id: state.sessionId,
179
- preimage: payResult.preimage,
180
- amount,
181
- });
182
- }
183
- else {
184
- warn(`Payment failed: ${payResult.error} — ending session`);
185
- endSession();
186
- }
187
- }
188
- break;
189
- }
190
165
  case 'session_tick_ack': {
191
- // Legacy: provider-pull model notification (backward compat)
166
+ // Provider debited our wallet and is reporting the result
192
167
  if (msg.amount !== undefined) {
193
168
  state.totalSpent += msg.amount;
194
169
  }
@@ -663,20 +638,22 @@ async function main() {
663
638
  state.satsPerMinute = satsPerMinute;
664
639
  log(`Pricing: ${satsPerMinute} sats/min`);
665
640
  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}}'`);
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).');
645
+ await node.destroy();
646
+ process.exit(1);
671
647
  }
672
- log(`Wallet balance: ${balance} sats`);
673
- // 5. Send session_start, wait for session_ack
648
+ log(`Payment: CLINK debit (provider pulls per-minute)`);
649
+ // 5. Send session_start with ndebit, wait for session_ack
674
650
  const startId = randomBytes(4).toString('hex');
675
651
  const ackResp = await sendAndWait({
676
652
  type: 'session_start',
677
653
  id: startId,
678
654
  budget: BUDGET,
679
655
  sats_per_minute: satsPerMinute,
656
+ ndebit: NDEBIT,
680
657
  }, 15_000);
681
658
  if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
682
659
  warn(`Unexpected response: ${ackResp.type}`);
@@ -691,7 +668,7 @@ async function main() {
691
668
  log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
692
669
  }
693
670
  log(`Session started: ${state.sessionId}`);
694
- log(`Billing: ${state.satsPerMinute} sats/min (wallet → provider invoice)`);
671
+ log(`Provider will debit ${state.satsPerMinute} sats/min via CLINK`);
695
672
  // 6. Start HTTP proxy
696
673
  try {
697
674
  await startHttpProxy();
package/dist/swarm.d.ts CHANGED
@@ -46,8 +46,6 @@ export interface SwarmMessage {
46
46
  balance?: number;
47
47
  duration_s?: number;
48
48
  ndebit?: string;
49
- bolt11?: string;
50
- preimage?: string;
51
49
  method?: string;
52
50
  path?: string;
53
51
  headers?: Record<string, 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.5",
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",