2020117-agent 0.3.6 → 0.3.7

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.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Unified Agent Runtime — runs as a long-lived daemon that handles:
4
4
  * 1. Async platform tasks (inbox polling → accept → process → submit result)
5
- * 2. P2P sessions (Hyperswarm + CLINK per-minute billing)
5
+ * 2. P2P sessions (Hyperswarm + Lightning invoice per-minute billing)
6
6
  *
7
7
  * Both channels share a single capacity counter so the agent never overloads.
8
8
  *
package/dist/agent.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * Unified Agent Runtime — runs as a long-lived daemon that handles:
4
4
  * 1. Async platform tasks (inbox polling → accept → process → submit result)
5
- * 2. P2P sessions (Hyperswarm + CLINK per-minute billing)
5
+ * 2. P2P sessions (Hyperswarm + Lightning invoice per-minute billing)
6
6
  *
7
7
  * Both channels share a single capacity counter so the agent never overloads.
8
8
  *
@@ -69,10 +69,11 @@ 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, generateInvoice } from './clink.js';
72
+ import { generateInvoice } from './clink.js';
73
+ import { receiveCashuToken } from './cashu.js';
73
74
  import { readFileSync } from 'fs';
74
75
  import WebSocket from 'ws';
75
- // Polyfill global WebSocket for Node.js < 22 (needed by @shocknet/clink-sdk)
76
+ // Polyfill global WebSocket for Node.js < 22 (needed by ws tunnel)
76
77
  if (!globalThis.WebSocket)
77
78
  globalThis.WebSocket = WebSocket;
78
79
  // --- Config from env ---
@@ -81,7 +82,7 @@ const MAX_CONCURRENT = Number(process.env.MAX_JOBS) || 3;
81
82
  const POLL_INTERVAL = Number(process.env.POLL_INTERVAL) || 30_000;
82
83
  const SATS_PER_CHUNK = Number(process.env.SATS_PER_CHUNK) || 1;
83
84
  const CHUNKS_PER_PAYMENT = Number(process.env.CHUNKS_PER_PAYMENT) || 10;
84
- // --- CLINK payment config ---
85
+ // --- Lightning payment config ---
85
86
  let LIGHTNING_ADDRESS = process.env.LIGHTNING_ADDRESS || '';
86
87
  // --- Sub-task delegation config ---
87
88
  const SUB_KIND = process.env.SUB_KIND ? Number(process.env.SUB_KIND) : null;
@@ -161,12 +162,7 @@ async function main() {
161
162
  console.log(`[${label}] Lightning Address loaded from platform: ${LIGHTNING_ADDRESS}`);
162
163
  }
163
164
  }
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
+ // 3. Platform registration + heartbeat
170
166
  await setupPlatform(label);
171
167
  // 5. Async inbox poller
172
168
  startInboxPoller(label);
@@ -319,6 +315,37 @@ async function delegateAPI(kind, input, bidSats, provider) {
319
315
  throw new Error(`Sub-task ${jobId} timed out after 120s`);
320
316
  }
321
317
  const activeSessions = new Map();
318
+ /** Send a billing tick to the customer — Cashu: request amount, Invoice: send bolt11 */
319
+ async function sendBillingTick(node, session, amount, label) {
320
+ const tickId = randomBytes(4).toString('hex');
321
+ if (session.paymentMethod === 'invoice') {
322
+ try {
323
+ const bolt11 = await generateInvoice(LIGHTNING_ADDRESS, amount);
324
+ node.send(session.socket, {
325
+ type: 'session_tick',
326
+ id: tickId,
327
+ session_id: session.sessionId,
328
+ bolt11,
329
+ amount,
330
+ });
331
+ console.log(`[${label}] Session ${session.sessionId}: sent invoice (${amount} sats)`);
332
+ }
333
+ catch (e) {
334
+ console.log(`[${label}] Session ${session.sessionId}: invoice error (${e.message}) — ending session`);
335
+ endSession(node, session, label);
336
+ }
337
+ }
338
+ else {
339
+ // Cashu: just request the amount, customer sends token
340
+ node.send(session.socket, {
341
+ type: 'session_tick',
342
+ id: tickId,
343
+ session_id: session.sessionId,
344
+ amount,
345
+ });
346
+ console.log(`[${label}] Session ${session.sessionId}: requested payment (${amount} sats)`);
347
+ }
348
+ }
322
349
  // Backend WebSocket connections for WS tunnel (keyed by ws_id)
323
350
  const backendWebSockets = new Map();
