2020117-agent 0.4.1 → 0.4.3

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
@@ -64,6 +64,15 @@ const MINT_URL = process.env.CASHU_MINT_URL || 'https://8333.space:3338';
64
64
  let cashuState = null;
65
65
  const TICK_INTERVAL_MS = 60_000;
66
66
  const HTTP_TIMEOUT_MS = 60_000;
67
+ function confirmPrompt(question) {
68
+ return new Promise(resolve => {
69
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
70
+ rl.question(question, answer => {
71
+ rl.close();
72
+ resolve(answer.trim().toLowerCase() === 'y' || answer.trim().toLowerCase() === 'yes');
73
+ });
74
+ });
75
+ }
67
76
  const state = {
68
77
  node: null,
69
78
  socket: null,
@@ -72,6 +81,7 @@ const state = {
72
81
  skill: null,
73
82
  satsPerMinute: 0,
74
83
  totalSpent: 0,
84
+ pendingAmount: 0, // accumulates fractional sats until >= 1
75
85
  startedAt: 0,
76
86
  httpServer: null,
77
87
  shuttingDown: false,
@@ -194,19 +204,32 @@ function setupMessageHandler() {
194
204
  }
195
205
  }
196
206
  else if (cashuState) {
197
- // Cashu mode: split tokens and send
198
- log(`Paying with Cashu: ${amount} sats...`);
207
+ // Cashu mode: accumulate fractional amounts, pay in whole sats
208
+ state.pendingAmount += amount;
209
+ const payNow = Math.floor(state.pendingAmount);
210
+ if (payNow < 1) {
211
+ // Not enough to pay yet — ack without token
212
+ state.node.send(state.socket, {
213
+ type: 'session_tick_ack',
214
+ id: msg.id,
215
+ session_id: state.sessionId,
216
+ amount: 0,
217
+ });
218
+ break;
219
+ }
220
+ log(`Paying with Cashu: ${payNow} sats (accumulated from ${state.pendingAmount.toFixed(2)})...`);
199
221
  try {
200
- const { token, change } = await sendCashuToken(cashuState.mintUrl, cashuState.proofs, amount);
222
+ const { token, change } = await sendCashuToken(cashuState.mintUrl, cashuState.proofs, payNow);
201
223
  cashuState.proofs = change;
202
- state.totalSpent += amount;
203
- log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
224
+ state.totalSpent += payNow;
225
+ state.pendingAmount -= payNow;
226
+ log(`Paid ${payNow} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
204
227
  state.node.send(state.socket, {
205
228
  type: 'session_tick_ack',
206
229
  id: msg.id,
207
230
  session_id: state.sessionId,
208
231
  cashu_token: token,
209
- amount,
232
+ amount: payNow,
210
233
  });
211
234
  }
212
235
  catch (e) {
@@ -687,8 +710,26 @@ async function main() {
687
710
  const pricing = skill?.pricing;
688
711
  const satsPerMinute = Number(pricing?.sats_per_minute) || 10;
689
712
  state.satsPerMinute = satsPerMinute;
690
- log(`Pricing: ${satsPerMinute} sats/min`);
691
- log(`Budget: ${BUDGET} sats (~${Math.floor(BUDGET / satsPerMinute)} min)`);
713
+ // Price confirmation
714
+ const minutesAvailable = Math.floor(BUDGET / satsPerMinute);
715
+ log('');
716
+ log('┌─────────────────────────────────');
717
+ log(`│ Rate: ${satsPerMinute} sats/min`);
718
+ log(`│ Budget: ${BUDGET} sats`);
719
+ log(`│ Duration: ~${minutesAvailable} min`);
720
+ if (skill?.payment_methods) {
721
+ log(`│ Payment: ${skill.payment_methods.join(', ')}`);
722
+ }
723
+ log('└─────────────────────────────────');
724
+ log('');
725
+ if (process.stdin.isTTY) {
726
+ const confirmed = await confirmPrompt('Start session? (y/n): ');
727
+ if (!confirmed) {
728
+ log('Session cancelled.');
729
+ await node.destroy();
730
+ process.exit(0);
731
+ }
732
+ }
692
733
  // 4. Determine payment method: Cashu (default) or invoice (fallback)
693
734
  let paymentMethod;
694
735
  if (CASHU_TOKEN) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "2020117 agent runtime — API polling + Hyperswarm P2P + Sovereign Nostr mode + Cashu/Lightning payments",
5
5
  "type": "module",
6
6
  "bin": {