2020117-agent 0.3.3 → 0.3.4

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 { 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,12 +161,7 @@ async function main() {
161
161
  console.log(`[${label}] Lightning Address loaded from platform: ${LIGHTNING_ADDRESS}`);
162
162
  }
163
163
  }
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
164
+ // 3. Platform registration + heartbeat
170
165
  await setupPlatform(label);
171
166
  // 5. Async inbox poller
172
167
  startInboxPoller(label);
@@ -341,10 +336,6 @@ async function startSwarmListener(label) {
341
336
  node.send(socket, { type: 'error', id: msg.id, message: 'Provider has no Lightning Address configured' });
342
337
  return;
343
338
  }
344
- if (!msg.ndebit) {
345
- node.send(socket, { type: 'error', id: msg.id, message: 'session_start requires ndebit authorization' });
346
- return;
347
- }
348
339
  const satsPerMinute = state.skill?.pricing?.sats_per_minute
349
340
  || Number(process.env.SATS_PER_MINUTE)
350
341
  || msg.sats_per_minute
@@ -358,7 +349,6 @@ async function startSwarmListener(label) {
358
349
  peerId,
359
350
  sessionId,
360
351
  satsPerMinute,
361
- ndebit: msg.ndebit,
362
352
  totalEarned: 0,
363
353
  startedAt: Date.now(),
364
354
  lastDebitAt: Date.now(),
@@ -366,78 +356,67 @@ async function startSwarmListener(label) {
366
356
  timeoutTimer: null,
367
357
  };
368
358
  activeSessions.set(sessionId, session);
369
- // Debit first 10 minutes immediately (prepaid model)
370
- let firstDebit;
359
+ // Generate first invoice and send to customer for payment
360
+ let firstInvoice;
371
361
  try {
372
- firstDebit = await collectPayment({
373
- ndebit: session.ndebit,
374
- lightningAddress: LIGHTNING_ADDRESS,
375
- amountSats: debitAmount,
376
- });
362
+ firstInvoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
377
363
  }
378
364
  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}` });
365
+ console.warn(`[${label}] Session ${sessionId}: invoice error: ${e.message}`);
366
+ node.send(socket, { type: 'error', id: msg.id, message: `Invoice error: ${e.message}` });
381
367
  activeSessions.delete(sessionId);
382
368
  return;
383
369
  }
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)`);
370
+ // Accept session first, then request payment
393
371
  node.send(socket, {
394
372
  type: 'session_ack',
395
373
  id: msg.id,
396
374
  session_id: sessionId,
397
375
  sats_per_minute: satsPerMinute,
398
376
  });
399
- // Notify customer of the debit
377
+ // Send invoice to customer wait for session_tick_ack with preimage
378
+ const tickId = randomBytes(4).toString('hex');
400
379
  node.send(socket, {
401
- type: 'session_tick_ack',
402
- id: sessionId,
380
+ type: 'session_tick',
381
+ id: tickId,
403
382
  session_id: sessionId,
383
+ bolt11: firstInvoice,
404
384
  amount: debitAmount,
405
- balance: msg.budget ? msg.budget - session.totalEarned : undefined,
406
385
  });
407
- // Debit every 10 minutes
386
+ console.log(`[${label}] Session ${sessionId}: waiting for first payment (${debitAmount} sats)`);
387
+ // Billing every 10 minutes — send invoice, wait for customer to pay
408
388
  session.debitTimer = setInterval(async () => {
409
- let debit;
389
+ let invoice;
410
390
  try {
411
- debit = await collectPayment({
412
- ndebit: session.ndebit,
413
- lightningAddress: LIGHTNING_ADDRESS,
414
- amountSats: debitAmount,
415
- });
391
+ invoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
416
392
  }
417
393
  catch (e) {
418
- console.log(`[${label}] Session ${sessionId}: debit error (${e.message}) — ending session`);
394
+ console.log(`[${label}] Session ${sessionId}: invoice error (${e.message}) — ending session`);
419
395
  endSession(node, session, label);
420
396
  return;
421
397
  }
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
398
+ const tid = randomBytes(4).toString('hex');
431
399
  node.send(socket, {
432
- type: 'session_tick_ack',
433
- id: sessionId,
400
+ type: 'session_tick',
401
+ id: tid,
434
402
  session_id: sessionId,
403
+ bolt11: invoice,
435
404
  amount: debitAmount,
436
- balance: msg.budget ? msg.budget - session.totalEarned : undefined,
437
405
  });
406
+ console.log(`[${label}] Session ${sessionId}: sent invoice for ${debitAmount} sats`);
438
407
  }, BILLING_INTERVAL_MIN * 60_000);
439
408
  return;
