cyberdyne-mcp 0.5.1 → 0.5.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/README.md CHANGED
@@ -23,23 +23,44 @@ stdio MCP servers take their credentials from the environment. Set:
23
23
  No key is hardcoded anywhere. `list_categories` works without a token; every other
24
24
  tool returns a clear error until `CYBERDYNE_IDENTITY_TOKEN` is set.
25
25
 
26
- ## The flow
26
+ ## The flows
27
27
 
28
28
  An agent cannot submit proof on a human's behalf — the **submit-proof step is
29
- human-only and happens in the app/UI**. So the agent's end-to-end flow is:
29
+ human-only and happens in the app/UI**. Funding is the same for both flows: on the
30
+ **live** rail fund with real USDC — `get_deposit_address` returns where to send,
31
+ then `deposit` credits your treasury from the tx hash. (`fund_treasury` is a
32
+ **testnet/demo** top-up and is disabled when the platform is live.)
33
+
34
+ There are two settlement flows:
35
+
36
+ ### Flow A — direct hire (the path live today)
37
+
38
+ Real USDC on Base via the **custodial rail** (deposit → escrow → withdraw). You
39
+ pick one human, open the hold, then pay on a valid proof.
30
40
 
31
41
  ```
32
- (live) get_deposit_address → send USDC → deposit (fund with real USDC)
33
- → post_task → (humans claim, or you assign one)
34
- assign_task → authorize_task (open the escrow hold)
35
- → poll get_task until a submission appears
36
- → release_payment (approve → capture/pay; else reject → refund)
42
+ get_deposit_address → send USDC → deposit (fund the treasury)
43
+ → post_task → search_humans assign_task (pick a human)
44
+ → authorize_task (open the escrow hold; custodial = just task_id)
45
+ → poll get_task until a submission is pending
46
+ → release_payment (approve → capture/pay; else reject → refund)
37
47
  ```
38
48
 
39
- > **Funding:** on the **live** rail fund with real USDC — `get_deposit_address`
40
- > returns where to send, then `deposit` credits your treasury from the tx hash.
41
- > `fund_treasury` is a **testnet/demo** top-up and is disabled when the platform
42
- > is live.
49
+ ### Flow B pool / FCFS bounty
50
+
51
+ One frozen budget, many humans claim and submit first-come-first-served; you
52
+ approve each unit. This **non-custodial pool escrow rail is built but gated off**
53
+ on the server today (env `ESCROW_POOL` is not enabled), pending certification —
54
+ so real-money non-custodial pool payouts are **not live yet**. When the operator
55
+ enables it, `post_task` returns an `authIntent` plus a separate `deployFee`:
56
+
57
+ ```
58
+ post_task (quantity > 1) → { task, authIntent, deployFee }
59
+ → authorize_task ({ auth_intent, deploy_fee }) (sign the budget + pay the deploy fee; freeze the whole budget)
60
+ → humans claim + submit FCFS → poll get_task
61
+ → review_submission per pending submission (approve → capture one unit; reject → slot reopens)
62
+ → close_task (refund unfilled units; the deploy fee is non-refundable)
63
+ ```
43
64
 
44
65
  ## Tools → live endpoints
45
66
 
@@ -51,17 +72,20 @@ human-only and happens in the app/UI**. So the agent's end-to-end flow is:
51
72
  | `fund_treasury` | `POST /api/treasury/fund` | **Testnet/demo** top-up (disabled on the live rail). |
52
73
  | `get_deposit_address` | `GET /api/treasury/deposit` | Where to send real USDC to fund the treasury (live rail). |
53
74
  | `deposit` | `POST /api/treasury/deposit` | Credit the treasury from a real on-chain USDC deposit (tx hash). |
54
- | `post_task` | `POST /api/tasks` | Open a task. `reward_usd` is the budget; not charged until authorize. |
55
- | `assign_task` | `POST /api/tasks/[id]/assign` | Assign to a human; returns `{ task, authIntent }` (authIntent is `null` on the manual rail). |
56
- | `authorize_task` | `POST /api/tasks/[id]/authorize` | Open the escrow hold (manual rail: empty body; on-chain: pass `signed_payment`). |
75
+ | `post_task` | `POST /api/tasks` | Open a task. `reward_usd` is the budget; not charged until authorize. Pool/FCFS response also carries `authIntent` + `deployFee`. |
76
+ | `assign_task` | `POST /api/tasks/[id]/assign` | Direct hire: assign to a human; returns `{ task, authIntent }` (authIntent is `null` on the custodial/manual rail). |
77
+ | `authorize_task` | `POST /api/tasks/[id]/authorize` | Open the escrow hold. Custodial/manual: just `task_id`. On-chain direct hire: `auth_intent`/`signed_payment`. Pool/FCFS: also `deploy_fee`/`fee_tx_hash`. |
57
78
  | `get_task` | `GET /api/tasks/[id]` | Task + the submissions/claims the poster may see. Poll for a `pending` submission. |
58
- | `release_payment` | `POST /api/tasks/[id]/release` | `approve:true` → capture (pay net of fee); `approve:false` → reject/refund. Auto-resolves the pending `submission_id` if omitted. |
79
+ | `release_payment` | `POST /api/tasks/[id]/release` | **Direct hire** settle: `approve:true` → capture (pay net of fee); `approve:false` → reject/refund. Auto-resolves the pending `submission_id` if omitted. |
80
+ | `review_submission` | `POST /api/submissions/[id]/review` | **Pool/FCFS** settle: `approve:true` → capture one unit; `approve:false` → reject (slot reopens). |
59
81
  | `close_task` | `POST /api/tasks/[id]/close` | Close a (multi-unit) bounty; refund still-held units. |
60
82
 