324
351
  async function startSwarmListener(label) {
@@ -336,174 +363,75 @@ async function startSwarmListener(label) {
336
363
  }
337
364
  // --- Session protocol ---
338
365
  if (msg.type === 'session_start') {
339
- if (!LIGHTNING_ADDRESS) {
340
- console.warn(`[${label}] Session rejected: no --lightning-address configured`);
341
- node.send(socket, { type: 'error', id: msg.id, message: 'Provider has no Lightning Address configured' });
342
- return;
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' });
366
+ // Negotiate payment method: cashu (default) or invoice (requires Lightning Address)
367
+ const paymentMethod = msg.payment_method || 'cashu';
368
+ if (paymentMethod === 'invoice' && !LIGHTNING_ADDRESS) {
369
+ node.send(socket, { type: 'error', id: msg.id, message: 'Invoice payment requires provider Lightning Address' });
348
370
  return;
349
371
  }
350
372
  const satsPerMinute = state.skill?.pricing?.sats_per_minute
351
373
  || Number(process.env.SATS_PER_MINUTE)
352
374
  || msg.sats_per_minute
353
375
  || 10;
354
- const BILLING_INTERVAL_MIN = 10;
355
- const debitAmount = satsPerMinute * BILLING_INTERVAL_MIN;
376
+ const BILLING_INTERVAL_MIN = 1;
377
+ const billingAmount = satsPerMinute * BILLING_INTERVAL_MIN;
356
378
  const sessionId = randomBytes(8).toString('hex');
357
- console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, payment=${paymentMethod}, billing every ${BILLING_INTERVAL_MIN}min (${debitAmount} sats)`);
379
+ console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, payment=${paymentMethod}, billing every ${BILLING_INTERVAL_MIN}min (${billingAmount} sats)`);
358
380
  const session = {
359
381
  socket,
360
382
  peerId,
361
383
  sessionId,
362
384
  satsPerMinute,
363
385
  paymentMethod,
364
- ndebit: msg.ndebit || '',
365
386
  totalEarned: 0,
366
387
  startedAt: Date.now(),
367
- lastDebitAt: Date.now(),
368
- debitTimer: null,
388
+ lastPaidAt: Date.now(),
389
+ billingTimer: null,
369
390
  timeoutTimer: null,
370
391
  };
371
392
  activeSessions.set(sessionId, session);
372
- if (paymentMethod === 'ndebit') {
373
- // --- CLINK pull mode: provider debits customer immediately ---
374
- let firstDebit;
393
+ node.send(socket, {
394
+ type: 'session_ack',
395
+ id: msg.id,
396
+ session_id: sessionId,
397
+ sats_per_minute: satsPerMinute,
398
+ payment_method: paymentMethod,
399
+ });
400
+ // Send first billing tick
401
+ await sendBillingTick(node, session, billingAmount, label);
402
+ // Recurring billing every 10 minutes
403
+ session.billingTimer = setInterval(() => {
404
+ sendBillingTick(node, session, billingAmount, label);
405
+ }, BILLING_INTERVAL_MIN * 60_000);
406
+ return;
407
+ }
408
+ // Customer sent payment (Cashu token or Lightning preimage)
409
+ if (msg.type === 'session_tick_ack') {
410
+ const session = activeSessions.get(msg.session_id || '');
411
+ if (!session)
412
+ return;
413
+ if (msg.cashu_token) {
414
+ // Cashu mode: verify token by swapping at mint
375
415
  try {
376
- firstDebit = await collectPayment({
377
- ndebit: session.ndebit,
378
- lightningAddress: LIGHTNING_ADDRESS,
379
- amountSats: debitAmount,
380
- });
416
+ const { amount } = await receiveCashuToken(msg.cashu_token);
417
+ session.totalEarned += amount;
418
+ session.lastPaidAt = Date.now();
419
+ console.log(`[${label}] Session ${session.sessionId}: Cashu payment received (+${amount}, total: ${session.totalEarned} sats)`);
381
420
  }
382
421
  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;
422
+ console.warn(`[${label}] Session ${session.sessionId}: Cashu token invalid: ${e.message} — ending session`);
423
+ endSession(node, session, label);
393
424
  }
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);
442
425
  }
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);
426
+ else if (msg.preimage) {
427
+ // Invoice mode: preimage proves payment
428
+ const amount = msg.amount || 0;
429
+ session.totalEarned += amount;
430
+ session.lastPaidAt = Date.now();
431
+ console.log(`[${label}] Session ${session.sessionId}: invoice payment received (+${amount}, total: ${session.totalEarned} sats)`);
493
432
  }
494
433
  return;
495
434
  }
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)`);
505
- return;
506
- }
507
435
  if (msg.type === 'session_end') {
508
436
  const session = activeSessions.get(msg.session_id || '');
509
437
  if (!session)
@@ -688,7 +616,7 @@ async function startSwarmListener(label) {
688
616
  return;
689
617
  }
690
618
  });
691
- // Handle customer disconnect — payments already settled via CLINK
619
+ // Handle customer disconnect
692
620
  node.on('peer-leave', (peerId) => {
693
621
  const tag = peerId.slice(0, 8);
694
622
  // Find and end all sessions for this peer
@@ -720,10 +648,10 @@ function findSessionBySocket(socket) {
720
648
  }
721
649
  function endSession(node, session, label) {
722
650
  const durationS = Math.round((Date.now() - session.startedAt) / 1000);
723
- // Stop debit timer
724
- if (session.debitTimer) {
725
- clearInterval(session.debitTimer);
726
- session.debitTimer = null;
651
+ // Stop billing timer
652
+ if (session.billingTimer) {
653
+ clearInterval(session.billingTimer);
654
+ session.billingTimer = null;
727
655
  }
728
656
  if (session.timeoutTimer) {
729
657
  clearTimeout(session.timeoutTimer);
@@ -752,7 +680,7 @@ function endSession(node, session, label) {
752
680
  // Socket may already be closed (peer disconnect)
753
681
  }
754
682
  console.log(`[${label}] Session ${session.sessionId} ended: ${session.totalEarned} sats, ${durationS}s`);
755
- // Update P2P lifetime counters — no batch claim needed with CLINK (payments settled instantly)
683
+ // Update P2P lifetime counters
756
684
  state.p2pSessionsCompleted++;
757
685
  state.p2pTotalEarnedSats += session.totalEarned;
758
686
  // Report session to platform activity feed (best-effort, no content exposed)
package/dist/api.d.ts CHANGED
@@ -7,8 +7,6 @@
7
7
  */
8
8
  /** Returns the resolved agent name from env or first key in file */
9
9
  export declare function loadAgentName(): string | null;
10
- /** Returns the ndebit from env or .2020117_keys file */
11
- export declare function loadNdebit(): string | null;
12
10
  export declare function hasApiKey(): boolean;
13
11
  /** Fetch the authenticated agent's profile from the platform. */
14
12
  export declare function getProfile(): Promise<{
@@ -99,16 +97,6 @@ export declare function walletPayInvoice(bolt11: string): Promise<{
99
97
  error?: string;
100
98
  }>;
101
99
  export declare function walletGetBalance(): Promise<number>;
102
- export interface ProxyDebitResult {
103
- ok: boolean;
104
- preimage?: string;
105
- error?: string;
106
- }
107
- export declare function proxyDebit(opts: {
108
- ndebit: string;
109
- lightningAddress: string;
110
- amountSats: number;
111
- }): Promise<ProxyDebitResult | null>;
112
100
  /**
113
101
  * Report a completed P2P session to the platform for the activity feed.
114
102
  * Content stays private — only metadata (kind, duration, sats) is sent.
package/dist/api.js CHANGED
@@ -51,29 +51,6 @@ export function loadAgentName() {
51
51
  }
52
52
  return null;
53
53
  }
54
- /** Returns the ndebit from env or .2020117_keys file */
55
- export function loadNdebit() {
56
- if (process.env.CLINK_NDEBIT)
57
- return process.env.CLINK_NDEBIT;
58
- const agentName = process.env.AGENT_NAME || process.env.AGENT;
59
- for (const dir of [process.cwd(), homedir()]) {
60
- try {
61
- const raw = readFileSync(join(dir, '.2020117_keys'), 'utf-8');
62
- const keys = JSON.parse(raw);
63
- if (agentName && keys[agentName]?.ndebit)
64
- return keys[agentName].ndebit;
65
- if (!agentName) {
66
- const first = Object.values(keys)[0];
67
- if (first?.ndebit)
68
- return first.ndebit;
69
- }
70
- }
71
- catch {
72
- // try next
73
- }
74
- }
75
- return null;
76
- }
77
54
  const API_KEY = loadApiKey();
78
55
  // --- Helpers ---
79
56
  async function apiGet(path, auth = true) {
@@ -303,7 +280,7 @@ export async function walletPayInvoice(bolt11) {
303
280
  if (!API_KEY)
304
281
  return { ok: false, error: 'No API key' };
305
282
  try {
306
- const resp = await fetch(`${BASE_URL}/api/wallet/send`, {
283
+ const resp = await fetch(`${BASE_URL}/api/wallet/pay`, {
307
284
  method: 'POST',
308
285
  headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${API_KEY}` },
