cyberdyne-mcp 0.6.1 → 0.6.3

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/README.md CHANGED
@@ -39,6 +39,34 @@ fund (`get_deposit_address` → send USDC on Base → `deposit`) and `post_task`
39
39
  > agent-side action — onboard, fund, post, assign, authorize, review, close — is
40
40
  > headless.
41
41
 
42
+ ## CLI
43
+
44
+ Beyond `onboard`/`login`, the package ships Bankr-style convenience subcommands —
45
+ each runs once, prints a summary, and exits (no MCP needed). They use the wallet +
46
+ `cyb_` key saved by `onboard`.
47
+
48
+ ```bash
49
+ npx -y cyberdyne-mcp onboard # generate a wallet + mint your cyb_ key (run this first)
50
+ npx -y cyberdyne-mcp treasury # balance + where to send USDC (alias: balance, fees)
51
+ npx -y cyberdyne-mcp post --title "Like our launch tweet" --token BNKR --reward 100 --quantity 1
52
+ npx -y cyberdyne-mcp tasks # list your posted tasks + status
53
+ ```
54
+
55
+ | Command | Usage | What it does |
56
+ |---|---|---|
57
+ | `treasury` | `cyberdyne-mcp treasury` (alias `balance`, `fees`) | Like `bankr fees`. Prints balance, total funded, total spent, **and** the deposit address to send USDC to. |
58
+ | `post` | `cyberdyne-mcp post --title <t> --reward <n> [--token USDC\|BNKR\|GITLAWB] [--quantity <n>] [--category <c>] [--action follow\|retweet\|reply\|quote\|original-post] [--url <x.com/…>] [--rail pool\|custodial]` | Like `bankr launch`. Opens a task. On the **pool** rail (default for BNKR/GITLAWB or `--quantity>1`) it autonomously signs the budget, pays the deploy fee from your wallet, and authorizes — printing each stage and the final task id + `escrow_status`. On the custodial rail it just prints the posted task. |
59
+ | `tasks` | `cyberdyne-mcp tasks` | Lists your own posted tasks: id, title, token, quantity, filled/remaining, status. |
60
+
61
+ Flags accept both `--flag value` and `--flag=value`. `--title` and `--reward` (per
62
+ unit, in the pay token) are required for `post`; everything else has a default
63
+ (`--token USDC`, `--quantity 1`, `--category social`). The pool rail needs the saved
64
+ signing wallet — if none is present, `post` tells you to run `onboard` first.
65
+
66
+ > The human **submit-proof** step still happens in the app (human-only). After a
67
+ > pool launch, humans claim + submit FCFS; review each submission (via the
68
+ > `review_submission` MCP tool) to capture a unit.
69
+
42
70
  ## Configuration (environment)
43
71
 
44
72
  stdio MCP servers take their credentials from the environment. Set:
package/dist/cli.js ADDED
@@ -0,0 +1,226 @@
1
+ /**
2
+ * Bankr-style convenience CLI for CYBERDYNE — `treasury` / `post` / `tasks`.
3
+ *
4
+ * These are ADDITIONAL command-line entry points (not MCP tools). They run, print
5
+ * a human-readable summary to stderr, and exit — exactly like `onboard`/`login`.
6
+ * They mirror the Bankr CLI UX (`bankr fees`, `bankr launch`): a single command
7
+ * that does the full thing autonomously using the saved `cyb_` key + wallet.
8
+ *
9
+ * treasury (alias balance, fees) — your balance + where to send USDC (bankr fees)
10
+ * post — open a task; on the pool rail, sign + pay +
11
+ * authorize in one shot (bankr launch)
12
+ * tasks — list your own posted tasks with status
13
+ *
14
+ * Networking reuses CyberdyneClient (the saved Bearer key); pool signing/fee
15
+ * payment reuses src/evm-signer.ts — no signing logic is reinvented here.
16
+ */
17
+ import { CyberdyneClient, readConfig, ApiError, MissingTokenError } from "./client.js";
18
+ // ── tiny argv parser ───────────────────────────────────────────────────────
19
+ // Supports `--flag value` and `--flag=value`. Bare `--flag` (no value) ⇒ "true".
20
+ export function parseFlags(argv) {
21
+ const out = {};
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const tok = argv[i];
24
+ if (!tok.startsWith("--"))
25
+ continue;
26
+ const body = tok.slice(2);
27
+ const eq = body.indexOf("=");
28
+ if (eq >= 0) {
29
+ out[body.slice(0, eq)] = body.slice(eq + 1);
30
+ continue;
31
+ }
32
+ const next = argv[i + 1];
33
+ if (next !== undefined && !next.startsWith("--")) {
34
+ out[body] = next;
35
+ i++;
36
+ }
37
+ else {
38
+ out[body] = "true";
39
+ }
40
+ }
41
+ return out;
42
+ }
43
+ function client() {
44
+ return new CyberdyneClient(readConfig());
45
+ }
46
+ function hasKey() {
47
+ return !!readConfig().token;
48
+ }
49
+ const NO_KEY = "No CYBERDYNE key saved. Run: npx -y cyberdyne-mcp onboard";
50
+ /** Format a USD-ish numeric value (handles string/number/null) to 2dp. */
51
+ function usd(v) {
52
+ const n = typeof v === "number" ? v : Number(v);
53
+ return Number.isFinite(n) ? `$${n.toFixed(2)}` : "—";
54
+ }
55
+ function fail(msg) {
56
+ console.error(`✗ ${msg}`);
57
+ process.exit(1);
58
+ }
59
+ /** Map a thrown client error to a clean one-line message. */
60
+ function describe(e) {
61
+ if (e instanceof MissingTokenError)
62
+ return NO_KEY;
63
+ if (e instanceof ApiError)
64
+ return e.message;
65
+ return e instanceof Error ? e.message : String(e);
66
+ }
67
+ // ── treasury (alias: balance, fees) ─────────────────────────────────────────
68
+ // `bankr fees` equivalent: your balance + the deposit address to fund it.
69
+ export async function runTreasury() {
70
+ if (!hasKey())
71
+ fail(NO_KEY);
72
+ const c = client();
73
+ try {
74
+ const { treasury } = await c.rest("GET", "/api/treasury");
75
+ // The deposit address only resolves on the live rail; treat a 403/503 as
76
+ // "not available yet" rather than failing the whole command.
77
+ let deposit = null;
78
+ try {
79
+ deposit = await c.rest("GET", "/api/treasury/deposit");
80
+ }
81
+ catch {
82
+ deposit = null;
83
+ }
84
+ const lines = ["CYBERDYNE treasury"];
85
+ if (!treasury) {
86
+ lines.push(" balance : — (no treasury yet — fund it to create one)");
87
+ }
88
+ else {
89
+ lines.push(` balance : ${usd(treasury.balance_usd)}`);
90
+ lines.push(` total funded : ${usd(treasury.total_funded ?? treasury.total_funded_usd)}`);
91
+ lines.push(` total spent : ${usd(treasury.total_spent ?? treasury.total_spent_usd)}`);
92
+ }
93
+ if (deposit?.deposit_address) {
94
+ lines.push("");
95
+ lines.push(` deposit USDC to: ${deposit.deposit_address}`);
96
+ lines.push(` chain : Base (chain id ${deposit.chain_id ?? 8453})`);
97
+ lines.push(" → send USDC from your verified wallet, then credit it with the `deposit` MCP tool.");
98
+ }
99
+ else {
100
+ lines.push("");
101
+ lines.push(" deposit address: not available (live deposits not enabled on this rail yet).");
102
+ }
103
+ console.error(lines.join("\n"));
104
+ process.exit(0);
105
+ }
106
+ catch (e) {
107
+ fail(describe(e));
108
+ }
109
+ }
110
+ // ── post (bankr launch) ──────────────────────────────────────────────────────
111
+ // Open a task. Per-unit `--reward` × `--quantity` = the budget. On the pool rail
112
+ // (BNKR/GITLAWB or quantity>1) the response carries authIntent + deployFee: sign
113
+ // the budget + pay the fee + authorize, all from the saved wallet, autonomously.
114
+ const PAY_TOKENS = new Set(["USDC", "BNKR", "GITLAWB"]);
115
+ export async function runPost(argv) {
116
+ if (!hasKey())
117
+ fail(NO_KEY);
118
+ const f = parseFlags(argv);
119
+ const title = f.title?.trim();
120
+ if (!title)
121
+ fail("--title is required");
122
+ const rewardPerUnit = Number(f.reward);
123
+ if (!Number.isFinite(rewardPerUnit) || rewardPerUnit <= 0)
124
+ fail("--reward <n> is required (per-unit, in the pay token)");
125
+ const token = (f.token ?? "USDC").toUpperCase();
126
+ if (!PAY_TOKENS.has(token))
127
+ fail(`--token must be one of USDC, BNKR, GITLAWB (got ${token})`);
128
+ const quantity = f.quantity != null ? Math.trunc(Number(f.quantity)) : 1;
129
+ if (!Number.isFinite(quantity) || quantity < 1)
130
+ fail("--quantity must be a positive integer");
131
+ const category = (f.category ?? "social").trim();
132
+ const action = f.action?.trim();
133
+ const url = f.url?.trim();
134
+ // Rail: default pool when token is a real ecosystem token (BNKR/GITLAWB) or it's
135
+ // a multi-unit bounty; else custodial single-hold. `--rail` overrides.
136
+ const railFlag = f.rail?.trim().toLowerCase();
137
+ const rail = railFlag === "pool" || railFlag === "custodial"
138
+ ? railFlag
139
+ : token === "BNKR" || token === "GITLAWB" || quantity > 1
140
+ ? "pool"
141
+ : "custodial";
142
+ // reward_usd is the TOTAL budget (= per-unit × quantity). For non-USDC tokens this
143
+ // figure is the TOKEN amount (the platform settles in-token on the pool rail).
144
+ const reward_usd = Number((rewardPerUnit * quantity).toFixed(6));
145
+ const body = {
146
+ title,
147
+ category,
148
+ reward_usd,
149
+ quantity,
150
+ pay_token: token,
151
+ rail,
152
+ };
153
+ if (category === "social" && action)
154
+ body.social_action = action;
155
+ if (category === "social" && url)
156
+ body.social_target_url = url;
157
+ const c = client();
158
+ try {
159
+ console.error(`→ posting "${title}" (${rewardPerUnit} ${token} × ${quantity} = ${reward_usd} ${token}, ${rail} rail)…`);
160
+ const res = await c.rest("POST", "/api/tasks", { body });
161
+ const taskId = res.task?.id;
162
+ console.error(` ✓ posted — task ${taskId}`);
163
+ // Custodial rail (no authIntent): nothing else to do; the hold opens later.
164
+ if (!res.authIntent || !res.deployFee) {
165
+ console.error(`\n✓ Task ${taskId} is open (custodial rail). Next: assign a human + authorize, then release on a valid proof.`);
166
+ process.exit(0);
167
+ }
168
+ // POOL rail — the autonomous `bankr launch` path. Sign the budget with the saved
169
+ // wallet, pay the separate deploy fee, then authorize. Reuses evm-signer (the
170
+ // exact logic the authorize_task MCP tool uses).
171
+ const { hasEvmKey, signAuthCapture, payDeployFee } = await import("./evm-signer.js");
172
+ if (!hasEvmKey()) {
173
+ fail("pool rail needs a signing wallet, but none is saved. Run `npx -y cyberdyne-mcp onboard` " +
174
+ `(the task ${taskId} is posted but not yet funded).`);
175
+ }
176
+ const requirements = res.authIntent.requirements ?? res.authIntent;
177
+ console.error("→ signing the budget authorization…");
178
+ const signedPayment = await signAuthCapture(requirements);
179
+ const fee = res.deployFee;
180
+ console.error(`→ paying the deploy fee (${usd(fee.usd)} in ${fee.token}) from your wallet…`);
181
+ const feeTx = await payDeployFee({ amountUsd: fee.usd, recipient: fee.recipient, token: fee.token });
182
+ console.error(` ✓ fee paid — ${feeTx}`);
183
+ console.error("→ freezing the budget (authorize)…");
184
+ const authed = await c.rest("POST", `/api/tasks/${taskId}/authorize`, { body: { signedPayment, fee_tx_hash: feeTx } });
185
+ const escrow = authed.task?.escrow_status ?? "held";
186
+ console.error(`\n✓ Launched. task ${taskId} — escrow_status: ${escrow}. ` +
187
+ "Humans can now claim + submit FCFS; review each submission to capture a unit.");
188
+ process.exit(0);
189
+ }
190
+ catch (e) {
191
+ fail(describe(e));
192
+ }
193
+ }
194
+ // ── tasks ─────────────────────────────────────────────────────────────────
195
+ // List the agent's own posted tasks (GET /api/tasks?mine=posted — works with the
196
+ // agent key). Short: id, title, token, qty, filled/remaining, status.
197
+ export async function runTasks() {
198
+ if (!hasKey())
199
+ fail(NO_KEY);
200
+ const c = client();
201
+ try {
202
+ const { tasks } = await c.rest("GET", "/api/tasks", {
203
+ query: { mine: "posted", limit: 50 },
204
+ });
205
+ if (!tasks || tasks.length === 0) {
206
+ console.error("No posted tasks yet. Post one: npx -y cyberdyne-mcp post --title \"…\" --reward 1");
207
+ process.exit(0);
208
+ }
209
+ const lines = [`Your posted tasks (${tasks.length}):`];
210
+ for (const t of tasks) {
211
+ const qty = Number(t.quantity ?? 1) || 1;
212
+ const filled = Number(t.filled_count ?? t.captured_count ?? 0) || 0;
213
+ const remaining = Math.max(qty - filled, 0);
214
+ const token = String(t.pay_token ?? "USDC");
215
+ const status = String(t.escrow_status ? `${t.status}/${t.escrow_status}` : t.status ?? "—");
216
+ const id = String(t.id ?? "—");
217
+ const title = String(t.title ?? "").slice(0, 40);
218
+ lines.push(` ${id} ${title.padEnd(40)} ${token.padEnd(7)} qty ${qty} filled ${filled}/${qty} (rem ${remaining}) ${status}`);
219
+ }
220
+ console.error(lines.join("\n"));
221
+ process.exit(0);
222
+ }
223
+ catch (e) {
224
+ fail(describe(e));
225
+ }
226
+ }
package/dist/server.js CHANGED
@@ -13,13 +13,13 @@
13
13
  * get_deposit_address — GET /api/treasury/deposit → where to send real USDC (live)
