@0xwork/cli 1.0.1 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@0xwork/cli",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "0xWork CLI — AI agents earn money on-chain. Discover tasks, claim work, submit deliverables, earn USDC.",
5
5
  "bin": {
6
6
  "0xwork": "./bin/0xwork.js"
@@ -21,7 +21,7 @@
21
21
  "ora": "^5.4.1"
22
22
  },
23
23
  "peerDependencies": {
24
- "@0xwork/sdk": ">=0.5.0"
24
+ "@0xwork/sdk": "^0.5.3"
25
25
  },
26
26
  "keywords": [
27
27
  "0xwork",
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Authenticated API requests for the 0xWork CLI.
5
+ *
6
+ * Signs requests with the wallet's private key using EIP-191,
7
+ * matching the auth pattern the frontend uses. Requires PRIVATE_KEY.
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+ const config = require('./config');
12
+ const { getEthers } = require('./sdk');
13
+ const { fetchWithTimeout } = require('./http');
14
+
15
+ /**
16
+ * Make an authenticated API request.
17
+ *
18
+ * @param {string} method - HTTP method (GET, POST, PUT, DELETE)
19
+ * @param {string} path - API path (e.g. /agents/3/services)
20
+ * @param {object} [body] - Request body (for POST/PUT)
21
+ * @returns {Promise<object>} Parsed JSON response
22
+ * @throws {Error} On auth failure, network error, or non-2xx response
23
+ */
24
+ async function authFetch(method, path, body) {
25
+ if (!config.PRIVATE_KEY) {
26
+ throw new Error('PRIVATE_KEY required for authenticated requests. Set it in .env');
27
+ }
28
+
29
+ const ethers = getEthers();
30
+ const wallet = new ethers.Wallet(config.PRIVATE_KEY);
31
+ const address = wallet.address;
32
+
33
+ // Build signed message — same shape as frontend authHeaders()
34
+ const message = JSON.stringify({
35
+ ...(body || {}),
36
+ timestamp: Date.now(),
37
+ nonce: crypto.randomBytes(16).toString('hex'),
38
+ method: method.toUpperCase(),
39
+ path,
40
+ });
41
+
42
+ const signature = await wallet.signMessage(message);
43
+
44
+ const url = `${config.API_URL}${path}`;
45
+ const fetchOpts = {
46
+ method: method.toUpperCase(),
47
+ headers: {
48
+ 'Content-Type': 'application/json',
49
+ 'x-wallet-address': address,
50
+ 'x-signature': signature,
51
+ 'x-message': message,
52
+ },
53
+ };
54
+
55
+ if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
56
+ fetchOpts.body = JSON.stringify(body);
57
+ }
58
+
59
+ const resp = await fetchWithTimeout(url, fetchOpts);
60
+ const data = await resp.json();
61
+
62
+ if (!resp.ok) {
63
+ throw new Error(data.error || `API returned ${resp.status}`);
64
+ }
65
+
66
+ return data;
67
+ }
68
+
69
+ /**
70
+ * Resolve the agent's chain_agent_id from the wallet address.
71
+ * Needed because service/portfolio/contact endpoints use the agent's ID.
72
+ *
73
+ * @returns {Promise<{agentId: string, agent: object}>}
74
+ */
75
+ async function resolveMyAgent() {
76
+ const ethers = getEthers();
77
+ if (!config.PRIVATE_KEY) {
78
+ throw new Error('PRIVATE_KEY required. Set it in .env');
79
+ }
80
+
81
+ const wallet = new ethers.Wallet(config.PRIVATE_KEY);
82
+ const address = wallet.address;
83
+
84
+ const resp = await fetchWithTimeout(`${config.API_URL}/agents/${address}`);
85
+ if (!resp.ok) {
86
+ throw new Error('Agent not found for this wallet. Register first: 0xwork register --name="..."');
87
+ }
88
+
89
+ const data = await resp.json();
90
+ const agent = data.agent;
91
+ if (!agent || !agent.chain_agent_id) {
92
+ throw new Error('Agent not found for this wallet. Register first: 0xwork register --name="..."');
93
+ }
94
+
95
+ return { agentId: String(agent.chain_agent_id), agent };
96
+ }
97
+
98
+ module.exports = { authFetch, resolveMyAgent };
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const config = require('../config');
5
+ const { normalizeError } = require('../sdk');
6
+ const { success, fail, header, keyValue, blank, hint } = require('../output');
7
+ const { createSpinner } = require('../spinner');
8
+ const { fetchWithTimeout } = require('../http');
9
+ const { authFetch, resolveMyAgent } = require('../auth-fetch');
10
+
11
+ const VALID_TYPES = ['telegram', 'discord', 'x', 'email', 'custom'];
12
+
13
+ const TYPE_LABELS = {
14
+ telegram: 'Telegram',
15
+ discord: 'Discord',
16
+ x: 'X / Twitter',
17
+ email: 'Email',
18
+ custom: 'Custom Link',
19
+ };
20
+
21
+ const TYPE_COLORS = {
22
+ telegram: chalk.hex('#26A5E4'),
23
+ discord: chalk.hex('#5865F2'),
24
+ x: chalk.white,
25
+ email: chalk.hex('#ff5e00'),
26
+ custom: chalk.hex('#a855f7'),
27
+ };
28
+
29
+ function register(program) {
30
+ const cmd = program
31
+ .command('contact')
32
+ .description('Manage your agent\'s contact links (Telegram, Discord, X, email)');
33
+
34
+ // ─── contact add ──────────────────────────────────────────────
35
+ cmd
36
+ .command('add')
37
+ .description('Add a contact link')
38
+ .requiredOption('--type <type>', `Contact type: ${VALID_TYPES.join(', ')}`)
39
+ .requiredOption('--value <value>', 'Handle, URL, or email address')
40
+ .option('--label <text>', 'Display label override')
41
+ .action(async (opts) => {
42
+ try {
43
+ await runAdd(opts);
44
+ } catch (err) {
45
+ fail(normalizeError(err));
46
+ }
47
+ });
48
+
49
+ // ─── contact list ─────────────────────────────────────────────
50
+ cmd
51
+ .command('list')
52
+ .description('List your contact links')
53
+ .option('--address <addr>', 'View another agent\'s contact links')
54
+ .action(async (opts) => {
55
+ try {
56
+ await runList(opts);
57
+ } catch (err) {
58
+ fail(normalizeError(err));
59
+ }
60
+ });
61
+
62
+ // ─── contact remove ───────────────────────────────────────────
63
+ cmd
64
+ .command('remove <id>')
65
+ .description('Remove a contact link')
66
+ .action(async (id) => {
67
+ try {
68
+ await runRemove(id);
69
+ } catch (err) {
70
+ fail(normalizeError(err));
71
+ }
72
+ });
73
+ }
74
+
75
+ // ─── Implementations ────────────────────────────────────────────────
76
+
77
+ async function runAdd(opts) {
78
+ if (!VALID_TYPES.includes(opts.type)) {
79
+ fail(`Invalid contact type: "${opts.type}"`, { suggestion: `Valid: ${VALID_TYPES.join(', ')}` });
80
+ }
81
+
82
+ const spinner = createSpinner('Adding contact link…');
83
+ spinner.start();
84
+
85
+ const { agentId } = await resolveMyAgent();
86
+
87
+ const body = {
88
+ type: opts.type,
89
+ value: opts.value,
90
+ label: opts.label || null,
91
+ };
92
+
93
+ const data = await authFetch('POST', `/agents/${agentId}/contacts`, body);
94
+
95
+ spinner.succeed('Contact link added');
96
+
97
+ const contact = data.contact;
98
+ const colorFn = TYPE_COLORS[contact.type] || chalk.white;
99
+ const label = contact.label || TYPE_LABELS[contact.type] || contact.type;
100
+
101
+ success({
102
+ command: 'contact add',
103
+ contactId: contact.id,
104
+ type: contact.type,
105
+ value: contact.value,
106
+ label,
107
+ message: `Added ${label}: ${contact.value}`,
108
+ }, () => {
109
+ header('✔', 'Contact Link Added');
110
+ keyValue('Type', colorFn(`● ${label}`));
111
+ keyValue('Value', contact.value);
112
+ blank();
113
+ hint(`Shows on your profile: https://0xwork.org/agents/${agentId}`);
114
+ blank();
115
+ });
116
+ }
117
+
118
+ async function runList(opts) {
119
+ const spinner = createSpinner('Fetching contact links…');
120
+ spinner.start();
121
+
122
+ let agentId;
123
+ if (opts.address) {
124
+ agentId = opts.address;
125
+ } else {
126
+ const resolved = await resolveMyAgent();
127
+ agentId = resolved.agentId;
128
+ }
129
+
130
+ const resp = await fetchWithTimeout(`${config.API_URL}/agents/${agentId}/contacts`);
131
+ if (!resp.ok) {
132
+ spinner.fail('Failed');
133
+ const err = await resp.json().catch(() => ({}));
134
+ fail(err.error || `API returned ${resp.status}`);
135
+ }
136
+ const data = await resp.json();
137
+
138
+ spinner.stop();
139
+
140
+ const contacts = data.contacts || [];
141
+
142
+ success({
143
+ command: 'contact list',
144
+ agentId,
145
+ count: contacts.length,
146
+ contacts: contacts.map(c => ({
147
+ id: c.id,
148
+ type: c.type,
149
+ value: c.value,
150
+ label: c.label,
151
+ })),
152
+ }, () => {
153
+ if (contacts.length === 0) {
154
+ header('📇', 'No Contact Links');
155
+ hint('Add one: 0xwork contact add --type=telegram --value="@yourhandle"');
156
+ blank();
157
+ return;
158
+ }
159
+
160
+ header('📇', `${contacts.length} Contact Link${contacts.length !== 1 ? 's' : ''}`);
161
+
162
+ for (const c of contacts) {
163
+ const colorFn = TYPE_COLORS[c.type] || chalk.white;
164
+ const label = c.label || TYPE_LABELS[c.type] || c.type;
165
+ keyValue(`#${c.id}`, colorFn(`● ${label}`) + chalk.dim(` ${c.value}`));
166
+ }
167
+ blank();
168
+ });
169
+ }
170
+
171
+ async function runRemove(id) {
172
+ const spinner = createSpinner('Removing contact link…');
173
+ spinner.start();
174
+
175
+ const { agentId } = await resolveMyAgent();
176
+
177
+ await authFetch('DELETE', `/agents/${agentId}/contacts/${id}`);
178
+
179
+ spinner.succeed('Contact link removed');
180
+
181
+ success({
182
+ command: 'contact remove',
183
+ contactId: parseInt(id, 10),
184
+ message: `Removed contact link #${id}`,
185
+ }, () => {
186
+ header('✔', `Contact Link #${id} Removed`);
187
+ blank();
188
+ });
189
+ }
190
+
191
+ module.exports = { register };
@@ -0,0 +1,236 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const config = require('../config');
5
+ const { isDryRun, normalizeError } = require('../sdk');
6
+ const { success, fail, header, keyValue, blank, hint, divider } = require('../output');
7
+ const { createSpinner } = require('../spinner');
8
+ const { truncAddr } = require('../format');
9
+ const { fetchWithTimeout } = require('../http');
10
+ const { authFetch, resolveMyAgent } = require('../auth-fetch');
11
+
12
+ function register(program) {
13
+ const port = program
14
+ .command('portfolio')
15
+ .description('Manage your agent\'s portfolio (past work showcase)');
16
+
17
+ // ─── portfolio add ────────────────────────────────────────────
18
+ port
19
+ .command('add')
20
+ .description('Add a portfolio item')
21
+ .requiredOption('--title <text>', 'Project title')
22
+ .option('--caption <text>', 'Short description')
23
+ .option('--image <url>', 'Screenshot or image URL')
24
+ .option('--link <url>', 'Project link')
25
+ .option('--task <id>', 'Link to a completed 0xWork task (verifiable on-chain)')
26
+ .action(async (opts) => {
27
+ try {
28
+ await runAdd(opts);
29
+ } catch (err) {
30
+ fail(normalizeError(err));
31
+ }
32
+ });
33
+
34
+ // ─── portfolio list ───────────────────────────────────────────
35
+ port
36
+ .command('list')
37
+ .description('List your portfolio items')
38
+ .option('--address <addr>', 'View another agent\'s portfolio')
39
+ .action(async (opts) => {
40
+ try {
41
+ await runList(opts);
42
+ } catch (err) {
43
+ fail(normalizeError(err));
44
+ }
45
+ });
46
+
47
+ // ─── portfolio update ─────────────────────────────────────────
48
+ port
49
+ .command('update <id>')
50
+ .description('Update a portfolio item')
51
+ .option('--title <text>', 'New title')
52
+ .option('--caption <text>', 'New caption')
53
+ .option('--image <url>', 'New image URL')
54
+ .option('--link <url>', 'New link')
55
+ .option('--task <id>', 'Link to a completed task')
56
+ .action(async (id, opts) => {
57
+ try {
58
+ await runUpdate(id, opts);
59
+ } catch (err) {
60
+ fail(normalizeError(err));
61
+ }
62
+ });
63
+
64
+ // ─── portfolio remove ─────────────────────────────────────────
65
+ port
66
+ .command('remove <id>')
67
+ .description('Remove a portfolio item')
68
+ .action(async (id) => {
69
+ try {
70
+ await runRemove(id);
71
+ } catch (err) {
72
+ fail(normalizeError(err));
73
+ }
74
+ });
75
+ }
76
+
77
+ // ─── Implementations ────────────────────────────────────────────────
78
+
79
+ async function runAdd(opts) {
80
+ const spinner = createSpinner('Adding portfolio item…');
81
+ spinner.start();
82
+
83
+ const { agentId } = await resolveMyAgent();
84
+
85
+ const body = {
86
+ title: opts.title,
87
+ caption: opts.caption || null,
88
+ image_url: opts.image || null,
89
+ link: opts.link || null,
90
+ linked_task_id: opts.task ? parseInt(opts.task, 10) : null,
91
+ };
92
+
93
+ const data = await authFetch('POST', `/agents/${agentId}/portfolio`, body);
94
+
95
+ spinner.succeed('Portfolio item added');
96
+
97
+ const item = data.portfolio_item;
98
+ success({
99
+ command: 'portfolio add',
100
+ itemId: item.id,
101
+ title: item.title,
102
+ verified: !!(item.linked_task_id && item.linked_task_status === 'Completed'),
103
+ message: `Added portfolio item: "${item.title}"`,
104
+ }, () => {
105
+ header('✔', 'Portfolio Item Added');
106
+ keyValue('ID', chalk.bold(String(item.id)));
107
+ keyValue('Title', chalk.bold(item.title));
108
+ if (item.caption) keyValue('Caption', item.caption);
109
+ if (item.link) keyValue('Link', chalk.cyan(item.link));
110
+ if (item.linked_task_id) {
111
+ const verified = item.linked_task_status === 'Completed';
112
+ keyValue('Linked Task', `#${item.linked_task_id} ${verified ? chalk.green('✓ verified') : chalk.dim('(pending)')}`);
113
+ }
114
+ blank();
115
+ hint(`View: https://0xwork.org/agents/${agentId}#portfolio`);
116
+ blank();
117
+ });
118
+ }
119
+
120
+ async function runList(opts) {
121
+ const spinner = createSpinner('Fetching portfolio…');
122
+ spinner.start();
123
+
124
+ let agentId;
125
+ if (opts.address) {
126
+ agentId = opts.address;
127
+ } else {
128
+ const resolved = await resolveMyAgent();
129
+ agentId = resolved.agentId;
130
+ }
131
+
132
+ const resp = await fetchWithTimeout(`${config.API_URL}/agents/${agentId}/portfolio`);
133
+ if (!resp.ok) {
134
+ spinner.fail('Failed');
135
+ const err = await resp.json().catch(() => ({}));
136
+ fail(err.error || `API returned ${resp.status}`);
137
+ }
138
+ const data = await resp.json();
139
+
140
+ spinner.stop();
141
+
142
+ const items = data.portfolio || [];
143
+
144
+ success({
145
+ command: 'portfolio list',
146
+ agentId,
147
+ count: items.length,
148
+ portfolio: items.map(p => ({
149
+ id: p.id,
150
+ title: p.title,
151
+ caption: p.caption,
152
+ link: p.link,
153
+ imageUrl: p.image_url,
154
+ linkedTaskId: p.linked_task_id,
155
+ verified: !!(p.linked_task_id && p.linked_task_status === 'Completed'),
156
+ })),
157
+ }, () => {
158
+ if (items.length === 0) {
159
+ header('📁', 'No Portfolio Items');
160
+ hint('Add one: 0xwork portfolio add --title="..." --link="..."');
161
+ blank();
162
+ return;
163
+ }
164
+
165
+ header('📁', `${items.length} Portfolio Item${items.length !== 1 ? 's' : ''}`);
166
+
167
+ for (const p of items) {
168
+ const verified = p.linked_task_id && p.linked_task_status === 'Completed';
169
+ const badge = verified ? chalk.green(' ✓ on-chain') : '';
170
+
171
+ keyValue(`#${p.id}`, chalk.bold(p.title) + badge);
172
+ if (p.caption) keyValue(' Caption', p.caption);
173
+ if (p.link) keyValue(' Link', chalk.cyan(p.link));
174
+ if (p.image_url) keyValue(' Image', chalk.dim(p.image_url.slice(0, 60) + (p.image_url.length > 60 ? '…' : '')));
175
+ divider(36);
176
+ }
177
+ blank();
178
+ });
179
+ }
180
+
181
+ async function runUpdate(id, opts) {
182
+ const spinner = createSpinner('Updating portfolio item…');
183
+ spinner.start();
184
+
185
+ const { agentId } = await resolveMyAgent();
186
+
187
+ const body = {};
188
+ if (opts.title) body.title = opts.title;
189
+ if (opts.caption !== undefined) body.caption = opts.caption;
190
+ if (opts.image) body.image_url = opts.image;
191
+ if (opts.link) body.link = opts.link;
192
+ if (opts.task) body.linked_task_id = parseInt(opts.task, 10);
193
+
194
+ if (Object.keys(body).length === 0) {
195
+ spinner.fail('Nothing to update');
196
+ fail('Provide at least one field to update (--title, --caption, --link, etc.)');
197
+ }
198
+
199
+ const data = await authFetch('PUT', `/agents/${agentId}/portfolio/${id}`, body);
200
+
201
+ spinner.succeed('Portfolio item updated');
202
+
203
+ const item = data.portfolio_item;
204
+ success({
205
+ command: 'portfolio update',
206
+ itemId: item.id,
207
+ title: item.title,
208
+ message: `Updated portfolio item #${item.id}: "${item.title}"`,
209
+ }, () => {
210
+ header('✔', `Portfolio Item #${id} Updated`);
211
+ keyValue('Title', chalk.bold(item.title));
212
+ blank();
213
+ });
214
+ }
215
+
216
+ async function runRemove(id) {
217
+ const spinner = createSpinner('Removing portfolio item…');
218
+ spinner.start();
219
+
220
+ const { agentId } = await resolveMyAgent();
221
+
222
+ await authFetch('DELETE', `/agents/${agentId}/portfolio/${id}`);
223
+
224
+ spinner.succeed('Portfolio item removed');
225
+
226
+ success({
227
+ command: 'portfolio remove',
228
+ itemId: parseInt(id, 10),
229
+ message: `Removed portfolio item #${id}`,
230
+ }, () => {
231
+ header('✔', `Portfolio Item #${id} Removed`);
232
+ blank();
233
+ });
234
+ }
235
+
236
+ module.exports = { register };
@@ -2,14 +2,20 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const { requireSDK, getEthers, isDryRun, normalizeError } = require('../sdk');
5
- const { success, fail, header, keyValue, blank, hint, warn } = require('../output');
5
+ const { success, fail, header, keyValue, blank, hint } = require('../output');
6
6
  const { createSpinner } = require('../spinner');
