2020117-agent 0.3.5 → 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 } 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,106 +363,73 @@ 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
- if (!msg.ndebit) {
345
- node.send(socket, { type: 'error', id: msg.id, message: 'session_start 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' });
346
370
  return;
347
371
  }
348
372
  const satsPerMinute = state.skill?.pricing?.sats_per_minute
349
373
  || Number(process.env.SATS_PER_MINUTE)
350
374
  || msg.sats_per_minute
351
375
  || 10;
352
- const BILLING_INTERVAL_MIN = 10;
353
- const debitAmount = satsPerMinute * BILLING_INTERVAL_MIN;
376
+ const BILLING_INTERVAL_MIN = 1;
377
+ const billingAmount = satsPerMinute * BILLING_INTERVAL_MIN;
354
378
  const sessionId = randomBytes(8).toString('hex');
355
- console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, billing every ${BILLING_INTERVAL_MIN}min (${debitAmount} sats)`);
379
+ console.log(`[${label}] Session ${sessionId} from ${tag}: ${satsPerMinute} sats/min, payment=${paymentMethod}, billing every ${BILLING_INTERVAL_MIN}min (${billingAmount} sats)`);
356
380
  const session = {
357
381
  socket,
358
382
  peerId,
359
383
  sessionId,
360
384
  satsPerMinute,
361
- ndebit: msg.ndebit,
385
+ paymentMethod,
362
386
  totalEarned: 0,
363
387
  startedAt: Date.now(),
364
- lastDebitAt: Date.now(),
365
- debitTimer: null,
388
+ lastPaidAt: Date.now(),
389
+ billingTimer: null,
366
390
  timeoutTimer: null,
367
391
  };
368
392
  activeSessions.set(sessionId, session);
369
- // Debit first 10 minutes immediately (prepaid model)
370
- let firstDebit;
371
- try {
372
- firstDebit = await collectPayment({
373
- ndebit: session.ndebit,
374
- lightningAddress: LIGHTNING_ADDRESS,
375
- amountSats: debitAmount,
376
- });
377
- }
378
- catch (e) {
379
- console.warn(`[${label}] Session ${sessionId}: first debit error: ${e.message}`);
380
- node.send(socket, { type: 'error', id: msg.id, message: `Payment error: ${e.message}` });
381
- activeSessions.delete(sessionId);
382
- return;
383
- }
384
- if (!firstDebit.ok) {
385
- console.warn(`[${label}] Session ${sessionId}: first debit failed: ${firstDebit.error}`);
386
- node.send(socket, { type: 'error', id: msg.id, message: `Payment failed: ${firstDebit.error}` });
387
- activeSessions.delete(sessionId);
388
- return;
389
- }
390
- session.totalEarned += debitAmount;
391
- session.lastDebitAt = Date.now();
392
- console.log(`[${label}] Session ${sessionId}: first ${BILLING_INTERVAL_MIN}min paid (${debitAmount} sats)`);
393
393
  node.send(socket, {
394
394
  type: 'session_ack',
395
395
  id: msg.id,
396
396
  session_id: sessionId,
397
397
  sats_per_minute: satsPerMinute,
398
+ payment_method: paymentMethod,
398
399
  });
399
- // Notify customer of the debit
400
- node.send(socket, {
401
- type: 'session_tick_ack',
402
- id: sessionId,
403
- session_id: sessionId,
404
- amount: debitAmount,
405
- balance: msg.budget ? msg.budget - session.totalEarned : undefined,
406
- });
407
- // Debit every 10 minutes
408
- session.debitTimer = setInterval(async () => {
409
- let debit;
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
410
415
  try {
411
- debit = await collectPayment({
412
- ndebit: session.ndebit,
413
- lightningAddress: LIGHTNING_ADDRESS,
414
- amountSats: debitAmount,
415
- });
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)`);
416
420
  }
