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 +51 -22
- package/dist/evm-signer.js +82 -0
- package/dist/server.js +90 -32
- package/llms.txt +31 -15
- package/package.json +8 -1
- package/src/evm-signer.ts +94 -0
- package/src/server.ts +100 -35
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
|
|
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**.
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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` |
|
|
56
|
-
| `authorize_task` | `POST /api/tasks/[id]/authorize` | Open the escrow hold
|
|
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
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
129
|
-
|
|
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).
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* →
|
|
33
|
-
*
|
|
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 → escrow → withdraw on Base mainnet). You pick one human:
|
|
33
|
+
* get_deposit_address → send USDC → deposit (fund the treasury)
|
|
34
|
+
* → post_task → search_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
|
-
//
|
|
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.
|
|
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
|
|
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
|
-
|
|
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("
|
|
147
|
-
|
|
148
|
-
|
|
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", "
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
200
|
-
"
|
|
201
|
-
"
|
|
202
|
-
"
|
|
203
|
-
"
|
|
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
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"
|
|
209
|
-
"
|
|
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
|
-
"
|
|
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
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
33
|
+
- fund_treasury — POST /api/treasury/fund; demo/testnet top-up (disabled on the live rail)
|
|
34
|
+
- get_deposit_address — GET /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
|
-
-
|
|
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
|
-
##
|
|
44
|
+
## Flows
|
|
42
45
|
|
|
43
|
-
The human submit-proof step is human-only (in the app/UI).
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
The human submit-proof step is human-only (in the app/UI). Funding both flows:
|
|
47
|
+
get_deposit_address → send 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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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.
|
|
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).
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* →
|
|
33
|
-
*
|
|
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 → escrow → withdraw on Base mainnet). You pick one human:
|
|
33
|
+
* get_deposit_address → send USDC → deposit (fund the treasury)
|
|
34
|
+
* → post_task → search_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
|
-
//
|
|
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.
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
"
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
302
|
-
"
|
|
303
|
-
"
|
|
304
|
-
"
|
|
305
|
-
"
|
|
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
|
-
"
|
|
308
|
-
"
|
|
309
|
-
"
|
|
310
|
-
"
|
|
311
|
-
"
|
|
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
|
-
"
|
|
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
|
);
|