cyberdyne-mcp 0.5.0 → 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 +66 -43
- package/dist/client.js +30 -13
- package/dist/evm-signer.js +82 -0
- package/dist/server.js +114 -36
- package/llms.txt +31 -15
- package/package.json +8 -1
- package/src/client.ts +28 -13
- package/src/evm-signer.ts +94 -0
- package/src/server.ts +125 -39
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
|
|
|
@@ -90,34 +114,28 @@ then releases payment on verify. The pattern behind x402-native traders like
|
|
|
90
114
|
CYBERDYNE_IDENTITY_TOKEN=cyb_… npm run build && npm run founder-check
|
|
91
115
|
```
|
|
92
116
|
|
|
93
|
-
## Install
|
|
94
|
-
|
|
95
|
-
Published on [npm](https://www.npmjs.com/package/cyberdyne-mcp), so `npx` runs it
|
|
96
|
-
instantly. You only need Node 18+ and your `cyb_…` agent key (mint one in the app's
|
|
97
|
-
Agent Console).
|
|
117
|
+
## Install
|
|
98
118
|
|
|
99
|
-
|
|
119
|
+
Published on [npm](https://www.npmjs.com/package/cyberdyne-mcp). Mint your `cyb_…`
|
|
120
|
+
agent key in the app's Agent Console, then:
|
|
100
121
|
|
|
101
122
|
```bash
|
|
102
|
-
|
|
123
|
+
npx cyberdyne-mcp login cyb_YOURKEY # save your key once (~/.cyberdyne/config.json, 0600)
|
|
124
|
+
claude mcp add cyberdyne -- npx -y cyberdyne-mcp
|
|
103
125
|
```
|
|
104
126
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"env": { "CYBERDYNE_IDENTITY_TOKEN": "cyb_…" }
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
127
|
+
*(Prefer not to save a login? Skip step 1 and pass it inline instead:
|
|
128
|
+
`claude mcp add cyberdyne -e CYBERDYNE_IDENTITY_TOKEN=cyb_… -- npx -y cyberdyne-mcp`.)*
|
|
129
|
+
|
|
130
|
+
### …or install the plugin (skill + MCP together)
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
/plugin marketplace add Cyberdyne-OS/cyberdyne-mcp
|
|
134
|
+
/plugin install cyberdyne@cyberdyne-os
|
|
117
135
|
```
|
|
118
136
|
|
|
119
|
-
|
|
120
|
-
|
|
137
|
+
Bundles the MCP gateway **and** the usage skill. Once connected, run
|
|
138
|
+
**`/mcp__cyberdyne__quickstart`** for the full fund → post → pay walkthrough.
|
|
121
139
|
|
|
122
140
|
Then ask the agent, e.g.:
|
|
123
141
|
|
|
@@ -125,14 +143,19 @@ Then ask the agent, e.g.:
|
|
|
125
143
|
> 10 phrases, assign it, authorize the hold, then verify and pay.*
|
|
126
144
|
|
|
127
145
|
The agent chains `search_humans → post_task → assign_task → authorize_task →
|
|
128
|
-
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).
|
|
129
149
|
|
|
130
150
|
## Honesty / accuracy
|
|
131
151
|
|
|
132
152
|
State only what is independently verifiable. This repository, its code, and the
|
|
133
153
|
fact that the tools run and call the documented endpoints are verifiable. The
|
|
134
|
-
backend is
|
|
135
|
-
|
|
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
|
|
136
159
|
metrics, any token/airdrop, named individuals, partnerships, or compliance status
|
|
137
160
|
— none are established.
|
|
138
161
|
|
package/dist/client.js
CHANGED
|
@@ -19,38 +19,55 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { homedir } from "node:os";
|
|
21
21
|
import { join } from "node:path";
|
|
22
|
-
import { readFileSync,
|
|
22
|
+
import { readFileSync, mkdirSync, openSync, writeSync, closeSync, fchmodSync, constants as FS } from "node:fs";
|
|
23
23
|
export const DEFAULT_API_URL = "https://app.cyberdyne-os.xyz";
|
|
24
24
|
/** Path to the persisted login (mode 600). */
|
|
25
25
|
export function configPath() {
|
|
26
26
|
return join(homedir(), ".cyberdyne", "config.json");
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
// Only the key is persisted. The API endpoint is intentionally NOT read from this
|
|
29
|
+
// file — a tampered config must never be able to redirect the agent's key to a
|
|
30
|
+
// hostile host (credential exfiltration). The endpoint overrides via env only.
|
|
31
|
+
function readSavedToken() {
|
|
29
32
|
try {
|
|
30
|
-
|
|
33
|
+
const parsed = JSON.parse(readFileSync(configPath(), "utf8"));
|
|
34
|
+
return typeof parsed?.identity_token === "string" ? parsed.identity_token.trim() : undefined;
|
|
31
35
|
}
|
|
32
36
|
catch {
|
|
33
|
-
return
|
|
37
|
+
return undefined;
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
|
-
/** Persist the agent key to ~/.cyberdyne/config.json
|
|
40
|
+
/** Persist the agent key to ~/.cyberdyne/config.json. Returns the path. */
|
|
37
41
|
export function saveToken(token) {
|
|
38
|
-
|
|
42
|
+
// Owner-only dir + atomic 0600 create (no world-readable window / TOCTOU), and
|
|
43
|
+
// O_NOFOLLOW so a planted symlink at the path can't redirect the write.
|
|
44
|
+
mkdirSync(join(homedir(), ".cyberdyne"), { recursive: true, mode: 0o700 });
|
|
39
45
|
const p = configPath();
|
|
40
|
-
|
|
46
|
+
const contents = JSON.stringify({ identity_token: token.trim() }, null, 2);
|
|
47
|
+
let flags = FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC;
|
|
48
|
+
if (typeof FS.O_NOFOLLOW === "number")
|
|
49
|
+
flags |= FS.O_NOFOLLOW;
|
|
50
|
+
let fd;
|
|
41
51
|
try {
|
|
42
|
-
|
|
52
|
+
fd = openSync(p, flags, 0o600);
|
|
43
53
|
}
|
|
44
54
|
catch {
|
|
45
|
-
|
|
55
|
+
// Platforms without O_NOFOLLOW semantics — fall back without it.
|
|
56
|
+
fd = openSync(p, FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC, 0o600);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
fchmodSync(fd, 0o600); // tighten perms on the open fd (covers a pre-existing file), race-free
|
|
60
|
+
writeSync(fd, contents);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
closeSync(fd);
|
|
46
64
|
}
|
|
47
65
|
return p;
|
|
48
66
|
}
|
|
49
|
-
/** Resolve config: env first, then the saved login.
|
|
67
|
+
/** Resolve config: token from env first, then the saved login. URL from env only. */
|
|
50
68
|
export function readConfig(env = process.env) {
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const token = env.CYBERDYNE_IDENTITY_TOKEN?.trim() || file.identity_token?.trim() || undefined;
|
|
69
|
+
const apiUrl = (env.CYBERDYNE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
70
|
+
const token = env.CYBERDYNE_IDENTITY_TOKEN?.trim() || readSavedToken() || undefined;
|
|
54
71
|
return { apiUrl, token };
|
|
55
72
|
}
|
|
56
73
|
/** An API error surfaced to the caller — carries the HTTP status + the API's error code. */
|
|
@@ -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,31 +26,65 @@
|
|
|
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"
|
|
37
52
|
* CYBERDYNE_IDENTITY_TOKEN the agent's cyb_ key (required for networked tools)
|
|
38
53
|
*/
|
|
54
|
+
import { readFileSync } from "node:fs";
|
|
39
55
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
40
56
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
41
57
|
import { z } from "zod";
|
|
42
58
|
import { CATEGORIES, TASK_CATEGORIES } from "./registry.js";
|
|
43
59
|
import { ApiError, CyberdyneClient, MissingTokenError, readConfig, saveToken } from "./client.js";
|
|
44
|
-
// `cyberdyne-mcp login
|
|
45
|
-
//
|
|
60
|
+
// `cyberdyne-mcp login` — persist the key so the MCP add line can omit it (short
|
|
61
|
+
// one-time-login install). Runs before the server boots, then exits. The key is read
|
|
62
|
+
// (most-private first) from: piped stdin → CYBERDYNE_LOGIN_TOKEN env → argv. argv
|
|
63
|
+
// works but lands the secret in shell history / `ps`, so we steer to the others.
|
|
46
64
|
if (process.argv[2] === "login") {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
65
|
+
const fromArg = process.argv[3]?.trim();
|
|
66
|
+
let token = "";
|
|
67
|
+
if (!process.stdin.isTTY) {
|
|
68
|
+
try {
|
|
69
|
+
token = readFileSync(0, "utf8").trim(); // piped: echo cyb_… | npx cyberdyne-mcp login
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
/* nothing piped */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
token = token || process.env.CYBERDYNE_LOGIN_TOKEN?.trim() || fromArg || "";
|
|
76
|
+
if (!token.startsWith("cyb_")) {
|
|
77
|
+
console.error("Save your CYBERDYNE key (most private first):\n" +
|
|
78
|
+
" echo cyb_<key> | npx cyberdyne-mcp login\n" +
|
|
79
|
+
" CYBERDYNE_LOGIN_TOKEN=cyb_<key> npx cyberdyne-mcp login\n" +
|
|
80
|
+
" npx cyberdyne-mcp login cyb_<key> (key is left in shell history / process list)\n" +
|
|
50
81
|
"Get your key at https://app.cyberdyne-os.xyz → Agent Console → Generate API key.");
|
|
51
82
|
process.exit(1);
|
|
52
83
|
}
|
|
84
|
+
if (fromArg && token === fromArg) {
|
|
85
|
+
console.error("⚠ Heads-up: passing the key as an argument leaves it in your shell history.\n" +
|
|
86
|
+
" Next time, pipe it instead: echo cyb_<key> | npx cyberdyne-mcp login");
|
|
87
|
+
}
|
|
53
88
|
const path = saveToken(token);
|
|
54
89
|
console.error(`✓ Saved your CYBERDYNE key to ${path}.\n` +
|
|
55
90
|
"Now run: claude mcp add cyberdyne -- npx -y cyberdyne-mcp");
|
|
@@ -102,7 +137,7 @@ server.tool("deposit", "Credit your treasury from a REAL on-chain USDC deposit (
|
|
|
102
137
|
.regex(/^0x[0-9a-fA-F]{64}$/)
|
|
103
138
|
.describe("The Base tx hash of your USDC transfer to the deposit address."),
|
|
104
139
|
}, async ({ tx_hash }) => guard(() => client.rest("POST", "/api/treasury/deposit", { body: { tx_hash } })));
|
|
105
|
-
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.", {
|
|
106
141
|
title: z.string().min(2).max(160).describe("Short task title."),
|
|
107
142
|
category: z.enum(TASK_CATEGORIES),
|
|
108
143
|
description: z.string().max(4000).optional().describe("What you need the human to do."),
|
|
@@ -118,17 +153,40 @@ server.tool("assign_task", "Assign an open task to a chosen human (poster-only)
|
|
|
118
153
|
task_id: z.string().uuid(),
|
|
119
154
|
human_id: z.string().uuid().describe("The human profile id (from search_humans / get_task claims)."),
|
|
120
155
|
}, async ({ task_id, human_id }) => guard(() => client.rest("POST", `/api/tasks/${task_id}/assign`, { body: { human_id } })));
|
|
121
|
-
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.", {
|
|
122
157
|
task_id: z.string().uuid(),
|
|
123
|
-
signed_payment: z
|
|
124
|
-
|
|
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()
|
|
125
162
|
.optional()
|
|
126
|
-
.describe("
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
}));
|
|
130
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}`)));
|
|
131
|
-
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).", {
|
|
132
190
|
task_id: z.string().uuid(),
|
|
133
191
|
approve: z.boolean().describe("true = proof meets criteria → pay; false = reject/refund."),
|
|
134
192
|
submission_id: z
|
|
@@ -159,6 +217,20 @@ server.tool("release_payment", "Settle a submitted proof (poster-only). approve:
|
|
|
159
217
|
},
|
|
160
218
|
});
|
|
161
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
|
+
})));
|
|
162
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`)));
|
|
163
235
|
// ---- Self-onboarding prompt -----------------------------------------------
|
|
164
236
|
// Surfaces as /mcp__cyberdyne__quickstart — the agent (or user) runs it once to
|
|
@@ -166,7 +238,7 @@ server.tool("close_task", "Close a (multi-unit) bounty (poster-only): refund eve
|
|
|
166
238
|
// shipped inside the MCP: guidance travels with the tools.
|
|
167
239
|
server.registerPrompt("quickstart", {
|
|
168
240
|
title: "CYBERDYNE quickstart",
|
|
169
|
-
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.",
|
|
170
242
|
}, () => ({
|
|
171
243
|
messages: [
|
|
172
244
|
{
|
|
@@ -174,22 +246,28 @@ server.registerPrompt("quickstart", {
|
|
|
174
246
|
content: {
|
|
175
247
|
type: "text",
|
|
176
248
|
text: [
|
|
177
|
-
"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.",
|
|
178
257
|
"",
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
"
|
|
182
|
-
"
|
|
183
|
-
"
|
|
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.",
|
|
184
263
|
"",
|
|
185
|
-
"
|
|
186
|
-
"
|
|
187
|
-
"
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"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).",
|
|
191
269
|
"",
|
|
192
|
-
"
|
|
270
|
+
"Every payout and fee on the live rail is a real on-chain transaction.",
|
|
193
271
|
].join("\n"),
|
|
194
272
|
},
|
|
195
273
|
},
|
|
@@ -200,4 +278,4 @@ const transport = new StdioServerTransport();
|
|
|
200
278
|
await server.connect(transport);
|
|
201
279
|
console.error(`CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
|
|
202
280
|
(config.token ? "" : " (no key — run `npx cyberdyne-mcp login cyb_…` or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
|
|
203
|
-
". 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": {
|
package/src/client.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
-
import { readFileSync,
|
|
23
|
+
import { readFileSync, mkdirSync, openSync, writeSync, closeSync, fchmodSync, constants as FS } from "node:fs";
|
|
24
24
|
|
|
25
25
|
export const DEFAULT_API_URL = "https://app.cyberdyne-os.xyz";
|
|
26
26
|
|
|
@@ -34,32 +34,47 @@ export function configPath(): string {
|
|
|
34
34
|
return join(homedir(), ".cyberdyne", "config.json");
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// Only the key is persisted. The API endpoint is intentionally NOT read from this
|
|
38
|
+
// file — a tampered config must never be able to redirect the agent's key to a
|
|
39
|
+
// hostile host (credential exfiltration). The endpoint overrides via env only.
|
|
40
|
+
function readSavedToken(): string | undefined {
|
|
38
41
|
try {
|
|
39
|
-
|
|
42
|
+
const parsed = JSON.parse(readFileSync(configPath(), "utf8")) as { identity_token?: unknown };
|
|
43
|
+
return typeof parsed?.identity_token === "string" ? parsed.identity_token.trim() : undefined;
|
|
40
44
|
} catch {
|
|
41
|
-
return
|
|
45
|
+
return undefined;
|
|
42
46
|
}
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
/** Persist the agent key to ~/.cyberdyne/config.json
|
|
49
|
+
/** Persist the agent key to ~/.cyberdyne/config.json. Returns the path. */
|
|
46
50
|
export function saveToken(token: string): string {
|
|
47
|
-
|
|
51
|
+
// Owner-only dir + atomic 0600 create (no world-readable window / TOCTOU), and
|
|
52
|
+
// O_NOFOLLOW so a planted symlink at the path can't redirect the write.
|
|
53
|
+
mkdirSync(join(homedir(), ".cyberdyne"), { recursive: true, mode: 0o700 });
|
|
48
54
|
const p = configPath();
|
|
49
|
-
|
|
55
|
+
const contents = JSON.stringify({ identity_token: token.trim() }, null, 2);
|
|
56
|
+
let flags = FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC;
|
|
57
|
+
if (typeof FS.O_NOFOLLOW === "number") flags |= FS.O_NOFOLLOW;
|
|
58
|
+
let fd: number;
|
|
50
59
|
try {
|
|
51
|
-
|
|
60
|
+
fd = openSync(p, flags, 0o600);
|
|
52
61
|
} catch {
|
|
53
|
-
|
|
62
|
+
// Platforms without O_NOFOLLOW semantics — fall back without it.
|
|
63
|
+
fd = openSync(p, FS.O_WRONLY | FS.O_CREAT | FS.O_TRUNC, 0o600);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
fchmodSync(fd, 0o600); // tighten perms on the open fd (covers a pre-existing file), race-free
|
|
67
|
+
writeSync(fd, contents);
|
|
68
|
+
} finally {
|
|
69
|
+
closeSync(fd);
|
|
54
70
|
}
|
|
55
71
|
return p;
|
|
56
72
|
}
|
|
57
73
|
|
|
58
|
-
/** Resolve config: env first, then the saved login.
|
|
74
|
+
/** Resolve config: token from env first, then the saved login. URL from env only. */
|
|
59
75
|
export function readConfig(env: NodeJS.ProcessEnv = process.env): CyberdyneConfig {
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
const token = env.CYBERDYNE_IDENTITY_TOKEN?.trim() || file.identity_token?.trim() || undefined;
|
|
76
|
+
const apiUrl = (env.CYBERDYNE_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
|
|
77
|
+
const token = env.CYBERDYNE_IDENTITY_TOKEN?.trim() || readSavedToken() || undefined;
|
|
63
78
|
return { apiUrl, token };
|
|
64
79
|
}
|
|
65
80
|
|
|
@@ -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,34 +26,69 @@
|
|
|
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"
|
|
37
52
|
* CYBERDYNE_IDENTITY_TOKEN the agent's cyb_ key (required for networked tools)
|
|
38
53
|
*/
|
|
54
|
+
import { readFileSync } from "node:fs";
|
|
39
55
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
40
56
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
41
57
|
import { z } from "zod";
|
|
42
58
|
import { CATEGORIES, TASK_CATEGORIES } from "./registry.js";
|
|
43
59
|
import { ApiError, CyberdyneClient, MissingTokenError, readConfig, saveToken } from "./client.js";
|
|
44
60
|
|
|
45
|
-
// `cyberdyne-mcp login
|
|
46
|
-
//
|
|
61
|
+
// `cyberdyne-mcp login` — persist the key so the MCP add line can omit it (short
|
|
62
|
+
// one-time-login install). Runs before the server boots, then exits. The key is read
|
|
63
|
+
// (most-private first) from: piped stdin → CYBERDYNE_LOGIN_TOKEN env → argv. argv
|
|
64
|
+
// works but lands the secret in shell history / `ps`, so we steer to the others.
|
|
47
65
|
if (process.argv[2] === "login") {
|
|
48
|
-
const
|
|
49
|
-
|
|
66
|
+
const fromArg = process.argv[3]?.trim();
|
|
67
|
+
let token = "";
|
|
68
|
+
if (!process.stdin.isTTY) {
|
|
69
|
+
try {
|
|
70
|
+
token = readFileSync(0, "utf8").trim(); // piped: echo cyb_… | npx cyberdyne-mcp login
|
|
71
|
+
} catch {
|
|
72
|
+
/* nothing piped */
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
token = token || process.env.CYBERDYNE_LOGIN_TOKEN?.trim() || fromArg || "";
|
|
76
|
+
if (!token.startsWith("cyb_")) {
|
|
50
77
|
console.error(
|
|
51
|
-
"
|
|
78
|
+
"Save your CYBERDYNE key (most private first):\n" +
|
|
79
|
+
" echo cyb_<key> | npx cyberdyne-mcp login\n" +
|
|
80
|
+
" CYBERDYNE_LOGIN_TOKEN=cyb_<key> npx cyberdyne-mcp login\n" +
|
|
81
|
+
" npx cyberdyne-mcp login cyb_<key> (key is left in shell history / process list)\n" +
|
|
52
82
|
"Get your key at https://app.cyberdyne-os.xyz → Agent Console → Generate API key.",
|
|
53
83
|
);
|
|
54
84
|
process.exit(1);
|
|
55
85
|
}
|
|
86
|
+
if (fromArg && token === fromArg) {
|
|
87
|
+
console.error(
|
|
88
|
+
"⚠ Heads-up: passing the key as an argument leaves it in your shell history.\n" +
|
|
89
|
+
" Next time, pipe it instead: echo cyb_<key> | npx cyberdyne-mcp login",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
56
92
|
const path = saveToken(token);
|
|
57
93
|
console.error(
|
|
58
94
|
`✓ Saved your CYBERDYNE key to ${path}.\n` +
|
|
@@ -154,7 +190,7 @@ server.tool(
|
|
|
154
190
|
|
|
155
191
|
server.tool(
|
|
156
192
|
"post_task",
|
|
157
|
-
"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.",
|
|
158
194
|
{
|
|
159
195
|
title: z.string().min(2).max(160).describe("Short task title."),
|
|
160
196
|
category: z.enum(TASK_CATEGORIES),
|
|
@@ -183,20 +219,41 @@ server.tool(
|
|
|
183
219
|
|
|
184
220
|
server.tool(
|
|
185
221
|
"authorize_task",
|
|
186
|
-
"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.",
|
|
187
223
|
{
|
|
188
224
|
task_id: z.string().uuid(),
|
|
189
|
-
signed_payment: z
|
|
190
|
-
|
|
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()
|
|
191
229
|
.optional()
|
|
192
|
-
.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)."),
|
|
193
232
|
},
|
|
194
|
-
async ({ task_id, signed_payment }) =>
|
|
195
|
-
guard(() =>
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
+
}),
|
|
200
257
|
);
|
|
201
258
|
|
|
202
259
|
server.tool(
|
|
@@ -208,7 +265,7 @@ server.tool(
|
|
|
208
265
|
|
|
209
266
|
server.tool(
|
|
210
267
|
"release_payment",
|
|
211
|
-
"
|
|
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).",
|
|
212
269
|
{
|
|
213
270
|
task_id: z.string().uuid(),
|
|
214
271
|
approve: z.boolean().describe("true = proof meets criteria → pay; false = reject/refund."),
|
|
@@ -251,6 +308,29 @@ server.tool(
|
|
|
251
308
|
}),
|
|
252
309
|
);
|
|
253
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
|
+
|
|
254
334
|
server.tool(
|
|
255
335
|
"close_task",
|
|
256
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.",
|
|
@@ -266,7 +346,7 @@ server.registerPrompt(
|
|
|
266
346
|
"quickstart",
|
|
267
347
|
{
|
|
268
348
|
title: "CYBERDYNE quickstart",
|
|
269
|
-
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.",
|
|
270
350
|
},
|
|
271
351
|
() => ({
|
|
272
352
|
messages: [
|
|
@@ -275,22 +355,28 @@ server.registerPrompt(
|
|
|
275
355
|
content: {
|
|
276
356
|
type: "text",
|
|
277
357
|
text: [
|
|
278
|
-
"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.",
|
|
279
366
|
"",
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
"
|
|
284
|
-
"
|
|
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.",
|
|
285
372
|
"",
|
|
286
|
-
"
|
|
287
|
-
"
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"
|
|
291
|
-
"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).",
|
|
292
378
|
"",
|
|
293
|
-
"
|
|
379
|
+
"Every payout and fee on the live rail is a real on-chain transaction.",
|
|
294
380
|
].join("\n"),
|
|
295
381
|
},
|
|
296
382
|
},
|
|
@@ -305,5 +391,5 @@ await server.connect(transport);
|
|
|
305
391
|
console.error(
|
|
306
392
|
`CYBERDYNE MCP server running on stdio → ${config.apiUrl}` +
|
|
307
393
|
(config.token ? "" : " (no key — run `npx cyberdyne-mcp login cyb_…` or set CYBERDYNE_IDENTITY_TOKEN; networked tools error until then)") +
|
|
308
|
-
". 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.",
|
|
309
395
|
);
|