agent-escrow 0.1.0

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/src/cli.cjs ADDED
@@ -0,0 +1,263 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { createMarketplace, TASK_STATUS } = require('./index.cjs');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ const COMMANDS = {
9
+ 'post': 'Post a new task',
10
+ 'browse': 'Browse open tasks',
11
+ 'bid': 'Bid on a task',
12
+ 'bids': 'View bids for a task',
13
+ 'deliver': 'Submit work for a task',
14
+ 'approve': 'Approve delivery and pay worker',
15
+ 'dispute': 'Dispute a delivery',
16
+ 'cancel': 'Cancel an open task',
17
+ 'help': 'Show this help'
18
+ };
19
+
20
+ function usage() {
21
+ console.log('agent-escrow — Decentralized marketplace for AI agents\n');
22
+ console.log('Usage: agent-escrow <command> [options]\n');
23
+ console.log('Commands:');
24
+ for (const [cmd, desc] of Object.entries(COMMANDS)) {
25
+ console.log(` ${cmd.padEnd(12)} ${desc}`);
26
+ }
27
+ console.log('\nEnvironment:');
28
+ console.log(' NOSTR_SECRET_KEY Nostr secret key (hex)');
29
+ console.log(' NWC_URL Nostr Wallet Connect URL');
30
+ console.log(' LIGHTNING_ADDRESS Your Lightning address');
31
+ console.log(' ESCROW_RELAYS Comma-separated relay URLs');
32
+ console.log('\nOr provide a JSON config via --config <path>');
33
+ }
34
+
35
+ function parseArgs(args) {
36
+ const result = { _: [] };
37
+ for (let i = 0; i < args.length; i++) {
38
+ if (args[i].startsWith('--')) {
39
+ const key = args[i].slice(2);
40
+ const val = args[i + 1] && !args[i + 1].startsWith('--') ? args[++i] : true;
41
+ result[key] = val;
42
+ } else {
43
+ result._.push(args[i]);
44
+ }
45
+ }
46
+ return result;
47
+ }
48
+
49
+ function loadConfig(args) {
50
+ // Try config file
51
+ if (args.config) {
52
+ const raw = fs.readFileSync(args.config, 'utf8');
53
+ return JSON.parse(raw);
54
+ }
55
+
56
+ // Try environment
57
+ const secretKey = args['secret-key'] || process.env.NOSTR_SECRET_KEY;
58
+ if (!secretKey) {
59
+ console.error('Error: No secret key. Set NOSTR_SECRET_KEY or use --secret-key');
60
+ process.exit(1);
61
+ }
62
+
63
+ return {
64
+ secretKey,
65
+ nwcUrl: args['nwc-url'] || process.env.NWC_URL,
66
+ lightningAddress: args['ln-address'] || process.env.LIGHTNING_ADDRESS,
67
+ relays: (args.relays || process.env.ESCROW_RELAYS || 'wss://relay.damus.io,wss://nos.lol').split(',')
68
+ };
69
+ }
70
+
71
+ async function main() {
72
+ const args = parseArgs(process.argv.slice(2));
73
+ const command = args._[0];
74
+
75
+ if (!command || command === 'help') {
76
+ usage();
77
+ return;
78
+ }
79
+
80
+ if (!COMMANDS[command]) {
81
+ console.error(`Unknown command: ${command}\nRun 'agent-escrow help' for usage`);
82
+ process.exit(1);
83
+ }
84
+
85
+ const config = loadConfig(args);
86
+ const market = createMarketplace(config);
87
+
88
+ try {
89
+ switch (command) {
90
+ case 'post': {
91
+ const title = args.title || args._[1];
92
+ const budget = parseInt(args.budget || args._[2], 10);
93
+ if (!title || !budget) {
94
+ console.error('Usage: agent-escrow post --title "..." --budget <sats> [--caps "a,b"] [--min-trust N] [--deadline <ms>]');
95
+ process.exit(1);
96
+ }
97
+ const task = await market.postTask({
98
+ title,
99
+ description: args.description || args.desc || '',
100
+ budget,
101
+ capabilities: args.caps ? args.caps.split(',') : [],
102
+ minTrust: args['min-trust'] ? parseInt(args['min-trust'], 10) : undefined,
103
+ deadline: args.deadline ? parseInt(args.deadline, 10) : undefined
104
+ });
105
+ console.log('✅ Task posted!');
106
+ console.log(` ID: ${task.taskId}`);
107
+ console.log(` Title: ${task.title}`);
108
+ console.log(` Budget: ${task.budget} sats`);
109
+ console.log(` Event: ${task.eventId}`);
110
+ break;
111
+ }
112
+
113
+ case 'browse': {
114
+ const tasks = await market.browseTasks({
115
+ capabilities: args.caps ? args.caps.split(',') : undefined,
116
+ minBudget: args['min-budget'] ? parseInt(args['min-budget'], 10) : undefined,
117
+ maxBudget: args['max-budget'] ? parseInt(args['max-budget'], 10) : undefined,
118
+ limit: args.limit ? parseInt(args.limit, 10) : 20
119
+ });
120
+ if (tasks.length === 0) {
121
+ console.log('No open tasks found.');
122
+ return;
123
+ }
124
+ console.log(`Found ${tasks.length} open tasks:\n`);
125
+ for (const task of tasks) {
126
+ const caps = task.capabilities.length > 0 ? ` [${task.capabilities.join(', ')}]` : '';
127
+ const trust = task.minTrust ? ` (trust≥${task.minTrust})` : '';
128
+ console.log(` 📋 ${task.title}${caps}${trust}`);
129
+ console.log(` Budget: ${task.budget} sats | Poster: ${task.poster.slice(0, 12)}...`);
130
+ console.log(` Event: ${task.eventId}`);
131
+ if (task.description) console.log(` ${task.description.slice(0, 100)}${task.description.length > 100 ? '...' : ''}`);
132
+ console.log();
133
+ }
134
+ break;
135
+ }
136
+
137
+ case 'bid': {
138
+ const taskEventId = args.task || args._[1];
139
+ const posterPubkey = args.poster || args._[2];
140
+ const amount = parseInt(args.amount || args._[3], 10);
141
+ if (!taskEventId || !posterPubkey || !amount) {
142
+ console.error('Usage: agent-escrow bid --task <event-id> --poster <pubkey> --amount <sats> [--message "..."] [--eta <ms>]');
143
+ process.exit(1);
144
+ }
145
+ const bid = await market.submitBid({
146
+ taskEventId,
147
+ posterPubkey,
148
+ amount,
149
+ message: args.message || '',
150
+ eta: args.eta ? parseInt(args.eta, 10) : undefined
151
+ });
152
+ console.log('✅ Bid submitted!');
153
+ console.log(` Amount: ${bid.amount} sats`);
154
+ console.log(` Event: ${bid.eventId}`);
155
+ break;
156
+ }
157
+
158
+ case 'bids': {
159
+ const taskEventId = args.task || args._[1];
160
+ if (!taskEventId) {
161
+ console.error('Usage: agent-escrow bids --task <event-id>');
162
+ process.exit(1);
163
+ }
164
+ const bids = await market.getBids(taskEventId);
165
+ if (bids.length === 0) {
166
+ console.log('No bids found.');
167
+ return;
168
+ }
169
+ console.log(`Found ${bids.length} bids:\n`);
170
+ for (const bid of bids) {
171
+ console.log(` 💰 ${bid.amount} sats — ${bid.bidder.slice(0, 12)}...`);
172
+ console.log(` LN: ${bid.lightningAddress}`);
173
+ if (bid.message) console.log(` "${bid.message}"`);
174
+ console.log(` Event: ${bid.eventId}`);
175
+ console.log();
176
+ }
177
+ break;
178
+ }
179
+
180
+ case 'deliver': {
181
+ const taskEventId = args.task || args._[1];
182
+ const posterPubkey = args.poster || args._[2];
183
+ const result = args.result || args._[3];
184
+ if (!taskEventId || !posterPubkey || !result) {
185
+ console.error('Usage: agent-escrow deliver --task <event-id> --poster <pubkey> --result "..."');
186
+ process.exit(1);
187
+ }
188
+ // If result is a file path, read it
189
+ let deliverable = result;
190
+ if (fs.existsSync(result)) {
191
+ deliverable = fs.readFileSync(result, 'utf8');
192
+ }
193
+ const delivery = await market.deliver({
194
+ taskEventId,
195
+ posterPubkey,
196
+ result: deliverable
197
+ });
198
+ console.log('✅ Work delivered!');
199
+ console.log(` Hash: ${delivery.hash}`);
200
+ console.log(` Event: ${delivery.eventId}`);
201
+ break;
202
+ }
203
+
204
+ case 'approve': {
205
+ const taskId = args.task || args._[1];
206
+ const workerPubkey = args.worker || args._[2];
207
+ const workerLn = args['worker-ln'] || args._[3];
208
+ const amount = parseInt(args.amount || args._[4], 10);
209
+ if (!taskId || !workerPubkey || !amount) {
210
+ console.error('Usage: agent-escrow approve --task <task-id> --worker <pubkey> --amount <sats> [--worker-ln <address>]');
211
+ process.exit(1);
212
+ }
213
+ const result = await market.approve({
214
+ taskId,
215
+ workerPubkey,
216
+ workerLightningAddress: workerLn,
217
+ amount,
218
+ message: args.message
219
+ });
220
+ console.log('✅ Task approved!');
221
+ if (result.payment && result.payment.preimage) {
222
+ console.log(` Payment: ${amount} sats`);
223
+ console.log(` Preimage: ${result.payment.preimage}`);
224
+ } else if (result.payment && result.payment.manual) {
225
+ console.log(` ⚠️ Manual payment needed: ${amount} sats to worker`);
226
+ }
227
+ break;
228
+ }
229
+
230
+ case 'dispute': {
231
+ const taskId = args.task || args._[1];
232
+ const workerPubkey = args.worker || args._[2];
233
+ const reason = args.reason || args._[3];
234
+ if (!taskId || !workerPubkey || !reason) {
235
+ console.error('Usage: agent-escrow dispute --task <task-id> --worker <pubkey> --reason "..."');
236
+ process.exit(1);
237
+ }
238
+ await market.dispute({ taskId, workerPubkey, reason });
239
+ console.log('⚠️ Task disputed.');
240
+ console.log(` Reason: ${reason}`);
241
+ break;
242
+ }
243
+
244
+ case 'cancel': {
245
+ const taskId = args.task || args._[1];
246
+ if (!taskId) {
247
+ console.error('Usage: agent-escrow cancel --task <task-id>');
248
+ process.exit(1);
249
+ }
250
+ await market.cancelTask(taskId);
251
+ console.log('✅ Task cancelled.');
252
+ break;
253
+ }
254
+ }
255
+ } finally {
256
+ await market.close();
257
+ }
258
+ }
259
+
260
+ main().catch(err => {
261
+ console.error('Error:', err.message);
262
+ process.exit(1);
263
+ });
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+ const { KINDS, queryRelays, getTag } = require('./nostr.cjs');
5
+
6
+ /**
7
+ * Create a delivery event template
8
+ */
9
+ function createDeliveryEvent(opts) {
10
+ const {
11
+ taskEventId,
12
+ posterPubkey,
13
+ result,
14
+ includeHash
15
+ } = opts;
16
+
17
+ if (!taskEventId) throw new Error('taskEventId is required');
18
+ if (!posterPubkey) throw new Error('posterPubkey is required');
19
+ if (!result) throw new Error('result is required');
20
+
21
+ const tags = [
22
+ ['e', taskEventId],
23
+ ['p', posterPubkey]
24
+ ];
25
+
26
+ if (includeHash !== false) {
27
+ const hash = crypto.createHash('sha256').update(result).digest('hex');
28
+ tags.push(['hash', hash]);
29
+ }
30
+
31
+ return {
32
+ kind: KINDS.DELIVERY,
33
+ created_at: Math.floor(Date.now() / 1000),
34
+ tags,
35
+ content: result
36
+ };
37
+ }
38
+
39
+ /**
40
+ * Parse a delivery event into a structured object
41
+ */
42
+ function parseDelivery(event) {
43
+ return {
44
+ eventId: event.id,
45
+ worker: event.pubkey,
46
+ taskEventId: getTag(event, 'e'),
47
+ posterPubkey: getTag(event, 'p'),
48
+ result: event.content,
49
+ hash: getTag(event, 'hash'),
50
+ createdAt: event.created_at * 1000,
51
+ raw: event
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Verify delivery hash matches content
57
+ */
58
+ function verifyDelivery(delivery) {
59
+ if (!delivery.hash) return { valid: true, reason: 'no hash to verify' };
60
+ const computed = crypto.createHash('sha256').update(delivery.result).digest('hex');
61
+ return {
62
+ valid: computed === delivery.hash,
63
+ expected: delivery.hash,
64
+ actual: computed
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Find deliveries for a specific task
70
+ */
71
+ async function findDeliveries(relays, taskEventId, timeoutMs = 8000) {
72
+ const filter = {
73
+ kinds: [KINDS.DELIVERY],
74
+ '#e': [taskEventId]
75
+ };
76
+ const events = await queryRelays(relays, filter, timeoutMs);
77
+ return events.map(parseDelivery);
78
+ }
79
+
80
+ module.exports = {
81
+ createDeliveryEvent,
82
+ parseDelivery,
83
+ verifyDelivery,
84
+ findDeliveries
85
+ };
package/src/escrow.cjs ADDED
@@ -0,0 +1,97 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Payment coordination — optional lightning-agent dependency
5
+ *
6
+ * Handles Lightning payments between poster and worker.
7
+ * Falls back gracefully if lightning-agent is not installed.
8
+ */
9
+
10
+ let lightningAgent = null;
11
+ try {
12
+ lightningAgent = require('lightning-agent');
13
+ } catch (e) {
14
+ // lightning-agent not installed — payment features require manual handling
15
+ }
16
+
17
+ /**
18
+ * Check if Lightning payment features are available
19
+ */
20
+ function paymentsAvailable() {
21
+ return lightningAgent !== null;
22
+ }
23
+
24
+ /**
25
+ * Create a wallet instance from NWC URL
26
+ */
27
+ function createWallet(nwcUrl) {
28
+ if (!lightningAgent) {
29
+ throw new Error('lightning-agent is required for payment features. Install with: npm install lightning-agent');
30
+ }
31
+ return lightningAgent.createWallet(nwcUrl);
32
+ }
33
+
34
+ /**
35
+ * Pay a worker by resolving their Lightning address and paying
36
+ */
37
+ async function payWorker(wallet, lightningAddress, amountSats, memo) {
38
+ if (!lightningAgent) {
39
+ throw new Error('lightning-agent is required for payment features');
40
+ }
41
+
42
+ // Resolve Lightning address to get invoice
43
+ const invoice = await lightningAgent.resolveLightningAddress
44
+ ? await resolveAndPay(wallet, lightningAddress, amountSats, memo)
45
+ : await wallet.payAddress(lightningAddress, amountSats * 1000, memo);
46
+
47
+ return invoice;
48
+ }
49
+
50
+ /**
51
+ * Internal: resolve address and pay
52
+ */
53
+ async function resolveAndPay(wallet, address, amountSats, memo) {
54
+ const resolved = await lightningAgent.resolveLightningAddress(address, amountSats * 1000);
55
+ if (!resolved || !resolved.pr) {
56
+ throw new Error(`Failed to resolve Lightning address: ${address}`);
57
+ }
58
+ const result = await wallet.payInvoice(resolved.pr);
59
+ return {
60
+ bolt11: resolved.pr,
61
+ preimage: result.preimage,
62
+ amountSats
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Check wallet balance
68
+ */
69
+ async function checkBalance(wallet) {
70
+ if (!wallet || !wallet.getBalance) return null;
71
+ try {
72
+ const balance = await wallet.getBalance();
73
+ return balance;
74
+ } catch (e) {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Payment result object for manual payment flows
81
+ */
82
+ function createManualPaymentResult(bolt11, preimage, amountSats) {
83
+ return {
84
+ bolt11: bolt11 || null,
85
+ preimage: preimage || null,
86
+ amountSats,
87
+ manual: true
88
+ };
89
+ }
90
+
91
+ module.exports = {
92
+ paymentsAvailable,
93
+ createWallet,
94
+ payWorker,
95
+ checkBalance,
96
+ createManualPaymentResult
97
+ };
package/src/index.cjs ADDED
@@ -0,0 +1,50 @@
1
+ 'use strict';
2
+
3
+ const { createMarketplace } = require('./marketplace.cjs');
4
+ const { KINDS, TASK_STATUS, RESOLUTION_TYPES } = require('./nostr.cjs');
5
+ const { createTaskEvent, parseTask, findTasks, getTask } = require('./task.cjs');
6
+ const { createBidEvent, parseBid, findBids } = require('./bid.cjs');
7
+ const { createDeliveryEvent, parseDelivery, verifyDelivery, findDeliveries } = require('./delivery.cjs');
8
+ const { createResolutionEvent, parseResolution, findResolutions } = require('./resolution.cjs');
9
+ const { trustAvailable, getTrustScore, meetsTrustThreshold } = require('./trust.cjs');
10
+ const { paymentsAvailable } = require('./escrow.cjs');
11
+
12
+ module.exports = {
13
+ // Main factory
14
+ createMarketplace,
15
+
16
+ // Constants
17
+ KINDS,
18
+ TASK_STATUS,
19
+ RESOLUTION_TYPES,
20
+
21
+ // Low-level event creators (for custom flows)
22
+ createTaskEvent,
23
+ createBidEvent,
24
+ createDeliveryEvent,
25
+ createResolutionEvent,
26
+
27
+ // Parsers
28
+ parseTask,
29
+ parseBid,
30
+ parseDelivery,
31
+ parseResolution,
32
+
33
+ // Queries
34
+ findTasks,
35
+ findBids,
36
+ findDeliveries,
37
+ findResolutions,
38
+ getTask,
39
+
40
+ // Verification
41
+ verifyDelivery,
42
+
43
+ // Trust
44
+ trustAvailable,
45
+ getTrustScore,
46
+ meetsTrustThreshold,
47
+
48
+ // Payments
49
+ paymentsAvailable
50
+ };