@obolos_tech/cli 0.2.3 → 0.3.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/dist/index.js CHANGED
@@ -10,6 +10,17 @@
10
10
  * npx @obolos_tech/cli categories
11
11
  * npx @obolos_tech/cli balance
12
12
  * npx @obolos_tech/cli setup-mcp
13
+ * npx @obolos_tech/cli job list --status=open
14
+ * npx @obolos_tech/cli job create --title "..." --evaluator 0x...
15
+ * npx @obolos_tech/cli job info <id>
16
+ * npx @obolos_tech/cli listing list --status=open
17
+ * npx @obolos_tech/cli listing create --title "..." --max-budget 10.00
18
+ * npx @obolos_tech/cli listing bid <id> --price 5.00
19
+ * npx @obolos_tech/cli anp list --status=open
20
+ * npx @obolos_tech/cli anp create --title "..." --min-budget 5 --max-budget 50
21
+ * npx @obolos_tech/cli anp bid <cid> --price 25 --delivery 48h
22
+ * npx @obolos_tech/cli anp accept <cid> --bid <bid_cid>
23
+ * npx @obolos_tech/cli anp verify <cid>
13
24
  */
14
25
  import { homedir } from 'os';
15
26
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
@@ -36,6 +47,103 @@ const config = loadConfig();
36
47
  const OBOLOS_API_URL = process.env.OBOLOS_API_URL || config.api_url || 'https://obolos.tech';
37
48
  const OBOLOS_PRIVATE_KEY = process.env.OBOLOS_PRIVATE_KEY || config.private_key || '';
38
49
  // ─── Colors (no deps) ──────────────────────────────────────────────────────
50
+ // ─── ACP Contract ABIs (ERC-8183) ─────────────────────────────────────────
51
+ const ACP_ADDRESS = '0xaF3148696242F7Fb74893DC47690e37950807362';
52
+ const USDC_CONTRACT = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
53
+ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
54
+ const ZERO_BYTES32 = '0x0000000000000000000000000000000000000000000000000000000000000000';
55
+ const ACP_ABI = [
56
+ {
57
+ type: 'function',
58
+ name: 'createJob',
59
+ inputs: [
60
+ { name: 'provider', type: 'address' },
61
+ { name: 'evaluator', type: 'address' },
62
+ { name: 'expiredAt', type: 'uint256' },
63
+ { name: 'description', type: 'string' },
64
+ { name: 'hook', type: 'address' },
65
+ ],
66
+ outputs: [{ name: 'jobId', type: 'uint256' }],
67
+ stateMutability: 'nonpayable',
68
+ },
69
+ {
70
+ type: 'function',
71
+ name: 'fund',
72
+ inputs: [
73
+ { name: 'jobId', type: 'uint256' },
74
+ { name: 'expectedBudget', type: 'uint256' },
75
+ { name: 'optParams', type: 'bytes' },
76
+ ],
77
+ outputs: [],
78
+ stateMutability: 'nonpayable',
79
+ },
80
+ {
81
+ type: 'function',
82
+ name: 'submit',
83
+ inputs: [
84
+ { name: 'jobId', type: 'uint256' },
85
+ { name: 'deliverable', type: 'bytes32' },
86
+ { name: 'optParams', type: 'bytes' },
87
+ ],
88
+ outputs: [],
89
+ stateMutability: 'nonpayable',
90
+ },
91
+ {
92
+ type: 'function',
93
+ name: 'complete',
94
+ inputs: [
95
+ { name: 'jobId', type: 'uint256' },
96
+ { name: 'reason', type: 'bytes32' },
97
+ { name: 'optParams', type: 'bytes' },
98
+ ],
99
+ outputs: [],
100
+ stateMutability: 'nonpayable',
101
+ },
102
+ {
103
+ type: 'function',
104
+ name: 'reject',
105
+ inputs: [
106
+ { name: 'jobId', type: 'uint256' },
107
+ { name: 'reason', type: 'bytes32' },
108
+ { name: 'optParams', type: 'bytes' },
109
+ ],
110
+ outputs: [],
111
+ stateMutability: 'nonpayable',
112
+ },
113
+ {
114
+ type: 'event',
115
+ name: 'JobCreated',
116
+ inputs: [
117
+ { name: 'jobId', type: 'uint256', indexed: true },
118
+ { name: 'client', type: 'address', indexed: true },
119
+ { name: 'provider', type: 'address', indexed: false },
120
+ { name: 'evaluator', type: 'address', indexed: false },
121
+ { name: 'expiredAt', type: 'uint256', indexed: false },
122
+ ],
123
+ },
124
+ ];
125
+ const ERC20_ABI = [
126
+ {
127
+ type: 'function',
128
+ name: 'approve',
129
+ inputs: [
130
+ { name: 'spender', type: 'address' },
131
+ { name: 'amount', type: 'uint256' },
132
+ ],
133
+ outputs: [{ name: '', type: 'bool' }],
134
+ stateMutability: 'nonpayable',
135
+ },
136
+ {
137
+ type: 'function',
138
+ name: 'allowance',
139
+ inputs: [
140
+ { name: 'owner', type: 'address' },
141
+ { name: 'spender', type: 'address' },
142
+ ],
143
+ outputs: [{ name: '', type: 'uint256' }],
144
+ stateMutability: 'view',
145
+ },
146
+ ];
39
147
  const c = {
40
148
  reset: '\x1b[0m',
41
149
  bold: '\x1b[1m',
@@ -59,6 +167,258 @@ async function apiGet(path) {
59
167
  throw new Error(`${res.status} ${res.statusText}`);
60
168
  return res.json();
61
169
  }
170
+ async function apiPost(path, body, headers) {
171
+ const res = await fetch(`${OBOLOS_API_URL}${path}`, {
172
+ method: 'POST',
173
+ headers: { 'Content-Type': 'application/json', ...headers },
174
+ body: body !== undefined ? JSON.stringify(body) : undefined,
175
+ });
176
+ if (!res.ok) {
177
+ let msg = `${res.status} ${res.statusText}`;
178
+ try {
179
+ const err = await res.json();
180
+ if (err.error)
181
+ msg = err.error;
182
+ else if (err.message)
183
+ msg = err.message;
184
+ }
185
+ catch { }
186
+ throw new Error(msg);
187
+ }
188
+ return res.json();
189
+ }
190
+ // ─── Helpers ────────────────────────────────────────────────────────────────
191
+ function getFlag(args, name) {
192
+ // Supports --name=value and --name value
193
+ for (let i = 0; i < args.length; i++) {
194
+ if (args[i] === `--${name}` && args[i + 1] && !args[i + 1].startsWith('--')) {
195
+ return args[i + 1];
196
+ }
197
+ if (args[i].startsWith(`--${name}=`)) {
198
+ return args[i].slice(`--${name}=`.length);
199
+ }
200
+ }
201
+ return undefined;
202
+ }
203
+ function getPositional(args, index) {
204
+ let pos = 0;
205
+ for (const arg of args) {
206
+ if (!arg.startsWith('--')) {
207
+ if (pos === index)
208
+ return arg;
209
+ pos++;
210
+ }
211
+ }
212
+ return undefined;
213
+ }
214
+ function shortenAddr(addr) {
215
+ if (!addr)
216
+ return `${c.dim}—${c.reset}`;
217
+ if (addr.length <= 12)
218
+ return addr;
219
+ return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
220
+ }
221
+ function shortenId(id) {
222
+ if (id.length <= 12)
223
+ return id;
224
+ return `${id.slice(0, 8)}...`;
225
+ }
226
+ function formatDate(iso) {
227
+ if (!iso)
228
+ return `${c.dim}—${c.reset}`;
229
+ const d = new Date(iso);
230
+ return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
231
+ }
232
+ function statusColor(status) {
233
+ switch (status) {
234
+ case 'open': return `${c.yellow}${status}${c.reset}`;
235
+ case 'funded': return `${c.blue}${status}${c.reset}`;
236
+ case 'submitted': return `${c.cyan}${status}${c.reset}`;
237
+ case 'completed': return `${c.green}${status}${c.reset}`;
238
+ case 'rejected': return `${c.red}${status}${c.reset}`;
239
+ case 'expired': return `${c.gray}${status}${c.reset}`;
240
+ case 'negotiating': return `${c.magenta}${status}${c.reset}`;
241
+ case 'accepted': return `${c.green}${status}${c.reset}`;
242
+ case 'cancelled': return `${c.gray}${status}${c.reset}`;
243
+ default: return status;
244
+ }
245
+ }
246
+ function parseRelativeTime(input) {
247
+ const match = input.match(/^(\d+)\s*(h|hr|hrs|hour|hours|d|day|days|m|min|mins|minute|minutes)$/i);
248
+ if (!match) {
249
+ // Try parsing as ISO date directly
250
+ const d = new Date(input);
251
+ if (!isNaN(d.getTime()))
252
+ return d.toISOString();
253
+ throw new Error(`Cannot parse expiry: "${input}". Use formats like "24h", "7d", "1h", or an ISO date.`);
254
+ }
255
+ const num = parseInt(match[1], 10);
256
+ const unit = match[2].toLowerCase();
257
+ const now = Date.now();
258
+ let ms = 0;
259
+ if (unit.startsWith('h'))
260
+ ms = num * 60 * 60 * 1000;
261
+ else if (unit.startsWith('d'))
262
+ ms = num * 24 * 60 * 60 * 1000;
263
+ else if (unit.startsWith('m'))
264
+ ms = num * 60 * 1000;
265
+ return new Date(now + ms).toISOString();
266
+ }
267
+ function resolveWalletAddress() {
268
+ const cfg = loadConfig();
269
+ const addr = cfg.wallet_address;
270
+ if (addr)
271
+ return addr;
272
+ // Derive from private key if available
273
+ if (!OBOLOS_PRIVATE_KEY) {
274
+ console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
275
+ process.exit(1);
276
+ }
277
+ // We'll derive it lazily — for now return empty and let callers handle async derivation
278
+ return '';
279
+ }
280
+ async function getWalletAddress() {
281
+ const cfg = loadConfig();
282
+ if (cfg.wallet_address)
283
+ return cfg.wallet_address;
284
+ if (!OBOLOS_PRIVATE_KEY) {
285
+ console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
286
+ process.exit(1);
287
+ }
288
+ const { privateKeyToAccount } = await import('viem/accounts');
289
+ const key = OBOLOS_PRIVATE_KEY.startsWith('0x') ? OBOLOS_PRIVATE_KEY : `0x${OBOLOS_PRIVATE_KEY}`;
290
+ const account = privateKeyToAccount(key);
291
+ return account.address;
292
+ }
293
+ async function getACPClient() {
294
+ const key = OBOLOS_PRIVATE_KEY;
295
+ if (!key) {
296
+ console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
297
+ process.exit(1);
298
+ }
299
+ const { createPublicClient, createWalletClient, http: viemHttp, parseUnits, keccak256, toHex, decodeEventLog } = await import('viem');
300
+ const { privateKeyToAccount } = await import('viem/accounts');
301
+ const { base } = await import('viem/chains');
302
+ const normalizedKey = key.startsWith('0x') ? key : `0x${key}`;
303
+ const account = privateKeyToAccount(normalizedKey);
304
+ const publicClient = createPublicClient({ chain: base, transport: viemHttp() });
305
+ const walletClient = createWalletClient({ account, chain: base, transport: viemHttp() });
306
+ return { account, publicClient, walletClient, parseUnits, keccak256, toHex, decodeEventLog };
307
+ }
308
+ function stateVisualization(status) {
309
+ const states = ['open', 'funded', 'submitted', 'completed'];
310
+ const parts = states.map(s => {
311
+ if (s === status)
312
+ return `${c.bold}[${s.charAt(0).toUpperCase() + s.slice(1)}]${c.reset}`;
313
+ return `${c.dim}${s.charAt(0).toUpperCase() + s.slice(1)}${c.reset}`;
314
+ });
315
+ // Handle terminal states that branch off
316
+ if (status === 'rejected') {
317
+ const base = states.slice(0, 3).map(s => `${c.dim}${s.charAt(0).toUpperCase() + s.slice(1)}${c.reset}`);
318
+ return ` ${base.join(` ${c.dim}->${c.reset} `)} ${c.dim}->${c.reset} ${c.bold}${c.red}[Rejected]${c.reset}`;
319
+ }
320
+ if (status === 'expired') {
321
+ return ` ${c.bold}${c.gray}[Expired]${c.reset}`;
322
+ }
323
+ return ` ${parts.join(` ${c.dim}->${c.reset} `)}`;
324
+ }
325
+ // ─── ANP Helpers ─────────────────────────────────────────────────────────────
326
+ function canonicalJSON(obj) {
327
+ if (obj === null || typeof obj !== 'object')
328
+ return JSON.stringify(obj);
329
+ if (Array.isArray(obj))
330
+ return '[' + obj.map(canonicalJSON).join(',') + ']';
331
+ const keys = Object.keys(obj).sort();
332
+ return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalJSON(obj[k])).join(',') + '}';
333
+ }
334
+ async function computeContentHash(data) {
335
+ const { createHash } = await import('crypto');
336
+ const hash = createHash('sha256').update(canonicalJSON(data)).digest('hex');
337
+ return `0x${hash}`;
338
+ }
339
+ function parseTimeToSeconds(input) {
340
+ const match = input.match(/^(\d+)\s*(s|sec|secs|second|seconds|h|hr|hrs|hour|hours|d|day|days|m|min|mins|minute|minutes)$/i);
341
+ if (!match) {
342
+ throw new Error(`Cannot parse time: "${input}". Use formats like "48h", "7d", "3d".`);
343
+ }
344
+ const num = parseInt(match[1], 10);
345
+ const unit = match[2].toLowerCase();
346
+ if (unit.startsWith('s'))
347
+ return num;
348
+ if (unit.startsWith('m'))
349
+ return num * 60;
350
+ if (unit.startsWith('h'))
351
+ return num * 3600;
352
+ if (unit.startsWith('d'))
353
+ return num * 86400;
354
+ return num;
355
+ }
356
+ const ANP_DOMAIN = {
357
+ name: 'ANP',
358
+ version: '1',
359
+ chainId: 8453,
360
+ verifyingContract: '0xfEa362Bf569e97B20681289fB4D4a64CEBDFa792',
361
+ };
362
+ const ANP_LISTING_TYPES = {
363
+ ListingIntent: [
364
+ { name: 'contentHash', type: 'bytes32' },
365
+ { name: 'minBudget', type: 'uint256' },
366
+ { name: 'maxBudget', type: 'uint256' },
367
+ { name: 'deadline', type: 'uint256' },
368
+ { name: 'jobDuration', type: 'uint256' },
369
+ { name: 'preferredEvaluator', type: 'address' },
370
+ { name: 'nonce', type: 'uint256' },
371
+ ],
372
+ };
373
+ const ANP_BID_TYPES = {
374
+ BidIntent: [
375
+ { name: 'listingHash', type: 'bytes32' },
376
+ { name: 'contentHash', type: 'bytes32' },
377
+ { name: 'price', type: 'uint256' },
378
+ { name: 'deliveryTime', type: 'uint256' },
379
+ { name: 'nonce', type: 'uint256' },
380
+ ],
381
+ };
382
+ const ANP_ACCEPT_TYPES = {
383
+ AcceptIntent: [
384
+ { name: 'listingHash', type: 'bytes32' },
385
+ { name: 'bidHash', type: 'bytes32' },
386
+ { name: 'nonce', type: 'uint256' },
387
+ ],
388
+ };
389
+ async function getANPSigningClient() {
390
+ if (!OBOLOS_PRIVATE_KEY) {
391
+ console.error(`${c.red}No wallet configured.${c.reset} Run ${c.cyan}obolos setup${c.reset} first.`);
392
+ process.exit(1);
393
+ }
394
+ const { createWalletClient, http: viemHttp, encodeAbiParameters, keccak256 } = await import('viem');
395
+ const { privateKeyToAccount } = await import('viem/accounts');
396
+ const { base } = await import('viem/chains');
397
+ const key = OBOLOS_PRIVATE_KEY.startsWith('0x') ? OBOLOS_PRIVATE_KEY : `0x${OBOLOS_PRIVATE_KEY}`;
398
+ const account = privateKeyToAccount(key);
399
+ const walletClient = createWalletClient({ account, chain: base, transport: viemHttp() });
400
+ const LISTING_TYPEHASH = keccak256(Buffer.from('ListingIntent(bytes32 contentHash,uint256 minBudget,uint256 maxBudget,uint256 deadline,uint256 jobDuration,address preferredEvaluator,uint256 nonce)'));
401
+ const BID_TYPEHASH = keccak256(Buffer.from('BidIntent(bytes32 listingHash,bytes32 contentHash,uint256 price,uint256 deliveryTime,uint256 nonce)'));
402
+ const ACCEPT_TYPEHASH = keccak256(Buffer.from('AcceptIntent(bytes32 listingHash,bytes32 bidHash,uint256 nonce)'));
403
+ function hashListingStruct(listing) {
404
+ return keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'address' }, { type: 'uint256' }], [LISTING_TYPEHASH, listing.contentHash, listing.minBudget, listing.maxBudget, listing.deadline, listing.jobDuration, listing.preferredEvaluator, listing.nonce]));
405
+ }
406
+ function hashBidStruct(bid) {
407
+ return keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }, { type: 'bytes32' }, { type: 'uint256' }, { type: 'uint256' }, { type: 'uint256' }], [BID_TYPEHASH, bid.listingHash, bid.contentHash, bid.price, bid.deliveryTime, bid.nonce]));
408
+ }
409
+ function hashAcceptStruct(accept) {
410
+ return keccak256(encodeAbiParameters([{ type: 'bytes32' }, { type: 'bytes32' }, { type: 'bytes32' }, { type: 'uint256' }], [ACCEPT_TYPEHASH, accept.listingHash, accept.bidHash, accept.nonce]));
411
+ }
412
+ return { account, walletClient, hashListingStruct, hashBidStruct, hashAcceptStruct };
413
+ }
414
+ function generateNonce() {
415
+ const bytes = new Uint8Array(8);
416
+ crypto.getRandomValues(bytes);
417
+ let val = 0n;
418
+ for (const b of bytes)
419
+ val = (val << 8n) | BigInt(b);
420
+ return val;
421
+ }
62
422
  // ─── Commands ───────────────────────────────────────────────────────────────