309
286
  body: JSON.stringify({ bolt11 }),
@@ -321,13 +298,6 @@ export async function walletGetBalance() {
321
298
  const data = await apiGet('/api/wallet/balance');
322
299
  return data?.balance_sats ?? 0;
323
300
  }
324
- export async function proxyDebit(opts) {
325
- return apiPost('/api/dvm/proxy-debit', {
326
- ndebit: opts.ndebit,
327
- lightning_address: opts.lightningAddress,
328
- amount_sats: opts.amountSats,
329
- });
330
- }
331
301
  /**
332
302
  * Report a completed P2P session to the platform for the activity feed.
333
303
  * Content stays private — only metadata (kind, duration, sats) is sent.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Cashu eCash utilities — send and receive tokens over P2P
3
+ *
4
+ * Customer: pre-loads tokens, splits per billing tick, sends to Provider
5
+ * Provider: receives tokens, swaps at mint to verify, accumulates proofs
6
+ */
7
+ export type Proof = {
8
+ id: string;
9
+ amount: number;
10
+ secret: string;
11
+ C: string;
12
+ };
13
+ /**
14
+ * Split proofs to create a Cashu token of the exact amount.
15
+ * Returns the encoded token string and remaining change proofs.
16
+ */
17
+ export declare function sendCashuToken(mintUrl: string, proofs: Proof[], amount: number): Promise<{
18
+ token: string;
19
+ change: Proof[];
20
+ }>;
21
+ /**
22
+ * Verify and claim a Cashu token — swaps proofs with the mint to prevent double-spend.
23
+ * Returns the claimed proofs (now owned by the receiver).
24
+ */
25
+ export declare function receiveCashuToken(tokenStr: string): Promise<{
26
+ proofs: Proof[];
27
+ amount: number;
28
+ mintUrl: string;
29
+ }>;
30
+ /**
31
+ * Inspect a token's amount and mint without claiming it.
32
+ */
33
+ export declare function peekCashuToken(tokenStr: string): {
34
+ amount: number;
35
+ mint: string;
36
+ };
37
+ /**
38
+ * Decode a token string to get the raw proofs and mint URL.
39
+ */
40
+ export declare function decodeCashuToken(tokenStr: string): {
41
+ mint: string;
42
+ proofs: Proof[];
43
+ };
44
+ /**
45
+ * Encode proofs back into a portable token string.
46
+ */
47
+ export declare function encodeCashuToken(mintUrl: string, proofs: Proof[]): string;
48
+ /**
49
+ * Request a mint quote — returns a Lightning invoice to pay for minting tokens.
50
+ */
51
+ export declare function createMintQuote(mintUrl: string, amountSats: number): Promise<{
52
+ quote: string;
53
+ invoice: string;
54
+ }>;
55
+ /**
56
+ * Claim a paid mint quote — polls for payment, then mints proofs and returns an encoded token.
57
+ * Throws if the quote is not paid within the timeout.
58
+ */
59
+ export declare function claimMintQuote(mintUrl: string, amountSats: number, quoteId: string, timeoutMs?: number): Promise<string>;
package/dist/cashu.js ADDED
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Cashu eCash utilities — send and receive tokens over P2P
3
+ *
4
+ * Customer: pre-loads tokens, splits per billing tick, sends to Provider
5
+ * Provider: receives tokens, swaps at mint to verify, accumulates proofs
6
+ */
7
+ import { CashuMint, CashuWallet, getEncodedTokenV4, getDecodedToken } from '@cashu/cashu-ts';
8
+ // Cache wallet instances per mint URL
9
+ const walletCache = new Map();
10
+ async function getWallet(mintUrl) {
11
+ let wallet = walletCache.get(mintUrl);
12
+ if (!wallet) {
13
+ const mint = new CashuMint(mintUrl);
14
+ wallet = new CashuWallet(mint);
15
+ await wallet.loadMint();
16
+ walletCache.set(mintUrl, wallet);
17
+ }
18
+ return wallet;
19
+ }
20
+ /**
21
+ * Split proofs to create a Cashu token of the exact amount.
22
+ * Returns the encoded token string and remaining change proofs.
23
+ */
24
+ export async function sendCashuToken(mintUrl, proofs, amount) {
25
+ const wallet = await getWallet(mintUrl);
26
+ const { send, keep } = await wallet.send(amount, proofs);
27
+ const token = getEncodedTokenV4({ mint: mintUrl, proofs: send });
28
+ return { token, change: keep };
29
+ }
30
+ /**
31
+ * Verify and claim a Cashu token — swaps proofs with the mint to prevent double-spend.
32
+ * Returns the claimed proofs (now owned by the receiver).
33
+ */
34
+ export async function receiveCashuToken(tokenStr) {
35
+ const decoded = getDecodedToken(tokenStr);
36
+ const mintUrl = decoded.mint;
37
+ const wallet = await getWallet(mintUrl);
38
+ const proofs = await wallet.receive(tokenStr);
39
+ const amount = proofs.reduce((sum, p) => sum + p.amount, 0);
40
+ return { proofs, amount, mintUrl };
41
+ }
42
+ /**
43
+ * Inspect a token's amount and mint without claiming it.
44
+ */
45
+ export function peekCashuToken(tokenStr) {
46
+ const decoded = getDecodedToken(tokenStr);
47
+ const amount = decoded.proofs.reduce((sum, p) => sum + p.amount, 0);
48
+ return { amount, mint: decoded.mint };
49
+ }
50
+ /**
51
+ * Decode a token string to get the raw proofs and mint URL.
52
+ */
53
+ export function decodeCashuToken(tokenStr) {
54
+ const decoded = getDecodedToken(tokenStr);
55
+ return { mint: decoded.mint, proofs: decoded.proofs };
56
+ }
57
+ /**
58
+ * Encode proofs back into a portable token string.
59
+ */
60
+ export function encodeCashuToken(mintUrl, proofs) {
61
+ return getEncodedTokenV4({ mint: mintUrl, proofs });
62
+ }
63
+ /**
64
+ * Request a mint quote — returns a Lightning invoice to pay for minting tokens.
65
+ */
66
+ export async function createMintQuote(mintUrl, amountSats) {
67
+ const wallet = await getWallet(mintUrl);
68
+ const quoteRes = await wallet.createMintQuote(amountSats);
69
+ return { quote: quoteRes.quote, invoice: quoteRes.request };
70
+ }
71
+ /**
72
+ * Claim a paid mint quote — polls for payment, then mints proofs and returns an encoded token.
73
+ * Throws if the quote is not paid within the timeout.
74
+ */
75
+ export async function claimMintQuote(mintUrl, amountSats, quoteId, timeoutMs = 60_000) {
76
+ const wallet = await getWallet(mintUrl);
77
+ // Poll until paid or timeout
78
+ const deadline = Date.now() + timeoutMs;
79
+ while (Date.now() < deadline) {
80
+ const check = await wallet.checkMintQuote(quoteId);
81
+ if (check.state === 'PAID') {
82
+ const proofs = await wallet.mintProofs(amountSats, quoteId);
83
+ return getEncodedTokenV4({ mint: mintUrl, proofs });
84
+ }
85
+ if (check.state === 'ISSUED') {
86
+ throw new Error('Mint quote already issued');
87
+ }
88
+ await new Promise(r => setTimeout(r, 2000));
89
+ }
90
+ throw new Error('Mint quote payment timeout');
91
+ }
package/dist/clink.d.ts CHANGED
@@ -1,27 +1,9 @@
1
1
  /**
2
- * CLINK payment utilities — debit-based Lightning payments for P2P sessions
2
+ * Lightning payment utilities — invoice generation via LNURL-pay
3
3
  *
4
- * Provider uses ndebit to pull payments from customer's wallet.
5
- * Invoice generation via LNURL-pay from provider's own Lightning Address.
4
+ * Provider generates invoices from their own Lightning Address.
5
+ * Customer pays invoices via built-in wallet (POST /api/wallet/send).
6
6
  */