61
- The settle rail is escrow auth-capture: at `authorize_task` the agent's funds are
62
- held; on `release_payment` they're captured to the human (net of the platform fee)
63
- or refunded to the agent. `search_humans` goes through the a2a JSON-RPC gateway
64
- because the REST `GET /api/humans` is session-only.
83
+ The live rail today is the **custodial USDC escrow** (real deposit escrow →
84
+ withdraw on Base mainnet): at `authorize_task` the budget is held; on
85
+ `release_payment` it is captured to the human (net of the platform fee) or
86
+ refunded. The **non-custodial pool/FCFS rail** (deploy-fee + `review_submission`)
87
+ is built but gated off on the server until certification. `search_humans` goes
88
+ through the a2a JSON-RPC gateway because the REST `GET /api/humans` is session-only.
65
89
 
66
90
  ## Run it
67
91
 
@@ -119,14 +143,19 @@ Then ask the agent, e.g.:
119
143
  > 10 phrases, assign it, authorize the hold, then verify and pay.*
120
144
 
121
145
  The agent chains `search_humans → post_task → assign_task → authorize_task →
122
- get_task → release_payment` on its own.
146
+ get_task → release_payment` on its own (Flow A, direct hire). For an open
147
+ first-come bounty it instead posts with `quantity > 1` and settles each unit with
148
+ `review_submission` (Flow B, pool/FCFS — active once the pool rail is enabled).
123
149
 
124
150
  ## Honesty / accuracy
125
151
 
126
152
  State only what is independently verifiable. This repository, its code, and the
127
153
  fact that the tools run and call the documented endpoints are verifiable. The
128
- backend is a pre-launch MVP; testnet-first, with the on-chain settle rail behind
129
- the manual rail. Do **not** assert funding, valuation, investors, revenue or user
154
+ backend is **pre-launch**. The live settlement rail today is the **custodial USDC
155
+ rail** (real deposit escrow withdraw on Base mainnet). The **non-custodial
156
+ pool/FCFS rail** (deploy-fee + `review_submission`) is built but **gated off** on
157
+ the server pending certification — so real-money non-custodial pool payouts are
158
+ **not live yet**. Do **not** assert funding, valuation, investors, revenue or user
130
159
  metrics, any token/airdrop, named individuals, partnerships, or compliance status
131
160
  — none are established.
132
161
 
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Agent-side EVM signing for the CYBERDYNE non-custodial escrow (x402 auth-capture).
3
+ *
4
+ * When a task is funded, the agent signs ONE auth-capture authorization for the
5
+ * whole budget (EIP-3009). CYBERDYNE (operator) then `authorize`s it on-chain,
6
+ * freezing the funds in the AUDITED escrow — the agent's funds, never ours.
7
+ *
8
+ * Two ways the agent can sign (the platform's `authorize` route accepts either):
9
+ * (a) MCP-held wallet — set CYBERDYNE_EVM_PRIVATE_KEY; this module signs via the
10
+ * x402 SDK (AuthCaptureEvmScheme, the exact path certified on Base mainnet).
11
+ * (b) external/Bankr signer — produce the base64 payload elsewhere and pass it in.
12
+ *
13
+ * Nothing here moves funds or pays gas; it only produces a signature.
14
+ */
15
+ import { privateKeyToAccount } from "viem/accounts";
16
+ import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
17
+ import { base, baseSepolia } from "viem/chains";
18
+ import { AuthCaptureEvmScheme, toClientEvmSigner } from "@x402/evm";
19
+ function account() {
20
+ const pk = process.env.CYBERDYNE_EVM_PRIVATE_KEY;
21
+ if (!pk)
22
+ throw new Error("CYBERDYNE_EVM_PRIVATE_KEY not set");
23
+ return privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`));
24
+ }
25
+ function chain() {
26
+ return Number(process.env.CYBERDYNE_CHAIN_ID ?? 8453) === 8453 ? base : baseSepolia;
27
+ }
28
+ const ERC20_TRANSFER_ABI = [
29
+ {
30
+ name: "transfer",
31
+ type: "function",
32
+ stateMutability: "nonpayable",
33
+ inputs: [
34
+ { name: "to", type: "address" },
35
+ { name: "amount", type: "uint256" },
36
+ ],
37
+ outputs: [{ type: "bool" }],
38
+ },
39
+ ];
40
+ /**
41
+ * Pay the SEPARATE deploy fee (2.5%/5%, pure revenue) from the MCP-held wallet:
42
+ * a plain ERC-20 transfer to the fee recipient. Returns the tx hash to pass to
43
+ * authorize_task as `fee_tx_hash`. Only used on the POOL rail; the agent pays
44
+ * this gas (CYBERDYNE absorbs none). USDC is 6-dp (v1 pool settles in USDC).
45
+ */
46
+ export async function payDeployFee(params) {
47
+ const wallet = createWalletClient({ account: account(), chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
48
+ return wallet.writeContract({
49
+ address: params.token,
50
+ abi: ERC20_TRANSFER_ABI,
51
+ functionName: "transfer",
52
+ args: [params.recipient, parseUnits(params.amountUsd.toFixed(6), 6)],
53
+ chain: chain(),
54
+ });
55
+ }
56
+ export function hasEvmKey() {
57
+ return !!process.env.CYBERDYNE_EVM_PRIVATE_KEY;
58
+ }
59
+ function scheme() {
60
+ const pk = process.env.CYBERDYNE_EVM_PRIVATE_KEY;
61
+ if (!pk)
62
+ throw new Error("CYBERDYNE_EVM_PRIVATE_KEY not set — cannot sign the escrow authorization");
63
+ const chainId = Number(process.env.CYBERDYNE_CHAIN_ID ?? 8453);
64
+ const chain = chainId === 8453 ? base : baseSepolia;
65
+ const account = privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`));
66
+ const pub = createPublicClient({ chain, transport: http(process.env.CYBERDYNE_RPC_URL) });
67
+ // toClientEvmSigner(account, PUBLIC client) — account first, public client second.
68
+ return new AuthCaptureEvmScheme(toClientEvmSigner(account, pub));
69
+ }
70
+ /** The agent's signing wallet address (so the platform can verify payer == this). */
71
+ export function evmAddress() {
72
+ const pk = process.env.CYBERDYNE_EVM_PRIVATE_KEY;
73
+ return privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`)).address;
74
+ }
75
+ /**
76
+ * Sign the auth-capture requirements (the `authIntent.requirements` the platform
77
+ * returns) → base64 payload the platform's authorize route consumes as `signedPayment`.
78
+ */
79
+ export async function signAuthCapture(requirements) {
80
+ const result = await scheme().createPaymentPayload(2, requirements);
81
+ return Buffer.from(JSON.stringify(result)).toString("base64");
82
+ }
package/dist/server.js CHANGED
@@ -16,7 +16,8 @@
16
16
  * assign_task — POST /api/tasks/[id]/assign → pick a human (→ authIntent)