63
423
  async function cmdSearch(args) {
64
424
  const query = args.join(' ');
@@ -469,6 +829,1428 @@ async function cmdSetupMcp() {
469
829
  console.log(` }`);
470
830
  console.log(` }${c.reset}\n`);
471
831
  }
832
+ // ─── Job Commands (ERC-8183 ACP) ────────────────────────────────────────────
833
+ async function cmdJobList(args) {
834
+ const params = new URLSearchParams();
835
+ const status = getFlag(args, 'status');
836
+ const client = getFlag(args, 'client');
837
+ const provider = getFlag(args, 'provider');
838
+ const limit = getFlag(args, 'limit') || '20';
839
+ if (status)
840
+ params.set('status', status);
841
+ if (client)
842
+ params.set('client', client);
843
+ if (provider)
844
+ params.set('provider', provider);
845
+ params.set('limit', limit);
846
+ const data = await apiGet(`/api/jobs?${params}`);
847
+ const jobs = data.jobs || data.data || [];
848
+ if (jobs.length === 0) {
849
+ console.log(`${c.yellow}No jobs found.${c.reset}`);
850
+ return;
851
+ }
852
+ const total = data.pagination?.total || data.total || jobs.length;
853
+ console.log(`\n${c.bold}${c.cyan}ACP Jobs${c.reset} ${c.dim}— ${total} jobs${c.reset}\n`);
854
+ // Table header
855
+ console.log(` ${c.bold}${'ID'.padEnd(12)} ${'Title'.padEnd(30)} ${'Status'.padEnd(12)} ${'Budget'.padEnd(12)} ${'Client'.padEnd(14)} ${'Provider'.padEnd(14)} Created${c.reset}`);
856
+ console.log(` ${c.dim}${'─'.repeat(110)}${c.reset}`);
857
+ for (const job of jobs) {
858
+ const id = shortenId(job.id || '');
859
+ const title = (job.title || 'Untitled').slice(0, 28).padEnd(30);
860
+ const st = statusColor((job.status || 'open').padEnd(10));
861
+ const budget = job.budget != null ? `$${Number(job.budget).toFixed(2)}`.padEnd(12) : `${c.dim}—${c.reset}`.padEnd(12);
862
+ const cl = shortenAddr(job.client).padEnd(14);
863
+ const prov = job.provider ? shortenAddr(job.provider).padEnd(14) : `${c.dim}Open${c.reset}`.padEnd(14);
864
+ const created = formatDate(job.created_at || job.createdAt);
865
+ console.log(` ${id.padEnd(12)} ${title} ${st} ${budget} ${cl} ${prov} ${created}`);
866
+ }
867
+ console.log(`\n${c.dim}Use: obolos job info <id> for full details${c.reset}\n`);
868
+ }
869
+ async function cmdJobCreate(args) {
870
+ const title = getFlag(args, 'title');
871
+ const description = getFlag(args, 'description');
872
+ const evaluator = getFlag(args, 'evaluator');
873
+ const provider = getFlag(args, 'provider');
874
+ const budget = getFlag(args, 'budget');
875
+ const expires = getFlag(args, 'expires');
876
+ if (!title) {
877
+ console.error(`${c.red}Usage: obolos job create --title "..." --description "..." --evaluator 0x... [--provider 0x...] [--budget 1.00] [--expires 24h]${c.reset}`);
878
+ process.exit(1);
879
+ }
880
+ if (!evaluator) {
881
+ console.error(`${c.red}--evaluator is required. Provide the evaluator address (0x...).${c.reset}`);
882
+ process.exit(1);
883
+ }
884
+ const walletAddress = await getWalletAddress();
885
+ // Create job on-chain first
886
+ let chainJobId = null;
887
+ let chainTxHash = null;
888
+ try {
889
+ const acp = await getACPClient();
890
+ // Parse expiry to unix timestamp (default: 7 days)
891
+ let expiredAt;
892
+ if (expires) {
893
+ const parsed = parseRelativeTime(expires);
894
+ expiredAt = Math.floor(new Date(parsed).getTime() / 1000);
895
+ }
896
+ else {
897
+ expiredAt = Math.floor((Date.now() + 7 * 86400000) / 1000);
898
+ }
899
+ console.log(`\n ${c.dim}Creating job on-chain...${c.reset}`);
900
+ const txHash = await acp.walletClient.writeContract({
901
+ address: ACP_ADDRESS,
902
+ abi: ACP_ABI,
903
+ functionName: 'createJob',
904
+ args: [
905
+ (provider || ZERO_ADDRESS),
906
+ evaluator,
907
+ BigInt(expiredAt),
908
+ description || title,
909
+ ZERO_ADDRESS,
910
+ ],
911
+ account: acp.account,
912
+ chain: (await import('viem/chains')).base,
913
+ });
914
+ console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
915
+ const receipt = await acp.publicClient.waitForTransactionReceipt({ hash: txHash });
916
+ // Extract jobId from JobCreated event
917
+ for (const log of receipt.logs) {
918
+ try {
919
+ const decoded = acp.decodeEventLog({
920
+ abi: ACP_ABI,
921
+ data: log.data,
922
+ topics: log.topics,
923
+ });
924
+ if (decoded.eventName === 'JobCreated') {
925
+ chainJobId = (decoded.args.jobId).toString();
926
+ break;
927
+ }
928
+ }
929
+ catch { }
930
+ }
931
+ chainTxHash = txHash;
932
+ console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
933
+ if (chainJobId) {
934
+ console.log(` ${c.green}Chain job ID: ${chainJobId}${c.reset}`);
935
+ }
936
+ }
937
+ catch (err) {
938
+ console.error(` ${c.yellow}On-chain creation failed: ${err.message}${c.reset}`);
939
+ console.error(` ${c.dim}Falling back to backend-only...${c.reset}`);
940
+ }
941
+ const payload = {
942
+ title,
943
+ evaluator,
944
+ };
945
+ if (description)
946
+ payload.description = description;
947
+ if (provider)
948
+ payload.provider = provider;
949
+ if (budget)
950
+ payload.budget = parseFloat(budget);
951
+ if (expires)
952
+ payload.expires_at = parseRelativeTime(expires);
953
+ if (chainJobId)
954
+ payload.chain_job_id = chainJobId;
955
+ if (chainTxHash)
956
+ payload.chain_tx_hash = chainTxHash;
957
+ const data = await apiPost('/api/jobs', payload, {
958
+ 'x-wallet-address': walletAddress,
959
+ });
960
+ const job = data.job || data;
961
+ console.log(`\n${c.green}Job created successfully!${c.reset}\n`);
962
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
963
+ console.log(` ${c.bold}ID:${c.reset} ${job.id}`);
964
+ if (chainJobId) {
965
+ console.log(` ${c.bold}Chain ID:${c.reset} ${chainJobId}`);
966
+ }
967
+ console.log(` ${c.bold}Title:${c.reset} ${job.title}`);
968
+ if (job.description) {
969
+ console.log(` ${c.bold}Description:${c.reset} ${job.description}`);
970
+ }
971
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
972
+ console.log(` ${c.bold}Client:${c.reset} ${job.client || walletAddress}`);
973
+ console.log(` ${c.bold}Evaluator:${c.reset} ${job.evaluator}`);
974
+ if (job.provider) {
975
+ console.log(` ${c.bold}Provider:${c.reset} ${job.provider}`);
976
+ }
977
+ if (job.budget != null) {
978
+ console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
979
+ }
980
+ if (job.expires_at) {
981
+ console.log(` ${c.bold}Expires:${c.reset} ${formatDate(job.expires_at)}`);
982
+ }
983
+ if (chainTxHash) {
984
+ console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${chainTxHash}${c.reset}`);
985
+ }
986
+ console.log(`\n${c.dim}Next: obolos job fund ${job.id}${c.reset}\n`);
987
+ }
988
+ async function cmdJobInfo(args) {
989
+ const id = getPositional(args, 0);
990
+ if (!id) {
991
+ console.error(`${c.red}Usage: obolos job info <id>${c.reset}`);
992
+ process.exit(1);
993
+ }
994
+ const data = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
995
+ const job = data.job || data;
996
+ console.log(`\n${c.bold}${c.cyan}${job.title || 'Untitled Job'}${c.reset}`);
997
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
998
+ console.log(` ${c.bold}ID:${c.reset} ${job.id}`);
999
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
1000
+ // State machine visualization
1001
+ console.log(` ${c.bold}Progress:${c.reset}`);
1002
+ console.log(stateVisualization(job.status || 'open'));
1003
+ console.log(` ${c.bold}Client:${c.reset} ${job.client || `${c.dim}—${c.reset}`}`);
1004
+ console.log(` ${c.bold}Evaluator:${c.reset} ${job.evaluator || `${c.dim}—${c.reset}`}`);
1005
+ console.log(` ${c.bold}Provider:${c.reset} ${job.provider || `${c.dim}Open (anyone can claim)${c.reset}`}`);
1006
+ if (job.budget != null) {
1007
+ console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
1008
+ }
1009
+ if (job.description) {
1010
+ console.log(`\n ${c.bold}Description:${c.reset}`);
1011
+ const descLines = job.description.split('\n');
1012
+ for (const line of descLines) {
1013
+ console.log(` ${line}`);
1014
+ }
1015
+ }
1016
+ if (job.deliverable) {
1017
+ console.log(`\n ${c.bold}Deliverable:${c.reset} ${c.cyan}${job.deliverable}${c.reset}`);
1018
+ }
1019
+ if (job.reason) {
1020
+ console.log(` ${c.bold}Reason:${c.reset} ${job.reason}`);
1021
+ }
1022
+ if (job.expires_at) {
1023
+ const expiryDate = new Date(job.expires_at);
1024
+ const now = new Date();
1025
+ const expired = expiryDate < now;
1026
+ console.log(` ${c.bold}Expires:${c.reset} ${expired ? c.red : c.dim}${formatDate(job.expires_at)}${expired ? ' (expired)' : ''}${c.reset}`);
1027
+ }
1028
+ console.log(` ${c.bold}Created:${c.reset} ${formatDate(job.created_at || job.createdAt)}`);
1029
+ if (job.updated_at || job.updatedAt) {
1030
+ console.log(` ${c.bold}Updated:${c.reset} ${formatDate(job.updated_at || job.updatedAt)}`);
1031
+ }
1032
+ // Actions hint based on status
1033
+ console.log();
1034
+ const s = job.status || 'open';
1035
+ if (s === 'open') {
1036
+ console.log(` ${c.bold}Actions:${c.reset}`);
1037
+ console.log(` obolos job fund ${job.id} ${c.dim}Fund the escrow${c.reset}`);
1038
+ }
1039
+ else if (s === 'funded') {
1040
+ console.log(` ${c.bold}Actions:${c.reset}`);
1041
+ console.log(` obolos job submit ${job.id} --deliverable <hash> ${c.dim}Submit work${c.reset}`);
1042
+ }
1043
+ else if (s === 'submitted') {
1044
+ console.log(` ${c.bold}Actions:${c.reset}`);
1045
+ console.log(` obolos job complete ${job.id} ${c.dim}Approve and release funds${c.reset}`);
1046
+ console.log(` obolos job reject ${job.id} ${c.dim}Reject the submission${c.reset}`);
1047
+ }
1048
+ console.log();
1049
+ }
1050
+ async function cmdJobFund(args) {
1051
+ const id = getPositional(args, 0);
1052
+ if (!id) {
1053
+ console.error(`${c.red}Usage: obolos job fund <id>${c.reset}`);
1054
+ process.exit(1);
1055
+ }
1056
+ const walletAddress = await getWalletAddress();
1057
+ // First fetch the job to show budget info
1058
+ const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1059
+ const job = jobData.job || jobData;
1060
+ console.log(`\n${c.bold}${c.cyan}Fund Job${c.reset}\n`);
1061
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1062
+ console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1063
+ if (job.budget != null) {
1064
+ console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${Number(job.budget).toFixed(2)} USDC${c.reset}`);
1065
+ }
1066
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'open')}`);
1067
+ console.log();
1068
+ const chainJobId = job.chain_job_id;
1069
+ let txHash = null;
1070
+ if (chainJobId && job.budget != null) {
1071
+ try {
1072
+ const acp = await getACPClient();
1073
+ const budgetStr = String(job.budget);
1074
+ // Check USDC allowance and approve if needed
1075
+ console.log(` ${c.dim}Checking USDC allowance...${c.reset}`);
1076
+ const amount = acp.parseUnits(budgetStr, 6);
1077
+ const allowance = await acp.publicClient.readContract({
1078
+ address: USDC_CONTRACT,
1079
+ abi: ERC20_ABI,
1080
+ functionName: 'allowance',
1081
+ args: [acp.account.address, ACP_ADDRESS],
1082
+ });
1083
+ if (allowance < amount) {
1084
+ console.log(` ${c.dim}Approving USDC spend...${c.reset}`);
1085
+ const approveTx = await acp.walletClient.writeContract({
1086
+ address: USDC_CONTRACT,
1087
+ abi: ERC20_ABI,
1088
+ functionName: 'approve',
1089
+ args: [ACP_ADDRESS, amount],
1090
+ account: acp.account,
1091
+ chain: (await import('viem/chains')).base,
1092
+ });
1093
+ await acp.publicClient.waitForTransactionReceipt({ hash: approveTx });
1094
+ console.log(` ${c.green}USDC approved${c.reset}`);
1095
+ }
1096
+ console.log(` ${c.dim}Funding escrow on-chain...${c.reset}`);
1097
+ const fundTx = await acp.walletClient.writeContract({
1098
+ address: ACP_ADDRESS,
1099
+ abi: ACP_ABI,
1100
+ functionName: 'fund',
1101
+ args: [BigInt(chainJobId), amount, '0x'],
1102
+ account: acp.account,
1103
+ chain: (await import('viem/chains')).base,
1104
+ });
1105
+ console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1106
+ await acp.publicClient.waitForTransactionReceipt({ hash: fundTx });
1107
+ txHash = fundTx;
1108
+ console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}\n`);
1109
+ }
1110
+ catch (err) {
1111
+ console.error(` ${c.yellow}On-chain funding failed: ${err.message}${c.reset}`);
1112
+ console.error(` ${c.dim}Recording funding intent in backend...${c.reset}\n`);
1113
+ }
1114
+ }
1115
+ // Update backend
1116
+ const fundPayload = {};
1117
+ if (txHash)
1118
+ fundPayload.tx_hash = txHash;
1119
+ if (chainJobId)
1120
+ fundPayload.chain_job_id = chainJobId;
1121
+ const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/fund`, fundPayload, {
1122
+ 'x-wallet-address': walletAddress,
1123
+ });
1124
+ const updated = data.job || data;
1125
+ console.log(`${c.green}Job funded successfully!${c.reset}`);
1126
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(updated.status || 'funded')}`);
1127
+ if (txHash) {
1128
+ console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
1129
+ }
1130
+ console.log(`${c.dim}Next: Provider submits work with: obolos job submit ${id} --deliverable <hash>${c.reset}\n`);
1131
+ }
1132
+ async function cmdJobSubmit(args) {
1133
+ const id = getPositional(args, 0);
1134
+ if (!id) {
1135
+ console.error(`${c.red}Usage: obolos job submit <id> --deliverable <hash/CID/URL>${c.reset}`);
1136
+ process.exit(1);
1137
+ }
1138
+ const deliverable = getFlag(args, 'deliverable');
1139
+ if (!deliverable) {
1140
+ console.error(`${c.red}--deliverable is required. Provide a hash, CID, or URL for the work product.${c.reset}`);
1141
+ process.exit(1);
1142
+ }
1143
+ const walletAddress = await getWalletAddress();
1144
+ // Fetch job to get chain_job_id
1145
+ const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1146
+ const existingJob = jobData.job || jobData;
1147
+ const chainJobId = existingJob.chain_job_id;
1148
+ let txHash = null;
1149
+ if (chainJobId) {
1150
+ try {
1151
+ const acp = await getACPClient();
1152
+ const deliverableHash = acp.keccak256(acp.toHex(deliverable));
1153
+ console.log(`\n ${c.dim}Submitting work on-chain...${c.reset}`);
1154
+ const submitTx = await acp.walletClient.writeContract({
1155
+ address: ACP_ADDRESS,
1156
+ abi: ACP_ABI,
1157
+ functionName: 'submit',
1158
+ args: [BigInt(chainJobId), deliverableHash, '0x'],
1159
+ account: acp.account,
1160
+ chain: (await import('viem/chains')).base,
1161
+ });
1162
+ console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1163
+ await acp.publicClient.waitForTransactionReceipt({ hash: submitTx });
1164
+ txHash = submitTx;
1165
+ console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1166
+ }
1167
+ catch (err) {
1168
+ console.error(` ${c.yellow}On-chain submission failed: ${err.message}${c.reset}`);
1169
+ }
1170
+ }
1171
+ const submitPayload = { deliverable };
1172
+ if (txHash)
1173
+ submitPayload.tx_hash = txHash;
1174
+ const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/submit`, submitPayload, {
1175
+ 'x-wallet-address': walletAddress,
1176
+ });
1177
+ const job = data.job || data;
1178
+ console.log(`\n${c.green}Work submitted successfully!${c.reset}\n`);
1179
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1180
+ console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1181
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'submitted')}`);
1182
+ console.log(` ${c.bold}Deliverable:${c.reset} ${c.cyan}${deliverable}${c.reset}`);
1183
+ if (txHash) {
1184
+ console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
1185
+ }
1186
+ console.log(`\n${c.dim}The evaluator will now review and approve or reject the submission.${c.reset}\n`);
1187
+ }
1188
+ async function cmdJobComplete(args) {
1189
+ const id = getPositional(args, 0);
1190
+ if (!id) {
1191
+ console.error(`${c.red}Usage: obolos job complete <id> [--reason "..."]${c.reset}`);
1192
+ process.exit(1);
1193
+ }
1194
+ const reason = getFlag(args, 'reason');
1195
+ const walletAddress = await getWalletAddress();
1196
+ // Fetch job to get chain_job_id
1197
+ const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1198
+ const existingJob = jobData.job || jobData;
1199
+ const chainJobId = existingJob.chain_job_id;
1200
+ let txHash = null;
1201
+ if (chainJobId) {
1202
+ try {
1203
+ const acp = await getACPClient();
1204
+ const reasonHash = reason
1205
+ ? acp.keccak256(acp.toHex(reason))
1206
+ : ZERO_BYTES32;
1207
+ console.log(`\n ${c.dim}Completing job on-chain...${c.reset}`);
1208
+ const completeTx = await acp.walletClient.writeContract({
1209
+ address: ACP_ADDRESS,
1210
+ abi: ACP_ABI,
1211
+ functionName: 'complete',
1212
+ args: [BigInt(chainJobId), reasonHash, '0x'],
1213
+ account: acp.account,
1214
+ chain: (await import('viem/chains')).base,
1215
+ });
1216
+ console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1217
+ await acp.publicClient.waitForTransactionReceipt({ hash: completeTx });
1218
+ txHash = completeTx;
1219
+ console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1220
+ }
1221
+ catch (err) {
1222
+ console.error(` ${c.yellow}On-chain completion failed: ${err.message}${c.reset}`);
1223
+ }
1224
+ }
1225
+ const payload = {};
1226
+ if (reason)
1227
+ payload.reason = reason;
1228
+ if (txHash)
1229
+ payload.tx_hash = txHash;
1230
+ const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/complete`, payload, {
1231
+ 'x-wallet-address': walletAddress,
1232
+ });
1233
+ const job = data.job || data;
1234
+ console.log(`\n${c.green}Job completed and approved!${c.reset}\n`);
1235
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1236
+ console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1237
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'completed')}`);
1238
+ if (reason) {
1239
+ console.log(` ${c.bold}Reason:${c.reset} ${reason}`);
1240
+ }
1241
+ if (txHash) {
1242
+ console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
1243
+ }
1244
+ if (job.budget != null) {
1245
+ console.log(`\n ${c.dim}Escrow of $${Number(job.budget).toFixed(2)} USDC released to provider.${c.reset}`);
1246
+ }
1247
+ console.log();
1248
+ }
1249
+ async function cmdJobReject(args) {
1250
+ const id = getPositional(args, 0);
1251
+ if (!id) {
1252
+ console.error(`${c.red}Usage: obolos job reject <id> [--reason "..."]${c.reset}`);
1253
+ process.exit(1);
1254
+ }
1255
+ const reason = getFlag(args, 'reason');
1256
+ const walletAddress = await getWalletAddress();
1257
+ // Fetch job to get chain_job_id
1258
+ const jobData = await apiGet(`/api/jobs/${encodeURIComponent(id)}`);
1259
+ const existingJob = jobData.job || jobData;
1260
+ const chainJobId = existingJob.chain_job_id;
1261
+ let txHash = null;
1262
+ if (chainJobId) {
1263
+ try {
1264
+ const acp = await getACPClient();
1265
+ const reasonHash = reason
1266
+ ? acp.keccak256(acp.toHex(reason))
1267
+ : ZERO_BYTES32;
1268
+ console.log(`\n ${c.dim}Rejecting job on-chain...${c.reset}`);
1269
+ const rejectTx = await acp.walletClient.writeContract({
1270
+ address: ACP_ADDRESS,
1271
+ abi: ACP_ABI,
1272
+ functionName: 'reject',
1273
+ args: [BigInt(chainJobId), reasonHash, '0x'],
1274
+ account: acp.account,
1275
+ chain: (await import('viem/chains')).base,
1276
+ });
1277
+ console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1278
+ await acp.publicClient.waitForTransactionReceipt({ hash: rejectTx });
1279
+ txHash = rejectTx;
1280
+ console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1281
+ }
1282
+ catch (err) {
1283
+ console.error(` ${c.yellow}On-chain rejection failed: ${err.message}${c.reset}`);
1284
+ }
1285
+ }
1286
+ const payload = {};
1287
+ if (reason)
1288
+ payload.reason = reason;
1289
+ if (txHash)
1290
+ payload.tx_hash = txHash;
1291
+ const data = await apiPost(`/api/jobs/${encodeURIComponent(id)}/reject`, payload, {
1292
+ 'x-wallet-address': walletAddress,
1293
+ });
1294
+ const job = data.job || data;
1295
+ console.log(`\n${c.red}Job rejected.${c.reset}\n`);
1296
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1297
+ console.log(` ${c.bold}Job:${c.reset} ${job.title || id}`);
1298
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(job.status || 'rejected')}`);
1299
+ if (reason) {
1300
+ console.log(` ${c.bold}Reason:${c.reset} ${reason}`);
1301
+ }
1302
+ if (txHash) {
1303
+ console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${txHash}${c.reset}`);
1304
+ }
1305
+ console.log();
1306
+ }
1307
+ function showJobHelp() {
1308
+ console.log(`
1309
+ ${c.bold}${c.cyan}obolos job${c.reset} — ERC-8183 Agentic Commerce Protocol (ACP) job management
1310
+
1311
+ ${c.bold}Usage:${c.reset}
1312
+ obolos job list [options] List jobs with optional filters
1313
+ obolos job create [options] Create a new job
1314
+ obolos job info <id> Get full job details
1315
+ obolos job fund <id> Fund a job's escrow
1316
+ obolos job submit <id> [options] Submit work for a job
1317
+ obolos job complete <id> [options] Approve a job (evaluator)
1318
+ obolos job reject <id> [options] Reject a job submission
1319
+
1320
+ ${c.bold}List Options:${c.reset}
1321
+ --status=open|funded|submitted|completed|rejected|expired
1322
+ --client=0x... Filter by client address
1323
+ --provider=0x... Filter by provider address
1324
+ --limit=20 Max results (default: 20)
1325
+
1326
+ ${c.bold}Create Options:${c.reset}
1327
+ --title "..." Job title (required)
1328
+ --description "..." Job description
1329
+ --evaluator 0x... Evaluator address (required)
1330
+ --provider 0x... Specific provider (optional, open if omitted)
1331
+ --budget 1.00 Budget in USDC
1332
+ --expires 24h Expiry (e.g., "24h", "7d", "1h")
1333
+
1334
+ ${c.bold}Submit Options:${c.reset}
1335
+ --deliverable <hash/CID/URL> Work product reference (required)
1336
+
1337
+ ${c.bold}Complete/Reject Options:${c.reset}
1338
+ --reason "..." Optional reason text
1339
+
1340
+ ${c.bold}Examples:${c.reset}
1341
+ obolos job list --status=open
1342
+ obolos job create --title "Analyze dataset" --evaluator 0xABC... --budget 5.00 --expires 7d
1343
+ obolos job info abc123
1344
+ obolos job fund abc123
1345
+ obolos job submit abc123 --deliverable ipfs://Qm...
1346
+ obolos job complete abc123 --reason "Looks great"
1347
+ obolos job reject abc123 --reason "Missing section 3"
1348
+ `);
1349
+ }
1350
+ async function cmdJob(args) {
1351
+ const subcommand = args[0];
1352
+ const subArgs = args.slice(1);
1353
+ switch (subcommand) {
1354
+ case 'list':
1355
+ case 'ls':
1356
+ await cmdJobList(subArgs);
1357
+ break;
1358
+ case 'create':
1359
+ case 'new':
1360
+ await cmdJobCreate(subArgs);
1361
+ break;
1362
+ case 'info':
1363
+ case 'show':
1364
+ await cmdJobInfo(subArgs);
1365
+ break;
1366
+ case 'fund':
1367
+ await cmdJobFund(subArgs);
1368
+ break;
1369
+ case 'submit':
1370
+ await cmdJobSubmit(subArgs);
1371
+ break;
1372
+ case 'complete':
1373
+ case 'approve':
1374
+ await cmdJobComplete(subArgs);
1375
+ break;
1376
+ case 'reject':
1377
+ await cmdJobReject(subArgs);
1378
+ break;
1379
+ case 'help':
1380
+ case '--help':
1381
+ case '-h':
1382
+ case undefined:
1383
+ showJobHelp();
1384
+ break;
1385
+ default:
1386
+ console.error(`${c.red}Unknown job subcommand: ${subcommand}${c.reset}`);
1387
+ showJobHelp();
1388
+ process.exit(1);
1389
+ }
1390
+ }
1391
+ // ─── Listing Commands (Negotiation Layer) ────────────────────────────────────
1392
+ async function cmdListingList(args) {
1393
+ const params = new URLSearchParams();
1394
+ const status = getFlag(args, 'status');
1395
+ const client = getFlag(args, 'client');
1396
+ const limit = getFlag(args, 'limit') || '20';
1397
+ if (status)
1398
+ params.set('status', status);
1399
+ if (client)
1400
+ params.set('client', client);
1401
+ params.set('limit', limit);
1402
+ const data = await apiGet(`/api/listings?${params}`);
1403
+ const listings = data.listings || data.data || [];
1404
+ if (listings.length === 0) {
1405
+ console.log(`${c.yellow}No listings found.${c.reset}`);
1406
+ return;
1407
+ }
1408
+ const total = data.pagination?.total || data.total || listings.length;
1409
+ console.log(`\n${c.bold}${c.cyan}Job Listings${c.reset} ${c.dim}— ${total} listings${c.reset}\n`);
1410
+ // Table header
1411
+ console.log(` ${c.bold}${'ID'.padEnd(12)} ${'Title'.padEnd(28)} ${'Status'.padEnd(14)} ${'Budget Range'.padEnd(20)} ${'Bids'.padEnd(6)} ${'Client'.padEnd(14)} Deadline${c.reset}`);
1412
+ console.log(` ${c.dim}${'─'.repeat(110)}${c.reset}`);
1413
+ for (const l of listings) {
1414
+ const id = shortenId(l.id || '');
1415
+ const title = (l.title || 'Untitled').slice(0, 26).padEnd(28);
1416
+ const st = statusColor((l.status || 'open').padEnd(12));
1417
+ const budgetMin = l.min_budget != null ? `$${Number(l.min_budget).toFixed(2)}` : '?';
1418
+ const budgetMax = l.max_budget != null ? `$${Number(l.max_budget).toFixed(2)}` : '?';
1419
+ const budget = `${budgetMin}-${budgetMax}`.padEnd(20);
1420
+ const bids = String(l.bid_count ?? l.bids?.length ?? 0).padEnd(6);
1421
+ const cl = shortenAddr(l.client_address || l.client).padEnd(14);
1422
+ const deadline = l.deadline ? formatDate(l.deadline) : `${c.dim}—${c.reset}`;
1423
+ console.log(` ${id.padEnd(12)} ${title} ${st} ${budget} ${bids} ${cl} ${deadline}`);
1424
+ }
1425
+ console.log(`\n${c.dim}Use: obolos listing info <id> for full details${c.reset}\n`);
1426
+ }
1427
+ async function cmdListingCreate(args) {
1428
+ const title = getFlag(args, 'title');
1429
+ const description = getFlag(args, 'description');
1430
+ const minBudget = getFlag(args, 'min-budget');
1431
+ const maxBudget = getFlag(args, 'max-budget');
1432
+ const deadline = getFlag(args, 'deadline');
1433
+ const duration = getFlag(args, 'duration');
1434
+ const evaluator = getFlag(args, 'evaluator');
1435
+ const hook = getFlag(args, 'hook');
1436
+ if (!title) {
1437
+ console.error(`${c.red}Usage: obolos listing create --title "..." --description "..." [--min-budget 1.00] [--max-budget 10.00] [--deadline 7d] [--duration 24]${c.reset}`);
1438
+ process.exit(1);
1439
+ }
1440
+ const walletAddress = await getWalletAddress();
1441
+ const payload = { title };
1442
+ if (description)
1443
+ payload.description = description;
1444
+ if (minBudget)
1445
+ payload.min_budget = minBudget;
1446
+ if (maxBudget)
1447
+ payload.max_budget = maxBudget;
1448
+ if (deadline)
1449
+ payload.deadline = deadline;
1450
+ if (duration)
1451
+ payload.job_duration = parseInt(duration, 10);
1452
+ if (evaluator)
1453
+ payload.preferred_evaluator = evaluator;
1454
+ if (hook)
1455
+ payload.hook_address = hook;
1456
+ const data = await apiPost('/api/listings', payload, {
1457
+ 'x-wallet-address': walletAddress,
1458
+ });
1459
+ const listing = data.listing || data;
1460
+ console.log(`\n${c.green}Listing created successfully!${c.reset}\n`);
1461
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1462
+ console.log(` ${c.bold}ID:${c.reset} ${listing.id}`);
1463
+ console.log(` ${c.bold}Title:${c.reset} ${listing.title}`);
1464
+ if (listing.description) {
1465
+ console.log(` ${c.bold}Description:${c.reset} ${listing.description}`);
1466
+ }
1467
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
1468
+ console.log(` ${c.bold}Client:${c.reset} ${listing.client_address || walletAddress}`);
1469
+ if (listing.min_budget != null || listing.max_budget != null) {
1470
+ const min = listing.min_budget != null ? `$${Number(listing.min_budget).toFixed(2)}` : '?';
1471
+ const max = listing.max_budget != null ? `$${Number(listing.max_budget).toFixed(2)}` : '?';
1472
+ console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
1473
+ }
1474
+ if (listing.deadline) {
1475
+ console.log(` ${c.bold}Deadline:${c.reset} ${formatDate(listing.deadline)}`);
1476
+ }
1477
+ if (listing.job_duration) {
1478
+ console.log(` ${c.bold}Duration:${c.reset} ${listing.job_duration}h`);
1479
+ }
1480
+ console.log(`\n${c.dim}Share this listing with providers. They can bid with: obolos listing bid ${listing.id} --price 5.00${c.reset}\n`);
1481
+ }
1482
+ async function cmdListingInfo(args) {
1483
+ const id = getPositional(args, 0);
1484
+ if (!id) {
1485
+ console.error(`${c.red}Usage: obolos listing info <id>${c.reset}`);
1486
+ process.exit(1);
1487
+ }
1488
+ const data = await apiGet(`/api/listings/${encodeURIComponent(id)}`);
1489
+ const listing = data.listing || data;
1490
+ console.log(`\n${c.bold}${c.cyan}${listing.title || 'Untitled Listing'}${c.reset}`);
1491
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1492
+ console.log(` ${c.bold}ID:${c.reset} ${listing.id}`);
1493
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
1494
+ console.log(` ${c.bold}Client:${c.reset} ${listing.client_address || `${c.dim}—${c.reset}`}`);
1495
+ if (listing.min_budget != null || listing.max_budget != null) {
1496
+ const min = listing.min_budget != null ? `$${Number(listing.min_budget).toFixed(2)}` : '?';
1497
+ const max = listing.max_budget != null ? `$${Number(listing.max_budget).toFixed(2)}` : '?';
1498
+ console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
1499
+ }
1500
+ if (listing.deadline) {
1501
+ const deadlineDate = new Date(listing.deadline);
1502
+ const now = new Date();
1503
+ const expired = deadlineDate < now;
1504
+ console.log(` ${c.bold}Deadline:${c.reset} ${expired ? c.red : c.dim}${formatDate(listing.deadline)}${expired ? ' (passed)' : ''}${c.reset}`);
1505
+ }
1506
+ if (listing.job_duration) {
1507
+ console.log(` ${c.bold}Duration:${c.reset} ${listing.job_duration}h`);
1508
+ }
1509
+ if (listing.preferred_evaluator) {
1510
+ console.log(` ${c.bold}Evaluator:${c.reset} ${listing.preferred_evaluator}`);
1511
+ }
1512
+ if (listing.description) {
1513
+ console.log(`\n ${c.bold}Description:${c.reset}`);
1514
+ const descLines = listing.description.split('\n');
1515
+ for (const line of descLines) {
1516
+ console.log(` ${line}`);
1517
+ }
1518
+ }
1519
+ console.log(` ${c.bold}Created:${c.reset} ${formatDate(listing.created_at || listing.createdAt)}`);
1520
+ // Bids
1521
+ const bids = listing.bids || [];
1522
+ if (bids.length > 0) {
1523
+ console.log(`\n ${c.bold}${c.cyan}Bids (${bids.length})${c.reset}`);
1524
+ console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1525
+ console.log(` ${c.bold}${'Bid ID'.padEnd(12)} ${'Provider'.padEnd(14)} ${'Price'.padEnd(12)} ${'Delivery'.padEnd(10)} Message${c.reset}`);
1526
+ console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1527
+ for (const bid of bids) {
1528
+ const bidId = shortenId(bid.id || '');
1529
+ const provider = shortenAddr(bid.provider_address);
1530
+ const price = bid.price != null ? `${c.green}$${Number(bid.price).toFixed(2)}${c.reset}` : `${c.dim}—${c.reset}`;
1531
+ const delivery = bid.delivery_time ? `${bid.delivery_time}h` : `${c.dim}—${c.reset}`;
1532
+ const msg = (bid.message || '').slice(0, 40);
1533
+ console.log(` ${bidId.padEnd(12)} ${provider.padEnd(14)} ${price.padEnd(12)} ${delivery.padEnd(10)} ${c.dim}${msg}${c.reset}`);
1534
+ }
1535
+ }
1536
+ else {
1537
+ console.log(`\n ${c.dim}No bids yet.${c.reset}`);
1538
+ }
1539
+ // Actions
1540
+ console.log();
1541
+ const s = listing.status || 'open';
1542
+ if (s === 'open') {
1543
+ console.log(` ${c.bold}Actions:${c.reset}`);
1544
+ console.log(` obolos listing bid ${listing.id} --price 5.00 ${c.dim}Submit a bid${c.reset}`);
1545
+ if (bids.length > 0) {
1546
+ console.log(` obolos listing accept ${listing.id} --bid <bid_id> ${c.dim}Accept a bid${c.reset}`);
1547
+ }
1548
+ console.log(` obolos listing cancel ${listing.id} ${c.dim}Cancel the listing${c.reset}`);
1549
+ }
1550
+ console.log();
1551
+ }
1552
+ async function cmdListingBid(args) {
1553
+ const listingId = getPositional(args, 0);
1554
+ if (!listingId) {
1555
+ console.error(`${c.red}Usage: obolos listing bid <listing_id> --price 5.00 [--delivery 24] [--message "..."]${c.reset}`);
1556
+ process.exit(1);
1557
+ }
1558
+ const price = getFlag(args, 'price');
1559
+ if (!price) {
1560
+ console.error(`${c.red}--price is required. Provide your bid amount in USDC.${c.reset}`);
1561
+ process.exit(1);
1562
+ }
1563
+ const delivery = getFlag(args, 'delivery');
1564
+ const message = getFlag(args, 'message');
1565
+ const proposalHash = getFlag(args, 'proposal-hash');
1566
+ const walletAddress = await getWalletAddress();
1567
+ const payload = { price };
1568
+ if (delivery)
1569
+ payload.delivery_time = parseInt(delivery, 10);
1570
+ if (message)
1571
+ payload.message = message;
1572
+ if (proposalHash)
1573
+ payload.proposal_hash = proposalHash;
1574
+ const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/bid`, payload, {
1575
+ 'x-wallet-address': walletAddress,
1576
+ });
1577
+ const bid = data.bid || data;
1578
+ console.log(`\n${c.green}Bid submitted successfully!${c.reset}\n`);
1579
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1580
+ console.log(` ${c.bold}Bid ID:${c.reset} ${bid.id}`);
1581
+ console.log(` ${c.bold}Listing:${c.reset} ${listingId}`);
1582
+ console.log(` ${c.bold}Price:${c.reset} ${c.green}$${Number(price).toFixed(2)} USDC${c.reset}`);
1583
+ if (delivery) {
1584
+ console.log(` ${c.bold}Delivery:${c.reset} ${delivery}h`);
1585
+ }
1586
+ if (message) {
1587
+ console.log(` ${c.bold}Message:${c.reset} ${message}`);
1588
+ }
1589
+ console.log(`\n${c.dim}The client will review your bid. You'll be notified if accepted.${c.reset}\n`);
1590
+ }
1591
+ async function cmdListingAccept(args) {
1592
+ const listingId = getPositional(args, 0);
1593
+ if (!listingId) {
1594
+ console.error(`${c.red}Usage: obolos listing accept <listing_id> --bid <bid_id>${c.reset}`);
1595
+ process.exit(1);
1596
+ }
1597
+ const bidId = getFlag(args, 'bid');
1598
+ if (!bidId) {
1599
+ console.error(`${c.red}--bid is required. Specify the bid ID to accept.${c.reset}`);
1600
+ process.exit(1);
1601
+ }
1602
+ const walletAddress = await getWalletAddress();
1603
+ // Create on-chain ACP job if wallet is available
1604
+ let chainJobId = null;
1605
+ let chainTxHash = null;
1606
+ try {
1607
+ // Fetch listing details to get terms
1608
+ const listingData = await apiGet(`/api/listings/${encodeURIComponent(listingId)}`);
1609
+ const listing = listingData.listing || listingData;
1610
+ const bids = listing.bids || [];
1611
+ const acceptedBid = bids.find((b) => b.id === bidId);
1612
+ if (acceptedBid && OBOLOS_PRIVATE_KEY) {
1613
+ const acp = await getACPClient();
1614
+ const providerAddress = acceptedBid.provider_address || ZERO_ADDRESS;
1615
+ const evaluatorAddress = listing.preferred_evaluator || walletAddress;
1616
+ // Default expiry: delivery_time hours or job_duration or 7 days
1617
+ const durationHours = acceptedBid.delivery_time || listing.job_duration || 168;
1618
+ const expiredAt = Math.floor((Date.now() + durationHours * 3600000) / 1000);
1619
+ const description = `${listing.title}: ${listing.description || ''}`.slice(0, 500);
1620
+ console.log(`\n ${c.dim}Creating ACP job on-chain...${c.reset}`);
1621
+ const txHash = await acp.walletClient.writeContract({
1622
+ address: ACP_ADDRESS,
1623
+ abi: ACP_ABI,
1624
+ functionName: 'createJob',
1625
+ args: [
1626
+ providerAddress,
1627
+ evaluatorAddress,
1628
+ BigInt(expiredAt),
1629
+ description,
1630
+ (listing.hook_address || ZERO_ADDRESS),
1631
+ ],
1632
+ account: acp.account,
1633
+ chain: (await import('viem/chains')).base,
1634
+ });
1635
+ console.log(` ${c.dim}Waiting for confirmation...${c.reset}`);
1636
+ const receipt = await acp.publicClient.waitForTransactionReceipt({ hash: txHash });
1637
+ // Extract jobId from JobCreated event
1638
+ for (const log of receipt.logs) {
1639
+ try {
1640
+ const decoded = acp.decodeEventLog({
1641
+ abi: ACP_ABI,
1642
+ data: log.data,
1643
+ topics: log.topics,
1644
+ });
1645
+ if (decoded.eventName === 'JobCreated') {
1646
+ chainJobId = (decoded.args.jobId).toString();
1647
+ break;
1648
+ }
1649
+ }
1650
+ catch { }
1651
+ }
1652
+ chainTxHash = txHash;
1653
+ console.log(` ${c.green}Transaction confirmed: ${txHash}${c.reset}`);
1654
+ if (chainJobId) {
1655
+ console.log(` ${c.green}Chain job ID: ${chainJobId}${c.reset}`);
1656
+ }
1657
+ }
1658
+ }
1659
+ catch (err) {
1660
+ console.error(` ${c.yellow}On-chain job creation failed: ${err.message}${c.reset}`);
1661
+ console.error(` ${c.dim}Proceeding with backend-only acceptance...${c.reset}`);
1662
+ }
1663
+ const payload = { bid_id: bidId };
1664
+ if (chainJobId)
1665
+ payload.acp_job_id = chainJobId;
1666
+ if (chainTxHash)
1667
+ payload.chain_tx_hash = chainTxHash;
1668
+ const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/accept`, payload, {
1669
+ 'x-wallet-address': walletAddress,
1670
+ });
1671
+ const listing = data.listing || data;
1672
+ console.log(`\n${c.green}Bid accepted! ACP job created.${c.reset}\n`);
1673
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1674
+ console.log(` ${c.bold}Listing:${c.reset} ${listing.title || listingId}`);
1675
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'accepted')}`);
1676
+ console.log(` ${c.bold}Bid:${c.reset} ${bidId}`);
1677
+ if (chainJobId) {
1678
+ console.log(` ${c.bold}Chain ID:${c.reset} ${chainJobId}`);
1679
+ }
1680
+ if (chainTxHash) {
1681
+ console.log(` ${c.bold}Tx:${c.reset} ${c.cyan}${chainTxHash}${c.reset}`);
1682
+ }
1683
+ if (listing.job_id || data.job_id) {
1684
+ console.log(` ${c.bold}Job ID:${c.reset} ${listing.job_id || data.job_id}`);
1685
+ }
1686
+ console.log(`\n${c.dim}Next: Fund the escrow with: obolos job fund <job-id>${c.reset}\n`);
1687
+ }
1688
+ async function cmdListingCancel(args) {
1689
+ const listingId = getPositional(args, 0);
1690
+ if (!listingId) {
1691
+ console.error(`${c.red}Usage: obolos listing cancel <listing_id>${c.reset}`);
1692
+ process.exit(1);
1693
+ }
1694
+ const walletAddress = await getWalletAddress();
1695
+ const data = await apiPost(`/api/listings/${encodeURIComponent(listingId)}/cancel`, {}, {
1696
+ 'x-wallet-address': walletAddress,
1697
+ });
1698
+ const listing = data.listing || data;
1699
+ console.log(`\n${c.yellow}Listing cancelled.${c.reset}\n`);
1700
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1701
+ console.log(` ${c.bold}Listing:${c.reset} ${listing.title || listingId}`);
1702
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'cancelled')}`);
1703
+ console.log();
1704
+ }
1705
+ function showListingHelp() {
1706
+ console.log(`
1707
+ ${c.bold}${c.cyan}obolos listing${c.reset} — Agent-to-agent negotiation layer
1708
+
1709
+ ${c.bold}Usage:${c.reset}
1710
+ obolos listing list [options] Browse open job listings
1711
+ obolos listing create [options] Create a new listing for agents to bid on
1712
+ obolos listing info <id> Get listing details with all bids
1713
+ obolos listing bid <id> [options] Submit a bid on a listing
1714
+ obolos listing accept <id> [options] Accept a bid (auto-creates ACP job)
1715
+ obolos listing cancel <id> Cancel a listing
1716
+
1717
+ ${c.bold}List Options:${c.reset}
1718
+ --status=open|negotiating|accepted|cancelled
1719
+ --client=0x... Filter by client address
1720
+ --limit=20 Max results (default: 20)
1721
+
1722
+ ${c.bold}Create Options:${c.reset}
1723
+ --title "..." Listing title (required)
1724
+ --description "..." Detailed description
1725
+ --min-budget 1.00 Minimum budget in USDC
1726
+ --max-budget 10.00 Maximum budget in USDC
1727
+ --deadline 7d Bidding deadline (e.g., "24h", "7d")
1728
+ --duration 24 Expected job duration in hours
1729
+ --evaluator 0x... Preferred evaluator address
1730
+ --hook 0x... Hook contract address
1731
+
1732
+ ${c.bold}Bid Options:${c.reset}
1733
+ --price 5.00 Your proposed price in USDC (required)
1734
+ --delivery 24 Estimated delivery time in hours
1735
+ --message "I can do this" Pitch to the client
1736
+ --proposal-hash <hash> Hash of detailed proposal
1737
+
1738
+ ${c.bold}Accept Options:${c.reset}
1739
+ --bid <bid_id> Bid ID to accept (required)
1740
+
1741
+ ${c.bold}Examples:${c.reset}
1742
+ obolos listing list --status=open
1743
+ obolos listing create --title "Analyze dataset" --description "Parse and summarize CSV" --max-budget 10.00 --deadline 7d
1744
+ obolos listing info abc123
1745
+ obolos listing bid abc123 --price 5.00 --delivery 24 --message "I can do this in 12h"
1746
+ obolos listing accept abc123 --bid bid456
1747
+ obolos listing cancel abc123
1748
+ `);
1749
+ }
1750
+ async function cmdListing(args) {
1751
+ const sub = args[0];
1752
+ const subArgs = args.slice(1);
1753
+ switch (sub) {
1754
+ case 'list':
1755
+ case 'ls':
1756
+ await cmdListingList(subArgs);
1757
+ break;
1758
+ case 'create':
1759
+ case 'new':
1760
+ await cmdListingCreate(subArgs);
1761
+ break;
1762
+ case 'info':
1763
+ case 'show':
1764
+ await cmdListingInfo(subArgs);
1765
+ break;
1766
+ case 'bid':
1767
+ await cmdListingBid(subArgs);
1768
+ break;
1769
+ case 'accept':
1770
+ await cmdListingAccept(subArgs);
1771
+ break;
1772
+ case 'cancel':
1773
+ await cmdListingCancel(subArgs);
1774
+ break;
1775
+ case 'help':
1776
+ case '--help':
1777
+ case '-h':
1778
+ case undefined:
1779
+ showListingHelp();
1780
+ break;
1781
+ default:
1782
+ console.error(`${c.red}Unknown listing subcommand: ${sub}${c.reset}`);
1783
+ showListingHelp();
1784
+ process.exit(1);
1785
+ }
1786
+ }
1787
+ // ─── ANP Commands (Agent Negotiation Protocol) ──────────────────────────────
1788
+ async function cmdAnpList(args) {
1789
+ const params = new URLSearchParams();
1790
+ const status = getFlag(args, 'status');
1791
+ const limit = getFlag(args, 'limit') || '20';
1792
+ if (status)
1793
+ params.set('status', status);
1794
+ params.set('limit', limit);
1795
+ const data = await apiGet(`/api/anp/listings?${params}`);
1796
+ const listings = data.listings || data.data || [];
1797
+ if (listings.length === 0) {
1798
+ console.log(`${c.yellow}No ANP listings found.${c.reset}`);
1799
+ return;
1800
+ }
1801
+ const total = data.pagination?.total || data.total || listings.length;
1802
+ console.log(`\n${c.bold}${c.cyan}ANP Listings${c.reset} ${c.dim}— ${total} listings${c.reset}\n`);
1803
+ // Table header
1804
+ console.log(` ${c.bold}${'CID'.padEnd(18)} ${'Title'.padEnd(28)} ${'Budget Range'.padEnd(20)} ${'Status'.padEnd(14)} ${'Bids'.padEnd(6)} Client${c.reset}`);
1805
+ console.log(` ${c.dim}${'─'.repeat(100)}${c.reset}`);
1806
+ for (const l of listings) {
1807
+ const cid = (l.cid || l.id || '').slice(0, 16).padEnd(18);
1808
+ const title = (l.title || 'Untitled').slice(0, 26).padEnd(28);
1809
+ const budgetMin = l.min_budget != null ? `$${Number(l.min_budget).toFixed(2)}` : '?';
1810
+ const budgetMax = l.max_budget != null ? `$${Number(l.max_budget).toFixed(2)}` : '?';
1811
+ const budget = `${budgetMin}-${budgetMax}`.padEnd(20);
1812
+ const st = statusColor((l.status || 'open').padEnd(12));
1813
+ const bids = String(l.bid_count ?? l.bids?.length ?? 0).padEnd(6);
1814
+ const cl = shortenAddr(l.client_address || l.client || l.signer);
1815
+ console.log(` ${cid} ${title} ${budget} ${st} ${bids} ${cl}`);
1816
+ }
1817
+ console.log(`\n${c.dim}Use: obolos anp info <cid> for full details${c.reset}\n`);
1818
+ }
1819
+ async function cmdAnpInfo(args) {
1820
+ const cid = getPositional(args, 0);
1821
+ if (!cid) {
1822
+ console.error(`${c.red}Usage: obolos anp info <cid>${c.reset}`);
1823
+ process.exit(1);
1824
+ }
1825
+ const data = await apiGet(`/api/anp/listings/${encodeURIComponent(cid)}`);
1826
+ const listing = data.listing || data;
1827
+ console.log(`\n${c.bold}${c.cyan}${listing.title || 'Untitled ANP Listing'}${c.reset}`);
1828
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1829
+ console.log(` ${c.bold}CID:${c.reset} ${listing.cid || cid}`);
1830
+ console.log(` ${c.bold}Status:${c.reset} ${statusColor(listing.status || 'open')}`);
1831
+ console.log(` ${c.bold}Signer:${c.reset} ${listing.signer || listing.client_address || `${c.dim}—${c.reset}`}`);
1832
+ if (listing.min_budget != null || listing.max_budget != null) {
1833
+ const min = listing.min_budget != null ? `$${Number(listing.min_budget).toFixed(2)}` : '?';
1834
+ const max = listing.max_budget != null ? `$${Number(listing.max_budget).toFixed(2)}` : '?';
1835
+ console.log(` ${c.bold}Budget:${c.reset} ${c.green}${min} – ${max} USDC${c.reset}`);
1836
+ }
1837
+ if (listing.deadline) {
1838
+ const deadlineDate = new Date(typeof listing.deadline === 'number' ? listing.deadline * 1000 : listing.deadline);
1839
+ const now = new Date();
1840
+ const expired = deadlineDate < now;
1841
+ console.log(` ${c.bold}Deadline:${c.reset} ${expired ? c.red : c.dim}${formatDate(deadlineDate.toISOString())}${expired ? ' (passed)' : ''}${c.reset}`);
1842
+ }
1843
+ if (listing.job_duration || listing.jobDuration) {
1844
+ const dur = listing.job_duration || listing.jobDuration;
1845
+ console.log(` ${c.bold}Duration:${c.reset} ${dur >= 86400 ? `${Math.floor(dur / 86400)}d` : dur >= 3600 ? `${Math.floor(dur / 3600)}h` : `${dur}s`}`);
1846
+ }
1847
+ if (listing.preferred_evaluator || listing.preferredEvaluator) {
1848
+ const ev = listing.preferred_evaluator || listing.preferredEvaluator;
1849
+ if (ev !== ZERO_ADDRESS) {
1850
+ console.log(` ${c.bold}Evaluator:${c.reset} ${ev}`);
1851
+ }
1852
+ }
1853
+ if (listing.description) {
1854
+ console.log(`\n ${c.bold}Description:${c.reset}`);
1855
+ const descLines = listing.description.split('\n');
1856
+ for (const line of descLines) {
1857
+ console.log(` ${line}`);
1858
+ }
1859
+ }
1860
+ if (listing.content_hash || listing.contentHash) {
1861
+ console.log(` ${c.bold}Content Hash:${c.reset} ${c.dim}${listing.content_hash || listing.contentHash}${c.reset}`);
1862
+ }
1863
+ if (listing.nonce != null) {
1864
+ console.log(` ${c.bold}Nonce:${c.reset} ${c.dim}${listing.nonce}${c.reset}`);
1865
+ }
1866
+ if (listing.signature) {
1867
+ console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${listing.signature.slice(0, 20)}...${c.reset}`);
1868
+ }
1869
+ // Bids
1870
+ const bids = listing.bids || [];
1871
+ if (bids.length > 0) {
1872
+ console.log(`\n ${c.bold}${c.cyan}Bids (${bids.length})${c.reset}`);
1873
+ console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1874
+ console.log(` ${c.bold}${'CID'.padEnd(18)} ${'Bidder'.padEnd(14)} ${'Price'.padEnd(12)} ${'Delivery'.padEnd(10)} Message${c.reset}`);
1875
+ console.log(` ${c.dim}${'─'.repeat(56)}${c.reset}`);
1876
+ for (const bid of bids) {
1877
+ const bidCid = (bid.cid || bid.id || '').slice(0, 16).padEnd(18);
1878
+ const bidder = shortenAddr(bid.signer || bid.provider_address);
1879
+ const price = bid.price != null ? `${c.green}$${Number(bid.price).toFixed(2)}${c.reset}` : `${c.dim}—${c.reset}`;
1880
+ const delivery = bid.delivery_time || bid.deliveryTime;
1881
+ const deliveryStr = delivery ? (delivery >= 86400 ? `${Math.floor(delivery / 86400)}d` : delivery >= 3600 ? `${Math.floor(delivery / 3600)}h` : `${delivery}s`) : `${c.dim}—${c.reset}`;
1882
+ const msg = (bid.message || '').slice(0, 40);
1883
+ console.log(` ${bidCid} ${bidder.padEnd(14)} ${price.padEnd(12)} ${deliveryStr.padEnd(10)} ${c.dim}${msg}${c.reset}`);
1884
+ }
1885
+ }
1886
+ else {
1887
+ console.log(`\n ${c.dim}No bids yet.${c.reset}`);
1888
+ }
1889
+ // Actions
1890
+ console.log();
1891
+ const s = listing.status || 'open';
1892
+ if (s === 'open' || s === 'negotiating') {
1893
+ console.log(` ${c.bold}Actions:${c.reset}`);
1894
+ console.log(` obolos anp bid ${cid} --price 5.00 ${c.dim}Submit a bid${c.reset}`);
1895
+ if (bids.length > 0) {
1896
+ console.log(` obolos anp accept ${cid} --bid <bid_cid> ${c.dim}Accept a bid${c.reset}`);
1897
+ }
1898
+ }
1899
+ console.log(` obolos anp verify ${cid} ${c.dim}Verify document${c.reset}`);
1900
+ console.log();
1901
+ }
1902
+ async function cmdAnpCreate(args) {
1903
+ const title = getFlag(args, 'title');
1904
+ const description = getFlag(args, 'description');
1905
+ const minBudget = getFlag(args, 'min-budget');
1906
+ const maxBudget = getFlag(args, 'max-budget');
1907
+ const deadline = getFlag(args, 'deadline');
1908
+ const duration = getFlag(args, 'duration');
1909
+ const evaluator = getFlag(args, 'evaluator');
1910
+ if (!title) {
1911
+ console.error(`${c.red}Usage: obolos anp create --title "..." --description "..." --min-budget 5 --max-budget 50 --deadline 7d --duration 3d [--evaluator 0x...]${c.reset}`);
1912
+ process.exit(1);
1913
+ }
1914
+ const anp = await getANPSigningClient();
1915
+ // Compute content hash
1916
+ const contentHash = await computeContentHash({ title, description: description || '' });
1917
+ // Generate nonce
1918
+ const nonce = generateNonce();
1919
+ // Parse deadline to unix timestamp (seconds from now)
1920
+ let deadlineTs;
1921
+ if (deadline) {
1922
+ const secs = parseTimeToSeconds(deadline);
1923
+ deadlineTs = BigInt(Math.floor(Date.now() / 1000) + secs);
1924
+ }
1925
+ else {
1926
+ deadlineTs = BigInt(Math.floor(Date.now() / 1000) + 7 * 86400); // default 7d
1927
+ }
1928
+ // Parse duration to seconds
1929
+ let jobDuration;
1930
+ if (duration) {
1931
+ jobDuration = BigInt(parseTimeToSeconds(duration));
1932
+ }
1933
+ else {
1934
+ jobDuration = BigInt(3 * 86400); // default 3d
1935
+ }
1936
+ const minBudgetWei = BigInt(Math.floor((minBudget ? parseFloat(minBudget) : 0) * 1e6));
1937
+ const maxBudgetWei = BigInt(Math.floor((maxBudget ? parseFloat(maxBudget) : 0) * 1e6));
1938
+ const preferredEvaluator = (evaluator || ZERO_ADDRESS);
1939
+ const message = {
1940
+ contentHash,
1941
+ minBudget: minBudgetWei,
1942
+ maxBudget: maxBudgetWei,
1943
+ deadline: deadlineTs,
1944
+ jobDuration: jobDuration,
1945
+ preferredEvaluator,
1946
+ nonce,
1947
+ };
1948
+ console.log(`\n ${c.dim}Signing ListingIntent...${c.reset}`);
1949
+ const signature = await anp.walletClient.signTypedData({
1950
+ account: anp.account,
1951
+ domain: ANP_DOMAIN,
1952
+ types: ANP_LISTING_TYPES,
1953
+ primaryType: 'ListingIntent',
1954
+ message,
1955
+ });
1956
+ console.log(` ${c.green}Signed.${c.reset} Publishing...`);
1957
+ const document = {
1958
+ type: 'ListingIntent',
1959
+ title,
1960
+ description: description || '',
1961
+ contentHash,
1962
+ minBudget: minBudgetWei.toString(),
1963
+ maxBudget: maxBudgetWei.toString(),
1964
+ deadline: deadlineTs.toString(),
1965
+ jobDuration: jobDuration.toString(),
1966
+ preferredEvaluator,
1967
+ nonce: nonce.toString(),
1968
+ signer: anp.account.address,
1969
+ signature,
1970
+ };
1971
+ const data = await apiPost('/api/anp/publish', document);
1972
+ const result = data.listing || data;
1973
+ console.log(`\n${c.green}ANP listing published!${c.reset}\n`);
1974
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
1975
+ console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
1976
+ console.log(` ${c.bold}Title:${c.reset} ${title}`);
1977
+ console.log(` ${c.bold}Budget:${c.reset} ${c.green}$${(minBudget || '0')} – $${(maxBudget || '0')} USDC${c.reset}`);
1978
+ console.log(` ${c.bold}Deadline:${c.reset} ${formatDate(new Date(Number(deadlineTs) * 1000).toISOString())}`);
1979
+ console.log(` ${c.bold}Duration:${c.reset} ${duration || '3d'}`);
1980
+ console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
1981
+ console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
1982
+ console.log(`\n${c.dim}Agents can bid with: obolos anp bid ${result.cid || result.id} --price 25 --delivery 48h${c.reset}\n`);
1983
+ }
1984
+ async function cmdAnpBid(args) {
1985
+ const listingCid = getPositional(args, 0);
1986
+ if (!listingCid) {
1987
+ console.error(`${c.red}Usage: obolos anp bid <listing_cid> --price 25 --delivery 48h [--message "..."]${c.reset}`);
1988
+ process.exit(1);
1989
+ }
1990
+ const price = getFlag(args, 'price');
1991
+ if (!price) {
1992
+ console.error(`${c.red}--price is required. Provide your bid amount in USDC.${c.reset}`);
1993
+ process.exit(1);
1994
+ }
1995
+ const delivery = getFlag(args, 'delivery');
1996
+ const message = getFlag(args, 'message');
1997
+ const anp = await getANPSigningClient();
1998
+ // Fetch listing document to compute listingHash
1999
+ console.log(`\n ${c.dim}Fetching listing document...${c.reset}`);
2000
+ const listingData = await apiGet(`/api/anp/objects/${encodeURIComponent(listingCid)}`);
2001
+ const listing = listingData.document || listingData.listing || listingData;
2002
+ // Reconstruct listing struct and compute hash
2003
+ const listingHash = anp.hashListingStruct({
2004
+ contentHash: listing.contentHash,
2005
+ minBudget: BigInt(listing.minBudget || listing.min_budget || '0'),
2006
+ maxBudget: BigInt(listing.maxBudget || listing.max_budget || '0'),
2007
+ deadline: BigInt(listing.deadline || '0'),
2008
+ jobDuration: BigInt(listing.jobDuration || listing.job_duration || '0'),
2009
+ preferredEvaluator: (listing.preferredEvaluator || listing.preferred_evaluator || ZERO_ADDRESS),
2010
+ nonce: BigInt(listing.nonce || '0'),
2011
+ });
2012
+ // Compute content hash for bid
2013
+ const contentHash = await computeContentHash({ message: message || '', proposalCid: '' });
2014
+ const nonce = generateNonce();
2015
+ const priceWei = BigInt(Math.floor(parseFloat(price) * 1e6));
2016
+ let deliveryTime;
2017
+ if (delivery) {
2018
+ deliveryTime = BigInt(parseTimeToSeconds(delivery));
2019
+ }
2020
+ else {
2021
+ deliveryTime = BigInt(86400); // default 24h
2022
+ }
2023
+ const bidMessage = {
2024
+ listingHash,
2025
+ contentHash,
2026
+ price: priceWei,
2027
+ deliveryTime,
2028
+ nonce,
2029
+ };
2030
+ console.log(` ${c.dim}Signing BidIntent...${c.reset}`);
2031
+ const signature = await anp.walletClient.signTypedData({
2032
+ account: anp.account,
2033
+ domain: ANP_DOMAIN,
2034
+ types: ANP_BID_TYPES,
2035
+ primaryType: 'BidIntent',
2036
+ message: bidMessage,
2037
+ });
2038
+ console.log(` ${c.green}Signed.${c.reset} Publishing...`);
2039
+ const document = {
2040
+ type: 'BidIntent',
2041
+ listingCid,
2042
+ listingHash,
2043
+ contentHash,
2044
+ message: message || '',
2045
+ price: priceWei.toString(),
2046
+ deliveryTime: deliveryTime.toString(),
2047
+ nonce: nonce.toString(),
2048
+ signer: anp.account.address,
2049
+ signature,
2050
+ };
2051
+ const data = await apiPost('/api/anp/publish', document);
2052
+ const result = data.bid || data;
2053
+ console.log(`\n${c.green}ANP bid published!${c.reset}\n`);
2054
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2055
+ console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
2056
+ console.log(` ${c.bold}Listing:${c.reset} ${listingCid}`);
2057
+ console.log(` ${c.bold}Price:${c.reset} ${c.green}$${parseFloat(price).toFixed(2)} USDC${c.reset}`);
2058
+ console.log(` ${c.bold}Delivery:${c.reset} ${delivery || '24h'}`);
2059
+ if (message) {
2060
+ console.log(` ${c.bold}Message:${c.reset} ${message}`);
2061
+ }
2062
+ console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
2063
+ console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
2064
+ console.log(`\n${c.dim}The listing owner can accept with: obolos anp accept ${listingCid} --bid ${result.cid || result.id}${c.reset}\n`);
2065
+ }
2066
+ async function cmdAnpAccept(args) {
2067
+ const listingCid = getPositional(args, 0);
2068
+ if (!listingCid) {
2069
+ console.error(`${c.red}Usage: obolos anp accept <listing_cid> --bid <bid_cid>${c.reset}`);
2070
+ process.exit(1);
2071
+ }
2072
+ const bidCid = getFlag(args, 'bid');
2073
+ if (!bidCid) {
2074
+ console.error(`${c.red}--bid is required. Specify the bid CID to accept.${c.reset}`);
2075
+ process.exit(1);
2076
+ }
2077
+ const anp = await getANPSigningClient();
2078
+ // Fetch listing and bid documents
2079
+ console.log(`\n ${c.dim}Fetching listing and bid documents...${c.reset}`);
2080
+ const [listingData, bidData] = await Promise.all([
2081
+ apiGet(`/api/anp/objects/${encodeURIComponent(listingCid)}`),
2082
+ apiGet(`/api/anp/objects/${encodeURIComponent(bidCid)}`),
2083
+ ]);
2084
+ const listing = listingData.document || listingData.listing || listingData;
2085
+ const bid = bidData.document || bidData.bid || bidData;
2086
+ // Compute listing hash
2087
+ const listingHash = anp.hashListingStruct({
2088
+ contentHash: listing.contentHash,
2089
+ minBudget: BigInt(listing.minBudget || listing.min_budget || '0'),
2090
+ maxBudget: BigInt(listing.maxBudget || listing.max_budget || '0'),
2091
+ deadline: BigInt(listing.deadline || '0'),
2092
+ jobDuration: BigInt(listing.jobDuration || listing.job_duration || '0'),
2093
+ preferredEvaluator: (listing.preferredEvaluator || listing.preferred_evaluator || ZERO_ADDRESS),
2094
+ nonce: BigInt(listing.nonce || '0'),
2095
+ });
2096
+ // Compute bid hash
2097
+ const bidHash = anp.hashBidStruct({
2098
+ listingHash: (bid.listingHash || listingHash),
2099
+ contentHash: bid.contentHash,
2100
+ price: BigInt(bid.price || '0'),
2101
+ deliveryTime: BigInt(bid.deliveryTime || bid.delivery_time || '0'),
2102
+ nonce: BigInt(bid.nonce || '0'),
2103
+ });
2104
+ const nonce = generateNonce();
2105
+ const acceptMessage = {
2106
+ listingHash,
2107
+ bidHash,
2108
+ nonce,
2109
+ };
2110
+ console.log(` ${c.dim}Signing AcceptIntent...${c.reset}`);
2111
+ const signature = await anp.walletClient.signTypedData({
2112
+ account: anp.account,
2113
+ domain: ANP_DOMAIN,
2114
+ types: ANP_ACCEPT_TYPES,
2115
+ primaryType: 'AcceptIntent',
2116
+ message: acceptMessage,
2117
+ });
2118
+ console.log(` ${c.green}Signed.${c.reset} Publishing...`);
2119
+ const document = {
2120
+ type: 'AcceptIntent',
2121
+ listingCid,
2122
+ bidCid,
2123
+ listingHash,
2124
+ bidHash,
2125
+ nonce: nonce.toString(),
2126
+ signer: anp.account.address,
2127
+ signature,
2128
+ };
2129
+ const data = await apiPost('/api/anp/publish', document);
2130
+ const result = data.accept || data;
2131
+ console.log(`\n${c.green}Bid accepted! ANP agreement published.${c.reset}\n`);
2132
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2133
+ console.log(` ${c.bold}CID:${c.reset} ${result.cid || result.id}`);
2134
+ console.log(` ${c.bold}Listing:${c.reset} ${listingCid}`);
2135
+ console.log(` ${c.bold}Bid:${c.reset} ${bidCid}`);
2136
+ console.log(` ${c.bold}Signer:${c.reset} ${anp.account.address}`);
2137
+ console.log(` ${c.bold}Signature:${c.reset} ${c.dim}${signature.slice(0, 20)}...${c.reset}`);
2138
+ console.log(`\n${c.dim}The agreement is now verifiable on-chain.${c.reset}\n`);
2139
+ }
2140
+ async function cmdAnpVerify(args) {
2141
+ const cid = getPositional(args, 0);
2142
+ if (!cid) {
2143
+ console.error(`${c.red}Usage: obolos anp verify <cid>${c.reset}`);
2144
+ process.exit(1);
2145
+ }
2146
+ console.log(`\n ${c.dim}Verifying document...${c.reset}`);
2147
+ const data = await apiGet(`/api/anp/verify/${encodeURIComponent(cid)}`);
2148
+ console.log(`\n${c.bold}${c.cyan}ANP Document Verification${c.reset}`);
2149
+ console.log(`${c.dim}${'─'.repeat(60)}${c.reset}`);
2150
+ console.log(` ${c.bold}CID:${c.reset} ${cid}`);
2151
+ console.log(` ${c.bold}Type:${c.reset} ${data.type || `${c.dim}—${c.reset}`}`);
2152
+ console.log(` ${c.bold}Signer:${c.reset} ${data.signer || `${c.dim}—${c.reset}`}`);
2153
+ if (data.valid || data.verified) {
2154
+ console.log(` ${c.bold}Signature:${c.reset} ${c.green}Valid${c.reset}`);
2155
+ }
2156
+ else {
2157
+ console.log(` ${c.bold}Signature:${c.reset} ${c.red}Invalid${c.reset}`);
2158
+ }
2159
+ if (data.content_valid != null) {
2160
+ console.log(` ${c.bold}Content Hash:${c.reset} ${data.content_valid ? `${c.green}Matches${c.reset}` : `${c.red}Mismatch${c.reset}`}`);
2161
+ }
2162
+ if (data.chain_refs != null) {
2163
+ console.log(` ${c.bold}Chain Refs:${c.reset} ${data.chain_refs ? `${c.green}Valid${c.reset}` : `${c.red}Invalid${c.reset}`}`);
2164
+ }
2165
+ if (data.details) {
2166
+ console.log(`\n ${c.bold}Details:${c.reset}`);
2167
+ const details = typeof data.details === 'string' ? data.details : JSON.stringify(data.details, null, 2);
2168
+ for (const line of details.split('\n')) {
2169
+ console.log(` ${c.dim}${line}${c.reset}`);
2170
+ }
2171
+ }
2172
+ console.log();
2173
+ }
2174
+ function showAnpHelp() {
2175
+ console.log(`
2176
+ ${c.bold}${c.cyan}obolos anp${c.reset} — Agent Negotiation Protocol (EIP-712 signed documents)
2177
+
2178
+ ${c.bold}Usage:${c.reset}
2179
+ obolos anp list [options] Browse ANP listings
2180
+ obolos anp info <cid> Get listing details with bids
2181
+ obolos anp create [options] Sign and publish a listing
2182
+ obolos anp bid <cid> [options] Sign and publish a bid
2183
+ obolos anp accept <cid> [options] Accept a bid (sign AcceptIntent)
2184
+ obolos anp verify <cid> Verify document integrity
2185
+
2186
+ ${c.bold}List Options:${c.reset}
2187
+ --status=open|negotiating|accepted Filter by status
2188
+ --limit=20 Max results (default: 20)
2189
+
2190
+ ${c.bold}Create Options:${c.reset}
2191
+ --title "..." Listing title (required)
2192
+ --description "..." Detailed description
2193
+ --min-budget 5 Minimum budget in USDC
2194
+ --max-budget 50 Maximum budget in USDC
2195
+ --deadline 7d Bidding deadline (e.g., "24h", "7d")
2196
+ --duration 3d Expected job duration (e.g., "48h", "3d")
2197
+ --evaluator 0x... Preferred evaluator address
2198
+
2199
+ ${c.bold}Bid Options:${c.reset}
2200
+ --price 25 Your proposed price in USDC (required)
2201
+ --delivery 48h Estimated delivery time (e.g., "24h", "3d")
2202
+ --message "I can do this" Message to the client
2203
+
2204
+ ${c.bold}Accept Options:${c.reset}
2205
+ --bid <bid_cid> Bid CID to accept (required)
2206
+
2207
+ ${c.bold}Examples:${c.reset}
2208
+ obolos anp list --status=open
2209
+ obolos anp create --title "Analyze dataset" --description "Parse CSV" --min-budget 5 --max-budget 50 --deadline 7d --duration 3d
2210
+ obolos anp info sha256-abc123...
2211
+ obolos anp bid sha256-abc123... --price 25 --delivery 48h --message "I can do this"
2212
+ obolos anp accept sha256-listing... --bid sha256-bid...
2213
+ obolos anp verify sha256-abc123...
2214
+ `);
2215
+ }
2216
+ async function cmdAnp(args) {
2217
+ const sub = args[0];
2218
+ const subArgs = args.slice(1);
2219
+ switch (sub) {
2220
+ case 'list':
2221
+ case 'ls':
2222
+ await cmdAnpList(subArgs);
2223
+ break;
2224
+ case 'info':
2225
+ case 'show':
2226
+ await cmdAnpInfo(subArgs);
2227
+ break;
2228
+ case 'create':
2229
+ case 'new':
2230
+ await cmdAnpCreate(subArgs);
2231
+ break;
2232
+ case 'bid':
2233
+ await cmdAnpBid(subArgs);
2234
+ break;
2235
+ case 'accept':
2236
+ await cmdAnpAccept(subArgs);
2237
+ break;
2238
+ case 'verify':
2239
+ await cmdAnpVerify(subArgs);
2240
+ break;
2241
+ case 'help':
2242
+ case '--help':
2243
+ case '-h':
2244
+ case undefined:
2245
+ showAnpHelp();
2246
+ break;
2247
+ default:
2248
+ console.error(`${c.red}Unknown anp subcommand: ${sub}${c.reset}`);
2249
+ showAnpHelp();
2250
+ process.exit(1);
2251
+ }
2252
+ }
2253
+ // ─── Help ───────────────────────────────────────────────────────────────────
472
2254
  function showHelp() {
473
2255
  console.log(`
474
2256
  ${c.bold}${c.cyan}obolos${c.reset} — CLI for the Obolos x402 API Marketplace
@@ -484,6 +2266,34 @@ ${c.bold}Usage:${c.reset}
484
2266
  obolos setup --show Show current wallet config
485
2267
  obolos setup-mcp Show MCP server setup instructions
486
2268
 
2269
+ ${c.bold}Job Commands (ERC-8183 ACP):${c.reset}
2270
+ obolos job list [options] List jobs with filters
2271
+ obolos job create [options] Create a new job
2272
+ obolos job info <id> Get full job details
2273
+ obolos job fund <id> Fund a job's escrow
2274
+ obolos job submit <id> [opts] Submit work for a job
2275
+ obolos job complete <id> Approve a job (evaluator)
2276
+ obolos job reject <id> Reject a job submission
2277
+ obolos job help Show job command help
2278
+
2279
+ ${c.bold}Listing Commands (Negotiation):${c.reset}
2280
+ obolos listing list [options] Browse open job listings
2281
+ obolos listing create [opts] Create a listing for bids
2282
+ obolos listing info <id> Get listing details + bids
2283
+ obolos listing bid <id> [opts] Submit a bid on a listing
2284
+ obolos listing accept <id> Accept a bid (creates job)
2285
+ obolos listing cancel <id> Cancel a listing
2286
+ obolos listing help Show listing command help
2287
+
2288
+ ${c.bold}ANP Commands (Agent Negotiation Protocol):${c.reset}
2289
+ obolos anp list [options] Browse ANP listings
2290
+ obolos anp info <cid> Get listing details + bids
2291
+ obolos anp create [options] Sign and publish a listing
2292
+ obolos anp bid <cid> [opts] Sign and publish a bid
2293
+ obolos anp accept <cid> [opts] Accept a bid (sign AcceptIntent)
2294
+ obolos anp verify <cid> Verify document integrity
2295
+ obolos anp help Show ANP command help
2296
+
487
2297
  ${c.bold}Call Options:${c.reset}
488
2298
  --method POST|GET|PUT HTTP method (default: GET)
489
2299
  --body '{"key":"value"}' Request body (JSON)
@@ -497,6 +2307,16 @@ ${c.bold}Examples:${c.reset}
497
2307
  obolos search "token price"
498
2308
  obolos info a59a0377-d77b-4fee-...
499
2309
  obolos call a59a0377-... --body '{"prompt":"a cat in space"}'
2310
+ obolos job list --status=open
2311
+ obolos job create --title "Analyze data" --evaluator 0xABC... --budget 5.00
2312
+ obolos listing list --status=open
2313
+ obolos listing create --title "Parse CSV data" --max-budget 10.00 --deadline 7d
2314
+ obolos listing bid abc123 --price 5.00 --message "I can do this"
2315
+ obolos listing accept abc123 --bid bid456
2316
+ obolos anp list --status=open
2317
+ obolos anp create --title "Analyze data" --min-budget 5 --max-budget 50 --deadline 7d
2318
+ obolos anp bid sha256-abc... --price 25 --delivery 48h --message "I can do this"
2319
+ obolos anp accept sha256-listing... --bid sha256-bid...
500
2320
  `);
501
2321
  }
502
2322
  // ─── Main ───────────────────────────────────────────────────────────────────
@@ -532,6 +2352,17 @@ async function main() {
532
2352
  case 'mcp':
533
2353
  await cmdSetupMcp();
534
2354
  break;
2355
+ case 'job':
2356
+ case 'j':
2357
+ await cmdJob(commandArgs);
2358
+ break;
2359
+ case 'listing':
2360
+ case 'l':
2361
+ await cmdListing(commandArgs);
2362
+ break;
2363
+ case 'anp':
2364
+ await cmdAnp(commandArgs);
2365
+ break;
535
2366
  case 'help':
536
2367
  case '--help':
537
2368
  case '-h':