417
421
  catch (e) {
418
- console.log(`[${label}] Session ${sessionId}: debit error (${e.message}) — ending session`);
419
- endSession(node, session, label);
420
- return;
421
- }
422
- if (!debit.ok) {
423
- console.log(`[${label}] Session ${sessionId}: debit failed (${debit.error}) — ending session`);
422
+ console.warn(`[${label}] Session ${session.sessionId}: Cashu token invalid: ${e.message} — ending session`);
424
423
  endSession(node, session, label);
425
- return;
426
424
  }
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
431
- node.send(socket, {
432
- type: 'session_tick_ack',
433
- id: sessionId,
434
- session_id: sessionId,
435
- amount: debitAmount,
436
- balance: msg.budget ? msg.budget - session.totalEarned : undefined,
437
- });
438
- }, BILLING_INTERVAL_MIN * 60_000);
425
+ }
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)`);
432
+ }
439
433
  return;
440
434
  }
441
435
  if (msg.type === 'session_end') {
@@ -622,7 +616,7 @@ async function startSwarmListener(label) {
622
616
  return;
623
617
  }
624
618
  });
625
- // Handle customer disconnect — payments already settled via CLINK
619
+ // Handle customer disconnect
626
620
  node.on('peer-leave', (peerId) => {
627
621
  const tag = peerId.slice(0, 8);
628
622
  // Find and end all sessions for this peer
@@ -654,10 +648,10 @@ function findSessionBySocket(socket) {
654
648
  }
655
649
  function endSession(node, session, label) {
656
650
  const durationS = Math.round((Date.now() - session.startedAt) / 1000);
657
- // Stop debit timer
658
- if (session.debitTimer) {
659
- clearInterval(session.debitTimer);
660
- session.debitTimer = null;
651
+ // Stop billing timer
652
+ if (session.billingTimer) {
653
+ clearInterval(session.billingTimer);
654
+ session.billingTimer = null;
661
655
  }
662
656
  if (session.timeoutTimer) {
663
657
  clearTimeout(session.timeoutTimer);
@@ -686,7 +680,7 @@ function endSession(node, session, label) {
686
680
  // Socket may already be closed (peer disconnect)
687
681
  }
688
682
  console.log(`[${label}] Session ${session.sessionId} ended: ${session.totalEarned} sats, ${durationS}s`);
689
- // Update P2P lifetime counters — no batch claim needed with CLINK (payments settled instantly)
683
+ // Update P2P lifetime counters
690
684
  state.p2pSessionsCompleted++;
691
685
  state.p2pTotalEarnedSats += session.totalEarned;
692
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 CLINK debit payments.
4
+ * per-minute Lightning invoice 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 Lightning invoice 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
  *
@@ -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 } 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 = {
@@ -112,7 +116,7 @@ function sendAndWait(msg, timeoutMs) {
112
116
  }
113
117
  // --- 6. Message handler ---
114
118
  function setupMessageHandler() {
115
- state.node.on('message', (msg) => {
119
+ state.node.on('message', async (msg) => {
116
120
  // Handle chunked HTTP responses — reassemble before resolving
117
121
  if (msg.type === 'http_response' && msg.chunk_total && msg.chunk_total > 1) {
118
122
  const id = msg.id;
@@ -162,14 +166,62 @@ function setupMessageHandler() {
162
166
  cleanup();
163
167
  break;
164
168
  }
165
- case 'session_tick_ack': {
166
- // Provider debited our wallet and is reporting the result
167
- if (msg.amount !== undefined) {
168
- state.totalSpent += msg.amount;
169
+ case 'session_tick': {
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;
169
175
  }
170
- if (msg.balance !== undefined) {
171
- log(`Debit: ${msg.amount ?? '?'} sats (balance: ${msg.balance} sats, ${estimatedMinutesLeft()} min left)`);
176
+ if (msg.bolt11) {
177
+ // Invoice mode: pay bolt11 via wallet
178
+ log(`Paying invoice: ${amount} sats...`);
179
+ const payResult = await walletPayInvoice(msg.bolt11);
180
+ if (payResult.ok) {
181
+ state.totalSpent += amount;
182
+ log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
183
+ state.node.send(state.socket, {
184
+ type: 'session_tick_ack',
185
+ id: msg.id,
186
+ session_id: state.sessionId,
187
+ preimage: payResult.preimage,
188
+ amount,
189
+ });
190
+ }
191
+ else {
192
+ warn(`Invoice payment failed: ${payResult.error} — ending session`);
193
+ endSession();
194
+ }
172
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`);
214
+ endSession();
215
+ }
216
+ }
217
+ else {
218
+ warn('No payment method available — ending session');
219
+ endSession();
220
+ }
221
+ break;
222
+ }
223
+ case 'session_tick_ack': {
224
+ // Ignore — this is our own ack echoed back
173
225
  break;
174
226
  }