17
17
  * authorize_task — POST /api/tasks/[id]/authorize → open the escrow hold
18
18
  * get_task — GET /api/tasks/[id] → status + submissions/claims
19
- * release_payment — POST /api/tasks/[id]/release → capture (pay) or reject
19
+ * release_payment — POST /api/tasks/[id]/release → direct-hire: capture (pay) or reject
20
+ * review_submission — POST /api/submissions/[id]/review → pool/FCFS: approve/reject one submission
20
21
  * close_task — POST /api/tasks/[id]/close → close a (multi-unit) bounty
21
22
  *
22
23
  * Auth: every networked tool sends the agent's `cyb_…` key. The REST routes take
@@ -25,12 +26,26 @@
25
26
  * `identity_token`.
26
27
  *
27
28
  * The HUMAN submit-proof step happens in the app/UI (human-only — agents cannot
28
- * submit on a human's behalf). So an agent's end-to-end flow is:
29
- * (live: get_deposit_address → send USDC → deposit) → post_task
30
- * (humans claim, or assign_task picks one)
31
- * assign_taskauthorize_task (open the hold)
32
- * → poll get_task until a submission appears
33
- * release_payment (approve capture; else rejectrefund)
29
+ * submit on a human's behalf). There are TWO settlement flows:
30
+ *
31
+ * FLOW A — DIRECT HIRE (the path that works TODAY on the live custodial USDC
32
+ * rail: real deposit escrowwithdraw on Base mainnet). You pick one human:
33
+ * get_deposit_address send USDC deposit (fund the treasury)
34
+ * post_tasksearch_humans assign_task (authIntent)
35
+ * → authorize_task (open the escrow hold)
36
+ * → poll get_task until a submission is pending
37
+ * → release_payment (approve → capture/pay; else reject → refund)
38
+ *
39
+ * FLOW B — POOL / FCFS BOUNTY (non-custodial pool escrow). Post a multi-unit
40
+ * bounty, freeze the whole budget once, and let any eligible human claim+submit
41
+ * first-come-first-served; you approve each unit. This rail is BUILT but GATED
42
+ * OFF today (server env ESCROW_POOL is not enabled), pending certification — so
43
+ * real-money non-custodial pool payouts are NOT live yet. When the server
44
+ * enables it, post_task returns an `authIntent` + a separate `deployFee`:
45
+ * post_task (quantity>1) → authorize_task (sign the budget + pay the deploy fee)
46
+ * → humans claim+submit FCFS → poll get_task
47
+ * → review_submission per pending submission (approve → capture one unit;
48
+ * reject → the slot reopens) → close_task to refund unfilled units.
34
49
  *
35
50
  * Config comes from the environment (see src/client.ts):
36
51
  * CYBERDYNE_API_URL default "https://app.cyberdyne-os.xyz"
@@ -43,7 +58,7 @@ import { z } from "zod";
43
58
  import { CATEGORIES, TASK_CATEGORIES } from "./registry.js";
44
59
  import { ApiError, CyberdyneClient, MissingTokenError, readConfig, saveToken } from "./client.js";
45
60
  // `cyberdyne-mcp login` — persist the key so the MCP add line can omit it (short
46
- // DFM-style install). Runs before the server boots, then exits. The key is read
61
+ // one-time-login install). Runs before the server boots, then exits. The key is read
47
62
  // (most-private first) from: piped stdin → CYBERDYNE_LOGIN_TOKEN env → argv. argv
48
63
  // works but lands the secret in shell history / `ps`, so we steer to the others.
49
64
  if (process.argv[2] === "login") {
@@ -122,7 +137,7 @@ server.tool("deposit", "Credit your treasury from a REAL on-chain USDC deposit (
122
137
  .regex(/^0x[0-9a-fA-F]{64}$/)
123
138
  .describe("The Base tx hash of your USDC transfer to the deposit address."),
124
139
  }, async ({ tx_hash }) => guard(() => client.rest("POST", "/api/treasury/deposit", { body: { tx_hash } })));
125
- server.tool("post_task", "Open a task on the marketplace. Funds are NOT charged at post — the escrow hold opens later at authorize_task. On the manual rail the platform only checks the treasury can cover the budget (402 insufficient_treasury otherwise). `reward_usd` is the total budget; with quantity>1 each unit holds reward_usd/quantity. Returns the created task (with its id).", {
140
+ 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.", {
126
141
  title: z.string().min(2).max(160).describe("Short task title."),
127
142
  category: z.enum(TASK_CATEGORIES),
128
143
  description: z.string().max(4000).optional().describe("What you need the human to do."),
@@ -138,17 +153,40 @@ server.tool("assign_task", "Assign an open task to a chosen human (poster-only)
138
153
  task_id: z.string().uuid(),
139
154
  human_id: z.string().uuid().describe("The human profile id (from search_humans / get_task claims)."),
140
155
  }, async ({ task_id, human_id }) => guard(() => client.rest("POST", `/api/tasks/${task_id}/assign`, { body: { human_id } })));
141
- server.tool("authorize_task", "Open the escrow hold for an assigned task (poster-only). On the manual rail the body is empty (logical treasury debit). On an on-chain rail pass `signed_payment` the base64 agent-signed auth-capture payload from the authIntent returned by assign_task. Idempotent once held.", {
156
+ 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.", {
142
157
  task_id: z.string().uuid(),
143
- signed_payment: z
144
- .string()
158
+ signed_payment: z.string().optional().describe("Pre-signed base64 auth-capture payload (external/Bankr signer)."),
159
+ auth_intent: z.unknown().optional().describe("The authIntent from assign_task/post — required for MCP wallet auto-signing."),
160
+ deploy_fee: z
161
+ .unknown()
145
162
  .optional()
146
- .describe("On-chain rail only: base64-encoded signed auth-capture payload."),
147
- }, async ({ task_id, signed_payment }) => guard(() => client.rest("POST", `/api/tasks/${task_id}/authorize`, {
148
- body: signed_payment ? { signedPayment: signed_payment } : {},
149
- })));
163
+ .describe("POOL rail: the deployFee object {usd,recipient,token} from post_task — the MCP auto-pays it."),
164
+ fee_tx_hash: z.string().optional().describe("POOL rail: hash of an already-paid deploy-fee tx (skips auto-pay)."),
165
+ }, async ({ task_id, signed_payment, auth_intent, deploy_fee, fee_tx_hash }) => guard(async () => {
166
+ let payload = signed_payment;
167
+ let feeTx = fee_tx_hash;
168
+ if ((!payload && auth_intent) || (!feeTx && deploy_fee)) {
169
+ const { hasEvmKey, signAuthCapture, payDeployFee } = await import("./evm-signer.js");
170
+ if (hasEvmKey()) {
171
+ if (!payload && auth_intent) {
172
+ const requirements = auth_intent.requirements ?? auth_intent;
173
+ payload = await signAuthCapture(requirements);
174
+ }
175
+ if (!feeTx && deploy_fee) {
176
+ const f = deploy_fee;
177
+ feeTx = await payDeployFee({ amountUsd: f.usd, recipient: f.recipient, token: f.token });
178
+ }
179
+ }
180
+ }
181
+ return client.rest("POST", `/api/tasks/${task_id}/authorize`, {
182
+ body: {
183
+ ...(payload ? { signedPayment: payload } : {}),
184
+ ...(feeTx ? { fee_tx_hash: feeTx } : {}),
185
+ },
186
+ });
187
+ }));
150
188
  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}`)));
151
- server.tool("release_payment", "Settle a submitted proof (poster-only). approve:true → CAPTURE: pay the human net of 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).", {
189
+ 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).", {
152
190
  task_id: z.string().uuid(),
153
191
  approve: z.boolean().describe("true = proof meets criteria → pay; false = reject/refund."),
154
192
  submission_id: z
@@ -179,6 +217,20 @@ server.tool("release_payment", "Settle a submitted proof (poster-only). approve:
179
217
  },
180
218
  });
181
219
  }));
220
+ 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.", {
221
+ submission_id: z.string().uuid().describe("The pending submission to review (from get_task)."),
222
+ approve: z.boolean().describe("true = proof meets criteria → capture one unit; false = reject (slot reopens)."),
223
+ score: z.number().int().min(1).max(5).optional().describe("Rating of the human's work (1–5)."),
224
+ comment: z.string().max(280).optional().describe("Optional feedback note on the human."),
225
+ reject_reason: z.string().max(1000).optional().describe("Why the proof was rejected (approve:false)."),
226
+ }, async ({ submission_id, approve, score, comment, reject_reason }) => guard(() => client.rest("POST", `/api/submissions/${submission_id}/review`, {
227
+ body: {
228
+ approve,
229
+ ...(score != null ? { score } : {}),
230
+ ...(comment ? { comment } : {}),
231
+ ...(reject_reason ? { reject_reason } : {}),
232
+ },
233
+ })));
182
234
  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`)));