7
- export declare function initClinkAgent(): {
8
- privateKey: Uint8Array;
9
- pubkey: string;
10
- };
11
- export interface DebitResult {
12
- ok: boolean;
13
- preimage?: string;
14
- error?: string;
15
- }
16
- /**
17
- * Provider calls this to debit customer's wallet via CLINK protocol.
18
- * Sends a Kind 21002 event to the customer's wallet service via Nostr relay.
19
- */
20
- export declare function debitCustomer(opts: {
21
- ndebit: string;
22
- bolt11: string;
23
- timeoutSeconds?: number;
24
- }): Promise<DebitResult>;
25
7
  /**
26
8
  * Resolve a Lightning Address to a bolt11 invoice via LNURL-pay protocol.
27
9
  * The provider calls this on their OWN Lightning Address to generate
@@ -30,18 +12,3 @@ export declare function debitCustomer(opts: {
30
12
  * Flow: address → .well-known/lnurlp → callback?amount= → bolt11
31
13
  */
32
14
  export declare function generateInvoice(lightningAddress: string, amountSats: number): Promise<string>;
33
- /**
34
- * Full payment cycle: generate invoice from provider's Lightning Address,
35
- * then debit customer's wallet via CLINK.
36
- *
37
- * Priority: platform proxy (if API key available) → direct debit (if agent key initialized).
38
- * Proxy uses the platform's pre-authorized CLINK identity, so providers don't need
39
- * individual DebitAccess on customer wallets. Direct debit works for power users
40
- * who pre-authorize each other's keys.
41
- */
42
- export declare function collectPayment(opts: {
43
- ndebit: string;
44
- lightningAddress: string;
45
- amountSats: number;
46
- timeoutSeconds?: number;
47
- }): Promise<DebitResult>;
package/dist/clink.js CHANGED
@@ -1,43 +1,9 @@
1
1
  /**
2
- * CLINK payment utilities — debit-based Lightning payments for P2P sessions
2
+ * Lightning payment utilities — invoice generation via LNURL-pay
3
3
  *
4
- * Provider uses ndebit to pull payments from customer's wallet.
5
- * Invoice generation via LNURL-pay from provider's own Lightning Address.
4
+ * Provider generates invoices from their own Lightning Address.
5
+ * Customer pays invoices via built-in wallet (POST /api/wallet/send).
6
6
  */