175
227
  case 'error': {
@@ -227,10 +279,9 @@ function setupMessageHandler() {
227
279
  }
228
280
  });
229
281
  }
230
- // --- 3. Payment tracking (CLINK — provider pulls, customer monitors) ---
231
- // With CLINK debit, the provider generates invoices and debits the customer's
232
- // wallet directly. The customer receives session_tick_ack notifications with
233
- // 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.
234
285
  // --- 4. HTTP proxy ---
235
286
  function startHttpProxy() {
236
287
  return new Promise((resolve, reject) => {
@@ -638,22 +689,70 @@ async function main() {
638
689
  state.satsPerMinute = satsPerMinute;
639
690
  log(`Pricing: ${satsPerMinute} sats/min`);
640
691
  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).');
692
+ // 4. Determine payment method: Cashu (default) or invoice (fallback)
693
+ let paymentMethod;
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
+ }
704
+ }
705
+ else if (hasApiKey()) {
706
+ // Auto-mint Cashu tokens via NWC wallet
707
+ const balance = await walletGetBalance();
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);
739
+ }
740
+ }
741
+ else {
742
+ warn('No payment method available.');
743
+ warn(' Option 1 (default): --cashu-token=cashuA... (Cashu eCash token)');
744
+ warn(' Option 2: --agent=NAME (auto-mints Cashu via NWC wallet)');
645
745
  await node.destroy();
646
746
  process.exit(1);
647
747
  }
648
- log(`Payment: CLINK debit (provider pulls per-minute)`);
649
- // 5. Send session_start with ndebit, wait for session_ack
748
+ // 5. Send session_start, wait for session_ack
650
749
  const startId = randomBytes(4).toString('hex');
651
750
  const ackResp = await sendAndWait({
652
751
  type: 'session_start',
653
752
  id: startId,
654
753
  budget: BUDGET,
655
754
  sats_per_minute: satsPerMinute,
656
- ndebit: NDEBIT,
755
+ payment_method: paymentMethod,
657
756
  }, 15_000);
658
757
  if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
659
758
  warn(`Unexpected response: ${ackResp.type}`);
@@ -668,7 +767,7 @@ async function main() {
668
767
  log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
669
768
  }
670
769
  log(`Session started: ${state.sessionId}`);
671
- log(`Provider will debit ${state.satsPerMinute} sats/min via CLINK`);
770
+ log(`Billing: ${state.satsPerMinute} sats/min via ${paymentMethod}`);
672
771
  // 6. Start HTTP proxy
673
772
  try {
674
773
  await startHttpProxy();
package/dist/swarm.d.ts CHANGED
@@ -45,7 +45,10 @@ export interface SwarmMessage {
45
45
  sats_per_minute?: number;
46
46
  balance?: number;
47
47
  duration_s?: number;
48
- ndebit?: string;
48
+ payment_method?: 'cashu' | 'invoice';
49
+ bolt11?: string;
50
+ preimage?: string;
51
+ cashu_token?: string;
49
52
  method?: string;
50
53
  path?: string;
51
54
  headers?: Record<string, string>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.3.5",
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
  },