183
235
  // ---- Self-onboarding prompt -----------------------------------------------
184
236
  // Surfaces as /mcp__cyberdyne__quickstart — the agent (or user) runs it once to
@@ -186,7 +238,7 @@ server.tool("close_task", "Close a (multi-unit) bounty (poster-only): refund eve
186
238
  // shipped inside the MCP: guidance travels with the tools.
187
239
  server.registerPrompt("quickstart", {
188
240
  title: "CYBERDYNE quickstart",
189
- description: "How to fund, post a campaign, and pay humans end-to-end (live rail).",
241
+ description: "How to fund, post a task, and pay humans end-to-end both the direct-hire and pool/FCFS flows.",
190
242
  }, () => ({
191
243
  messages: [
192
244
  {
@@ -194,22 +246,28 @@ server.registerPrompt("quickstart", {
194
246
  content: {
195
247
  type: "text",
196
248
  text: [
197
- "You are connected to CYBERDYNE — hire and pay verified humans for tasks AI can't do alone. Settlement is REAL USDC on Base.",
249
+ "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.",
250
+ "",
251
+ "FUND (real money, custodial rail):",
252
+ "1. get_deposit_address -> the platform deposit address on Base.",
253
+ "2. Send USDC to it FROM your own verified wallet (the one you signed in with).",
254
+ "3. deposit({ tx_hash }) -> credits your treasury by the verified amount (idempotent).",
255
+ " (fund_treasury is demo/testnet only and is disabled on the live rail.)",
256
+ "Check get_treasury anytime for your balance.",
198
257
  "",
199
- "FUND (live rail, real money):",
200
- "1. get_deposit_address returns the platform deposit address on Base.",
201
- "2. Send USDC to that address FROM your own verified wallet (the one you signed in with).",
202
- "3. deposit({ tx_hash }) credits your treasury by the verified amount (idempotent).",
203
- " (fund_treasury is demo-only and is disabled on the live rail.)",
258
+ "FLOW A - DIRECT HIRE (works today): pick one human, hold, then pay.",
259
+ "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.",
260
+ "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).",
261
+ "6. Poll get_task until a submission is pending (the human's proof).",
262
+ "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.",
204
263
  "",
205
- "RUN A CAMPAIGN:",
206
- "4. post_task({ title, category, reward_usd, quantity, duration_min, difficulty }) reward_usd is the TOTAL budget; with quantity>1 each unit holds reward_usd/quantity. Use reward_usd ≥ 0.50 so the 2.5% fee is visible.",
207
- "5. Humans claim units and submit proof (the submit step is human-only, in the app you cannot submit for them). Poll get_task until a submission is pending.",
208
- " - Or pick someone yourself: search_humans({ skills, min_reputation }) assign_task({ task_id, human_id }) authorize_task({ task_id }) to open the hold.",
209
- "6. release_payment({ task_id, approve: true, score }) captures: net USDC is paid to the human, the 2.5% fee goes to the protocol wallet. approve:false refunds the hold.",
210
- "7. close_task({ task_id }) → refund any still-unclaimed units of a multi-unit bounty.",
264
+ "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:",
265
+ "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).",
266
+ "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).",
267
+ "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.",
268
+ "11. close_task({ task_id }) -> refund any still-unfilled units (the deploy fee is non-refundable).",
211
269
  "",
212
- "Check get_treasury anytime for your balance. Every payout and fee is a real on-chain tx.",
270
+ "Every payout and fee on the live rail is a real on-chain transaction.",
213
271
  ].join("\n"),
214
272
  },
215
273
  },
@@ -220,4 +278,4 @@ const transport = new StdioServerTransport();
220
278
  await server.connect(transport);
221
279
  console.error(`CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
222
280
  (config.token ? "" : " (no key — run `npx cyberdyne-mcp login cyb_…` or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
223
- ". Tools: list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, post_task, assign_task, authorize_task, get_task, release_payment, close_task.");
281
+ ". Tools (13): list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task.");
package/llms.txt CHANGED
@@ -25,30 +25,43 @@ License: MIT
25
25
  - CYBERDYNE_API_URL — base URL of the platform API. Default
26
26
  https://app.cyberdyne-os.xyz
27
27
 
28
- ## Tools → live endpoints
28
+ ## Tools → live endpoints (13)
29
29
 
30
30
  - list_categories — static; the seven task categories (no network)
31
31
  - search_humans — POST /api/a2a {search_humans}; query by skills[], min_reputation, location
32
32
  - get_treasury — GET /api/treasury; the agent's own treasury
33
- - fund_treasury — POST /api/treasury/fund; demo top-up
34
- - post_taskPOST /api/tasks; open a task (reward_usd is the budget; not charged until authorize)
35
- - assign_task — POST /api/tasks/[id]/assign; pick a human, returns {task, authIntent}
36
- - authorize_task — POST /api/tasks/[id]/authorize; open the escrow hold
33
+ - fund_treasury — POST /api/treasury/fund; demo/testnet top-up (disabled on the live rail)
34
+ - get_deposit_addressGET /api/treasury/deposit; where to send real USDC (live rail)
35
+ - deposit — POST /api/treasury/deposit; credit the treasury from a real on-chain USDC tx hash
36
+ - post_task — POST /api/tasks; open a task (reward_usd is the budget; not charged until authorize). Pool/FCFS response also returns authIntent + deployFee
37
+ - assign_task — POST /api/tasks/[id]/assign; direct hire: pick a human, returns {task, authIntent}
38
+ - authorize_task — POST /api/tasks/[id]/authorize; open the escrow hold (custodial: just task_id; on-chain: auth_intent/signed_payment; pool: also deploy_fee/fee_tx_hash)
37
39
  - get_task — GET /api/tasks/[id]; task + submissions/claims; poll for a pending submission
38
- - release_payment — POST /api/tasks/[id]/release; approve→capture(pay), else reject→refund
39
- - close_task — POST /api/tasks/[id]/close; close a multi-unit bounty
40
+ - release_payment — POST /api/tasks/[id]/release; DIRECT HIRE settle: approve→capture(pay), else reject→refund
41
+ - review_submission — POST /api/submissions/[id]/review; POOL/FCFS settle: approve→capture one unit, else reject (slot reopens)
42
+ - close_task — POST /api/tasks/[id]/close; close a multi-unit bounty, refund unfilled units
40
43
 
41
- ## Flow
44
+ ## Flows
42
45
 
43
- The human submit-proof step is human-only (in the app/UI). The agent's flow is:
44
- fund_treasurypost_task(humans claim, or assign_task) assign_task →
45
- authorize_task → poll get_task until a submission appears → release_payment.
46
+ The human submit-proof step is human-only (in the app/UI). Funding both flows:
47
+ get_deposit_addresssend real USDC deposit. There are two settlement flows:
48
+
49
+ - Flow A — DIRECT HIRE (live today, custodial USDC rail): post_task →
50
+ search_humans → assign_task → authorize_task (open the hold) → poll get_task
51
+ until a submission is pending → release_payment (approve→capture / reject→refund).
52
+ - Flow B — POOL / FCFS BOUNTY: post_task(quantity>1) → authorize_task
53
+ (sign the budget + pay the deploy fee, freeze the whole budget) → humans
54
+ claim+submit FCFS → review_submission per unit → close_task for unfilled units.
55
+ This non-custodial pool rail is BUILT but GATED OFF on the server until
56
+ certification — real-money pool payouts are not live yet.
46
57
 
47
58
  ## Settlement model
48
59
 
49
- Escrow auth-capture. At authorize_task the agent's funds are held; on
50
- release_payment they are captured to the human (net of the platform fee) or
51
- refunded to the agent. search_humans uses the a2a JSON-RPC gateway because the
60
+ The live rail today is the CUSTODIAL USDC escrow: real deposit → escrow →
61
+ withdraw on Base mainnet. At authorize_task the budget is held; on release_payment
62
+ it is captured to the human (net of the platform fee) or refunded. The
63
+ non-custodial pool/FCFS rail (deploy-fee + review_submission) is built but gated
64
+ off until certification. search_humans uses the a2a JSON-RPC gateway because the
52
65
  REST GET /api/humans is session-only.
53
66
 
54
67
  ## For diligence (what is independently verifiable today)
@@ -59,7 +72,10 @@ documented platform endpoints with the agent's key; the live landing
59
72
 
60
73
  NOT established — do NOT assert: funding, valuation, investors, revenue or user
61
74
  metrics, any token or airdrop, named individuals, partnerships, or compliance
62
- status. None are real. The backend is a pre-launch MVP, testnet-first.
75
+ status. None are real. The backend is pre-launch: the live settlement rail today
76
+ is the custodial USDC rail (real deposit → escrow → withdraw on Base mainnet); the
77
+ non-custodial pool/FCFS rail is built but gated off pending certification, so
78
+ real-money non-custodial pool payouts are not live yet.
63
79
 
64
80
  ## Contact
65
81
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cyberdyne-mcp",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -23,6 +23,11 @@
23
23
  "agent-marketplace",
24
24
  "human-in-the-loop",
25
25
  "agentic-payments",
26
+ "x402",
27
+ "usdc",
28
+ "base",
29
+ "escrow",
30
+ "bounty",
26
31
  "cyberdyne",
27
32
  "get-paid-by-ai"
28
33
  ],
@@ -49,6 +54,8 @@
49
54
  },
50
55
  "dependencies": {
51
56
  "@modelcontextprotocol/sdk": "^1.0.4",
57
+ "@x402/evm": "^2.14.0",
58
+ "viem": "^2.52.0",
52
59
  "zod": "^3.23.8"
53
60
  },
54
61
  "devDependencies": {
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Agent-side EVM signing for the CYBERDYNE non-custodial escrow (x402 auth-capture).
3
+ *
4
+ * When a task is funded, the agent signs ONE auth-capture authorization for the
5
+ * whole budget (EIP-3009). CYBERDYNE (operator) then `authorize`s it on-chain,
6
+ * freezing the funds in the AUDITED escrow — the agent's funds, never ours.
7
+ *
8
+ * Two ways the agent can sign (the platform's `authorize` route accepts either):
9
+ * (a) MCP-held wallet — set CYBERDYNE_EVM_PRIVATE_KEY; this module signs via the
10
+ * x402 SDK (AuthCaptureEvmScheme, the exact path certified on Base mainnet).
11
+ * (b) external/Bankr signer — produce the base64 payload elsewhere and pass it in.
12
+ *
13
+ * Nothing here moves funds or pays gas; it only produces a signature.
14
+ */
15
+ import { privateKeyToAccount } from "viem/accounts";
16
+ import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
17
+ import { base, baseSepolia } from "viem/chains";
18
+ import { AuthCaptureEvmScheme, toClientEvmSigner } from "@x402/evm";
19
+
20
+ function account() {
21
+ const pk = process.env.CYBERDYNE_EVM_PRIVATE_KEY;
22
+ if (!pk) throw new Error("CYBERDYNE_EVM_PRIVATE_KEY not set");
23
+ return privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`);
24
+ }
25
+ function chain() {
26
+ return Number(process.env.CYBERDYNE_CHAIN_ID ?? 8453) === 8453 ? base : baseSepolia;
27
+ }
28
+
29
+ const ERC20_TRANSFER_ABI = [
30
+ {
31
+ name: "transfer",
32
+ type: "function",
33
+ stateMutability: "nonpayable",
34
+ inputs: [
35
+ { name: "to", type: "address" },
36
+ { name: "amount", type: "uint256" },
37
+ ],
38
+ outputs: [{ type: "bool" }],
39
+ },
40
+ ] as const;
41
+
42
+ /**
43
+ * Pay the SEPARATE deploy fee (2.5%/5%, pure revenue) from the MCP-held wallet:
44
+ * a plain ERC-20 transfer to the fee recipient. Returns the tx hash to pass to
45
+ * authorize_task as `fee_tx_hash`. Only used on the POOL rail; the agent pays
46
+ * this gas (CYBERDYNE absorbs none). USDC is 6-dp (v1 pool settles in USDC).
47
+ */
48
+ export async function payDeployFee(params: {
49
+ amountUsd: number;
50
+ recipient: string;
51
+ token: string;
52
+ }): Promise<string> {
53
+ const wallet = createWalletClient({ account: account(), chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
54
+ return wallet.writeContract({
55
+ address: params.token as `0x${string}`,
56
+ abi: ERC20_TRANSFER_ABI,
57
+ functionName: "transfer",
58
+ args: [params.recipient as `0x${string}`, parseUnits(params.amountUsd.toFixed(6), 6)],
59
+ chain: chain(),
60
+ });
61
+ }
62
+
63
+ export function hasEvmKey(): boolean {
64
+ return !!process.env.CYBERDYNE_EVM_PRIVATE_KEY;
65
+ }
66
+
67
+ function scheme() {
68
+ const pk = process.env.CYBERDYNE_EVM_PRIVATE_KEY;
69
+ if (!pk) throw new Error("CYBERDYNE_EVM_PRIVATE_KEY not set — cannot sign the escrow authorization");
70
+ const chainId = Number(process.env.CYBERDYNE_CHAIN_ID ?? 8453);
71
+ const chain = chainId === 8453 ? base : baseSepolia;
72
+ const account = privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`);
73
+ const pub = createPublicClient({ chain, transport: http(process.env.CYBERDYNE_RPC_URL) });
74
+ // toClientEvmSigner(account, PUBLIC client) — account first, public client second.
75
+ return new AuthCaptureEvmScheme(toClientEvmSigner(account, pub));
76
+ }
77
+
78
+ /** The agent's signing wallet address (so the platform can verify payer == this). */
79
+ export function evmAddress(): string {
80
+ const pk = process.env.CYBERDYNE_EVM_PRIVATE_KEY!;
81
+ return privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`).address;
82
+ }
83
+
84
+ /**
85
+ * Sign the auth-capture requirements (the `authIntent.requirements` the platform
86
+ * returns) → base64 payload the platform's authorize route consumes as `signedPayment`.
87
+ */
88
+ export async function signAuthCapture(requirements: unknown): Promise<string> {
89
+ const result = await scheme().createPaymentPayload(
90
+ 2,
91
+ requirements as Parameters<AuthCaptureEvmScheme["createPaymentPayload"]>[1],
92
+ );
93
+ return Buffer.from(JSON.stringify(result)).toString("base64");
94
+ }
package/src/server.ts CHANGED
@@ -16,7 +16,8 @@
16
16
  * assign_task — POST /api/tasks/[id]/assign → pick a human (→ authIntent)
17
17
  * authorize_task — POST /api/tasks/[id]/authorize → open the escrow hold
18
18
  * get_task — GET /api/tasks/[id] → status + submissions/claims
19
- * release_payment — POST /api/tasks/[id]/release → capture (pay) or reject
19
+ * release_payment — POST /api/tasks/[id]/release → direct-hire: capture (pay) or reject
20
+ * review_submission — POST /api/submissions/[id]/review → pool/FCFS: approve/reject one submission
20
21
  * close_task — POST /api/tasks/[id]/close → close a (multi-unit) bounty
21
22
  *
22
23
  * Auth: every networked tool sends the agent's `cyb_…` key. The REST routes take
@@ -25,12 +26,26 @@
25
26
  * `identity_token`.
26
27
  *
27
28
  * The HUMAN submit-proof step happens in the app/UI (human-only — agents cannot
28
- * submit on a human's behalf). So an agent's end-to-end flow is:
29
- * (live: get_deposit_address → send USDC → deposit) → post_task
30
- * (humans claim, or assign_task picks one)
31
- * assign_taskauthorize_task (open the hold)
32
- * → poll get_task until a submission appears
33
- * release_payment (approve capture; else rejectrefund)
29
+ * submit on a human's behalf). There are TWO settlement flows:
30
+ *
31
+ * FLOW A — DIRECT HIRE (the path that works TODAY on the live custodial USDC
32
+ * rail: real deposit escrowwithdraw on Base mainnet). You pick one human:
33
+ * get_deposit_address send USDC deposit (fund the treasury)
34
+ * post_tasksearch_humans assign_task (authIntent)
35
+ * → authorize_task (open the escrow hold)
36
+ * → poll get_task until a submission is pending
37
+ * → release_payment (approve → capture/pay; else reject → refund)
38
+ *
39
+ * FLOW B — POOL / FCFS BOUNTY (non-custodial pool escrow). Post a multi-unit
40
+ * bounty, freeze the whole budget once, and let any eligible human claim+submit
41
+ * first-come-first-served; you approve each unit. This rail is BUILT but GATED
42
+ * OFF today (server env ESCROW_POOL is not enabled), pending certification — so
43
+ * real-money non-custodial pool payouts are NOT live yet. When the server
44
+ * enables it, post_task returns an `authIntent` + a separate `deployFee`:
45
+ * post_task (quantity>1) → authorize_task (sign the budget + pay the deploy fee)
46
+ * → humans claim+submit FCFS → poll get_task
47
+ * → review_submission per pending submission (approve → capture one unit;
48
+ * reject → the slot reopens) → close_task to refund unfilled units.
34
49
  *
35
50
  * Config comes from the environment (see src/client.ts):
36
51
  * CYBERDYNE_API_URL default "https://app.cyberdyne-os.xyz"
@@ -44,7 +59,7 @@ import { CATEGORIES, TASK_CATEGORIES } from "./registry.js";
44
59
  import { ApiError, CyberdyneClient, MissingTokenError, readConfig, saveToken } from "./client.js";
45
60
 
46
61
  // `cyberdyne-mcp login` — persist the key so the MCP add line can omit it (short
47
- // DFM-style install). Runs before the server boots, then exits. The key is read
62
+ // one-time-login install). Runs before the server boots, then exits. The key is read
48
63
  // (most-private first) from: piped stdin → CYBERDYNE_LOGIN_TOKEN env → argv. argv
49
64
  // works but lands the secret in shell history / `ps`, so we steer to the others.
50
65
  if (process.argv[2] === "login") {
@@ -175,7 +190,7 @@ server.tool(
175
190
 
176
191
  server.tool(
177
192
  "post_task",
178
- "Open a task on the marketplace. Funds are NOT charged at post — the escrow hold opens later at authorize_task. On the manual rail the platform only checks the treasury can cover the budget (402 insufficient_treasury otherwise). `reward_usd` is the total budget; with quantity>1 each unit holds reward_usd/quantity. Returns the created task (with its id).",
193
+ "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.",
179
194
  {
180
195
  title: z.string().min(2).max(160).describe("Short task title."),
181
196
  category: z.enum(TASK_CATEGORIES),
@@ -204,20 +219,41 @@ server.tool(
204
219
 
205
220
  server.tool(
206
221
  "authorize_task",
207
- "Open the escrow hold for an assigned task (poster-only). On the manual rail the body is empty (logical treasury debit). On an on-chain rail pass `signed_payment` the base64 agent-signed auth-capture payload from the authIntent returned by assign_task. Idempotent once held.",
222
+ "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.",
208
223
  {
209
224
  task_id: z.string().uuid(),
210
- signed_payment: z
211
- .string()
225
+ signed_payment: z.string().optional().describe("Pre-signed base64 auth-capture payload (external/Bankr signer)."),
226
+ auth_intent: z.unknown().optional().describe("The authIntent from assign_task/post — required for MCP wallet auto-signing."),
227
+ deploy_fee: z
228
+ .unknown()
212
229
  .optional()
213
- .describe("On-chain rail only: base64-encoded signed auth-capture payload."),
230
+ .describe("POOL rail: the deployFee object {usd,recipient,token} from post_task — the MCP auto-pays it."),
231
+ fee_tx_hash: z.string().optional().describe("POOL rail: hash of an already-paid deploy-fee tx (skips auto-pay)."),
214
232
  },
215
- async ({ task_id, signed_payment }) =>
216
- guard(() =>
217
- client.rest("POST", `/api/tasks/${task_id}/authorize`, {
218
- body: signed_payment ? { signedPayment: signed_payment } : {},
219
- }),
220
- ),
233
+ async ({ task_id, signed_payment, auth_intent, deploy_fee, fee_tx_hash }) =>
234
+ guard(async () => {
235
+ let payload = signed_payment;
236
+ let feeTx = fee_tx_hash;
237
+ if ((!payload && auth_intent) || (!feeTx && deploy_fee)) {
238
+ const { hasEvmKey, signAuthCapture, payDeployFee } = await import("./evm-signer.js");
239
+ if (hasEvmKey()) {
240
+ if (!payload && auth_intent) {
241
+ const requirements = (auth_intent as { requirements?: unknown }).requirements ?? auth_intent;
242
+ payload = await signAuthCapture(requirements);
243
+ }
244
+ if (!feeTx && deploy_fee) {
245
+ const f = deploy_fee as { usd: number; recipient: string; token: string };
246
+ feeTx = await payDeployFee({ amountUsd: f.usd, recipient: f.recipient, token: f.token });
247
+ }
248
+ }
249
+ }
250
+ return client.rest("POST", `/api/tasks/${task_id}/authorize`, {
251
+ body: {
252
+ ...(payload ? { signedPayment: payload } : {}),
253
+ ...(feeTx ? { fee_tx_hash: feeTx } : {}),
254
+ },
255
+ });
256
+ }),
221
257
  );
222
258
 
223
259
  server.tool(
@@ -229,7 +265,7 @@ server.tool(
229
265
 
230
266
  server.tool(
231
267
  "release_payment",
232
- "Settle a submitted proof (poster-only). approve:true → CAPTURE: pay the human net of 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).",
268
+ "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).",
233
269
  {
234
270
  task_id: z.string().uuid(),
235
271
  approve: z.boolean().describe("true = proof meets criteria → pay; false = reject/refund."),
@@ -272,6 +308,29 @@ server.tool(
272
308
  }),
273
309
  );
274
310
 
311
+ server.tool(
312
+ "review_submission",
313
+ "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.",
314
+ {
315
+ submission_id: z.string().uuid().describe("The pending submission to review (from get_task)."),
316
+ approve: z.boolean().describe("true = proof meets criteria → capture one unit; false = reject (slot reopens)."),
317
+ score: z.number().int().min(1).max(5).optional().describe("Rating of the human's work (1–5)."),
318
+ comment: z.string().max(280).optional().describe("Optional feedback note on the human."),
319
+ reject_reason: z.string().max(1000).optional().describe("Why the proof was rejected (approve:false)."),
320
+ },
321
+ async ({ submission_id, approve, score, comment, reject_reason }) =>
322
+ guard(() =>
323
+ client.rest("POST", `/api/submissions/${submission_id}/review`, {
324
+ body: {
325
+ approve,
326
+ ...(score != null ? { score } : {}),
327
+ ...(comment ? { comment } : {}),
328
+ ...(reject_reason ? { reject_reason } : {}),
329
+ },
330
+ }),
331
+ ),
332
+ );
333
+
275
334
  server.tool(
276
335
  "close_task",
277
336
  "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.",
@@ -287,7 +346,7 @@ server.registerPrompt(
287
346
  "quickstart",
288
347
  {
289
348
  title: "CYBERDYNE quickstart",
290
- description: "How to fund, post a campaign, and pay humans end-to-end (live rail).",
349
+ description: "How to fund, post a task, and pay humans end-to-end both the direct-hire and pool/FCFS flows.",
291
350
  },
292
351
  () => ({
293
352
  messages: [
@@ -296,22 +355,28 @@ server.registerPrompt(
296
355
  content: {
297
356
  type: "text",
298
357
  text: [
299
- "You are connected to CYBERDYNE — hire and pay verified humans for tasks AI can't do alone. Settlement is REAL USDC on Base.",
358
+ "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.",
359
+ "",
360
+ "FUND (real money, custodial rail):",
361
+ "1. get_deposit_address -> the platform deposit address on Base.",
362
+ "2. Send USDC to it FROM your own verified wallet (the one you signed in with).",
363
+ "3. deposit({ tx_hash }) -> credits your treasury by the verified amount (idempotent).",
364
+ " (fund_treasury is demo/testnet only and is disabled on the live rail.)",
365
+ "Check get_treasury anytime for your balance.",
300
366
  "",
301
- "FUND (live rail, real money):",
302
- "1. get_deposit_address returns the platform deposit address on Base.",
303
- "2. Send USDC to that address FROM your own verified wallet (the one you signed in with).",
304
- "3. deposit({ tx_hash }) credits your treasury by the verified amount (idempotent).",
305
- " (fund_treasury is demo-only and is disabled on the live rail.)",
367
+ "FLOW A - DIRECT HIRE (works today): pick one human, hold, then pay.",
368
+ "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.",
369
+ "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).",
370
+ "6. Poll get_task until a submission is pending (the human's proof).",
371
+ "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
372
  "",
307
- "RUN A CAMPAIGN:",
308
- "4. post_task({ title, category, reward_usd, quantity, duration_min, difficulty }) reward_usd is the TOTAL budget; with quantity>1 each unit holds reward_usd/quantity. Use reward_usd ≥ 0.50 so the 2.5% fee is visible.",
309
- "5. Humans claim units and submit proof (the submit step is human-only, in the app you cannot submit for them). Poll get_task until a submission is pending.",
310
- " - Or pick someone yourself: search_humans({ skills, min_reputation }) assign_task({ task_id, human_id }) authorize_task({ task_id }) to open the hold.",
311
- "6. release_payment({ task_id, approve: true, score }) captures: net USDC is paid to the human, the 2.5% fee goes to the protocol wallet. approve:false refunds the hold.",
312
- "7. close_task({ task_id }) → refund any still-unclaimed units of a multi-unit bounty.",
373
+ "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:",
374
+ "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).",
375
+ "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).",
376
+ "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.",
377
+ "11. close_task({ task_id }) -> refund any still-unfilled units (the deploy fee is non-refundable).",
313
378
  "",
314
- "Check get_treasury anytime for your balance. Every payout and fee is a real on-chain tx.",
379
+ "Every payout and fee on the live rail is a real on-chain transaction.",
315
380
  ].join("\n"),
316
381
  },
317
382
  },
@@ -326,5 +391,5 @@ await server.connect(transport);
326
391
  console.error(
327
392
  `CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
328
393
  (config.token ? "" : " (no key — run `npx cyberdyne-mcp login cyb_…` or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
329
- ". Tools: list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, post_task, assign_task, authorize_task, get_task, release_payment, close_task.",
394
+ ". Tools (13): list_categories, search_humans, get_treasury, fund_treasury, get_deposit_address, deposit, post_task, assign_task, authorize_task, get_task, release_payment, review_submission, close_task.",
330
395
  );