14
14
  * deposit — POST /api/treasury/deposit → credit treasury from a real USDC tx
15
15
  * withdraw_treasury — POST /api/treasury/withdraw → pull unspent treasury back to your wallet (live)
16
- * post_task — POST /api/tasks → open a task
17
- * assign_task — POST /api/tasks/[id]/assign pick a human (→ authIntent)
18
- * authorize_task — POST /api/tasks/[id]/authorize → open the escrow hold
16
+ * post_task — POST /api/tasks → open an FCFS pool bounty
17
+ * authorize_task — POST /api/tasks/[id]/authorize sign budget + pay fee + freeze
19
18
  * get_task — GET /api/tasks/[id] → status + submissions/claims
20
- * release_payment — POST /api/tasks/[id]/release direct-hire: capture (pay) or reject
21
- * review_submission — POST /api/submissions/[id]/review pool/FCFS: approve/reject one submission
22
- * close_task — POST /api/tasks/[id]/close close a (multi-unit) bounty
19
+ * review_submission — POST /api/submissions/[id]/review approve (pay one unit) / reject (reopen)
20
+ * close_task — POST /api/tasks/[id]/close refund the unfilled budget
21
+ * assign_task — POST /api/tasks/[id]/assign DEPRECATED (testnet-only; no direct hire)
22
+ * release_payment — POST /api/tasks/[id]/release → DEPRECATED (testnet-only; use review_submission)
23
23
  *
24
24
  * Auth: every networked tool sends the agent's `cyb_…` key. The REST routes take
25
25
  * it as `Authorization: Bearer …`; search_humans goes through the a2a JSON-RPC
@@ -27,26 +27,24 @@
27
27
  * `identity_token`.
28
28
  *
29
29
  * The HUMAN submit-proof step happens in the app/UI (human-only — agents cannot
30
- * submit on a human's behalf). There are TWO settlement flows:
30
+ * submit on a human's behalf). There is ONE settlement model for real tokens:
31
31
  *
32
- * FLOW A DIRECT HIRE (the path that works TODAY on the live custodial USDC
33
- * rail: real deposit escrow withdraw on Base mainnet). You pick one human:
34
- * get_deposit_address send USDC deposit (fund the treasury)
35
- * post_task search_humans assign_task (→ authIntent)
36
- * authorize_task (open the escrow hold)
37
- * poll get_task until a submission is pending
38
- * → release_payment (approvecapture/pay; else reject refund)
32
+ * FCFS POOL BOUNTY (non-custodial pool escrow). There is NO direct hire and NO
33
+ * agent-picks-human. EVERY task is an open bounty: the agent freezes a budget once,
34
+ * ANY eligible human submits first-come-first-served, and the agent approves/rejects
35
+ * each submission approved pays one unit in-token, rejected reopens the slot, and
36
+ * any unfilled budget is refunded on close.
37
+ * get_deposit_address send USDC deposit (optional, fund treasury)
38
+ * → post_task({ ..., quantity }) returns { task, authIntent, deployFee }
39
+ * → authorize_task({ task_id, auth_intent, deploy_fee }) (sign budget + pay fee + freeze)
40
+ * → humans submit FCFS → poll get_task
41
+ * → review_submission per pending submission (approve → pay one unit;
42
+ * reject → the slot reopens)
43
+ * → close_task to refund the unfilled budget.
39
44
  *
40
- * FLOW B POOL / FCFS BOUNTY (non-custodial pool escrow). Post a multi-unit
41
- * bounty, freeze the whole budget once, and let any eligible human claim+submit
42
- * first-come-first-served; you approve each unit. This rail is BUILT but GATED
43
- * OFF today (server env ESCROW_POOL is not enabled), pending certification — so
44
- * real-money non-custodial pool payouts are NOT live yet. When the server
45
- * enables it, post_task returns an `authIntent` + a separate `deployFee`:
46
- * post_task (quantity>1) → authorize_task (sign the budget + pay the deploy fee)
47
- * → humans claim+submit FCFS → poll get_task
48
- * → review_submission per pending submission (approve → capture one unit;
49
- * reject → the slot reopens) → close_task to refund unfilled units.
45
+ * DEPRECATED (testnet-only): assign_task + release_payment were the old direct-hire
46
+ * path. There is no direct hire for real tokens a real-token assign/release returns
47
+ * 409 direct_hire_retired. The tools remain only for the testnet/non-real demo rail.
50
48
  *
51
49
  * Config comes from the environment (see src/client.ts):
52
50
  * CYBERDYNE_API_URL default "https://app.cyberdyne-os.xyz"
@@ -113,6 +111,23 @@ if (process.argv[2] === "login") {
113
111
  "Now run: claude mcp add cyberdyne -- npx -y cyberdyne-mcp");
114
112
  process.exit(0);
115
113
  }
