2020117-agent 0.4.4 → 0.4.6

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
@@ -743,6 +743,7 @@ async function startSwarmListener(label) {
743
743
  lastPaidAt: Date.now(),
744
744
  billingTimer: null,
745
745
  timeoutTimer: null,
746
+ customerPubkey: msg.pubkey || undefined,
746
747
  };
747
748
  activeSessions.set(sessionId, session);
748
749
  node.send(socket, {
@@ -751,6 +752,7 @@ async function startSwarmListener(label) {
751
752
  session_id: sessionId,
752
753
  sats_per_minute: satsPerMinute,
753
754
  payment_method: paymentMethod,
755
+ pubkey: state.sovereignKeys?.pubkey,
754
756
  });
755
757
  // Send first billing tick
756
758
  await sendBillingTick(node, session, billingAmount, label);
@@ -1035,6 +1037,32 @@ function endSession(node, session, label) {
1035
1037
  // Socket may already be closed (peer disconnect)
1036
1038
  }
1037
1039
  console.log(`[${label}] Session ${session.sessionId} ended: ${session.totalEarned} sats, ${durationS}s`);
1040
+ // Publish Kind 30311 endorsement for customer (best-effort)
1041
+ if (state.sovereignKeys && state.relayPool && session.customerPubkey) {
1042
+ try {
1043
+ const endorsement = signEvent({
1044
+ kind: 30311,
1045
+ tags: [
1046
+ ['d', session.customerPubkey],
1047
+ ['p', session.customerPubkey],
1048
+ ['rating', '5'],
1049
+ ['k', String(KIND)],
1050
+ ],
1051
+ content: JSON.stringify({
1052
+ rating: 5,
1053
+ context: {
1054
+ session_duration_s: durationS,
1055
+ total_sats: session.totalEarned,
1056
+ kinds: [KIND],
1057
+ last_job_at: Math.floor(Date.now() / 1000),
1058
+ },
1059
+ }),
1060
+ }, state.sovereignKeys.privkey);
1061
+ state.relayPool.publish(endorsement).catch(() => { });
1062
+ console.log(`[${label}] Published endorsement for customer ${session.customerPubkey.slice(0, 8)}`);
1063
+ }
1064
+ catch { }
1065
+ }
1038
1066
  // Update P2P lifetime counters
1039
1067
  state.p2pSessionsCompleted++;
1040
1068
  state.p2pTotalEarnedSats += session.totalEarned;
package/dist/cashu.d.ts CHANGED
@@ -45,6 +45,22 @@ export declare function decodeCashuToken(tokenStr: string): {
45
45
  * Encode proofs back into a portable token string.
46
46
  */
47
47
  export declare function encodeCashuToken(mintUrl: string, proofs: Proof[]): string;
48
+ /**
49
+ * Melt proofs back into a Lightning invoice — converts Cashu back to Lightning.
50
+ * Returns the payment preimage and any change proofs.
51
+ */
52
+ export declare function meltProofs(mintUrl: string, proofs: Proof[], invoice: string): Promise<{
53
+ preimage: string;
54
+ change: Proof[];
55
+ }>;
56
+ /**
57
+ * Estimate total cost to melt (invoice amount + fee_reserve) for a given invoice.
58
+ */
59
+ export declare function estimateMeltFee(mintUrl: string, invoice: string): Promise<{
60
+ amount: number;
61
+ fee: number;
62
+ total: number;
63
+ }>;
48
64
  /**
49
65
  * Request a mint quote — returns a Lightning invoice to pay for minting tokens.
50
66
  */
package/dist/cashu.js CHANGED
@@ -60,6 +60,31 @@ export function decodeCashuToken(tokenStr) {
60
60
  export function encodeCashuToken(mintUrl, proofs) {
61
61
  return getEncodedTokenV4({ mint: mintUrl, proofs });
62
62
  }
63
+ /**
64
+ * Melt proofs back into a Lightning invoice — converts Cashu back to Lightning.
65
+ * Returns the payment preimage and any change proofs.
66
+ */
67
+ export async function meltProofs(mintUrl, proofs, invoice) {
68
+ const wallet = await getWallet(mintUrl);
69
+ const meltQuote = await wallet.createMeltQuote(invoice);
70
+ const amountNeeded = meltQuote.amount + meltQuote.fee_reserve;
71
+ const total = proofs.reduce((s, p) => s + p.amount, 0);
72
+ if (total < amountNeeded) {
73
+ throw new Error(`Need ${amountNeeded} sats (invoice ${meltQuote.amount} + fee ${meltQuote.fee_reserve}) but only have ${total}`);
74
+ }
75
+ const { send, keep } = await wallet.send(amountNeeded, proofs, { includeFees: true });
76
+ const result = await wallet.meltProofs(meltQuote, send);
77
+ const change = [...keep, ...(result.change || [])];
78
+ return { preimage: result.quote.payment_preimage || '', change };
79
+ }
80
+ /**
81
+ * Estimate total cost to melt (invoice amount + fee_reserve) for a given invoice.
82
+ */
83
+ export async function estimateMeltFee(mintUrl, invoice) {
84
+ const wallet = await getWallet(mintUrl);
85
+ const quote = await wallet.createMeltQuote(invoice);
86
+ return { amount: quote.amount, fee: quote.fee_reserve, total: quote.amount + quote.fee_reserve };
87
+ }
63
88
  /**
64
89
  * Request a mint quote — returns a Lightning invoice to pay for minting tokens.
65
90
  */
package/dist/session.js CHANGED
@@ -51,9 +51,9 @@ for (const arg of process.argv.slice(2)) {
51
51
  import { SwarmNode, topicFromKind } from './swarm.js';
52
52
  import { queryProviderSkill } from './p2p-customer.js';
53
53
  import { walletPayInvoice, walletGetBalance, hasApiKey } from './api.js';
54
- import { decodeCashuToken, sendCashuToken, createMintQuote, claimMintQuote } from './cashu.js';
55
- import { parseNwcUri, nwcGetBalance, nwcPayInvoice } from './nwc.js';
56
- import { loadSovereignKeys } from './nostr.js';
54
+ import { decodeCashuToken, sendCashuToken, createMintQuote, claimMintQuote, meltProofs, estimateMeltFee } from './cashu.js';
55
+ import { parseNwcUri, nwcGetBalance, nwcPayInvoice, nwcMakeInvoice } from './nwc.js';
56
+ import { loadSovereignKeys, signEvent, RelayPool } from './nostr.js';
57
57
  import { randomBytes } from 'crypto';
58
58
  import { createServer } from 'http';
59
59
  import { createInterface } from 'readline';
@@ -65,11 +65,15 @@ const BUDGET = Number(process.env.BUDGET_SATS) || 500;
65
65
  const PORT = Number(process.env.SESSION_PORT) || 8080;
66
66
  const CASHU_TOKEN = process.env.CASHU_TOKEN || '';
67
67
  const MINT_URL = process.env.CASHU_MINT_URL || 'https://8333.space:3338';
68
+ // Load sovereign keys (for pubkey exchange + endorsement publishing)
69
+ const sovereignKeys = loadSovereignKeys(process.env.AGENT);
68
70
  // Load NWC URI: --nwc flag > .2020117_keys nwc_uri > none
69
71
  let nwcParsed = null;
70
72
  const nwcUri = process.env.NWC_URI
71
- || loadSovereignKeys(process.env.AGENT)?.nwc_uri
73
+ || sovereignKeys?.nwc_uri
72
74
  || null;
75
+ // Track provider's Nostr pubkey (received in session_ack)
76
+ let providerPubkey = null;
73
77
  // Mutable Cashu wallet state (loaded from CASHU_TOKEN at startup)
74
78
  let cashuState = null;
75
79
  const TICK_INTERVAL_MS = 60_000;
@@ -694,6 +698,62 @@ async function endSession() {
694
698
  }
695
699
  log(`Session ended. Total: ${state.totalSpent} sats for ${duration}s.`);
696
700
  }
701
+ // Publish Kind 30311 endorsement for provider (best-effort)
702
+ if (sovereignKeys?.privkey && providerPubkey) {
703
+ try {
704
+ const endorsement = signEvent({
705
+ kind: 30311,
706
+ tags: [
707
+ ['d', providerPubkey],
708
+ ['p', providerPubkey],
709
+ ['rating', '5'],
710
+ ['k', String(KIND)],
711
+ ],
712
+ content: JSON.stringify({
713
+ rating: 5,
714
+ context: {
715
+ session_duration_s: elapsedSeconds(),
716
+ total_sats: state.totalSpent,
717
+ kinds: [KIND],
718
+ last_job_at: Math.floor(Date.now() / 1000),
719
+ },
720
+ }),
721
+ }, sovereignKeys.privkey);
722
+ const relayUrls = sovereignKeys.relays?.length ? sovereignKeys.relays : ['wss://relay.2020117.xyz'];
723
+ const relay = new RelayPool(relayUrls);
724
+ await relay.connect();
725
+ await relay.publish(endorsement);
726
+ await relay.close();
727
+ log(`Published endorsement for provider ${providerPubkey.slice(0, 8)}`);
728
+ }
729
+ catch { }
730
+ }
731
+ // Refund remaining Cashu proofs back to NWC wallet
732
+ if (cashuState && cashuState.proofs.length > 0 && nwcParsed) {
733
+ const remaining = cashuState.proofs.reduce((s, p) => s + p.amount, 0);
734
+ if (remaining >= 3) {
735
+ log(`Refunding ${remaining} sats Cashu → NWC wallet...`);
736
+ try {
737
+ // Probe: create 1-sat invoice to discover melt fee_reserve + swap fee
738
+ const probe = await nwcMakeInvoice(nwcParsed, 1000, 'fee-probe');
739
+ const { fee } = await estimateMeltFee(cashuState.mintUrl, probe.bolt11);
740
+ // fee = melt fee_reserve; +1 for Cashu internal swap fee
741
+ const refundAmount = remaining - fee - 1;
742
+ if (refundAmount < 1) {
743
+ warn(`Remaining ${remaining} sats < melt fee (${fee} sats), cannot refund`);
744
+ }
745
+ else {
746
+ const { bolt11 } = await nwcMakeInvoice(nwcParsed, refundAmount * 1000, '2020117-session refund');
747
+ const { preimage } = await meltProofs(cashuState.mintUrl, cashuState.proofs, bolt11);
748
+ cashuState.proofs = [];
749
+ log(`Refunded ${refundAmount} sats (fee: ${fee}, preimage: ${preimage?.slice(0, 16)}...)`);
750
+ }
751
+ }
752
+ catch (e) {
753
+ warn(`Refund failed: ${e.message} — ${remaining} sats lost`);
754
+ }
755
+ }
756
+ }
697
757
  // Close HTTP proxy
698
758
  if (state.httpServer) {
699
759
  state.httpServer.close();
@@ -774,36 +834,20 @@ async function main() {
774
834
  }
775
835
  }
776
836
  else if (nwcUri) {
777
- // NWC direct: auto-mint Cashu tokens via local NWC wallet (no platform API)
837
+ // NWC direct: pay provider invoices via Lightning no Cashu needed
778
838
  nwcParsed = parseNwcUri(nwcUri);
779
839
  const { balance_msats } = await nwcGetBalance(nwcParsed);
780
840
  const balance = Math.floor(balance_msats / 1000);
781
841
  if (balance <= 0) {
782
- warn('NWC wallet balance is 0. Cannot auto-mint Cashu tokens.');
842
+ warn('NWC wallet balance is 0.');
783
843
  await node.destroy();
784
844
  process.exit(1);
785
845
  }
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);
846
+ if (balance < BUDGET) {
847
+ warn(`NWC wallet balance (${balance} sats) < budget (${BUDGET} sats)`);
806
848
  }
849
+ paymentMethod = 'invoice';
850
+ log(`Payment: NWC direct (balance: ${balance} sats, pay-per-tick via Lightning)`);
807
851
  }
808
852
  else if (hasApiKey()) {
809
853
  // Platform API fallback: auto-mint Cashu tokens via platform wallet proxy
@@ -854,6 +898,7 @@ async function main() {
854
898
  budget: BUDGET,
855
899
  sats_per_minute: satsPerMinute,
856
900
  payment_method: paymentMethod,
901
+ pubkey: sovereignKeys?.pubkey,
857
902
  }, 15_000);
858
903
  if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
859
904
  warn(`Unexpected response: ${ackResp.type}`);
@@ -862,11 +907,39 @@ async function main() {
862
907
  }
863
908
  state.sessionId = ackResp.session_id;
864
909
  state.startedAt = Date.now();
910
+ providerPubkey = ackResp.pubkey || null;
865
911
  // If the provider dictated a different rate, use it
866
912
  if (ackResp.sats_per_minute && ackResp.sats_per_minute !== satsPerMinute) {
867
913
  state.satsPerMinute = ackResp.sats_per_minute;
868
914
  log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
869
915
  }
916
+ // Provider may override payment method (e.g. no Lightning Address → force cashu)
917
+ if (ackResp.payment_method && ackResp.payment_method !== paymentMethod) {
918
+ if (ackResp.payment_method === 'cashu' && paymentMethod === 'invoice' && nwcParsed) {
919
+ // Provider doesn't support invoice — fall back to NWC-minted Cashu
920
+ log(`Provider requires Cashu — minting via NWC...`);
921
+ try {
922
+ const { balance_msats } = await nwcGetBalance(nwcParsed);
923
+ const balance = Math.floor(balance_msats / 1000);
924
+ const mintAmount = Math.min(balance, BUDGET);
925
+ const { quote, invoice } = await createMintQuote(MINT_URL, mintAmount);
926
+ const { preimage } = await nwcPayInvoice(nwcParsed, invoice);
927
+ const token = await claimMintQuote(MINT_URL, mintAmount, quote);
928
+ const { mint, proofs } = decodeCashuToken(token);
929
+ cashuState = { mintUrl: mint, proofs };
930
+ paymentMethod = 'cashu';
931
+ log(`Minted ${proofs.reduce((s, p) => s + p.amount, 0)} sats Cashu — fallback ready`);
932
+ }
933
+ catch (e) {
934
+ warn(`Cashu fallback mint failed: ${e.message}`);
935
+ await node.destroy();
936
+ process.exit(1);
937
+ }
938
+ }
939
+ else {
940
+ paymentMethod = ackResp.payment_method;
941
+ }
942
+ }
870
943
  log(`Session started: ${state.sessionId}`);
871
944
  log(`Billing: ${state.satsPerMinute} sats/min via ${paymentMethod}`);
872
945
  // 6. Start HTTP proxy
package/dist/swarm.d.ts CHANGED
@@ -46,6 +46,7 @@ export interface SwarmMessage {
46
46
  balance?: number;
47
47
  duration_s?: number;
48
48
  payment_method?: 'cashu' | 'invoice';
49
+ pubkey?: string;
49
50
  bolt11?: string;
50
51
  preimage?: string;
51
52
  cashu_token?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "2020117-agent",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "2020117 agent runtime — API polling + Hyperswarm P2P + Sovereign Nostr mode + Cashu/Lightning payments",
5
5
  "type": "module",
6
6
  "bin": {