2020117-agent 0.3.3 → 0.3.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 +33 -54
- package/dist/api.d.ts +7 -0
- package/dist/api.js +23 -0
- package/dist/session.d.ts +2 -2
- package/dist/session.js +43 -20
- package/dist/swarm.d.ts +2 -0
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -69,7 +69,7 @@ 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 {
|
|
72
|
+
import { generateInvoice } from './clink.js';
|
|
73
73
|
import { readFileSync } from 'fs';
|
|
74
74
|
import WebSocket from 'ws';
|
|
75
75
|
// Polyfill global WebSocket for Node.js < 22 (needed by @shocknet/clink-sdk)
|
|
@@ -161,12 +161,7 @@ async function main() {
|
|
|
161
161
|
console.log(`[${label}] Lightning Address loaded from platform: ${LIGHTNING_ADDRESS}`);
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
|
-
// 3.
|
|
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
|
|
164
|
+
// 3. Platform registration + heartbeat
|
|
170
165
|
await setupPlatform(label);
|
|
171
166
|
// 5. Async inbox poller
|
|
172
167
|
startInboxPoller(label);
|
|
@@ -341,10 +336,6 @@ async function startSwarmListener(label) {
|
|
|
341
336
|
node.send(socket, { type: 'error', id: msg.id, message: 'Provider has no Lightning Address configured' });
|
|
342
337
|
return;
|
|
343
338
|
}
|
|
344
|
-
if (!msg.ndebit) {
|
|
345
|
-
node.send(socket, { type: 'error', id: msg.id, message: 'session_start requires ndebit authorization' });
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
339
|
const satsPerMinute = state.skill?.pricing?.sats_per_minute
|
|
349
340
|
|| Number(process.env.SATS_PER_MINUTE)
|
|
350
341
|
|| msg.sats_per_minute
|
|
@@ -358,7 +349,6 @@ async function startSwarmListener(label) {
|
|
|
358
349
|
peerId,
|
|
359
350
|
sessionId,
|
|
360
351
|
satsPerMinute,
|
|
361
|
-
ndebit: msg.ndebit,
|
|
362
352
|
totalEarned: 0,
|
|
363
353
|
startedAt: Date.now(),
|
|
364
354
|
lastDebitAt: Date.now(),
|
|
@@ -366,78 +356,67 @@ async function startSwarmListener(label) {
|
|
|
366
356
|
timeoutTimer: null,
|
|
367
357
|
};
|
|
368
358
|
activeSessions.set(sessionId, session);
|
|
369
|
-
//
|
|
370
|
-
let
|
|
359
|
+
// Generate first invoice and send to customer for payment
|
|
360
|
+
let firstInvoice;
|
|
371
361
|
try {
|
|
372
|
-
|
|
373
|
-
ndebit: session.ndebit,
|
|
374
|
-
lightningAddress: LIGHTNING_ADDRESS,
|
|
375
|
-
amountSats: debitAmount,
|
|
376
|
-
});
|
|
362
|
+
firstInvoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
|
|
377
363
|
}
|
|
378
364
|
catch (e) {
|
|
379
|
-
console.warn(`[${label}] Session ${sessionId}:
|
|
380
|
-
node.send(socket, { type: 'error', id: msg.id, message: `
|
|
365
|
+
console.warn(`[${label}] Session ${sessionId}: invoice error: ${e.message}`);
|
|
366
|
+
node.send(socket, { type: 'error', id: msg.id, message: `Invoice error: ${e.message}` });
|
|
381
367
|
activeSessions.delete(sessionId);
|
|
382
368
|
return;
|
|
383
369
|
}
|
|
384
|
-
|
|
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)`);
|
|
370
|
+
// Accept session first, then request payment
|
|
393
371
|
node.send(socket, {
|
|
394
372
|
type: 'session_ack',
|
|
395
373
|
id: msg.id,
|
|
396
374
|
session_id: sessionId,
|
|
397
375
|
sats_per_minute: satsPerMinute,
|
|
398
376
|
});
|
|
399
|
-
//
|
|
377
|
+
// Send invoice to customer — wait for session_tick_ack with preimage
|
|
378
|
+
const tickId = randomBytes(4).toString('hex');
|
|
400
379
|
node.send(socket, {
|
|
401
|
-
type: '
|
|
402
|
-
id:
|
|
380
|
+
type: 'session_tick',
|
|
381
|
+
id: tickId,
|
|
403
382
|
session_id: sessionId,
|
|
383
|
+
bolt11: firstInvoice,
|
|
404
384
|
amount: debitAmount,
|
|
405
|
-
balance: msg.budget ? msg.budget - session.totalEarned : undefined,
|
|
406
385
|
});
|
|
407
|
-
|
|
386
|
+
console.log(`[${label}] Session ${sessionId}: waiting for first payment (${debitAmount} sats)`);
|
|
387
|
+
// Billing every 10 minutes — send invoice, wait for customer to pay
|
|
408
388
|
session.debitTimer = setInterval(async () => {
|
|
409
|
-
let
|
|
389
|
+
let invoice;
|
|
410
390
|
try {
|
|
411
|
-
|
|
412
|
-
ndebit: session.ndebit,
|
|
413
|
-
lightningAddress: LIGHTNING_ADDRESS,
|
|
414
|
-
amountSats: debitAmount,
|
|
415
|
-
});
|
|
391
|
+
invoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
|
|
416
392
|
}
|
|
417
393
|
catch (e) {
|
|
418
|
-
console.log(`[${label}] Session ${sessionId}:
|
|
394
|
+
console.log(`[${label}] Session ${sessionId}: invoice error (${e.message}) — ending session`);
|
|
419
395
|
endSession(node, session, label);
|
|
420
396
|
return;
|
|
421
397
|
}
|
|
422
|
-
|
|
423
|
-
console.log(`[${label}] Session ${sessionId}: debit failed (${debit.error}) — ending session`);
|
|
424
|
-
endSession(node, session, label);
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
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
|
|
398
|
+
const tid = randomBytes(4).toString('hex');
|
|
431
399
|
node.send(socket, {
|
|
432
|
-
type: '
|
|
433
|
-
id:
|
|
400
|
+
type: 'session_tick',
|
|
401
|
+
id: tid,
|
|
434
402
|
session_id: sessionId,
|
|
403
|
+
bolt11: invoice,
|
|
435
404
|
amount: debitAmount,
|
|
436
|
-
balance: msg.budget ? msg.budget - session.totalEarned : undefined,
|
|
437
405
|
});
|
|
406
|
+
console.log(`[${label}] Session ${sessionId}: sent invoice for ${debitAmount} sats`);
|
|
438
407
|
}, BILLING_INTERVAL_MIN * 60_000);
|
|
439
408
|
return;
|
|
440
409
|
}
|
|
410
|
+
if (msg.type === 'session_tick_ack') {
|
|
411
|
+
const session = activeSessions.get(msg.session_id || '');
|
|
412
|
+
if (!session)
|
|
413
|
+
return;
|
|
414
|
+
const amount = msg.amount || 0;
|
|
415
|
+
session.totalEarned += amount;
|
|
416
|
+
session.lastDebitAt = Date.now();
|
|
417
|
+
console.log(`[${label}] Session ${session.sessionId}: payment received (+${amount}, total: ${session.totalEarned} sats)`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
441
420
|
if (msg.type === 'session_end') {
|
|
442
421
|
const session = activeSessions.get(msg.session_id || '');
|
|
443
422
|
if (!session)
|
package/dist/api.d.ts
CHANGED
|
@@ -92,6 +92,13 @@ export declare function updateProfile(fields: {
|
|
|
92
92
|
about?: string;
|
|
93
93
|
lightning_address?: string;
|
|
94
94
|
}): Promise<boolean>;
|
|
95
|
+
export declare function walletPayInvoice(bolt11: string): Promise<{
|
|
96
|
+
ok: boolean;
|
|
97
|
+
preimage?: string;
|
|
98
|
+
amount_sats?: number;
|
|
99
|
+
error?: string;
|
|
100
|
+
}>;
|
|
101
|
+
export declare function walletGetBalance(): Promise<number>;
|
|
95
102
|
export interface ProxyDebitResult {
|
|
96
103
|
ok: boolean;
|
|
97
104
|
preimage?: string;
|
package/dist/api.js
CHANGED
|
@@ -298,6 +298,29 @@ export async function updateProfile(fields) {
|
|
|
298
298
|
const result = await apiPut('/api/me', fields);
|
|
299
299
|
return result !== null;
|
|
300
300
|
}
|
|
301
|
+
// --- Wallet API (built-in Lightning wallet) ---
|
|
302
|
+
export async function walletPayInvoice(bolt11) {
|
|
303
|
+
if (!API_KEY)
|
|
304
|
+
return { ok: false, error: 'No API key' };
|
|
305
|
+
try {
|
|
306
|
+
const resp = await fetch(`${BASE_URL}/api/wallet/send`, {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${API_KEY}` },
|
|
309
|
+
body: JSON.stringify({ bolt11 }),
|
|
310
|
+
});
|
|
311
|
+
const data = await resp.json();
|
|
312
|
+
if (!resp.ok)
|
|
313
|
+
return { ok: false, error: data.error || `HTTP ${resp.status}` };
|
|
314
|
+
return { ok: true, preimage: data.preimage, amount_sats: data.amount_sats };
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
return { ok: false, error: e.message };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
export async function walletGetBalance() {
|
|
321
|
+
const data = await apiGet('/api/wallet/balance');
|
|
322
|
+
return data?.balance_sats ?? 0;
|
|
323
|
+
}
|
|
301
324
|
export async function proxyDebit(opts) {
|
|
302
325
|
return apiPost('/api/dvm/proxy-debit', {
|
|
303
326
|
ndebit: opts.ndebit,
|
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
|
|
4
|
+
* per-minute wallet payments.
|
|
5
5
|
*
|
|
6
6
|
* Features:
|
|
7
|
-
* -
|
|
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
|
|
4
|
+
* per-minute wallet payments.
|
|
5
5
|
*
|
|
6
6
|
* Features:
|
|
7
|
-
* -
|
|
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
|
*
|
|
@@ -39,25 +39,21 @@ for (const arg of process.argv.slice(2)) {
|
|
|
39
39
|
break;
|
|
40
40
|
case '--ndebit':
|
|
41
41
|
process.env.CLINK_NDEBIT = val;
|
|
42
|
-
break;
|
|
42
|
+
break; // legacy, ignored
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
46
46
|
import { queryProviderSkill } from './p2p-customer.js';
|
|
47
|
-
import {
|
|
47
|
+
import { walletPayInvoice, walletGetBalance } from './api.js';
|
|
48
48
|
import { randomBytes } from 'crypto';
|
|
49
49
|
import { createServer } from 'http';
|
|
50
50
|
import { createInterface } from 'readline';
|
|
51
51
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
52
52
|
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
53
|
// --- Config ---
|
|
57
54
|
const KIND = Number(process.env.DVM_KIND) || 5200;
|
|
58
55
|
const BUDGET = Number(process.env.BUDGET_SATS) || 500;
|
|
59
56
|
const PORT = Number(process.env.SESSION_PORT) || 8080;
|
|
60
|
-
const NDEBIT = process.env.CLINK_NDEBIT || loadNdebit() || '';
|
|
61
57
|
const TICK_INTERVAL_MS = 60_000;
|
|
62
58
|
const HTTP_TIMEOUT_MS = 60_000;
|
|
63
59
|
const state = {
|
|
@@ -112,7 +108,7 @@ function sendAndWait(msg, timeoutMs) {
|
|
|
112
108
|
}
|
|
113
109
|
// --- 6. Message handler ---
|
|
114
110
|
function setupMessageHandler() {
|
|
115
|
-
state.node.on('message', (msg) => {
|
|
111
|
+
state.node.on('message', async (msg) => {
|
|
116
112
|
// Handle chunked HTTP responses — reassemble before resolving
|
|
117
113
|
if (msg.type === 'http_response' && msg.chunk_total && msg.chunk_total > 1) {
|
|
118
114
|
const id = msg.id;
|
|
@@ -162,8 +158,37 @@ function setupMessageHandler() {
|
|
|
162
158
|
cleanup();
|
|
163
159
|
break;
|
|
164
160
|
}
|
|
161
|
+
case 'session_tick': {
|
|
162
|
+
// Provider sent an invoice for the next billing period — pay it
|
|
163
|
+
if (msg.bolt11 && msg.amount !== undefined) {
|
|
164
|
+
const amount = msg.amount;
|
|
165
|
+
if (state.totalSpent + amount > BUDGET) {
|
|
166
|
+
log(`Budget exhausted (need ${amount}, remaining ${remainingSats()}) — ending session`);
|
|
167
|
+
endSession();
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
log(`Paying invoice: ${amount} sats...`);
|
|
171
|
+
const payResult = await walletPayInvoice(msg.bolt11);
|
|
172
|
+
if (payResult.ok) {
|
|
173
|
+
state.totalSpent += amount;
|
|
174
|
+
log(`Paid ${amount} sats (total: ${state.totalSpent}, ~${estimatedMinutesLeft()} min left)`);
|
|
175
|
+
state.node.send(state.socket, {
|
|
176
|
+
type: 'session_tick_ack',
|
|
177
|
+
id: msg.id,
|
|
178
|
+
session_id: state.sessionId,
|
|
179
|
+
preimage: payResult.preimage,
|
|
180
|
+
amount,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
warn(`Payment failed: ${payResult.error} — ending session`);
|
|
185
|
+
endSession();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
165
190
|
case 'session_tick_ack': {
|
|
166
|
-
//
|
|
191
|
+
// Legacy: provider-pull model notification (backward compat)
|
|
167
192
|
if (msg.amount !== undefined) {
|
|
168
193
|
state.totalSpent += msg.amount;
|
|
169
194
|
}
|
|
@@ -638,22 +663,20 @@ async function main() {
|
|
|
638
663
|
state.satsPerMinute = satsPerMinute;
|
|
639
664
|
log(`Pricing: ${satsPerMinute} sats/min`);
|
|
640
665
|
log(`Budget: ${BUDGET} sats (~${Math.floor(BUDGET / satsPerMinute)} min)`);
|
|
641
|
-
// 4.
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
warn(
|
|
645
|
-
|
|
646
|
-
process.exit(1);
|
|
666
|
+
// 4. Check wallet balance
|
|
667
|
+
const balance = await walletGetBalance();
|
|
668
|
+
if (balance < BUDGET) {
|
|
669
|
+
warn(`Wallet balance (${balance} sats) is less than budget (${BUDGET} sats).`);
|
|
670
|
+
warn(`Fund your wallet: curl -X POST ${process.env.API_2020117_URL || 'https://2020117.xyz'}/api/wallet/invoice -H "Authorization: Bearer ..." -d '{"amount_sats":${BUDGET}}'`);
|
|
647
671
|
}
|
|
648
|
-
log(`
|
|
649
|
-
// 5. Send session_start
|
|
672
|
+
log(`Wallet balance: ${balance} sats`);
|
|
673
|
+
// 5. Send session_start, wait for session_ack
|
|
650
674
|
const startId = randomBytes(4).toString('hex');
|
|
651
675
|
const ackResp = await sendAndWait({
|
|
652
676
|
type: 'session_start',
|
|
653
677
|
id: startId,
|
|
654
678
|
budget: BUDGET,
|
|
655
679
|
sats_per_minute: satsPerMinute,
|
|
656
|
-
ndebit: NDEBIT,
|
|
657
680
|
}, 15_000);
|
|
658
681
|
if (ackResp.type !== 'session_ack' || !ackResp.session_id) {
|
|
659
682
|
warn(`Unexpected response: ${ackResp.type}`);
|
|
@@ -668,7 +691,7 @@ async function main() {
|
|
|
668
691
|
log(`Provider adjusted rate: ${ackResp.sats_per_minute} sats/min`);
|
|
669
692
|
}
|
|
670
693
|
log(`Session started: ${state.sessionId}`);
|
|
671
|
-
log(`
|
|
694
|
+
log(`Billing: ${state.satsPerMinute} sats/min (wallet → provider invoice)`);
|
|
672
695
|
// 6. Start HTTP proxy
|
|
673
696
|
try {
|
|
674
697
|
await startHttpProxy();
|
package/dist/swarm.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "2020117-agent",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "2020117 agent runtime — API polling + Hyperswarm P2P +
|
|
3
|
+
"version": "0.3.4",
|
|
4
|
+
"description": "2020117 agent runtime — API polling + Hyperswarm P2P + wallet Lightning payments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"2020117-agent": "./dist/agent.js",
|