114
+ // Bankr-style convenience CLI subcommands (additional entry points, not MCP tools).
115
+ // Each runs autonomously with the saved key/wallet, prints a summary, and exits.
116
+ // treasury (alias balance, fees) — balance + deposit address (like `bankr fees`)
117
+ // post — open a task; pool rail auto sign+pay+authorize (like `bankr launch`)
118
+ // tasks — list your own posted tasks with status
119
+ if (["treasury", "balance", "fees"].includes(process.argv[2] ?? "")) {
120
+ const { runTreasury } = await import("./cli.js");
121
+ await runTreasury();
122
+ }
123
+ if (process.argv[2] === "post") {
124
+ const { runPost } = await import("./cli.js");
125
+ await runPost(process.argv.slice(3));
126
+ }
127
+ if (process.argv[2] === "tasks") {
128
+ const { runTasks } = await import("./cli.js");
129
+ await runTasks();
130
+ }
116
131
  const config = readConfig();
117
132
  const client = new CyberdyneClient(config);
118
133
  // ---- Result helpers -------------------------------------------------------
@@ -137,7 +152,7 @@ async function guard(fn) {
137
152
  }
138
153
  }
139
154
  // ---- Server ---------------------------------------------------------------
140
- const server = new McpServer({ name: "cyberdyne", version: "0.6.1" });
155
+ const server = new McpServer({ name: "cyberdyne", version: "0.6.3" });
141
156
  server.tool("list_categories", "List the kinds of real-world work CYBERDYNE humans can do. Static (no network). Use this to learn the valid `category` values before posting a task.", {}, async () => json(Object.entries(CATEGORIES).map(([id, blurb]) => ({ id, blurb }))));
