2020117-agent 0.3.2 → 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 +39 -43
- 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,61 +356,67 @@ async function startSwarmListener(label) {
|
|
|
366
356
|
timeoutTimer: null,
|
|
367
357
|
};
|
|
368
358
|
activeSessions.set(sessionId, session);
|
|
369
|
-
//
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
node.send(socket, { type: 'error', id: msg.id, message: `Payment failed: ${firstDebit.error}` });
|
|
359
|
+
// Generate first invoice and send to customer for payment
|
|
360
|
+
let firstInvoice;
|
|
361
|
+
try {
|
|
362
|
+
firstInvoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
365
|
+
console.warn(`[${label}] Session ${sessionId}: invoice error: ${e.message}`);
|
|
366
|
+
node.send(socket, { type: 'error', id: msg.id, message: `Invoice error: ${e.message}` });
|
|
378
367
|
activeSessions.delete(sessionId);
|
|
379
368
|
return;
|
|
380
369
|
}
|
|
381
|
-
session
|
|
382
|
-
session.lastDebitAt = Date.now();
|
|
383
|
-
console.log(`[${label}] Session ${sessionId}: first ${BILLING_INTERVAL_MIN}min paid (${debitAmount} sats)`);
|
|
370
|
+
// Accept session first, then request payment
|
|
384
371
|
node.send(socket, {
|
|
385
372
|
type: 'session_ack',
|
|
386
373
|
id: msg.id,
|
|
387
374
|
session_id: sessionId,
|
|
388
375
|
sats_per_minute: satsPerMinute,
|
|
389
376
|
});
|
|
390
|
-
//
|
|
377
|
+
// Send invoice to customer — wait for session_tick_ack with preimage
|
|
378
|
+
const tickId = randomBytes(4).toString('hex');
|
|
391
379
|
node.send(socket, {
|
|
392
|
-
type: '
|
|
393
|
-
id:
|
|
380
|
+
type: 'session_tick',
|
|
381
|
+
id: tickId,
|
|
394
382
|
session_id: sessionId,
|
|
383
|
+
bolt11: firstInvoice,
|
|
395
384
|
amount: debitAmount,
|
|
396
|
-
balance: msg.budget ? msg.budget - session.totalEarned : undefined,
|
|
397
385
|
});
|
|
398
|
-
|
|
386
|
+
console.log(`[${label}] Session ${sessionId}: waiting for first payment (${debitAmount} sats)`);
|
|
387
|
+
// Billing every 10 minutes — send invoice, wait for customer to pay
|
|
399
388
|
session.debitTimer = setInterval(async () => {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
console.log(`[${label}] Session ${sessionId}: debit failed (${debit.error}) — ending session`);
|
|
389
|
+
let invoice;
|
|
390
|
+
try {
|
|
391
|
+
invoice = await generateInvoice(LIGHTNING_ADDRESS, debitAmount);
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
console.log(`[${label}] Session ${sessionId}: invoice error (${e.message}) — ending session`);
|
|
407
395
|
endSession(node, session, label);
|
|
408
396
|
return;
|
|
409
397
|
}
|
|
410
|
-
|
|
411
|
-
session.lastDebitAt = Date.now();
|
|
412
|
-
console.log(`[${label}] Session ${sessionId}: debit OK (+${debitAmount}, total: ${session.totalEarned} sats)`);
|
|
413
|
-
// Notify customer
|
|
398
|
+
const tid = randomBytes(4).toString('hex');
|
|
414
399
|
node.send(socket, {
|
|
415
|
-
type: '
|
|
416
|
-
id:
|
|
400
|
+
type: 'session_tick',
|
|
401
|
+
id: tid,
|
|
417
402
|
session_id: sessionId,
|
|
403
|
+
bolt11: invoice,
|
|
418
404
|
amount: debitAmount,
|
|
419
|
-
balance: msg.budget ? msg.budget - session.totalEarned : undefined,
|
|
420
405
|
});
|
|
406
|
+
console.log(`[${label}] Session ${sessionId}: sent invoice for ${debitAmount} sats`);
|
|
421
407
|
}, BILLING_INTERVAL_MIN * 60_000);
|
|
422
408
|
return;
|
|
423
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
|
+
}
|
|
424
420
|
if (msg.type === 'session_end') {
|
|
425
421
|
const session = activeSessions.get(msg.session_id || '');
|
|
426
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",
|