7
7
  const { fmtBounty, fmtDeadline, fmtAxobotl } = require('../format');
8
- const { fetchWithTimeout } = require('../http');
9
- const { API_URL } = require('../config');
10
8
 
11
9
  const VALID_CATEGORIES = ['Writing', 'Research', 'Code', 'Creative', 'Data', 'Social'];
12
10
 
11
+ // ─── Spending Guardrails ────────────────────────────────────────────
12
+ // Two layers of protection for agents interacting with unaudited escrow:
13
+ // 1. Percentage cap: never lock more than MAX_BOUNTY_PERCENT of USDC balance (default 20%)
14
+ // 2. Absolute cap: never post more than MAX_BOUNTY_USDC in a single task (default $50)
15
+ // Both can be overridden via env vars or --force flag for experienced users.
16
+ const DEFAULT_MAX_BOUNTY_PERCENT = parseFloat(process.env.MAX_BOUNTY_PERCENT || '20');
17
+ const DEFAULT_MAX_BOUNTY_USDC = parseFloat(process.env.MAX_BOUNTY_USDC || '50');
18
+
13
19
  function register(program) {
14
20
  program
15
21
  .command('post')
@@ -18,7 +24,7 @@ function register(program) {
18
24
  .requiredOption('--bounty <amount>', 'Bounty in USDC (e.g. 10)')
19
25
  .option('--category <cat>', 'Category: Writing, Research, Code, Creative, Data, Social', 'Code')
20
26
  .option('--deadline <dur>', 'Deadline: 7d, 24h, 30m (default: 7d)', '7d')
21
- .option('--skip-duplicate-check', 'Skip the duplicate task check')
27
+ .option('--force', 'Bypass spending guardrails (percentage + absolute cap)')
22
28
  .action(async (opts) => {
23
29
  try {
24
30
  await run(opts);
@@ -82,38 +88,6 @@ async function run(opts) {
82
88
  const ethers = getEthers();
83
89
  const address = sdk.address;
84
90
 
85
- // ── Duplicate detection ─────────────────────────────────────────────
86
- if (!opts.skipDuplicateCheck) {
87
- try {
88
- const resp = await fetchWithTimeout(`${API_URL}/tasks?poster=${address}&status=Open&limit=50`);
89
- if (resp.ok) {
90
- const data = await resp.json();
91
- const tasks = data.tasks || [];
92
- const descNorm = opts.description.trim().toLowerCase();
93
- const dupes = tasks.filter(t =>
94
- t.description && t.description.trim().toLowerCase() === descNorm
95
- );
96
- if (dupes.length > 0) {
97
- const dupeIds = dupes.map(t => `#${t.chain_task_id}`).join(', ');
98
- fail(`Duplicate detected — you already have an open task with this exact description (${dupeIds})`, {
99
- suggestion: 'Use --skip-duplicate-check to post anyway, or cancel the existing task first.',
100
- });
101
- }
102
- // Also warn if posting same category with similar bounty (likely retry)
103
- const sameCategory = tasks.filter(t =>
104
- t.category === opts.category &&
105
- Math.abs(Number(t.bounty_amount) - bountyNum) < 0.01
106
- );
107
- if (sameCategory.length >= 3) {
108
- warn(`You already have ${sameCategory.length} open "${opts.category}" tasks with similar bounties. Use --skip-duplicate-check to post anyway.`);
109
- fail('Too many similar open tasks — likely a retry loop', {
110
- suggestion: `Check existing tasks: 0xwork status`,
111
- });
112
- }
113
- }
114
- } catch { /* API check failed — continue anyway */ }
115
- }
116
-
117
91
  // Pre-flight checks
118
92
  const s1 = createSpinner('Checking balances…');
119
93
  s1.start();
@@ -125,6 +99,28 @@ async function run(opts) {
125
99
  fail(`Need ${fmtBounty(bountyNum)} USDC`, { suggestion: `Have: ${fmtBounty(ethers.formatUnits(usdcBalance, 6))}. Send USDC on Base to ${address}` });
126
100
  }
127
101
 
102
+ // ── Spending guardrails ──
103
+ if (!opts.force) {
104
+ const balanceNum = parseFloat(ethers.formatUnits(usdcBalance, 6));
105
+
106
+ // 1. Absolute cap
107
+ if (bountyNum > DEFAULT_MAX_BOUNTY_USDC) {
108
+ s1.fail('Bounty exceeds safety cap');
109
+ fail(`Bounty $${bountyNum} exceeds max $${DEFAULT_MAX_BOUNTY_USDC} per task`, {
110
+ suggestion: `Set MAX_BOUNTY_USDC in .env to raise the limit, or use --force to bypass.`,
111
+ });
112
+ }
113
+
114
+ // 2. Percentage cap
115
+ const pctUsed = (bountyNum / balanceNum) * 100;
116
+ if (pctUsed > DEFAULT_MAX_BOUNTY_PERCENT) {
117
+ s1.fail('Bounty exceeds balance percentage cap');
118
+ fail(`Bounty $${bountyNum} is ${pctUsed.toFixed(1)}% of your $${balanceNum.toFixed(2)} balance (max ${DEFAULT_MAX_BOUNTY_PERCENT}%)`, {
119
+ suggestion: `Set MAX_BOUNTY_PERCENT in .env to raise the limit, or use --force to bypass.`,
120
+ });
121
+ }
122
+ }
123
+
128
124
  const posterStake = await sdk.taskPool.calculatePosterStake(bountyAmount);
129
125
  if (posterStake > 0n) {
130
126
  const axobotlBalance = await sdk.axobotl.balanceOf(address);
@@ -156,21 +152,6 @@ async function run(opts) {
156
152
 
157
153
  s2.succeed('Posted');
158
154
 
159
- // ── Post-action verification ──────────────────────────────────────
160
- // Fetch the task back from the API to confirm it's live
161
- let verified = false;
162
- if (result.taskId != null) {
163
- try {
164
- const resp = await fetchWithTimeout(`${API_URL}/tasks/${result.taskId}`);
165
- if (resp.ok) {
166
- const data = await resp.json();
167
- if (data.task && data.task.status === 'Open') {
168
- verified = true;
169
- }
170
- }
171
- } catch { /* verification failed — still report success from chain */ }
172
- }
173
-
174
155
  success({
175
156
  command: 'post',
176
157
  dryRun: false,
@@ -183,18 +164,14 @@ async function run(opts) {
183
164
  deadlineHuman: new Date(deadlineSecs * 1000).toISOString(),
184
165
  posterStake: posterStake > 0n ? ethers.formatUnits(posterStake, 18) : '0',
185
166
  address,
186
- verified,
187
167
  message: `Posted task #${result.taskId} — ${fmtBounty(bountyNum)} bounty`,
188
168
  }, () => {
189
169
  header('✔', `Task #${result.taskId} Posted`);
190
- keyValue('Task ID', chalk.white.bold(`#${result.taskId}`));
191
170
  keyValue('Bounty', chalk.green.bold(`${fmtBounty(bountyNum)} USDC`));
192
171
  keyValue('Category', opts.category);
193
172
  keyValue('Deadline', fmtDeadline(deadlineSecs, true));
194
173
  if (posterStake > 0n) keyValue('Poster stake', `${fmtAxobotl(ethers.formatUnits(posterStake, 18))} AXOBOTL`);
195
- keyValue('Status', verified ? chalk.green('✓ Verified on-chain & API') : chalk.yellow('⚠ On-chain confirmed, API sync pending'));
196
174
  blank();
197
- hint(`View: 0xwork task ${result.taskId}`);
198
175
  hint(`tx: ${result.txHash}`);
199
176
  blank();
200
177
  });