7
- import { ClinkSDK, decodeBech32, generateSecretKey, getPublicKey, newNdebitPaymentRequest } from '@shocknet/clink-sdk';
8
- import { hasApiKey, proxyDebit } from './api.js';
9
- // --- Agent identity ---
10
- let agentKey = null;
11
- let agentPubkey = null;
12
- export function initClinkAgent() {
13
- agentKey = generateSecretKey();
14
- agentPubkey = getPublicKey(agentKey);
15
- console.log(`[clink] Agent identity: ${agentPubkey.slice(0, 16)}...`);
16
- return { privateKey: agentKey, pubkey: agentPubkey };
17
- }
18
- /**
19
- * Provider calls this to debit customer's wallet via CLINK protocol.
20
- * Sends a Kind 21002 event to the customer's wallet service via Nostr relay.
21
- */
22
- export async function debitCustomer(opts) {
23
- if (!agentKey)
24
- throw new Error('CLINK agent not initialized — call initClinkAgent() first');
25
- const decoded = decodeBech32(opts.ndebit);
26
- if (decoded.type !== 'ndebit')
27
- throw new Error(`Invalid ndebit string (got type: ${decoded.type})`);
28
- const sdk = new ClinkSDK({
29
- privateKey: agentKey,
30
- relays: [decoded.data.relay],
31
- toPubKey: decoded.data.pubkey,
32
- defaultTimeoutSeconds: opts.timeoutSeconds ?? 30,
33
- });
34
- const result = await sdk.Ndebit(newNdebitPaymentRequest(opts.bolt11, undefined, decoded.data.pointer));
35
- if (result.res === 'ok') {
36
- return { ok: true, preimage: result.preimage };
37
- }
38
- return { ok: false, error: result.error || 'Debit rejected' };
39
- }
40
- // --- Invoice generation via LNURL-pay ---
41
7
  /**
42
8
  * Resolve a Lightning Address to a bolt11 invoice via LNURL-pay protocol.
43
9
  * The provider calls this on their OWN Lightning Address to generate
@@ -73,35 +39,3 @@ export async function generateInvoice(lightningAddress, amountSats) {
73
39
  throw new Error(`No invoice returned: ${invoiceData.reason || 'unknown error'}`);
74
40
  return invoiceData.pr;
75
41
  }
76
- // --- Combined: generate invoice + debit ---
77
- /**
78
- * Full payment cycle: generate invoice from provider's Lightning Address,
79
- * then debit customer's wallet via CLINK.
80
- *
81
- * Priority: platform proxy (if API key available) → direct debit (if agent key initialized).
82
- * Proxy uses the platform's pre-authorized CLINK identity, so providers don't need
83
- * individual DebitAccess on customer wallets. Direct debit works for power users
84
- * who pre-authorize each other's keys.
85
- */
86
- export async function collectPayment(opts) {
87
- // Try platform proxy first (provider doesn't need DebitAccess)
88
- if (hasApiKey()) {
89
- console.log(`[clink] Proxy debit: ${opts.amountSats} sats → ${opts.lightningAddress}`);
90
- const result = await proxyDebit({
91
- ndebit: opts.ndebit,
92
- lightningAddress: opts.lightningAddress,
93
- amountSats: opts.amountSats,
94
- });
95
- if (result) {
96
- return { ok: result.ok, preimage: result.preimage, error: result.error };
97
- }
98
- console.warn('[clink] Proxy debit unavailable, trying direct...');
99
- }
100
- // Fallback: direct debit (requires provider's CLINK key to be authorized)
101
- const bolt11 = await generateInvoice(opts.lightningAddress, opts.amountSats);
102
- return debitCustomer({
103
- ndebit: opts.ndebit,
104
- bolt11,
105
- timeoutSeconds: opts.timeoutSeconds,
106
- });
107
- }
@@ -1,58 +1,11 @@
1
1
  /**
2
- * Shared P2P customer protocol — connects to a provider via Hyperswarm,
3
- * negotiates price, authorizes CLINK debit payments, and streams chunks.
4
- *
5
- * The customer sends an ndebit authorization with the request. The provider
6
- * pulls payments directly from the customer's wallet via CLINK debit —
7
- * the customer does not send payment messages.
2
+ * Shared P2P customer protocol — utility functions for connecting
3
+ * to providers via Hyperswarm.
8
4
  *
9
5
  * Exports:
10
- * - P2PStreamOptions — config interface
11
- * - streamFromProvider() — async generator that yields chunks from a provider
12
6
  * - queryProviderSkill() — queries a provider's skill manifest via an existing connection
13
7
  */
14
8
  import { SwarmNode } from './swarm.js';
15
- /**
16
- * Configuration for a P2P connection.
17
- */
18
- export interface P2PStreamOptions {
19
- /** DVM kind number (e.g. 5100 for text generation) */
20
- kind: number;
21
- /** The input/prompt to send to the provider */
22
- input: string;
23
- /** Total budget in sats for this session */
24
- budgetSats: number;
25
- /** Customer's ndebit1... authorization for CLINK debit payments */
26
- ndebit: string;
27
- /** Maximum acceptable price per chunk in sats (default: 5) */
28
- maxSatsPerChunk?: number;
29
- /** Overall timeout in milliseconds (default: 120_000) */
30
- timeoutMs?: number;
31
- /** Log prefix label (default: 'p2p') */
32
- label?: string;
33
- /** Additional job parameters passed in the request message */
34
- params?: Record<string, unknown>;
35
- }
36
- /**
37
- * Connect to a provider via Hyperswarm, authorize CLINK debit payments,
38
- * and yield output chunks as they arrive.
39
- *
40
- * Creates and destroys its own temporary SwarmNode — callers do not need
41
- * to manage any swarm state.
42
- *
43
- * @example
44
- * ```ts
45
- * for await (const chunk of streamFromProvider({
46
- * kind: 5100,
47
- * input: 'Explain quantum computing',
48
- * budgetSats: 50,
49
- * ndebit: 'ndebit1...',
50
- * })) {
51
- * process.stdout.write(chunk)
52
- * }
53
- * ```
54
- */
55
- export declare function streamFromProvider(opts: P2PStreamOptions): AsyncGenerator<string>;
56
9
  /**
57
10
  * Query a provider's skill manifest over an existing P2P connection.
58
11
  *
@@ -1,165 +1,11 @@
1
1
  /**
2
- * Shared P2P customer protocol — connects to a provider via Hyperswarm,
3
- * negotiates price, authorizes CLINK debit payments, and streams chunks.
4
- *
5
- * The customer sends an ndebit authorization with the request. The provider
6
- * pulls payments directly from the customer's wallet via CLINK debit —
7
- * the customer does not send payment messages.
2
+ * Shared P2P customer protocol — utility functions for connecting
3
+ * to providers via Hyperswarm.
8
4
  *
9
5
  * Exports:
10
- * - P2PStreamOptions — config interface
11
- * - streamFromProvider() — async generator that yields chunks from a provider
12
6
  * - queryProviderSkill() — queries a provider's skill manifest via an existing connection
13
7
  */
