2020117-agent 0.3.2 → 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,61 +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
- const firstDebit = await collectPayment({
371
- ndebit: session.ndebit,
372
- lightningAddress: LIGHTNING_ADDRESS,
373
- amountSats: debitAmount,
374
- });
375
- if (!firstDebit.ok) {
376
- console.warn(`[${label}] Session ${sessionId}: first debit failed: ${firstDebit.error}`);
377
- node.send(socket, { type: 'error', id: msg.id, message: `Payment failed: ${firstDebit.error}` });
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}` });
378
367
  activeSessions.delete(sessionId);
379
368
  return;
380
369
  }
381
- session.totalEarned += debitAmount;
382
- session.lastDebitAt = Date.now();
383
- console.log(`[${label}] Session ${sessionId}: first ${BILLING_INTERVAL_MIN}min paid (${debitAmount} sats)`);
370
+ // Accept session first, then request payment
384
371
  node.send(socket, {
385
372
  type: 'session_ack',
386
373
  id: msg.id,
387
374
  session_id: sessionId,
388
375
  sats_per_minute: satsPerMinute,
389
376
  });
390
- // Notify customer of the debit
377
+ // Send invoice to customer wait for session_tick_ack with preimage
378
+ const tickId = randomBytes(4).toString('hex');
391
379
  node.send(socket, {
392
- type: 'session_tick_ack',
393
- id: sessionId,
380
+ type: 'session_tick',
381
+ id: tickId,
394
382
  session_id: sessionId,
383
+ bolt11: firstInvoice,
395
384
  amount: debitAmount,
396
- balance: msg.budget ? msg.budget - session.totalEarned : undefined,
397
385
  });
398
- // 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
399
388
  session.debitTimer = setInterval(async () => {
400
- const debit = await collectPayment({
401
- ndebit: session.ndebit,
402
- lightningAddress: LIGHTNING_ADDRESS,
403
- amountSats: debitAmount,
404
- });
405
- if (!debit.ok) {
406
- console.log(`[${label}] Session ${sessionId}: debit failed (${debit.error}) — ending session`);
389
+ let invoice;
390
+ try {
391
+ invoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
392
+ }
393
+ catch (e) {
394
+ console.log(`[${label}] Session ${sessionId}: invoice error (${e.message}) — ending session`);
407
395
  endSession(node, session, label);
408
396
  return;
409
397
  }
410
- session.totalEarned += debitAmount;
411
- session.lastDebitAt = Date.now();
412
- console.log(`[${label}] Session ${sessionId}: debit OK (+${debitAmount}, total: ${session.totalEarned} sats)`);
413
- // Notify customer
398
+ const tid = randomBytes(4).toString('hex');
414
399
  node.send(socket, {
415
- type: 'session_tick_ack',
416
- id: sessionId,
400
+ type: 'session_tick',
401
+ id: tid,
417
402
  session_id: sessionId,
403
+ bolt11: invoice,
418
404
  amount: debitAmount,
419
- balance: msg.budget ? msg.budget - session.totalEarned : undefined,
420
405
  });
406
+ console.log(`[${label}] Session ${sessionId}: sent invoice for ${debitAmount} sats`);
421
407
  }, BILLING_INTERVAL_MIN * 60_000);
422
408
  return;
423
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
+ }
424
420
  if (msg.type === 'session_end') {
425
421
  const session = activeSessions.get(msg.session_id || '');
426
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.2",
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",