440
409
  }
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
+ }
441
420
  if (msg.type === 'session_end') {
442
421
  const session = activeSessions.get(msg.session_id || '');
443
422
  if (!session)
package/dist/api.d.ts CHANGED
@@ -92,6 +92,13 @@ export declare function updateProfile(fields: {
92
92
  about?: string;
93
93
  lightning_address?: string;
94
94
  }): Promise<boolean>;
95
+ export declare function walletPayInvoice(bolt11: string): Promise<{
96
+ ok: boolean;
97
+ preimage?: string;
98
+ amount_sats?: number;
99
+ error?: string;
100
+ }>;
101
+ export declare function walletGetBalance(): Promise<number>;
95
102
  export interface ProxyDebitResult {
96
103
  ok: boolean;
97
104
  preimage?: string;
package/dist/api.js CHANGED
@@ -298,6 +298,29 @@ export async function updateProfile(fields) {
298
298
  const result = await apiPut('/api/me', fields);
299
299
  return result !== null;
300
300
  }
301
+ // --- Wallet API (built-in Lightning wallet) ---
302
+ export async function walletPayInvoice(bolt11) {
303
+ if (!API_KEY)
304
+ return { ok: false, error: 'No API key' };
305
+ try {
306
+ const resp = await fetch(`${BASE_URL}/api/wallet/send`, {
307
+ method: 'POST',
308
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${API_KEY}` },
309
+ body: JSON.stringify({ bolt11 }),
310
+ });
311
+ const data = await resp.json();
312
+ if (!resp.ok)
313
+ return { ok: false, error: data.error || `HTTP ${resp.status}` };
314
+ return { ok: true, preimage: data.preimage, amount_sats: data.amount_sats };
315
+ }
316
+ catch (e) {
317
+ return { ok: false, error: e.message };
318
+ }
319
+ }
320
+ export async function walletGetBalance() {
321
+ const data = await apiGet('/api/wallet/balance');
322
+ return data?.balance_sats ?? 0;
323
+ }
301
324
  export async function proxyDebit(opts) {
302
325
  return apiPost('/api/dvm/proxy-debit', {
303
326
  ndebit: opts.ndebit,
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 wallet payments.
5
5
  *
6
6
  * Features:
7
- * - Provider pulls per-minute payments via CLINK debit (ndebit authorization)
7
+ * - Customer pays provider's invoices via built-in wallet
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 wallet payments.
5
5
  *
6
6
  * Features:
7
- * - Provider pulls per-minute payments via CLINK debit (ndebit authorization)
7
+ * - Customer pays provider's invoices via built-in wallet
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,25 +39,21 @@ for (const arg of process.argv.slice(2)) {
39
39
  break;
40
40
  case '--ndebit':
41
41
  process.env.CLINK_NDEBIT = val;
42
- break;
42
+ break; // legacy, ignored
43
43
  }
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 { walletPayInvoice, walletGetBalance } 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;
56
53
  // --- Config ---
57
54
  const KIND = Number(process.env.DVM_KIND) || 5200;
58
55
  const BUDGET = Number(process.env.BUDGET_SATS) || 500;
59
56
  const PORT = Number(process.env.SESSION_PORT) || 8080;
60
- const NDEBIT = process.env.CLINK_NDEBIT || loadNdebit() || '';
61
57
  const TICK_INTERVAL_MS = 60_000;
62
58
  const HTTP_TIMEOUT_MS = 60_000;
63
59
  const state = {
@@ -112,7 +108,7 @@ function sendAndWait(msg, timeoutMs) {
112
108
  }
113
109
  // --- 6. Message handler ---
114
110
  function setupMessageHandler() {
115
- state.node.on('message', (msg) => {
111
+ state.node.on('message', async (msg) => {
116
112
  // Handle chunked HTTP responses — reassemble before resolving
117
113
  if (msg.type === 'http_response' && msg.chunk_total && msg.chunk_total > 1) {
118
114
  const id = msg.id;
@@ -162,8 +158,37 @@ function setupMessageHandler() {
162
158
  cleanup();
163
159
  break;
164
160
  }
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
+ }
165
190
  case 'session_tick_ack': {
166
- // Provider debited our wallet and is reporting the result
191
+ // Legacy: provider-pull model notification (backward compat)
167
192
  if (msg.amount !== undefined) {
168
193
  state.totalSpent += msg.amount;
169
194
  }
@@ -638,22 +663,20 @@ async function main() {
638
663
  state.satsPerMinute = satsPerMinute;
639
664
  log(`Pricing: ${satsPerMinute} sats/min`);
640
665
  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).');
645
- await node.destroy();
646
- process.exit(1);
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}}'`);
647
671
  }
648
- log(`Payment: CLINK debit (provider pulls per-minute)`);
649
- // 5. Send session_start with ndebit, wait for session_ack
672
+ log(`Wallet balance: ${balance} sats`);
673
+ // 5. Send session_start, wait for session_ack
650
674
  const startId = randomBytes(4).toString('hex');
651
675
  const ackResp = await sendAndWait({
652
676
  type: 'session_start',
653
677
  id: startId,
654
678
  budget: BUDGET,
655
679
  sats_per_minute: satsPerMinute,
656
- ndebit: NDEBIT,
657
680
  }, 15_000);
658
681
  if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
659
682
  warn(`Unexpected response: ${ackResp.type}`);
@@ -668,7 +691,7 @@ async function main() {
668
691
  log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
669
692
  }
670
693
  log(`Session started: ${state.sessionId}`);
671
- log(`Provider will debit ${state.satsPerMinute} sats/min via CLINK`);
694
+ log(`Billing: ${state.satsPerMinute} sats/min (wallet → provider invoice)`);
672
695
  // 6. Start HTTP proxy
673
696
  try {
674
697
  await startHttpProxy();
package/dist/swarm.d.ts CHANGED
@@ -46,6 +46,8 @@ export interface SwarmMessage {
46
46
  balance?: number;
47
47
  duration_s?: number;
48
48
  ndebit?: string;
49
+ bolt11?: string;
50
+ preimage?: string;
49
51
  method?: string;
50
52
  path?: string;
51
53
  headers?: Record<string, string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.3.3",
4
- "description": "2020117 agent runtime — API polling + Hyperswarm P2P + CLINK Lightning payments",
3
+ "version": "0.3.4",
4
+ "description": "2020117 agent runtime — API polling + Hyperswarm P2P + wallet Lightning payments",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "2020117-agent": "./dist/agent.js",