14
- import { SwarmNode, topicFromKind } from './swarm.js';
15
8
  import { randomBytes } from 'crypto';
16
- /**
17
- * Connect to a provider via Hyperswarm, authorize CLINK debit payments,
18
- * and yield output chunks as they arrive.
19
- *
20
- * Creates and destroys its own temporary SwarmNode — callers do not need
21
- * to manage any swarm state.
22
- *
23
- * @example
24
- * ```ts
25
- * for await (const chunk of streamFromProvider({
26
- * kind: 5100,
27
- * input: 'Explain quantum computing',
28
- * budgetSats: 50,
29
- * ndebit: 'ndebit1...',
30
- * })) {
31
- * process.stdout.write(chunk)
32
- * }
33
- * ```
34
- */
35
- export async function* streamFromProvider(opts) {
36
- const { kind, input, budgetSats, ndebit, maxSatsPerChunk = 5, timeoutMs = 120_000, label = 'p2p', params, } = opts;
37
- if (!ndebit) {
38
- throw new Error('ndebit authorization required for P2P connection');
39
- }
40
- const jobId = randomBytes(8).toString('hex');
41
- const tag = `${label}-${jobId.slice(0, 8)}`;
42
- const node = new SwarmNode();
43
- const topic = topicFromKind(kind);
44
- try {
45
- console.log(`[${tag}] Looking for kind ${kind} provider...`);
46
- await node.connect(topic);
47
- const peer = await node.waitForPeer(30_000);
48
- console.log(`[${tag}] Connected to provider: ${peer.peerId.slice(0, 12)}...`);
49
- // Query provider's skill manifest for pricing before committing
50
- const skill = await queryProviderSkill(node, peer.socket, kind);
51
- if (skill) {
52
- const pricing = skill.pricing;
53
- const jobPrice = pricing?.sats_per_job ?? pricing?.[String(kind)];
54
- if (jobPrice !== undefined) {
55
- console.log(`[${tag}] Provider pricing: ${jobPrice} sats/job`);
56
- if (typeof jobPrice === 'number' && jobPrice > budgetSats) {
57
- throw new Error(`Provider price ${jobPrice} sats exceeds budget ${budgetSats} sats`);
58
- }
59
- }
60
- if (skill.name)
61
- console.log(`[${tag}] Provider: ${skill.name}`);
62
- }
63
- // Channel: message handler pushes chunks, generator consumes them
64
- const chunks = [];
65
- let finished = false;
66
- let error = null;
67
- let notify = null;
68
- function wake() {
69
- if (notify) {
70
- notify();
71
- notify = null;
72
- }
73
- }
74
- function waitForChunk() {
75
- if (chunks.length > 0 || finished || error)
76
- return Promise.resolve();
77
- return new Promise(r => { notify = r; });
78
- }
79
- // Timeout
80
- const timeout = setTimeout(() => {
81
- error = new Error(`P2P delegation timed out after ${timeoutMs / 1000}s`);
82
- wake();
83
- }, timeoutMs);
84
- node.on('message', async (msg) => {
85
- if (msg.id !== jobId)
86
- return;
87
- switch (msg.type) {
88
- case 'offer': {
89
- const spc = msg.sats_per_chunk ?? 0;
90
- if (spc > maxSatsPerChunk) {
91
- node.send(peer.socket, { type: 'stop', id: jobId });
92
- error = new Error(`Price too high: ${spc} sat/chunk > max ${maxSatsPerChunk}`);
93
- wake();
94
- return;
95
- }
96
- console.log(`[${tag}] Offer: ${spc} sat/chunk, ${msg.chunks_per_payment ?? 0} chunks/payment`);
97
- // No action needed — provider will debit our wallet via CLINK
98
- break;
99
- }
100
- case 'payment_ack':
101
- // Provider debited our wallet and is reporting the amount
102
- console.log(`[${tag}] Provider debited: ${msg.amount} sats`);
103
- break;
104
- case 'accepted':
105
- console.log(`[${tag}] Accepted, streaming...`);
106
- break;
107
- case 'chunk':
108
- if (msg.data) {
109
- chunks.push(msg.data);
110
- wake();
111
- }
112
- break;
113
- case 'pay_required':
114
- // With CLINK, provider handles payment collection directly
115
- // This shouldn't normally fire, but log it if it does
116
- console.log(`[${tag}] Provider requested payment (will be debited via CLINK)`);
117
- break;
118
- case 'result':
119
- console.log(`[${tag}] Result: ${(msg.output || '').length} chars, ${msg.total_sats ?? '?'} sats`);
120
- finished = true;
121
- wake();
122
- break;
123
- case 'error':
124
- error = new Error(`Provider error: ${msg.message}`);
125
- wake();
126
- break;
127
- }
128
- });
129
- // Send request with ndebit authorization (provider will use it to debit)
130
- const requestMsg = {
131
- type: 'request',
132
- id: jobId,
133
- kind,
134
- input,
135
- budget: budgetSats,
136
- ndebit,
137
- };
138
- if (params) {
139
- requestMsg.params = params;
140
- }
141
- node.send(peer.socket, requestMsg);
142
- // Yield chunks as they arrive
143
- while (true) {
144
- await waitForChunk();
145
- if (error) {
146
- clearTimeout(timeout);
147
- throw error;
148
- }
149
- // Drain all available chunks
150
- while (chunks.length > 0) {
151
- yield chunks.shift();
152
- }
153
- if (finished) {
154
- clearTimeout(timeout);
155
- return;
156
- }
157
- }
158
- }
159
- finally {
160
- await node.destroy();
161
- }
162
- }
163
9
  /**
164
10
  * Query a provider's skill manifest over an existing P2P connection.
165
11
  *
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 payments (CLINK ndebit or Lightning invoice).
4
+ * per-minute Lightning invoice payments.
5
5
  *
6
6
  * Features:
7
- * - Payment negotiation: ndebit (provider pulls) or invoice (customer pushes)
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 payments (CLINK ndebit or Lightning invoice).
4
+ * per-minute Lightning invoice payments.
5
5
  *
6
6
  * Features:
7
- * - Payment negotiation: ndebit (provider pulls) or invoice (customer pushes)
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
  *
@@ -37,27 +37,31 @@ for (const arg of process.argv.slice(2)) {
37
37
  case '--provider':
38
38
  process.env.PROVIDER_PEER = val;
39
39
  break;
40
- case '--ndebit':
41
- process.env.CLINK_NDEBIT = val;
40
+ case '--cashu-token':
41
+ process.env.CASHU_TOKEN = val;
42
+ break;
43
+ case '--mint':
44
+ process.env.CASHU_MINT_URL = val;
42
45
  break;
43
46
  }
44
47
  }
45
48
  import { SwarmNode, topicFromKind } from './swarm.js';
46
49
  import { queryProviderSkill } from './p2p-customer.js';
47
- import { loadNdebit, walletPayInvoice, walletGetBalance, hasApiKey } from './api.js';
50
+ import { walletPayInvoice, walletGetBalance, hasApiKey } from './api.js';
51
+ import { decodeCashuToken, sendCashuToken, createMintQuote, claimMintQuote } from './cashu.js';
48
52
  import { randomBytes } from 'crypto';
49
53
  import { createServer } from 'http';
50
54
  import { createInterface } from 'readline';
51
55
  import { mkdirSync, writeFileSync } from 'fs';
52
56
  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
57
  // --- Config ---
57
58
  const KIND = Number(process.env.DVM_KIND) || 5200;
58
59
  const BUDGET = Number(process.env.BUDGET_SATS) || 500;
59
60
  const PORT = Number(process.env.SESSION_PORT) || 8080;
60
- const NDEBIT = process.env.CLINK_NDEBIT || loadNdebit() || '';
61
+ const CASHU_TOKEN = process.env.CASHU_TOKEN || '';
62
+ const MINT_URL = process.env.CASHU_MINT_URL || 'https://8333.space:3338';
63
+ // Mutable Cashu wallet state (loaded from CASHU_TOKEN at startup)
64
+ let cashuState = null;
61
65
  const TICK_INTERVAL_MS = 60_000;
62
66
  const HTTP_TIMEOUT_MS = 60_000;
63
67
  const state = {
@@ -163,14 +167,14 @@ function setupMessageHandler() {
163
167
  break;
164
168
  }
165
169
  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
- }
170
+ const amount = msg.amount || 0;
171
+ if (state.totalSpent + amount > BUDGET) {
172
+ log(`Budget exhausted (need ${amount}, remaining ${remainingSats()}) — ending session`);
173
+ endSession();
174
+ break;
175
+ }
176
+ if (msg.bolt11) {
177
+ // Invoice mode: pay bolt11 via wallet
174
178
  log(`Paying invoice: ${amount} sats...`);
175
179
  const payResult = await walletPayInvoice(msg.bolt11);
176
180
  if (payResult.ok) {
@@ -185,20 +189,39 @@ function setupMessageHandler() {
185
189
  });
186
190
  }
187
191
  else {
188
- warn(`Payment failed: ${payResult.error} — ending session`);
192
+ warn(`Invoice payment failed: ${payResult.error} — ending session`);
193
+ endSession();
194
+ }
195
+ }
196
+ else if (cashuState) {
197
+ // Cashu mode: split tokens and send
198
+ log(`Paying with Cashu: ${amount} sats...`);
199
+ try {
200
+ const { token, change } = await sendCashuToken(cashuState.mintUrl, cashuState.proofs, amount);
201
+ cashuState.proofs = change;
202
+ state.totalSpent += amount;
203
+ log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
204
+ state.node.send(state.socket, {
205
+ type: 'session_tick_ack',
206
+ id: msg.id,
207
+ session_id: state.sessionId,
208
+ cashu_token: token,
209
+ amount,
210
+ });
211
+ }
212
+ catch (e) {
213
+ warn(`Cashu payment failed: ${e.message} — ending session`);
189
214
  endSession();
190
215
  }
191
216
  }
217
+ else {
218
+ warn('No payment method available — ending session');
219
+ endSession();
220
+ }
192
221
  break;
193
222
  }
194
223
  case 'session_tick_ack': {
195
- // Ndebit mode: provider debited our wallet and is reporting the result
196
- if (msg.amount !== undefined) {
197
- state.totalSpent += msg.amount;
198
- }
199
- if (msg.balance !== undefined) {
200
- log(`Debit: ${msg.amount ?? '?'} sats (balance: ${msg.balance} sats, ${estimatedMinutesLeft()} min left)`);
201
- }
224
+ // Ignore this is our own ack echoed back
202
225
  break;
203
226
  }
204
227
  case 'error': {
@@ -256,10 +279,9 @@ function setupMessageHandler() {
256
279
  }
257
280
  });
258
281
  }
259
- // --- 3. Payment tracking (CLINK — provider pulls, customer monitors) ---
260
- // With CLINK debit, the provider generates invoices and debits the customer's
261
- // wallet directly. The customer receives session_tick_ack notifications with
262
- // updated balance. No tick timer needed on the customer side.
282
+ // --- 3. Payment tracking ---
283
+ // Provider sends session_tick with bolt11 invoice every billing period.
284
+ // Customer pays via built-in wallet and sends session_tick_ack with preimage.
263
285
  // --- 4. HTTP proxy ---
264
286
  function startHttpProxy() {
265
287
  return new Promise((resolve, reject) => {
@@ -667,25 +689,59 @@ async function main() {
667
689
  state.satsPerMinute = satsPerMinute;
668
690
  log(`Pricing: ${satsPerMinute} sats/min`);
669
691
  log(`Budget: ${BUDGET} sats (~${Math.floor(BUDGET / satsPerMinute)} min)`);
670
- // 4. Negotiate payment method: ndebit (CLINK pull) or invoice (wallet push)
692
+ // 4. Determine payment method: Cashu (default) or invoice (fallback)
671
693
  let paymentMethod;
672
- if (NDEBIT) {
673
- paymentMethod = 'ndebit';
674
- log(`Payment: CLINK ndebit (provider pulls per-minute)`);
694
+ if (CASHU_TOKEN) {
695
+ // Load pre-existing Cashu token
696
+ const { mint, proofs } = decodeCashuToken(CASHU_TOKEN);
697
+ const tokenAmount = proofs.reduce((sum, p) => sum + p.amount, 0);
698
+ cashuState = { mintUrl: mint, proofs };
699
+ paymentMethod = 'cashu';
700
+ log(`Payment: Cashu (${tokenAmount} sats from ${mint})`);
701
+ if (tokenAmount < BUDGET) {
702
+ warn(`Cashu token (${tokenAmount} sats) is less than budget (${BUDGET} sats)`);
703
+ }
675
704
  }
676
705
  else if (hasApiKey()) {
677
- paymentMethod = 'invoice';
706
+ // Auto-mint Cashu tokens via NWC wallet
678
707
  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`);
708
+ if (balance <= 0) {
709
+ warn('Wallet balance is 0. Cannot auto-mint Cashu tokens.');
710
+ await node.destroy();
711
+ process.exit(1);
712
+ }
713
+ const mintAmount = Math.min(balance, BUDGET);
714
+ log(`Wallet balance: ${balance} sats — minting ${mintAmount} sats from ${MINT_URL}`);
715
+ try {
716
+ // 1. Request mint quote (Lightning invoice)
717
+ const { quote, invoice } = await createMintQuote(MINT_URL, mintAmount);
718
+ log(`Mint quote: ${quote} (invoice: ${invoice.slice(0, 30)}...)`);
719
+ // 2. Pay the invoice via platform NWC wallet
720
+ log('Paying mint invoice via NWC wallet...');
721
+ const payResult = await walletPayInvoice(invoice);
722
+ if (!payResult.ok) {
723
+ throw new Error(`Payment failed: ${payResult.error}`);
724
+ }
725
+ log(`Invoice paid (preimage: ${payResult.preimage?.slice(0, 16)}...)`);
726
+ // 3. Claim minted proofs
727
+ log('Claiming minted tokens...');
728
+ const token = await claimMintQuote(MINT_URL, mintAmount, quote);
729
+ const { mint, proofs } = decodeCashuToken(token);
730
+ cashuState = { mintUrl: mint, proofs };
731
+ paymentMethod = 'cashu';
732
+ const totalMinted = proofs.reduce((s, p) => s + p.amount, 0);
733
+ log(`Minted ${totalMinted} sats Cashu token — using Cashu payment mode`);
734
+ }
735
+ catch (e) {
736
+ warn(`Auto-mint failed: ${e.message}`);
737
+ await node.destroy();
738
+ process.exit(1);
682
739
  }
683
- log(`Payment: invoice (wallet pays provider, balance: ${balance} sats)`);
684
740
  }
685
741
  else {
686
742
  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)');
743
+ warn(' Option 1 (default): --cashu-token=cashuA... (Cashu eCash token)');
744
+ warn(' Option 2: --agent=NAME (auto-mints Cashu via NWC wallet)');
689
745
  await node.destroy();
690
746
  process.exit(1);
691
747
  }
@@ -697,7 +753,6 @@ async function main() {
697
753
  budget: BUDGET,
698
754
  sats_per_minute: satsPerMinute,
699
755
  payment_method: paymentMethod,
700
- ...(paymentMethod === 'ndebit' ? { ndebit: NDEBIT } : {}),
701
756
  }, 15_000);
702
757
  if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
703
758
  warn(`Unexpected response: ${ackResp.type}`);
package/dist/swarm.d.ts CHANGED
@@ -45,10 +45,10 @@ export interface SwarmMessage {
45
45
  sats_per_minute?: number;
46
46
  balance?: number;
47
47
  duration_s?: number;
48
- ndebit?: string;
49
- payment_method?: 'ndebit' | 'invoice';
48
+ payment_method?: 'cashu' | 'invoice';
50
49
  bolt11?: string;
51
50
  preimage?: string;
51
+ cashu_token?: string;
52
52
  method?: string;
53
53
  path?: string;
54
54
  headers?: Record<string, string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.3.6",
4
- "description": "2020117 agent runtime — API polling + Hyperswarm P2P + CLINK Lightning payments",
3
+ "version": "0.3.7",
4
+ "description": "2020117 agent runtime — API polling + Hyperswarm P2P + Cashu/Lightning payments",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "2020117-agent": "./dist/agent.js",
@@ -13,7 +13,8 @@
13
13
  "exports": {
14
14
  "./processor": "./dist/processor.js",
15
15
  "./swarm": "./dist/swarm.js",
16
- "./clink": "./dist/clink.js",
16
+ "./cashu": "./dist/cashu.js",
17
+ "./lightning": "./dist/clink.js",
17
18
  "./api": "./dist/api.js"
18
19
  },
19
20
  "scripts": {
@@ -26,7 +27,7 @@
26
27
  "dev:session": "npx tsx src/session.ts"
27
28
  },
28
29
  "dependencies": {
29
- "@shocknet/clink-sdk": "^1.5.4",
30
+ "@cashu/cashu-ts": "^2.5.3",
30
31
  "hyperswarm": "^4.17.0",
31
32
  "ws": "^8.19.0"
32
33
  },