142
157
  server.tool("onboard", "BOOTSTRAP (works WITHOUT an existing key — the one tool that self-onboards). Zero-browser: generates a fresh wallet if you don't have one, signs in to CYBERDYNE with it (SIWE), mints your `cyb_` agent API key, and saves both to ~/.cyberdyne/config.json (0600) so every other tool here authenticates automatically. No web dashboard, no env vars. Returns your wallet address, the cyb_ key (shown once), and the next steps (fund via get_deposit_address+deposit → post_task → assign → authorize → release). The same generated wallet auto-signs pool budgets. Idempotent-ish: re-running with a saved wallet reuses it and mints a fresh key.", {}, async () => guard(async () => {
143
158
  const r = await onboard();
@@ -172,7 +187,7 @@ server.tool("deposit", "Credit your treasury from a REAL on-chain USDC deposit (
172
187
  .describe("The Base tx hash of your USDC transfer to the deposit address."),
173
188
  }, async ({ tx_hash }) => guard(() => client.rest("POST", "/api/treasury/deposit", { body: { tx_hash } })));
174
189
  server.tool("withdraw_treasury", "Recover UNSPENT treasury to your wallet (live rail): pull USDC out of your treasury back to your own VERIFIED deposit wallet on Base — no browser. Available balance is your treasury balance net of any open escrow holds. Funds can ONLY go to your verified wallet (no destination param), so a leaked key can't redirect them. Returns { ok, tx_hash, amount_usd, to }. 400 insufficient_treasury if the balance can't cover it; 403 withdraws_disabled on the demo rail.", { amount_usd: z.number().positive().describe("USD to withdraw from your treasury to your verified wallet.") }, async ({ amount_usd }) => guard(() => client.rest("POST", "/api/treasury/withdraw", { body: { amount_usd } })));
175
- server.tool("post_task", "Open a task on the marketplace. Funds are NOT charged at post — the escrow hold opens later at authorize_task. `reward_usd` is the total budget; with quantity>1 each unit holds reward_usd/quantity (each unit must be >= $0.01). Returns the created task (with its id). DIRECT-HIRE / custodial rail (the path live today): the platform checks the prefunded treasury can cover the budget (402 insufficient_treasury otherwise); response is { task }. POOL/FCFS rail (only when the server enables it): response also includes `authIntent` (the budget authorization to sign) and `deployFee` { usd, bps, recipient, token } (a SEPARATE non-refundable fee tx) — pass both to authorize_task.", {
190
+ server.tool("post_task", "Open an FCFS pool bounty on the marketplace. There is NO direct hire and NO agent-picks-human — every task is an open bounty: you freeze a budget, ANY eligible human submits first-come-first-served, and you approve/reject each submission. Funds are NOT charged at post — the budget is frozen later at authorize_task. `reward_usd` is the total budget; `quantity` is how many identical units (humans) it pays — each unit holds reward_usd/quantity (each unit must be >= $0.01). Returns the created task (with its id). REAL-TOKEN POOL rail (USDC/BNKR on Base): the response also includes `authIntent` (the budget authorization to sign) and `deployFee` { usd, bps, recipient, token } (a SEPARATE non-refundable fee tx) — pass BOTH to authorize_task. TESTNET/non-real token (CYOS): no on-chain freeze; response is just { task } (treasury-checked, 402 insufficient_treasury if low).", {
176
191
  title: z.string().min(2).max(160).describe("Short task title."),
177
192
  category: z.enum(TASK_CATEGORIES),
178
193
  description: z.string().max(4000).optional().describe("What you need the human to do."),
@@ -184,11 +199,11 @@ server.tool("post_task", "Open a task on the marketplace. Funds are NOT charged
184
199
  pay_token: z.enum(["USDC", "BNKR", "CYOS"]).optional().describe("Settlement token (default USDC)."),
185
200
  deadline_hours: z.number().int().positive().optional(),
186
201
  }, async (args) => guard(() => client.rest("POST", "/api/tasks", { body: args })));
187
- server.tool("assign_task", "Assign an open task to a chosen human (poster-only) and open the escrow intent. Returns `{ task, authIntent }`: on an on-chain rail `authIntent` is the auth-capture requirements the agent must sign; on the manual rail it is null. Next call authorize_task to actually open the hold.", {
202
+ server.tool("assign_task", "DEPRECATED — TESTNET/DEMO ONLY. There is NO direct hire: every real-token task is an open FCFS pool bounty, so a real-token assign returns 409 direct_hire_retired ('post with a budget; humans submit FCFS; review each submission'). Do NOT use this to hire post_task + authorize_task + review_submission is the flow. This tool remains only for the testnet/non-real (CYOS) demo rail, where it assigns an open task to a chosen human and returns `{ task, authIntent: null }`.", {
188
203
  task_id: z.string().uuid(),
189
204
  human_id: z.string().uuid().describe("The human profile id (from search_humans / get_task claims)."),
190
205
  }, async ({ task_id, human_id }) => guard(() => client.rest("POST", `/api/tasks/${task_id}/assign`, { body: { human_id } })));
191
- server.tool("authorize_task", "Open the escrow hold for a task. CUSTODIAL/MANUAL rail (the path live today): call with just { task_id } — the prefunded treasury is debited into a logical escrow hold, no signature needed. TRUSTLESS on-chain DIRECT-HIRE rail: the agent signs an auth-capture authorization — if CYBERDYNE_EVM_PRIVATE_KEY is set pass `auth_intent` (the authIntent from assign_task) and the MCP signs automatically, else pass a pre-signed `signed_payment`. POOL/FCFS rail (only when the server enables it): pass BOTH `auth_intent` (from post_task) AND `deploy_fee` (the deployFee object from post_task) — the MCP signs the budget and pays the separate 2.5% USDC / 5% other-token fee tx from its wallet, then submits both; or pass a pre-signed `signed_payment` and a pre-paid `fee_tx_hash`. Idempotent once held.", {
206
+ server.tool("authorize_task", "Freeze the bounty budget on-chain (the second step of the FCFS flow). REAL-TOKEN POOL rail: pass BOTH `auth_intent` (the authIntent from post_task) AND `deploy_fee` (the deployFee object from post_task) — with CYBERDYNE_EVM_PRIVATE_KEY set, the MCP signs the whole-budget authorization AND pays the separate 2.5% USDC / 5% other-token deploy fee tx from its wallet, then freezes the budget on the audited escrow; or pass a pre-signed `signed_payment` and a pre-paid `fee_tx_hash`. After this, any eligible human submits FCFS and you review_submission each. TESTNET/non-real (CYOS) rail: call with just { task_id } — the prefunded treasury opens a logical hold, no signature. Idempotent once frozen.", {
192
207
  task_id: z.string().uuid(),
193
208
  signed_payment: z.string().optional().describe("Pre-signed base64 auth-capture payload (external/Bankr signer)."),
194
209
  auth_intent: z.unknown().optional().describe("The authIntent from assign_task/post — required for MCP wallet auto-signing."),
@@ -220,8 +235,8 @@ server.tool("authorize_task", "Open the escrow hold for a task. CUSTODIAL/MANUAL
220
235
  },
221
236
  });
222
237
  }));
223
- server.tool("get_task", "Get the live state of a task: the task row plus the submissions and per-unit claims the agent (as poster) may see. Poll this after authorize_task until a submission with status 'pending' appears — that is the human's proof, ready for release_payment.", { task_id: z.string().uuid() }, async ({ task_id }) => guard(() => client.rest("GET", `/api/tasks/${task_id}`)));
224
- server.tool("release_payment", "DIRECT-HIRE settle (poster-only): settle a submitted proof for a single-human task (the custodial-rail path live today). approve:true CAPTURE: pay the human net of the platform fee. approve:false REJECT/REFUND the held escrow. Requires the `submission_id` to act on; if omitted, the gateway fetches the task and uses the latest pending submission (and errors if none is pending yet poll get_task first). For POOL/FCFS bounties use review_submission instead (this path captures the whole task hold, not one unit).", {
238
+ server.tool("get_task", "Get the live state of a task: the task row plus the submissions and per-unit claims the agent (as poster) may see. Poll this after authorize_task until a submission with status 'pending' appears — that is the human's proof, ready for review_submission (approve pays one unit; reject reopens the slot).", { task_id: z.string().uuid() }, async ({ task_id }) => guard(() => client.rest("GET", `/api/tasks/${task_id}`)));
239
+ server.tool("release_payment", "DEPRECATED TESTNET/DEMO ONLY. There is NO direct hire: a real-token task is an FCFS pool bounty whose units settle ONE per approved submission, so a real-token release returns 409 direct_hire_retired use review_submission instead (approve pays one unit, reject reopens the slot). This tool remains only for the testnet/non-real (CYOS) demo rail, where it settles a submitted proof for a single-human task: approve:true pay net of fee, approve:false reject/refund. `submission_id` auto-resolves to the latest pending one if omitted.", {
225
240
  task_id: z.string().uuid(),
226
241
  approve: z.boolean().describe("true = proof meets criteria → pay; false = reject/refund."),
227
242
  submission_id: z
@@ -252,7 +267,7 @@ server.tool("release_payment", "DIRECT-HIRE settle (poster-only): settle a submi
252
267
  },
253
268
  });
254
269
  }));
255
- server.tool("review_submission", "POOL / FCFS settle (poster-only): approve or reject ONE submission on a pool bounty. approve:true → CAPTURE one unit from the frozen pool budget to the human and consume a slot; approve:false → reject (the slot reopens for the next submitter — no spot-blocking). For single-human direct-hire tasks use release_payment instead. Poll get_task for pending submissions. NOTE: the pool/FCFS rail is gated off on the server until certification this tool acts on pool tasks once the operator enables that rail.", {
270
+ server.tool("review_submission", "THE settle tool (poster-only): approve or reject ONE submission on your FCFS pool bounty — this is how you pay humans (there is no direct hire). approve:true → CAPTURE one unit from the frozen budget to the human (full reward, in-token) and consume a slot; approve:false → reject (the slot reopens for the next submitter — no spot-blocking). Poll get_task for pending submissions and review each one. When the budget is consumed (or you're done) call close_task to refund the unfilled remainder.", {
256
271
  submission_id: z.string().uuid().describe("The pending submission to review (from get_task)."),
257
272
  approve: z.boolean().describe("true = proof meets criteria → capture one unit; false = reject (slot reopens)."),
258
273
  score: z.number().int().min(1).max(5).optional().describe("Rating of the human's work (1–5)."),
@@ -266,14 +281,14 @@ server.tool("review_submission", "POOL / FCFS settle (poster-only): approve or r
266
281
  ...(reject_reason ? { reject_reason } : {}),
267
282
  },
268
283
  })));
269
- server.tool("close_task", "Close a (multi-unit) bounty (poster-only): refund every still-held unit to the agent, mark unclaimed units done, and stop further claims. Idempotent on an already-closed task.", { task_id: z.string().uuid() }, async ({ task_id }) => guard(() => client.rest("POST", `/api/tasks/${task_id}/close`)));
284
+ server.tool("close_task", "Close your FCFS pool bounty (poster-only): refund the unfilled budget back to your wallet on-chain (the uncaptured remainder = unfilled units × per-unit reward) and stop further submissions. The deploy fee is non-refundable. Idempotent on an already-closed task.", { task_id: z.string().uuid() }, async ({ task_id }) => guard(() => client.rest("POST", `/api/tasks/${task_id}/close`)));
270
285
  // ---- Self-onboarding prompt -----------------------------------------------
271
286
  // Surfaces as /mcp__cyberdyne__quickstart — the agent (or user) runs it once to
272
287
  // learn the end-to-end campaign flow without reading docs. This is the "skill"
273
288
  // shipped inside the MCP: guidance travels with the tools.
274
289
  server.registerPrompt("quickstart", {
275
290
  title: "CYBERDYNE quickstart",
276
- description: "How to fund, post a task, and pay humans end-to-end — both the direct-hire and pool/FCFS flows.",
291
+ description: "How to fund, post an FCFS pool bounty, and pay verified humans end-to-end.",
277
292
  }, () => ({
278
293
  messages: [
279
294
  {
@@ -281,26 +296,20 @@ server.registerPrompt("quickstart", {
281
296
  content: {
282
297
  type: "text",
283
298
  text: [
284
- "You are connected to CYBERDYNE — hire and pay verified humans for tasks AI can't do alone. The live settlement rail is REAL USDC on Base (custodial: deposit -> escrow -> withdraw). The human submit-proof step is human-only, in the app; you drive everything else.",
299
+ "You are connected to CYBERDYNE — pay verified humans for tasks AI can't do alone. There is ONE model: every task is an open FCFS pool bounty. There is NO direct hire and NO picking a human — you freeze a budget, ANY eligible human submits first-come-first-served, and you approve/reject each submission (approved = paid one unit in-token, rejected = the slot reopens). The live settlement rail is REAL tokens on Base (non-custodial freeze-at-deploy). The human submit-proof step is human-only, in the app; you drive everything else.",
285
300
  "",
286
- "FUND (real money, custodial rail):",
301
+ "FUND (optional — the pool freezes from your wallet at deploy, but a treasury can cover fees):",
287
302
  "1. get_deposit_address -> the platform deposit address on Base.",
288
- "2. Send USDC to it FROM your own verified wallet (the one you signed in with).",
289
- "3. deposit({ tx_hash }) -> credits your treasury by the verified amount (idempotent).",
290
- " (fund_treasury is demo/testnet only and is disabled on the live rail.)",
291
- "Check get_treasury anytime for your balance.",
303
+ "2. Send USDC to it FROM your own verified wallet, then deposit({ tx_hash }) -> credits your treasury (idempotent).",
304
+ " (fund_treasury is demo/testnet only and is disabled on the live rail.) Check get_treasury anytime.",
292
305
  "",
293
- "FLOW A - DIRECT HIRE (works today): pick one human, hold, then pay.",
294
- "4. post_task({ title, category, reward_usd, duration_min, difficulty }). Returns { task }. Use reward_usd >= 0.50 so the 2.5% fee is visible; each unit must be >= $0.01.",
295
- "5. search_humans({ skills, min_reputation }) -> assign_task({ task_id, human_id }) -> authorize_task({ task_id }) to open the escrow hold (custodial rail = no signature; just task_id).",
296
- "6. Poll get_task until a submission is pending (the human's proof).",
297
- "7. release_payment({ task_id, approve: true, score }) -> CAPTURE: net USDC to the human, the 2.5% fee to the protocol wallet. approve:false rejects and refunds the hold.",
306
+ "POST + PAY (the single FCFS flow):",
307
+ "3. post_task({ title, category, reward_usd, quantity, duration_min, difficulty }) -> returns { task, authIntent, deployFee }. reward_usd is the TOTAL budget; quantity is how many humans it pays (each unit must be >= $0.01). authIntent is the whole-budget authorization; deployFee is a SEPARATE non-refundable fee tx (2.5% USDC / 5% other token).",
308
+ "4. authorize_task({ task_id, auth_intent, deploy_fee }) -> with CYBERDYNE_EVM_PRIVATE_KEY set, the MCP signs the budget AND pays the deploy fee, then FREEZES the whole budget on the audited escrow (or pass pre-made signed_payment + fee_tx_hash).",
309
+ "5. Any eligible human submits FCFS. Poll get_task; for EACH pending submission call review_submission({ submission_id, approve, score }) -> approve captures one unit (full reward to the human, in-token); reject reopens the slot for the next submitter.",
310
+ "6. close_task({ task_id }) -> refunds the unfilled budget back to your wallet (the deploy fee is non-refundable).",
298
311
  "",
299
- "FLOW B - POOL / FCFS BOUNTY: one frozen budget, many humans claim+submit first-come-first-served. NOTE: this non-custodial pool rail is BUILT but GATED OFF on the server today (pending certification) - real-money pool payouts are not live yet. When the operator enables it:",
300
- "8. post_task({ ..., quantity: N }) -> returns { task, authIntent, deployFee }. authIntent is the budget authorization; deployFee is a SEPARATE non-refundable fee tx (2.5% USDC / 5% other token).",
301
- "9. authorize_task({ task_id, auth_intent, deploy_fee }) -> with CYBERDYNE_EVM_PRIVATE_KEY set, the MCP signs the budget AND pays the deploy fee, then freezes the whole budget (or pass pre-made signed_payment + fee_tx_hash).",
302
- "10. Humans claim+submit FCFS. Poll get_task; for each pending submission call review_submission({ submission_id, approve, score }) -> approve captures one unit; reject reopens the slot.",
303
- "11. close_task({ task_id }) -> refund any still-unfilled units (the deploy fee is non-refundable).",
312
+ "DEPRECATED: assign_task + release_payment were the old direct-hire path and now return 409 direct_hire_retired for real tokens use post_task + authorize_task + review_submission instead. They remain only for the testnet/non-real (CYOS) demo rail.",
304
313
  "",
305
314
  "Every payout and fee on the live rail is a real on-chain transaction.",
306
315
  ].join("\n"),
@@ -313,4 +322,5 @@ const transport = new StdioServerTransport();
313
322
  await server.connect(transport);
314
323
  console.error(`CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
315
324
  (config.token ? "" : " (no key — run `npx cyberdyne-mcp onboard` to self-generate a wallet + key, or `login cyb_…`, or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
316
- ". Tools (15): onboard, list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, withdraw_treasury, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task.");
325
+ ". Tools (15): onboard, list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, withdraw_treasury, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task." +
326
+ " CLI: onboard, login, treasury (alias balance/fees), post, tasks.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberdyne-mcp",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/cli.ts ADDED
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Bankr-style convenience CLI for CYBERDYNE — `treasury` / `post` / `tasks`.
3
+ *
4
+ * These are ADDITIONAL command-line entry points (not MCP tools). They run, print
5
+ * a human-readable summary to stderr, and exit — exactly like `onboard`/`login`.
6
+ * They mirror the Bankr CLI UX (`bankr fees`, `bankr launch`): a single command
7
+ * that does the full thing autonomously using the saved `cyb_` key + wallet.
8
+ *
9
+ * treasury (alias balance, fees) — your balance + where to send USDC (bankr fees)
10
+ * post — open a task; on the pool rail, sign + pay +
11
+ * authorize in one shot (bankr launch)
12
+ * tasks — list your own posted tasks with status
13
+ *
14
+ * Networking reuses CyberdyneClient (the saved Bearer key); pool signing/fee
15
+ * payment reuses src/evm-signer.ts — no signing logic is reinvented here.
16
+ */
17
+ import { CyberdyneClient, readConfig, ApiError, MissingTokenError } from "./client.js";
18
+
19
+ // ── tiny argv parser ───────────────────────────────────────────────────────
20
+ // Supports `--flag value` and `--flag=value`. Bare `--flag` (no value) ⇒ "true".
21
+ export function parseFlags(argv: string[]): Record<string, string> {
22
+ const out: Record<string, string> = {};
23
+ for (let i = 0; i < argv.length; i++) {
24
+ const tok = argv[i];
25
+ if (!tok.startsWith("--")) continue;
26
+ const body = tok.slice(2);
27
+ const eq = body.indexOf("=");
28
+ if (eq >= 0) {
29
+ out[body.slice(0, eq)] = body.slice(eq + 1);
30
+ continue;
31
+ }
32
+ const next = argv[i + 1];
33
+ if (next !== undefined && !next.startsWith("--")) {
34
+ out[body] = next;
35
+ i++;
36
+ } else {
37
+ out[body] = "true";
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ function client(): CyberdyneClient {
44
+ return new CyberdyneClient(readConfig());
45
+ }
46
+
47
+ function hasKey(): boolean {
48
+ return !!readConfig().token;
49
+ }
50
+
51
+ const NO_KEY = "No CYBERDYNE key saved. Run: npx -y cyberdyne-mcp onboard";
52
+
53
+ /** Format a USD-ish numeric value (handles string/number/null) to 2dp. */
54
+ function usd(v: unknown): string {
55
+ const n = typeof v === "number" ? v : Number(v);
56
+ return Number.isFinite(n) ? `$${n.toFixed(2)}` : "—";
57
+ }
58
+
59
+ function fail(msg: string): never {
60
+ console.error(`✗ ${msg}`);
61
+ process.exit(1);
62
+ }
63
+
64
+ /** Map a thrown client error to a clean one-line message. */
65
+ function describe(e: unknown): string {
66
+ if (e instanceof MissingTokenError) return NO_KEY;
67
+ if (e instanceof ApiError) return e.message;
68
+ return e instanceof Error ? e.message : String(e);
69
+ }
70
+
71
+ // ── treasury (alias: balance, fees) ─────────────────────────────────────────
72
+ // `bankr fees` equivalent: your balance + the deposit address to fund it.
73
+ export async function runTreasury(): Promise<void> {
74
+ if (!hasKey()) fail(NO_KEY);
75
+ const c = client();
76
+ try {
77
+ const { treasury } = await c.rest<{ treasury: Record<string, unknown> | null }>("GET", "/api/treasury");
78
+
79
+ // The deposit address only resolves on the live rail; treat a 403/503 as
80
+ // "not available yet" rather than failing the whole command.
81
+ let deposit: { deposit_address?: string; chain_id?: number; usdc_address?: string } | null = null;
82
+ try {
83
+ deposit = await c.rest("GET", "/api/treasury/deposit");
84
+ } catch {
85
+ deposit = null;
86
+ }
87
+
88
+ const lines: string[] = ["CYBERDYNE treasury"];
89
+ if (!treasury) {
90
+ lines.push(" balance : — (no treasury yet — fund it to create one)");
91
+ } else {
92
+ lines.push(` balance : ${usd(treasury.balance_usd)}`);
93
+ lines.push(` total funded : ${usd(treasury.total_funded ?? treasury.total_funded_usd)}`);
94
+ lines.push(` total spent : ${usd(treasury.total_spent ?? treasury.total_spent_usd)}`);
95
+ }
96
+ if (deposit?.deposit_address) {
97
+ lines.push("");
98
+ lines.push(` deposit USDC to: ${deposit.deposit_address}`);
99
+ lines.push(` chain : Base (chain id ${deposit.chain_id ?? 8453})`);
100
+ lines.push(" → send USDC from your verified wallet, then credit it with the `deposit` MCP tool.");
101
+ } else {
102
+ lines.push("");
103
+ lines.push(" deposit address: not available (live deposits not enabled on this rail yet).");
104
+ }
105
+ console.error(lines.join("\n"));
106
+ process.exit(0);
107
+ } catch (e) {
108
+ fail(describe(e));
109
+ }
110
+ }
111
+
112
+ // ── post (bankr launch) ──────────────────────────────────────────────────────
113
+ // Open a task. Per-unit `--reward` × `--quantity` = the budget. On the pool rail
114
+ // (BNKR/GITLAWB or quantity>1) the response carries authIntent + deployFee: sign
115
+ // the budget + pay the fee + authorize, all from the saved wallet, autonomously.
116
+ const PAY_TOKENS = new Set(["USDC", "BNKR", "GITLAWB"]);
117
+
118
+ export async function runPost(argv: string[]): Promise<void> {
119
+ if (!hasKey()) fail(NO_KEY);
120
+ const f = parseFlags(argv);
121
+
122
+ const title = f.title?.trim();
123
+ if (!title) fail("--title is required");
124
+ const rewardPerUnit = Number(f.reward);
125
+ if (!Number.isFinite(rewardPerUnit) || rewardPerUnit <= 0) fail("--reward <n> is required (per-unit, in the pay token)");
126
+
127
+ const token = (f.token ?? "USDC").toUpperCase();
128
+ if (!PAY_TOKENS.has(token)) fail(`--token must be one of USDC, BNKR, GITLAWB (got ${token})`);
129
+
130
+ const quantity = f.quantity != null ? Math.trunc(Number(f.quantity)) : 1;
131
+ if (!Number.isFinite(quantity) || quantity < 1) fail("--quantity must be a positive integer");
132
+
133
+ const category = (f.category ?? "social").trim();
134
+ const action = f.action?.trim();
135
+ const url = f.url?.trim();
136
+
137
+ // Rail: default pool when token is a real ecosystem token (BNKR/GITLAWB) or it's
138
+ // a multi-unit bounty; else custodial single-hold. `--rail` overrides.
139
+ const railFlag = f.rail?.trim().toLowerCase();
140
+ const rail =
141
+ railFlag === "pool" || railFlag === "custodial"
142
+ ? railFlag
143
+ : token === "BNKR" || token === "GITLAWB" || quantity > 1
144
+ ? "pool"
145
+ : "custodial";
146
+
147
+ // reward_usd is the TOTAL budget (= per-unit × quantity). For non-USDC tokens this
148
+ // figure is the TOKEN amount (the platform settles in-token on the pool rail).
149
+ const reward_usd = Number((rewardPerUnit * quantity).toFixed(6));
150
+
151
+ const body: Record<string, unknown> = {
152
+ title,
153
+ category,
154
+ reward_usd,
155
+ quantity,
156
+ pay_token: token,
157
+ rail,
158
+ };
159
+ if (category === "social" && action) body.social_action = action;
160
+ if (category === "social" && url) body.social_target_url = url;
161
+
162
+ const c = client();
163
+ try {
164
+ console.error(
165
+ `→ posting "${title}" (${rewardPerUnit} ${token} × ${quantity} = ${reward_usd} ${token}, ${rail} rail)…`,
166
+ );
167
+ const res = await c.rest<{
168
+ task: { id: string; escrow_status?: string };
169
+ authIntent?: { requirements?: unknown };
170
+ deployFee?: { usd: number; recipient: string; token: string };
171
+ }>("POST", "/api/tasks", { body });
172
+ const taskId = res.task?.id;
173
+ console.error(` ✓ posted — task ${taskId}`);
174
+
175
+ // Custodial rail (no authIntent): nothing else to do; the hold opens later.
176
+ if (!res.authIntent || !res.deployFee) {
177
+ console.error(
178
+ `\n✓ Task ${taskId} is open (custodial rail). Next: assign a human + authorize, then release on a valid proof.`,
179
+ );
180
+ process.exit(0);
181
+ }
182
+
183
+ // POOL rail — the autonomous `bankr launch` path. Sign the budget with the saved
184
+ // wallet, pay the separate deploy fee, then authorize. Reuses evm-signer (the
185
+ // exact logic the authorize_task MCP tool uses).
186
+ const { hasEvmKey, signAuthCapture, payDeployFee } = await import("./evm-signer.js");
187
+ if (!hasEvmKey()) {
188
+ fail(
189
+ "pool rail needs a signing wallet, but none is saved. Run `npx -y cyberdyne-mcp onboard` " +
190
+ `(the task ${taskId} is posted but not yet funded).`,
191
+ );
192
+ }
193
+
194
+ const requirements =
195
+ (res.authIntent as { requirements?: unknown }).requirements ?? res.authIntent;
196
+ console.error("→ signing the budget authorization…");
197
+ const signedPayment = await signAuthCapture(requirements);
198
+
199
+ const fee = res.deployFee;
200
+ console.error(`→ paying the deploy fee (${usd(fee.usd)} in ${fee.token}) from your wallet…`);
201
+ const feeTx = await payDeployFee({ amountUsd: fee.usd, recipient: fee.recipient, token: fee.token });
202
+ console.error(` ✓ fee paid — ${feeTx}`);
203
+
204
+ console.error("→ freezing the budget (authorize)…");
205
+ const authed = await c.rest<{ task?: { escrow_status?: string } }>(
206
+ "POST",
207
+ `/api/tasks/${taskId}/authorize`,
208
+ { body: { signedPayment, fee_tx_hash: feeTx } },
209
+ );
210
+ const escrow = authed.task?.escrow_status ?? "held";
211
+ console.error(
212
+ `\n✓ Launched. task ${taskId} — escrow_status: ${escrow}. ` +
213
+ "Humans can now claim + submit FCFS; review each submission to capture a unit.",
214
+ );
215
+ process.exit(0);
216
+ } catch (e) {
217
+ fail(describe(e));
218
+ }
219
+ }
220
+
221
+ // ── tasks ─────────────────────────────────────────────────────────────────
222
+ // List the agent's own posted tasks (GET /api/tasks?mine=posted — works with the
223
+ // agent key). Short: id, title, token, qty, filled/remaining, status.
224
+ export async function runTasks(): Promise<void> {
225
+ if (!hasKey()) fail(NO_KEY);
226
+ const c = client();
227
+ try {
228
+ const { tasks } = await c.rest<{ tasks: Array<Record<string, unknown>> }>("GET", "/api/tasks", {
229
+ query: { mine: "posted", limit: 50 },
230
+ });
231
+ if (!tasks || tasks.length === 0) {
232
+ console.error("No posted tasks yet. Post one: npx -y cyberdyne-mcp post --title \"…\" --reward 1");
233
+ process.exit(0);
234
+ }
235
+ const lines = [`Your posted tasks (${tasks.length}):`];
236
+ for (const t of tasks) {
237
+ const qty = Number(t.quantity ?? 1) || 1;
238
+ const filled = Number(t.filled_count ?? t.captured_count ?? 0) || 0;
239
+ const remaining = Math.max(qty - filled, 0);
240
+ const token = String(t.pay_token ?? "USDC");
241
+ const status = String(t.escrow_status ? `${t.status}/${t.escrow_status}` : t.status ?? "—");
242
+ const id = String(t.id ?? "—");
243
+ const title = String(t.title ?? "").slice(0, 40);
244
+ lines.push(
245
+ ` ${id} ${title.padEnd(40)} ${token.padEnd(7)} qty ${qty} filled ${filled}/${qty} (rem ${remaining}) ${status}`,
246
+ );
247
+ }
248
+ console.error(lines.join("\n"));
249
+ process.exit(0);
250
+ } catch (e) {
251
+ fail(describe(e));
252
+ }
253
+ }
package/src/server.ts CHANGED
@@ -13,13 +13,13 @@
13
13
  * get_deposit_address — GET /api/treasury/deposit → where to send real USDC (live)
14
14
  * deposit — POST /api/treasury/deposit → credit treasury from a real USDC tx
15
15
  * withdraw_treasury — POST /api/treasury/withdraw → pull unspent treasury back to your wallet (live)
16
- * post_task — POST /api/tasks → open a task
17
- * assign_task — POST /api/tasks/[id]/assign pick a human (→ authIntent)
18
- * authorize_task — POST /api/tasks/[id]/authorize → open the escrow hold
16
+ * post_task — POST /api/tasks → open an FCFS pool bounty
17
+ * authorize_task — POST /api/tasks/[id]/authorize sign budget + pay fee + freeze
19
18
  * get_task — GET /api/tasks/[id] → status + submissions/claims
20
- * release_payment — POST /api/tasks/[id]/release direct-hire: capture (pay) or reject
21
- * review_submission — POST /api/submissions/[id]/review pool/FCFS: approve/reject one submission
22
- * close_task — POST /api/tasks/[id]/close close a (multi-unit) bounty
19
+ * review_submission — POST /api/submissions/[id]/review approve (pay one unit) / reject (reopen)
20
+ * close_task — POST /api/tasks/[id]/close refund the unfilled budget
21
+ * assign_task — POST /api/tasks/[id]/assign DEPRECATED (testnet-only; no direct hire)
22
+ * release_payment — POST /api/tasks/[id]/release → DEPRECATED (testnet-only; use review_submission)
23
23
  *
24
24
  * Auth: every networked tool sends the agent's `cyb_…` key. The REST routes take
25
25
  * it as `Authorization: Bearer …`; search_humans goes through the a2a JSON-RPC
@@ -27,26 +27,24 @@
27
27
  * `identity_token`.
28
28
  *
29
29
  * The HUMAN submit-proof step happens in the app/UI (human-only — agents cannot
30
- * submit on a human's behalf). There are TWO settlement flows:
30
+ * submit on a human's behalf). There is ONE settlement model for real tokens:
31
31
  *
32
- * FLOW A DIRECT HIRE (the path that works TODAY on the live custodial USDC
33
- * rail: real deposit escrow withdraw on Base mainnet). You pick one human:
34
- * get_deposit_address send USDC deposit (fund the treasury)
35
- * post_task search_humans assign_task (→ authIntent)
36
- * authorize_task (open the escrow hold)
37
- * poll get_task until a submission is pending
38
- * → release_payment (approvecapture/pay; else reject refund)
32
+ * FCFS POOL BOUNTY (non-custodial pool escrow). There is NO direct hire and NO
33
+ * agent-picks-human. EVERY task is an open bounty: the agent freezes a budget once,
34
+ * ANY eligible human submits first-come-first-served, and the agent approves/rejects
35
+ * each submission approved pays one unit in-token, rejected reopens the slot, and
36
+ * any unfilled budget is refunded on close.
37
+ * get_deposit_address send USDC deposit (optional, fund treasury)
38
+ * → post_task({ ..., quantity }) returns { task, authIntent, deployFee }
39
+ * → authorize_task({ task_id, auth_intent, deploy_fee }) (sign budget + pay fee + freeze)
40
+ * → humans submit FCFS → poll get_task
41
+ * → review_submission per pending submission (approve → pay one unit;
42
+ * reject → the slot reopens)
43
+ * → close_task to refund the unfilled budget.
39
44
  *
40
- * FLOW B POOL / FCFS BOUNTY (non-custodial pool escrow). Post a multi-unit
41
- * bounty, freeze the whole budget once, and let any eligible human claim+submit
42
- * first-come-first-served; you approve each unit. This rail is BUILT but GATED
43
- * OFF today (server env ESCROW_POOL is not enabled), pending certification — so
44
- * real-money non-custodial pool payouts are NOT live yet. When the server
45
- * enables it, post_task returns an `authIntent` + a separate `deployFee`:
46
- * post_task (quantity>1) → authorize_task (sign the budget + pay the deploy fee)
47
- * → humans claim+submit FCFS → poll get_task
48
- * → review_submission per pending submission (approve → capture one unit;
49
- * reject → the slot reopens) → close_task to refund unfilled units.
45
+ * DEPRECATED (testnet-only): assign_task + release_payment were the old direct-hire
46
+ * path. There is no direct hire for real tokens a real-token assign/release returns
47
+ * 409 direct_hire_retired. The tools remain only for the testnet/non-real demo rail.
50
48
  *
51
49
  * Config comes from the environment (see src/client.ts):
52
50
  * CYBERDYNE_API_URL default "https://app.cyberdyne-os.xyz"
@@ -122,6 +120,24 @@ if (process.argv[2] === "login") {
122
120
  process.exit(0);
123
121
  }
124
122
 
123
+ // Bankr-style convenience CLI subcommands (additional entry points, not MCP tools).
124
+ // Each runs autonomously with the saved key/wallet, prints a summary, and exits.
125
+ // treasury (alias balance, fees) — balance + deposit address (like `bankr fees`)
126
+ // post — open a task; pool rail auto sign+pay+authorize (like `bankr launch`)
127
+ // tasks — list your own posted tasks with status
128
+ if (["treasury", "balance", "fees"].includes(process.argv[2] ?? "")) {
129
+ const { runTreasury } = await import("./cli.js");
130
+ await runTreasury();
131
+ }
132
+ if (process.argv[2] === "post") {
133
+ const { runPost } = await import("./cli.js");
134
+ await runPost(process.argv.slice(3));
135
+ }
136
+ if (process.argv[2] === "tasks") {
137
+ const { runTasks } = await import("./cli.js");
138
+ await runTasks();
139
+ }
140
+
125
141
  const config = readConfig();
126
142
  const client = new CyberdyneClient(config);
127
143
 
@@ -148,7 +164,7 @@ async function guard<T>(fn: () => Promise<T>) {
148
164
 
149
165
  // ---- Server ---------------------------------------------------------------
150
166
 
151
- const server = new McpServer({ name: "cyberdyne", version: "0.6.1" });
167
+ const server = new McpServer({ name: "cyberdyne", version: "0.6.3" });
152
168
 
153
169
  server.tool(
154
170
  "list_categories",
@@ -241,7 +257,7 @@ server.tool(
241
257
 
242
258
  server.tool(
243
259
  "post_task",
244
- "Open a task on the marketplace. Funds are NOT charged at post — the escrow hold opens later at authorize_task. `reward_usd` is the total budget; with quantity>1 each unit holds reward_usd/quantity (each unit must be >= $0.01). Returns the created task (with its id). DIRECT-HIRE / custodial rail (the path live today): the platform checks the prefunded treasury can cover the budget (402 insufficient_treasury otherwise); response is { task }. POOL/FCFS rail (only when the server enables it): response also includes `authIntent` (the budget authorization to sign) and `deployFee` { usd, bps, recipient, token } (a SEPARATE non-refundable fee tx) — pass both to authorize_task.",
260
+ "Open an FCFS pool bounty on the marketplace. There is NO direct hire and NO agent-picks-human — every task is an open bounty: you freeze a budget, ANY eligible human submits first-come-first-served, and you approve/reject each submission. Funds are NOT charged at post — the budget is frozen later at authorize_task. `reward_usd` is the total budget; `quantity` is how many identical units (humans) it pays — each unit holds reward_usd/quantity (each unit must be >= $0.01). Returns the created task (with its id). REAL-TOKEN POOL rail (USDC/BNKR on Base): the response also includes `authIntent` (the budget authorization to sign) and `deployFee` { usd, bps, recipient, token } (a SEPARATE non-refundable fee tx) — pass BOTH to authorize_task. TESTNET/non-real token (CYOS): no on-chain freeze; response is just { task } (treasury-checked, 402 insufficient_treasury if low).",
245
261
  {
246
262
  title: z.string().min(2).max(160).describe("Short task title."),
247
263
  category: z.enum(TASK_CATEGORIES),
@@ -259,7 +275,7 @@ server.tool(
259
275
 
260
276
  server.tool(
261
277
  "assign_task",
262
- "Assign an open task to a chosen human (poster-only) and open the escrow intent. Returns `{ task, authIntent }`: on an on-chain rail `authIntent` is the auth-capture requirements the agent must sign; on the manual rail it is null. Next call authorize_task to actually open the hold.",
278
+ "DEPRECATED — TESTNET/DEMO ONLY. There is NO direct hire: every real-token task is an open FCFS pool bounty, so a real-token assign returns 409 direct_hire_retired ('post with a budget; humans submit FCFS; review each submission'). Do NOT use this to hire post_task + authorize_task + review_submission is the flow. This tool remains only for the testnet/non-real (CYOS) demo rail, where it assigns an open task to a chosen human and returns `{ task, authIntent: null }`.",
263
279
  {
264
280
  task_id: z.string().uuid(),
265
281
  human_id: z.string().uuid().describe("The human profile id (from search_humans / get_task claims)."),
@@ -270,7 +286,7 @@ server.tool(
270
286
 
271
287
  server.tool(
272
288
  "authorize_task",
273
- "Open the escrow hold for a task. CUSTODIAL/MANUAL rail (the path live today): call with just { task_id } — the prefunded treasury is debited into a logical escrow hold, no signature needed. TRUSTLESS on-chain DIRECT-HIRE rail: the agent signs an auth-capture authorization — if CYBERDYNE_EVM_PRIVATE_KEY is set pass `auth_intent` (the authIntent from assign_task) and the MCP signs automatically, else pass a pre-signed `signed_payment`. POOL/FCFS rail (only when the server enables it): pass BOTH `auth_intent` (from post_task) AND `deploy_fee` (the deployFee object from post_task) — the MCP signs the budget and pays the separate 2.5% USDC / 5% other-token fee tx from its wallet, then submits both; or pass a pre-signed `signed_payment` and a pre-paid `fee_tx_hash`. Idempotent once held.",
289
+ "Freeze the bounty budget on-chain (the second step of the FCFS flow). REAL-TOKEN POOL rail: pass BOTH `auth_intent` (the authIntent from post_task) AND `deploy_fee` (the deployFee object from post_task) — with CYBERDYNE_EVM_PRIVATE_KEY set, the MCP signs the whole-budget authorization AND pays the separate 2.5% USDC / 5% other-token deploy fee tx from its wallet, then freezes the budget on the audited escrow; or pass a pre-signed `signed_payment` and a pre-paid `fee_tx_hash`. After this, any eligible human submits FCFS and you review_submission each. TESTNET/non-real (CYOS) rail: call with just { task_id } — the prefunded treasury opens a logical hold, no signature. Idempotent once frozen.",
274
290
  {
275
291
  task_id: z.string().uuid(),
276
292
  signed_payment: z.string().optional().describe("Pre-signed base64 auth-capture payload (external/Bankr signer)."),
@@ -309,14 +325,14 @@ server.tool(
309
325
 
310
326
  server.tool(
311
327
  "get_task",
312
- "Get the live state of a task: the task row plus the submissions and per-unit claims the agent (as poster) may see. Poll this after authorize_task until a submission with status 'pending' appears — that is the human's proof, ready for release_payment.",
328
+ "Get the live state of a task: the task row plus the submissions and per-unit claims the agent (as poster) may see. Poll this after authorize_task until a submission with status 'pending' appears — that is the human's proof, ready for review_submission (approve pays one unit; reject reopens the slot).",
313
329
  { task_id: z.string().uuid() },
314
330
  async ({ task_id }) => guard(() => client.rest("GET", `/api/tasks/${task_id}`)),
315
331
  );
316
332
 
317
333
  server.tool(
318
334
  "release_payment",
319
- "DIRECT-HIRE settle (poster-only): settle a submitted proof for a single-human task (the custodial-rail path live today). approve:true CAPTURE: pay the human net of the platform fee. approve:false REJECT/REFUND the held escrow. Requires the `submission_id` to act on; if omitted, the gateway fetches the task and uses the latest pending submission (and errors if none is pending yet poll get_task first). For POOL/FCFS bounties use review_submission instead (this path captures the whole task hold, not one unit).",
335
+ "DEPRECATED TESTNET/DEMO ONLY. There is NO direct hire: a real-token task is an FCFS pool bounty whose units settle ONE per approved submission, so a real-token release returns 409 direct_hire_retired use review_submission instead (approve pays one unit, reject reopens the slot). This tool remains only for the testnet/non-real (CYOS) demo rail, where it settles a submitted proof for a single-human task: approve:true pay net of fee, approve:false reject/refund. `submission_id` auto-resolves to the latest pending one if omitted.",
320
336
  {
321
337
  task_id: z.string().uuid(),
322
338
  approve: z.boolean().describe("true = proof meets criteria → pay; false = reject/refund."),
@@ -361,7 +377,7 @@ server.tool(
361
377
 
362
378
  server.tool(
363
379
  "review_submission",
364
- "POOL / FCFS settle (poster-only): approve or reject ONE submission on a pool bounty. approve:true → CAPTURE one unit from the frozen pool budget to the human and consume a slot; approve:false → reject (the slot reopens for the next submitter — no spot-blocking). For single-human direct-hire tasks use release_payment instead. Poll get_task for pending submissions. NOTE: the pool/FCFS rail is gated off on the server until certification this tool acts on pool tasks once the operator enables that rail.",
380
+ "THE settle tool (poster-only): approve or reject ONE submission on your FCFS pool bounty — this is how you pay humans (there is no direct hire). approve:true → CAPTURE one unit from the frozen budget to the human (full reward, in-token) and consume a slot; approve:false → reject (the slot reopens for the next submitter — no spot-blocking). Poll get_task for pending submissions and review each one. When the budget is consumed (or you're done) call close_task to refund the unfilled remainder.",
365
381
  {
366
382
  submission_id: z.string().uuid().describe("The pending submission to review (from get_task)."),
367
383
  approve: z.boolean().describe("true = proof meets criteria → capture one unit; false = reject (slot reopens)."),
@@ -384,7 +400,7 @@ server.tool(
384
400
 
385
401
  server.tool(
386
402
  "close_task",
387
- "Close a (multi-unit) bounty (poster-only): refund every still-held unit to the agent, mark unclaimed units done, and stop further claims. Idempotent on an already-closed task.",
403
+ "Close your FCFS pool bounty (poster-only): refund the unfilled budget back to your wallet on-chain (the uncaptured remainder = unfilled units × per-unit reward) and stop further submissions. The deploy fee is non-refundable. Idempotent on an already-closed task.",
388
404
  { task_id: z.string().uuid() },
389
405
  async ({ task_id }) => guard(() => client.rest("POST", `/api/tasks/${task_id}/close`)),
390
406
  );
@@ -397,7 +413,7 @@ server.registerPrompt(
397
413
  "quickstart",
398
414
  {
399
415
  title: "CYBERDYNE quickstart",
400
- description: "How to fund, post a task, and pay humans end-to-end — both the direct-hire and pool/FCFS flows.",
416
+ description: "How to fund, post an FCFS pool bounty, and pay verified humans end-to-end.",
401
417
  },
402
418
  () => ({
403
419
  messages: [
@@ -406,26 +422,20 @@ server.registerPrompt(
406
422
  content: {
407
423
  type: "text",
408
424
  text: [
409
- "You are connected to CYBERDYNE — hire and pay verified humans for tasks AI can't do alone. The live settlement rail is REAL USDC on Base (custodial: deposit -> escrow -> withdraw). The human submit-proof step is human-only, in the app; you drive everything else.",
425
+ "You are connected to CYBERDYNE — pay verified humans for tasks AI can't do alone. There is ONE model: every task is an open FCFS pool bounty. There is NO direct hire and NO picking a human — you freeze a budget, ANY eligible human submits first-come-first-served, and you approve/reject each submission (approved = paid one unit in-token, rejected = the slot reopens). The live settlement rail is REAL tokens on Base (non-custodial freeze-at-deploy). The human submit-proof step is human-only, in the app; you drive everything else.",
410
426
  "",
411
- "FUND (real money, custodial rail):",
427
+ "FUND (optional — the pool freezes from your wallet at deploy, but a treasury can cover fees):",
412
428
  "1. get_deposit_address -> the platform deposit address on Base.",
413
- "2. Send USDC to it FROM your own verified wallet (the one you signed in with).",
414
- "3. deposit({ tx_hash }) -> credits your treasury by the verified amount (idempotent).",
415
- " (fund_treasury is demo/testnet only and is disabled on the live rail.)",
416
- "Check get_treasury anytime for your balance.",
429
+ "2. Send USDC to it FROM your own verified wallet, then deposit({ tx_hash }) -> credits your treasury (idempotent).",
430
+ " (fund_treasury is demo/testnet only and is disabled on the live rail.) Check get_treasury anytime.",
417
431
  "",
418
- "FLOW A - DIRECT HIRE (works today): pick one human, hold, then pay.",
419
- "4. post_task({ title, category, reward_usd, duration_min, difficulty }). Returns { task }. Use reward_usd >= 0.50 so the 2.5% fee is visible; each unit must be >= $0.01.",
420
- "5. search_humans({ skills, min_reputation }) -> assign_task({ task_id, human_id }) -> authorize_task({ task_id }) to open the escrow hold (custodial rail = no signature; just task_id).",
421
- "6. Poll get_task until a submission is pending (the human's proof).",
422
- "7. release_payment({ task_id, approve: true, score }) -> CAPTURE: net USDC to the human, the 2.5% fee to the protocol wallet. approve:false rejects and refunds the hold.",
432
+ "POST + PAY (the single FCFS flow):",
433
+ "3. post_task({ title, category, reward_usd, quantity, duration_min, difficulty }) -> returns { task, authIntent, deployFee }. reward_usd is the TOTAL budget; quantity is how many humans it pays (each unit must be >= $0.01). authIntent is the whole-budget authorization; deployFee is a SEPARATE non-refundable fee tx (2.5% USDC / 5% other token).",
434
+ "4. authorize_task({ task_id, auth_intent, deploy_fee }) -> with CYBERDYNE_EVM_PRIVATE_KEY set, the MCP signs the budget AND pays the deploy fee, then FREEZES the whole budget on the audited escrow (or pass pre-made signed_payment + fee_tx_hash).",
435
+ "5. Any eligible human submits FCFS. Poll get_task; for EACH pending submission call review_submission({ submission_id, approve, score }) -> approve captures one unit (full reward to the human, in-token); reject reopens the slot for the next submitter.",
436
+ "6. close_task({ task_id }) -> refunds the unfilled budget back to your wallet (the deploy fee is non-refundable).",
423
437
  "",
424
- "FLOW B - POOL / FCFS BOUNTY: one frozen budget, many humans claim+submit first-come-first-served. NOTE: this non-custodial pool rail is BUILT but GATED OFF on the server today (pending certification) - real-money pool payouts are not live yet. When the operator enables it:",
425
- "8. post_task({ ..., quantity: N }) -> returns { task, authIntent, deployFee }. authIntent is the budget authorization; deployFee is a SEPARATE non-refundable fee tx (2.5% USDC / 5% other token).",
426
- "9. authorize_task({ task_id, auth_intent, deploy_fee }) -> with CYBERDYNE_EVM_PRIVATE_KEY set, the MCP signs the budget AND pays the deploy fee, then freezes the whole budget (or pass pre-made signed_payment + fee_tx_hash).",
427
- "10. Humans claim+submit FCFS. Poll get_task; for each pending submission call review_submission({ submission_id, approve, score }) -> approve captures one unit; reject reopens the slot.",
428
- "11. close_task({ task_id }) -> refund any still-unfilled units (the deploy fee is non-refundable).",
438
+ "DEPRECATED: assign_task + release_payment were the old direct-hire path and now return 409 direct_hire_retired for real tokens use post_task + authorize_task + review_submission instead. They remain only for the testnet/non-real (CYOS) demo rail.",
429
439
  "",
430
440
  "Every payout and fee on the live rail is a real on-chain transaction.",
431
441
  ].join("\n"),
@@ -442,5 +452,6 @@ await server.connect(transport);
442
452
  console.error(
443
453
  `CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
444
454
  (config.token ? "" : " (no key — run `npx cyberdyne-mcp onboard` to self-generate a wallet + key, or `login cyb_…`, or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
445
- ". Tools (15): onboard, list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, withdraw_treasury, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task.",
455
+ ". Tools (15): onboard, list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, withdraw_treasury, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task." +
456
+ " CLI: onboard, login, treasury (alias balance/fees), post, tasks.",
446
457
  );