2020117-agent 0.4.2 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent.js CHANGED
@@ -333,6 +333,10 @@ async function publishHandlerInfo(label) {
333
333
  about: state.skill?.description || `DVM agent (kind ${KIND})`,
334
334
  pricing: { [String(KIND)]: SATS_PER_CHUNK * CHUNKS_PER_PAYMENT },
335
335
  };
336
+ if (LIGHTNING_ADDRESS) {
337
+ content.payment = { lightning_address: LIGHTNING_ADDRESS };
338
+ content.lud16 = LIGHTNING_ADDRESS;
339
+ }
336
340
  const event = signEvent({
337
341
  kind: 31990,
338
342
  tags: [
@@ -449,17 +453,43 @@ async function handleDvmRequest(label, event) {
449
453
  console.log(`[${label}] DVM result: ${result.length} chars`);
450
454
  // Send result (Kind 6xxx = request kind + 1000)
451
455
  const resultKind = KIND + 1000;
456
+ const resultTags = [
457
+ ['p', event.pubkey],
458
+ ['e', event.id],
459
+ ['request', JSON.stringify(event)],
460
+ ['amount', String(SATS_PER_CHUNK * CHUNKS_PER_PAYMENT * 1000)], // msats
461
+ ];
462
+ if (LIGHTNING_ADDRESS) {
463
+ resultTags.push(['lightning_address', LIGHTNING_ADDRESS]);
464
+ }
452
465
  const resultEvent = signEvent({
453
466
  kind: resultKind,
454
- tags: [
455
- ['p', event.pubkey],
456
- ['e', event.id],
457
- ['request', JSON.stringify(event)],
458
- ],
467
+ tags: resultTags,
459
468
  content: result,
460
469
  }, state.sovereignKeys.privkey);
461
470
  await state.relayPool.publish(resultEvent);
462
471
  console.log(`[${label}] Published DVM result (Kind ${resultKind}) via relay`);
472
+ // Publish reputation endorsement (Kind 30311) for customer
473
+ try {
474
+ const endorsementEvent = signEvent({
475
+ kind: 30311,
476
+ tags: [
477
+ ['d', event.pubkey],
478
+ ['p', event.pubkey],
479
+ ['rating', '5'],
480
+ ['k', String(KIND)],
481
+ ],
482
+ content: JSON.stringify({
483
+ rating: 5,
484
+ context: { jobs_together: 1, kinds: [KIND], last_job_at: Math.floor(Date.now() / 1000) },
485
+ }),
486
+ }, state.sovereignKeys.privkey);
487
+ await state.relayPool.publish(endorsementEvent);
488
+ console.log(`[${label}] Published endorsement (Kind 30311) for ${event.pubkey.slice(0, 8)}`);
489
+ }
490
+ catch (e) {
491
+ console.warn(`[${label}] Failed to publish endorsement: ${e.message}`);
492
+ }
463
493
  }
464
494
  finally {
465
495
  releaseSlot();
@@ -676,7 +706,14 @@ async function startSwarmListener(label) {
676
706
  node.on('message', async (msg, socket, peerId) => {
677
707
  const tag = peerId.slice(0, 8);
678
708
  if (msg.type === 'skill_request') {
679
- node.send(socket, { type: 'skill_response', id: msg.id, skill: state.skill });
709
+ const satsPerMinute = state.skill?.pricing?.sats_per_minute
710
+ || Number(process.env.SATS_PER_MINUTE)
711
+ || 10;
712
+ // Always include runtime pricing, even without a skill file
713
+ const skillWithPricing = state.skill
714
+ ? { ...state.skill, pricing: { ...(state.skill.pricing || {}), sats_per_minute: satsPerMinute } }
715
+ : { pricing: { sats_per_minute: satsPerMinute }, payment_methods: LIGHTNING_ADDRESS ? ['cashu', 'invoice'] : ['cashu'] };
716
+ node.send(socket, { type: 'skill_response', id: msg.id, skill: skillWithPricing });
680
717
  return;
681
718
  }
682
719
  // --- Session protocol ---
package/dist/session.js CHANGED
@@ -43,12 +43,17 @@ for (const arg of process.argv.slice(2)) {
43
43
  case '--mint':
44
44
  process.env.CASHU_MINT_URL = val;
45
45
  break;
46
+ case '--nwc':
47
+ process.env.NWC_URI = val;
48
+ break;
46
49
  }
47
50
  }
48
51
  import { SwarmNode, topicFromKind } from './swarm.js';
49
52
  import { queryProviderSkill } from './p2p-customer.js';
50
53
  import { walletPayInvoice, walletGetBalance, hasApiKey } from './api.js';
51
54
  import { decodeCashuToken, sendCashuToken, createMintQuote, claimMintQuote } from './cashu.js';
55
+ import { parseNwcUri, nwcGetBalance, nwcPayInvoice } from './nwc.js';
56
+ import { loadSovereignKeys } from './nostr.js';
52
57
  import { randomBytes } from 'crypto';
53
58
  import { createServer } from 'http';
54
59
  import { createInterface } from 'readline';
@@ -60,10 +65,24 @@ const BUDGET = Number(process.env.BUDGET_SATS) || 500;
60
65
  const PORT = Number(process.env.SESSION_PORT) || 8080;
61
66
  const CASHU_TOKEN = process.env.CASHU_TOKEN || '';
62
67
  const MINT_URL = process.env.CASHU_MINT_URL || 'https://8333.space:3338';
68
+ // Load NWC URI: --nwc flag > .2020117_keys nwc_uri > none
69
+ let nwcParsed = null;
70
+ const nwcUri = process.env.NWC_URI
71
+ || loadSovereignKeys(process.env.AGENT)?.nwc_uri
72
+ || null;
63
73
  // Mutable Cashu wallet state (loaded from CASHU_TOKEN at startup)
64
74
  let cashuState = null;
65
75
  const TICK_INTERVAL_MS = 60_000;
66
76
  const HTTP_TIMEOUT_MS = 60_000;
77
+ function confirmPrompt(question) {
78
+ return new Promise(resolve => {
79
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
80
+ rl.question(question, answer => {
81
+ rl.close();
82
+ resolve(answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes');
83
+ });
84
+ });
85
+ }
67
86
  const state = {
68
87
  node: null,
69
88
  socket: null,
@@ -175,23 +194,43 @@ function setupMessageHandler() {
175
194
  break;
176
195
  }
177
196
  if (msg.bolt11) {
178
- // Invoice mode: pay bolt11 via wallet
197
+ // Invoice mode: pay bolt11 via NWC direct or platform API
179
198
  log(`Paying invoice: ${amount} sats...`);
180
- const payResult = await walletPayInvoice(msg.bolt11);
181
- if (payResult.ok) {
182
- state.totalSpent += amount;
183
- log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
184
- state.node.send(state.socket, {
185
- type: 'session_tick_ack',
186
- id: msg.id,
187
- session_id: state.sessionId,
188
- preimage: payResult.preimage,
189
- amount,
190
- });
199
+ if (nwcParsed) {
200
+ try {
201
+ const { preimage } = await nwcPayInvoice(nwcParsed, msg.bolt11);
202
+ state.totalSpent += amount;
203
+ log(`Paid ${amount} sats via NWC (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
+ preimage,
209
+ amount,
210
+ });
211
+ }
212
+ catch (e) {
213
+ warn(`NWC invoice payment failed: ${e.message} — ending session`);
214
+ endSession();
215
+ }
191
216
  }
192
217
  else {
193
- warn(`Invoice payment failed: ${payResult.error} ending session`);
194
- endSession();
218
+ const payResult = await walletPayInvoice(msg.bolt11);
219
+ if (payResult.ok) {
220
+ state.totalSpent += amount;
221
+ log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
222
+ state.node.send(state.socket, {
223
+ type: 'session_tick_ack',
224
+ id: msg.id,
225
+ session_id: state.sessionId,
226
+ preimage: payResult.preimage,
227
+ amount,
228
+ });
229
+ }
230
+ else {
231
+ warn(`Invoice payment failed: ${payResult.error} — ending session`);
232
+ endSession();
233
+ }
195
234
  }
196
235
  }
197
236
  else if (cashuState) {
@@ -701,8 +740,26 @@ async function main() {
701
740
  const pricing = skill?.pricing;
702
741
  const satsPerMinute = Number(pricing?.sats_per_minute) || 10;
703
742
  state.satsPerMinute = satsPerMinute;
704
- log(`Pricing: ${satsPerMinute} sats/min`);
705
- log(`Budget: ${BUDGET} sats (~${Math.floor(BUDGET / satsPerMinute)} min)`);
743
+ // Price confirmation
744
+ const minutesAvailable = Math.floor(BUDGET / satsPerMinute);
745
+ log('');
746
+ log('┌─────────────────────────────────');
747
+ log(`│ Rate: ${satsPerMinute} sats/min`);
748
+ log(`│ Budget: ${BUDGET} sats`);
749
+ log(`│ Duration: ~${minutesAvailable} min`);
750
+ if (skill?.payment_methods) {
751
+ log(`│ Payment: ${skill.payment_methods.join(', ')}`);
752
+ }
753
+ log('└─────────────────────────────────');
754
+ log('');
755
+ if (process.stdin.isTTY) {
756
+ const confirmed = await confirmPrompt('Start session? (y/n): ');
757
+ if (!confirmed) {
758
+ log('Session cancelled.');
759
+ await node.destroy();
760
+ process.exit(0);
761
+ }
762
+ }
706
763
  // 4. Determine payment method: Cashu (default) or invoice (fallback)
707
764
  let paymentMethod;
708
765
  if (CASHU_TOKEN) {
@@ -716,8 +773,40 @@ async function main() {
716
773
  warn(`Cashu token (${tokenAmount} sats) is less than budget (${BUDGET} sats)`);
717
774
  }
718
775
  }
776
+ else if (nwcUri) {
777
+ // NWC direct: auto-mint Cashu tokens via local NWC wallet (no platform API)
778
+ nwcParsed = parseNwcUri(nwcUri);
779
+ const { balance_msats } = await nwcGetBalance(nwcParsed);
780
+ const balance = Math.floor(balance_msats / 1000);
781
+ if (balance <= 0) {
782
+ warn('NWC wallet balance is 0. Cannot auto-mint Cashu tokens.');
783
+ await node.destroy();
784
+ process.exit(1);
785
+ }
786
+ const mintAmount = Math.min(balance, BUDGET);
787
+ log(`NWC wallet balance: ${balance} sats — minting ${mintAmount} sats from ${MINT_URL}`);
788
+ try {
789
+ const { quote, invoice } = await createMintQuote(MINT_URL, mintAmount);
790
+ log(`Mint quote: ${quote} (invoice: ${invoice.slice(0, 30)}...)`);
791
+ log('Paying mint invoice via NWC...');
792
+ const { preimage } = await nwcPayInvoice(nwcParsed, invoice);
793
+ log(`Invoice paid (preimage: ${preimage?.slice(0, 16)}...)`);
794
+ log('Claiming minted tokens...');
795
+ const token = await claimMintQuote(MINT_URL, mintAmount, quote);
796
+ const { mint, proofs } = decodeCashuToken(token);
797
+ cashuState = { mintUrl: mint, proofs };
798
+ paymentMethod = 'cashu';
799
+ const totalMinted = proofs.reduce((s, p) => s + p.amount, 0);
800
+ log(`Minted ${totalMinted} sats Cashu token — using Cashu payment mode`);
801
+ }
802
+ catch (e) {
803
+ warn(`NWC auto-mint failed: ${e.message}`);
804
+ await node.destroy();
805
+ process.exit(1);
806
+ }
807
+ }
719
808
  else if (hasApiKey()) {
720
- // Auto-mint Cashu tokens via NWC wallet
809
+ // Platform API fallback: auto-mint Cashu tokens via platform wallet proxy
721
810
  const balance = await walletGetBalance();
722
811
  if (balance <= 0) {
723
812
  warn('Wallet balance is 0. Cannot auto-mint Cashu tokens.');
@@ -727,17 +816,14 @@ async function main() {
727
816
  const mintAmount = Math.min(balance, BUDGET);
728
817
  log(`Wallet balance: ${balance} sats — minting ${mintAmount} sats from ${MINT_URL}`);
729
818
  try {
730
- // 1. Request mint quote (Lightning invoice)
731
819
  const { quote, invoice } = await createMintQuote(MINT_URL, mintAmount);
732
820
  log(`Mint quote: ${quote} (invoice: ${invoice.slice(0, 30)}...)`);
733
- // 2. Pay the invoice via platform NWC wallet
734
- log('Paying mint invoice via NWC wallet...');
821
+ log('Paying mint invoice via platform wallet...');
735
822
  const payResult = await walletPayInvoice(invoice);
736
823
  if (!payResult.ok) {
737
824
  throw new Error(`Payment failed: ${payResult.error}`);
738
825
  }
739
826
  log(`Invoice paid (preimage: ${payResult.preimage?.slice(0, 16)}...)`);
740
- // 3. Claim minted proofs
741
827
  log('Claiming minted tokens...');
742
828
  const token = await claimMintQuote(MINT_URL, mintAmount, quote);
743
829
  const { mint, proofs } = decodeCashuToken(token);
@@ -755,7 +841,8 @@ async function main() {
755
841
  else {
756
842
  warn('No payment method available.');
757
843
  warn(' Option 1 (default): --cashu-token=cashuA... (Cashu eCash token)');
758
- warn(' Option 2: --agent=NAME (auto-mints Cashu via NWC wallet)');
844
+ warn(' Option 2: --nwc="nostr+walletconnect://..." (NWC direct wallet)');
845
+ warn(' Option 3: --agent=NAME (auto-mints via platform API)');
759
846
  await node.destroy();
760
847
  process.exit(1);
761
848
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "description": "2020117 agent runtime — API polling + Hyperswarm P2P + Sovereign Nostr mode + Cashu/Lightning payments",
5
5
  "type": "module",
6
6
  "bin": {