cyberdyne-mcp 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,8 +2,15 @@
2
2
 
3
3
  This is the **agent-facing** side of CYBERDYNE. The app at
4
4
  [app.cyberdyne-os.xyz](https://app.cyberdyne-os.xyz) is what a human sees; this is
5
- the door an AI agent walks through to **discover, hire, verify and pay** that
6
- human — no human clicking buttons required.
5
+ the door an AI agent walks through to **post bounties, verify and pay** verified
6
+ humans — no human clicking buttons required.
7
+
8
+ CYBERDYNE is **one non-custodial FCFS bounty rail**. There is **no direct hire**.
9
+ Every task is an open first-come-first-served bounty: you freeze a budget, **any**
10
+ eligible human submits, and you approve (pay one unit) or reject (reopen the slot)
11
+ each submission. If CYBERDYNE's operator is ever down, you can **`reclaim`** your
12
+ unfilled budget directly from the audited escrow yourself — the deepest
13
+ non-custodial guarantee.
7
14
 
8
15
  It's a [Model Context Protocol](https://modelcontextprotocol.io) server. Any
9
16
  MCP-capable agent (Claude Desktop, Claude Code, or a custom client) connects over
@@ -17,26 +24,46 @@ An agent can go from **nothing** to **wallet + API key + ready to post/pay** wit
17
24
  one command — no web dashboard, no manual key copy/paste:
18
25
 
19
26
  ```bash
20
- npx -y cyberdyne-mcp onboard # generates a wallet + mints your cyb_ API key (no dashboard)
27
+ npx -y cyberdyne-mcp onboard # create a wallet (or import yours) + mint your cyb_ API key (no dashboard)
21
28
  claude mcp add cyberdyne -- npx -y cyberdyne-mcp # the MCP now auto-uses the saved key
22
29
  ```
23
30
 
24
- `onboard` resolves a wallet (uses `CYBERDYNE_EVM_PRIVATE_KEY` if set, else a wallet
25
- it generated on a prior run, else it **generates a fresh one**), signs in to
26
- CYBERDYNE with it (SIWE just a signature, no gas, no transaction), mints your
27
- `cyb_` agent key, and saves **both** to `~/.cyberdyne/config.json` (mode `0600`).
28
- It prints your wallet address and the `cyb_` key **once**. The same generated
29
- wallet is then used automatically for pool-budget signing — zero env vars.
31
+ `onboard` resolves a wallet, signs in to CYBERDYNE with it (SIWE — just a
32
+ signature, no gas, no transaction), mints your `cyb_` agent key, and saves **both**
33
+ to `~/.cyberdyne/config.json` (mode `0600`). It prints your wallet address and the
34
+ `cyb_` key **once**. The same wallet is then used automatically for pool-budget
35
+ signing and `reclaim` zero env vars.
36
+
37
+ **Import your own wallet, or create a fresh one:**
38
+
39
+ ```bash
40
+ # Import a private key or a BIP-39 mnemonic — pipe it (most private, off shell history):
41
+ echo 0xYOUR_PRIVATE_KEY | npx -y cyberdyne-mcp onboard --import
42
+ echo "twelve word mnemonic …" | npx -y cyberdyne-mcp onboard --import
43
+ CYBERDYNE_IMPORT_KEY=0xYOUR_KEY npx -y cyberdyne-mcp onboard --import # or via env
44
+ npx -y cyberdyne-mcp onboard --import 0xYOUR_KEY # works too, but lands in shell history (you'll be warned)
45
+
46
+ # Generate a brand-new wallet:
47
+ npx -y cyberdyne-mcp onboard --create
48
+ ```
49
+
50
+ With **no flag in a terminal**, `onboard` asks: paste an existing key/mnemonic, or
51
+ press enter to create a fresh wallet. In a non-interactive/CI shell with no flag or
52
+ `CYBERDYNE_IMPORT_KEY`, it defaults to **create**. A mnemonic derives account index
53
+ 0 (`m/44'/60'/0'/0/0`). An imported key is validated (0x + 64 hex, or a valid BIP-39
54
+ mnemonic) before any network call. `CYBERDYNE_EVM_PRIVATE_KEY` (env) still works as a
55
+ no-flag default and overrides the saved wallet.
30
56
 
31
57
  An agent already running inside an LLM with this MCP connected can **self-onboard
32
58
  with zero web interaction** by calling the `onboard` tool — it's the one tool that
33
- works without an existing key and bootstraps everything else. After that it can
34
- fund (`get_deposit_address` send USDC on Base `deposit`) and `post_task` and
35
- `withdraw_treasury` to recover unspent treasury to your wallet.
59
+ works without an existing key and bootstraps everything else. (The `onboard` tool
60
+ generates/reuses a wallet; to **import** your own, use the `--import` CLI.) After
61
+ that it can fund (`get_deposit_address` send USDC on Base `deposit`) and
62
+ `post_task` — and `withdraw_treasury` to recover unspent treasury to your wallet.
36
63
 
37
64
  > Mirrors the Bankr CLI UX (`bankr login` → wallet + API key in one shot). The
38
65
  > human **submit-proof** step is intentionally still in the app (human-only); every
39
- > agent-side action — onboard, fund, post, assign, authorize, review, close — is
66
+ > agent-side action — onboard, fund, post, authorize, review, close, reclaim — is
40
67
  > headless.
41
68
 
42
69
  ## CLI
@@ -80,43 +107,41 @@ No key is hardcoded anywhere. `list_categories` and `onboard` work without a tok
80
107
  (`onboard` mints one); every other tool returns a clear error until a key is set —
81
108
  via `CYBERDYNE_IDENTITY_TOKEN`, a saved `onboard`/`login`, or the `onboard` tool.
82
109
 
83
- ## The flows
110
+ ## The flow
84
111
 
85
112
  An agent cannot submit proof on a human's behalf — the **submit-proof step is
86
- human-only and happens in the app/UI**. Funding is the same for both flows: on the
87
- **live** rail fund with real USDC — `get_deposit_address` returns where to send,
88
- then `deposit` credits your treasury from the tx hash. (`fund_treasury` is a
89
- **testnet/demo** top-up and is disabled when the platform is live.)
90
-
91
- There are two settlement flows:
113
+ human-only and happens in the app/UI**. On the **live** rail fund with real USDC —
114
+ `get_deposit_address` returns where to send, then `deposit` credits your treasury
115
+ from the tx hash. (`fund_treasury` is a **testnet/demo** top-up and is disabled when
116
+ the platform is live.)
92
117
 
93
- ### Flow A direct hire (the path live today)
94
-
95
- Real USDC on Base via the **custodial rail** (deposit escrow → withdraw). You
96
- pick one human, open the hold, then pay on a valid proof.
118
+ There is **one** settlement model: the **non-custodial FCFS pool bounty**. You
119
+ freeze a budget once; **any** eligible human submits first-come-first-served; you
120
+ approve each unit (pay one) or reject it (the slot reopens). `post_task` returns an
121
+ `authIntent` (the whole-budget authorization to sign) plus a separate `deployFee`
122
+ (a non-refundable 2.5% USDC / 5% other-token fee tx).
97
123
 
98
124
  ```
99
- get_deposit_address → send USDC → deposit (fund the treasury)
100
- → post_task search_humansassign_task (pick a human)
101
- → authorize_task (open the escrow hold; custodial = just task_id)
102
- poll get_task until a submission is pending
103
- release_payment (approve → capture/pay; else reject → refund)
125
+ get_deposit_address → send USDC → deposit (optional, fund the treasury)
126
+ → post_task ({ …, quantity }) { task, authIntent, deployFee }
127
+ → authorize_task ({ task_id, auth_intent, deploy_fee }) (sign the budget + pay the deploy fee; freeze the whole budget on the audited escrow)
128
+ humans submit FCFS poll get_task
129
+ review_submission per pending submission (approve → capture one unit, full reward in-token; reject → slot reopens)
130
+ → close_task (operator voids the unfilled budget back to you; the deploy fee is non-refundable)
104
131
  ```
105
132
 
106
- ### Flow Bpool / FCFS bounty
133
+ ### Trustless backstop`reclaim`
107
134
 
108
- One frozen budget, many humans claim and submit first-come-first-served; you
109
- approve each unit. This **non-custodial pool escrow rail is built but gated off**
110
- on the server today (env `ESCROW_POOL` is not enabled), pending certification —
111
- so real-money non-custodial pool payouts are **not live yet**. When the operator
112
- enables it, `post_task` returns an `authIntent` plus a separate `deployFee`:
135
+ `close_task` asks CYBERDYNE's operator to void the unfilled budget. If the operator
136
+ is ever **down**, you don't need it: after the on-chain **authorization deadline**,
137
+ your own wallet (the budget's `payer`) calls the audited escrow's payer-only
138
+ `reclaim(paymentInfo)` **directly**, with zero platform involvement, and recovers
139
+ the unfilled budget itself. This is the deepest non-custodial guarantee.
113
140
 
114
141
  ```
115
- post_task (quantity > 1) { task, authIntent, deployFee }
116
- authorize_task ({ auth_intent, deploy_fee }) (sign the budget + pay the deploy fee; freeze the whole budget)
117
- humans claim + submit FCFS poll get_task
118
- → review_submission per pending submission (approve → capture one unit; reject → slot reopens)
119
- → close_task (refund unfilled units; the deploy fee is non-refundable)
142
+ reclaim ({ task_id }) your MCP wallet reads escrow_payment_info, reconstructs the exact
143
+ PaymentInfo struct, and calls reclaim() on the audited escrow on Base.
144
+ Errors clearly if it's too early, already settled, or you're not the payer.
120
145
  ```
121
146
 
122
147
  ## Tools → live endpoints
@@ -131,20 +156,20 @@ post_task (quantity > 1) → { task, authIntent, depl
131
156
  | `get_deposit_address` | `GET /api/treasury/deposit` | Where to send real USDC to fund the treasury (live rail). |
132
157
  | `deposit` | `POST /api/treasury/deposit` | Credit the treasury from a real on-chain USDC deposit (tx hash). |
133
158
  | `withdraw_treasury` | `POST /api/treasury/withdraw` | Recover unspent treasury to your wallet — pull USDC back to your own verified deposit wallet on Base (live rail; no destination param, so funds can only go to you). |
134
- | `post_task` | `POST /api/tasks` | Open a task. `reward_usd` is the budget; not charged until authorize. Pool/FCFS response also carries `authIntent` + `deployFee`. |
135
- | `assign_task` | `POST /api/tasks/[id]/assign` | Direct hire: assign to a human; returns `{ task, authIntent }` (authIntent is `null` on the custodial/manual rail). |
136
- | `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`. |
159
+ | `post_task` | `POST /api/tasks` | Open an FCFS pool bounty. `reward_usd` is the total budget; `quantity` units; not charged until authorize. Response carries `authIntent` + `deployFee`. |
160
+ | `authorize_task` | `POST /api/tasks/[id]/authorize` | Freeze the whole budget on the audited escrow. With a signing wallet: pass `auth_intent` + `deploy_fee` (the MCP signs + pays the fee); or pre-made `signed_payment` + `fee_tx_hash`. |
137
161
  | `get_task` | `GET /api/tasks/[id]` | Task + the submissions/claims the poster may see. Poll for a `pending` submission. |
138
- | `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. |
139
- | `review_submission` | `POST /api/submissions/[id]/review` | **Pool/FCFS** settle: `approve:true` capture one unit; `approve:false` reject (slot reopens). |
140
- | `close_task` | `POST /api/tasks/[id]/close` | Close a (multi-unit) bounty; refund still-held units. |
141
-
142
- The live rail today is the **custodial USDC escrow** (real deposit → escrow →
143
- withdraw on Base mainnet): at `authorize_task` the budget is held; on
144
- `release_payment` it is captured to the human (net of the platform fee) or
145
- refunded. The **non-custodial pool/FCFS rail** (deploy-fee + `review_submission`)
146
- is built but gated off on the server until certification. `search_humans` goes
147
- through the a2a JSON-RPC gateway because the REST `GET /api/humans` is session-only.
162
+ | `review_submission` | `POST /api/submissions/[id]/review` | Settle one submission: `approve:true` → capture one unit (full reward in-token); `approve:false` → reject (slot reopens). This is how you pay humans. |
163
+ | `close_task` | `POST /api/tasks/[id]/close` | Close the bounty; the operator voids the unfilled remainder back to you. The deploy fee is non-refundable. |
164
+ | `reclaim` | on-chain `reclaim(paymentInfo)` | **Trustless backstop.** After the authorization deadline, your wallet (the payer) recovers the unfilled budget directly from the audited escrow — no CYBERDYNE operator. Returns `{ ok, tx_hash, reclaimed }`. |
165
+
166
+ The settlement model is the **non-custodial FCFS pool escrow** (freeze-at-deploy on
167
+ the audited base/commerce-payments `AuthCaptureEscrow`): at `authorize_task` the whole
168
+ budget is frozen; `review_submission` captures one unit to the human (full reward,
169
+ in-token); `close_task` voids the unfilled remainder via the operator, and `reclaim`
170
+ is your own payer-only on-chain recovery if the operator is ever unavailable.
171
+ `search_humans` (discovery only there is no direct hire) goes through the a2a
172
+ JSON-RPC gateway because the REST `GET /api/humans` is session-only.
148
173
 
149
174
  ## Run it
150
175
 
@@ -207,28 +232,24 @@ Bundles the MCP gateway **and** the usage skill. Once connected, run
207
232
 
208
233
  Then ask the agent, e.g.:
209
234
 
210
- > *Find a Spanish-speaking human who can record audio, post a $3.50 task to read
211
- > 10 phrases, assign it, authorize the hold, then verify and pay.*
235
+ > *Post a $3.50 FCFS bounty for a human to record 10 phrases, freeze the budget,
236
+ > then verify the first valid submission and pay it.*
212
237
 
213
- The agent chains `search_humanspost_taskassign_taskauthorize_task
214
- get_task → release_payment` on its own (Flow A, direct hire). For an open
215
- first-come bounty it instead posts with `quantity > 1` and settles each unit with
216
- `review_submission` (Flow B, pool/FCFS active once the pool rail is enabled).
238
+ The agent chains `post_taskauthorize_taskget_taskreview_submission
239
+ close_task` on its own. If CYBERDYNE's operator is ever down, it can `reclaim` the
240
+ unfilled budget directly from the escrow after the authorization deadline. There is
241
+ **no direct hire**every task is an open FCFS pool bounty.
217
242
 
218
243
  ## Honesty / accuracy
219
244
 
220
245
  State only what is independently verifiable. This repository, its code, and the
221
246
  fact that the tools run and call the documented endpoints are verifiable. The
222
- backend is **pre-launch**. The live settlement rail today is the **custodial USDC
223
- rail** (real deposit → escrow → withdraw on Base mainnet). The **non-custodial
224
- pool/FCFS rail** (deploy-fee + `review_submission`) is built but **gated off** on
225
- the server pending certification so real-money non-custodial pool payouts are
226
- **not live yet**. Do **not** assert funding, valuation, investors, revenue or user
227
- metrics, any token/airdrop, named individuals, partnerships, or compliance status
228
- — none are established.
247
+ backend is **pre-launch**. The settlement model is the **non-custodial FCFS pool
248
+ escrow** (freeze-at-deploy on the audited base/commerce-payments `AuthCaptureEscrow`,
249
+ with the agent's own payer-only `reclaim` backstop). Do **not** assert funding,
250
+ valuation, investors, revenue or user metrics, any token/airdrop, named individuals,
251
+ partnerships, or compliance status none are established.
229
252
 
230
253
  ## Follow-ups (not in this server)
231
254
 
232
- - The **paid `hire` path** (x402 402→pay→200 over `POST /api/a2a`) is implemented
233
- on the platform but not surfaced here — it needs an x402 signing client.
234
255
  - A **remote/HTTP MCP** variant (vs. stdio) for hosted agents.
package/dist/cli.js CHANGED
@@ -160,9 +160,9 @@ export async function runPost(argv) {
160
160
  const res = await c.rest("POST", "/api/tasks", { body });
161
161
  const taskId = res.task?.id;
162
162
  console.error(` ✓ posted — task ${taskId}`);
163
- // Custodial rail (no authIntent): nothing else to do; the hold opens later.
163
+ // Custodial/testnet rail (no authIntent): nothing else to do; the hold opens later.
164
164
  if (!res.authIntent || !res.deployFee) {
165
- console.error(`\n✓ Task ${taskId} is open (custodial rail). Next: assign a human + authorize, then release on a valid proof.`);
165
+ console.error(`\n✓ Task ${taskId} is open. Humans submit FCFS; review each submission to capture a unit (review_submission), then close_task.`);
166
166
  process.exit(0);
167
167
  }
168
168
  // POOL rail — the autonomous `bankr launch` path. Sign the budget with the saved
@@ -13,7 +13,7 @@
13
13
  * Nothing here moves funds or pays gas; it only produces a signature.
14
14
  */
15
15
  import { privateKeyToAccount } from "viem/accounts";
16
- import { createPublicClient, createWalletClient, http, parseUnits } from "viem";
16
+ import { createPublicClient, createWalletClient, getAddress, http, parseUnits } from "viem";
17
17
  import { base, baseSepolia } from "viem/chains";
18
18
  import { AuthCaptureEvmScheme, toClientEvmSigner } from "@x402/evm";
19
19
  import { readSavedWalletKey } from "./client.js";
@@ -99,3 +99,115 @@ export async function signAuthCapture(requirements) {
99
99
  const result = await scheme().createPaymentPayload(2, requirements);
100
100
  return Buffer.from(JSON.stringify(result)).toString("base64");
101
101
  }
102
+ // ── Trustless self-recovery: reclaim ────────────────────────────────────────
103
+ // The deepest non-custodial guarantee. The AGENT (payer) calls the AUDITED
104
+ // AuthCaptureEscrow `reclaim(paymentInfo)` DIRECTLY — no CYBERDYNE operator. The
105
+ // contract requires `msg.sender == paymentInfo.payer` and `block.timestamp >=
106
+ // authorizationExpiry`, then returns the uncaptured remainder to the payer. This
107
+ // is the same PaymentInfo tuple shape used by authorize/capture/void.
108
+ /** The canonical audited base/commerce-payments AuthCaptureEscrow on Base. */
109
+ export const AUTH_CAPTURE_ESCROW_ADDRESS = "0xBdEA0D1bcC5966192B070Fdf62aB4EF5b4420cff";
110
+ /** PaymentInfo tuple components — EXACTLY the order the escrow hashes/expects. */
111
+ const PAYMENT_INFO_COMPONENTS = [
112
+ { name: "operator", type: "address" },
113
+ { name: "payer", type: "address" },
114
+ { name: "receiver", type: "address" },
115
+ { name: "token", type: "address" },
116
+ { name: "maxAmount", type: "uint120" },
117
+ { name: "preApprovalExpiry", type: "uint48" },
118
+ { name: "authorizationExpiry", type: "uint48" },
119
+ { name: "refundExpiry", type: "uint48" },
120
+ { name: "minFeeBps", type: "uint16" },
121
+ { name: "maxFeeBps", type: "uint16" },
122
+ { name: "feeReceiver", type: "address" },
123
+ { name: "salt", type: "uint256" },
124
+ ];
125
+ const paymentInfoArg = { name: "paymentInfo", type: "tuple", components: PAYMENT_INFO_COMPONENTS };
126
+ /** Minimal ABI: just `reclaim(PaymentInfo)` — payer-callable after authorizationExpiry. */
127
+ export const reclaimAbi = [
128
+ { type: "function", name: "reclaim", stateMutability: "nonpayable", inputs: [paymentInfoArg], outputs: [] },
129
+ ];
130
+ /** Decode the base64 signed payment → { payer (authorization.from), validBefore, salt }. */
131
+ function decodeSignedPayment(signedPayment) {
132
+ let payload;
133
+ try {
134
+ const decoded = JSON.parse(Buffer.from(signedPayment, "base64").toString("utf8"));
135
+ payload = (decoded.payload ?? decoded);
136
+ }
137
+ catch {
138
+ throw new Error("malformed_signed_payment");
139
+ }
140
+ const auth = payload.authorization;
141
+ const permit = payload.permit2Authorization;
142
+ const from = auth?.from ?? permit?.from;
143
+ const validBefore = auth?.validBefore ?? permit?.deadline;
144
+ const salt = payload.salt;
145
+ if (!from || !validBefore || !salt)
146
+ throw new Error("incomplete_signed_payment (need authorization.from + validBefore + salt)");
147
+ return { payer: from, preApprovalExpiry: Number(validBefore), salt, value: auth?.value };
148
+ }
149
+ /**
150
+ * Reconstruct the on-chain PaymentInfo struct EXACTLY like the platform's
151
+ * `structFor` (lib/payments/escrow-pool.ts) — the contract recomputes the hash, so
152
+ * every field must match the one signed at deploy or `reclaim` reverts.
153
+ */
154
+ function structFor(info, payer, preApprovalExpiry, salt, value) {
155
+ const x = info.extra;
156
+ return {
157
+ operator: getAddress(x.captureAuthorizer),
158
+ payer: getAddress(payer),
159
+ receiver: getAddress(info.payTo),
160
+ token: getAddress(info.asset),
161
+ maxAmount: BigInt(value ?? info.amount),
162
+ preApprovalExpiry,
163
+ authorizationExpiry: x.captureDeadline,
164
+ refundExpiry: x.refundDeadline,
165
+ minFeeBps: x.minFeeBps ?? 0,
166
+ maxFeeBps: x.maxFeeBps ?? 0,
167
+ feeReceiver: getAddress(x.feeRecipient),
168
+ salt: BigInt(salt),
169
+ };
170
+ }
171
+ /**
172
+ * TRUSTLESS SELF-RECOVERY. The agent (payer) calls `reclaim(paymentInfo)` on the
173
+ * audited escrow directly to recover its OWN unfilled budget — no operator. Asserts
174
+ * the MCP wallet == payer (reclaim is payer-only on-chain) and that the authorization
175
+ * deadline has passed, then signs+sends from the agent wallet and waits for the receipt.
176
+ * Returns { ok, tx_hash, reclaimed } (reclaimed = the human-readable atomic maxAmount).
177
+ */
178
+ export async function reclaimBudget(info) {
179
+ const signed = info.signedPayment;
180
+ if (!signed)
181
+ throw new Error("no signedPayment on this task — it was never frozen on the escrow (nothing to reclaim)");
182
+ const { payer, preApprovalExpiry, salt, value } = decodeSignedPayment(signed);
183
+ // reclaim is payer-only on-chain — assert this wallet IS the payer before sending.
184
+ const me = evmAddress();
185
+ if (me.toLowerCase() !== payer.toLowerCase()) {
186
+ throw new Error(`wallet ${me} is not the payer (${payer}) of this budget — reclaim is payer-only on-chain. ` +
187
+ "Use the same wallet that authorized/froze the budget.");
188
+ }
189
+ const paymentInfo = structFor(info, payer, preApprovalExpiry, salt, value);
190
+ // reclaim requires block.timestamp >= authorizationExpiry — check first for a clear error.
191
+ const now = Math.floor(Date.now() / 1000);
192
+ if (now < paymentInfo.authorizationExpiry) {
193
+ const when = new Date(paymentInfo.authorizationExpiry * 1000).toISOString();
194
+ throw new Error(`too_early: the authorization deadline has not passed yet (reclaim opens at ${when}, ` +
195
+ `in ~${Math.ceil((paymentInfo.authorizationExpiry - now) / 60)} min). ` +
196
+ "Until then, close_task asks CYBERDYNE's operator to void the unfilled budget back to you.");
197
+ }
198
+ const wallet = createWalletClient({ account: account(), chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
199
+ const pub = createPublicClient({ chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
200
+ const hash = await wallet.writeContract({
201
+ address: AUTH_CAPTURE_ESCROW_ADDRESS,
202
+ abi: reclaimAbi,
203
+ functionName: "reclaim",
204
+ args: [paymentInfo],
205
+ chain: chain(),
206
+ });
207
+ const receipt = await pub.waitForTransactionReceipt({ hash });
208
+ if (receipt.status !== "success") {
209
+ throw new Error("reclaim reverted on-chain — the budget may already be fully captured/settled, already reclaimed, " +
210
+ "or the deadline window is wrong. Nothing was recovered.");
211
+ }
212
+ return { ok: true, tx_hash: hash, reclaimed: String(paymentInfo.maxAmount) };
213
+ }
@@ -65,24 +65,27 @@ const posted = await call("post_task", {
65
65
  });
66
66
  const taskId = posted.task.id;
67
67
  console.log(`post_task -> ${taskId} reward $${posted.task.reward_usd}`);
68
- // 3. Assign it to the chosen human and open the escrow hold.
69
- if (human?.id) {
70
- const assigned = await call("assign_task", { task_id: taskId, human_id: human.id });
71
- console.log(`assign_task -> ${assigned.task.status}, authIntent ${assigned.authIntent ? "present" : "null"}`);
72
- const authd = await call("authorize_task", { task_id: taskId });
73
- console.log(`authorize_task -> escrow ${authd.task?.escrow_status}`);
74
- }
68
+ // (search_humans is discovery only there is NO direct hire. The task is an open
69
+ // FCFS pool bounty: ANY eligible human submits first-come-first-served.)
70
+ void human;
71
+ // 3. Authorize freeze the budget (POOL: authIntent + deployFee; manual: just task_id).
72
+ const authd = await call("authorize_task", {
73
+ task_id: taskId,
74
+ ...(posted.authIntent ? { auth_intent: posted.authIntent } : {}),
75
+ ...(posted.deployFee ? { deploy_fee: posted.deployFee } : {}),
76
+ });
77
+ console.log(`authorize_task -> escrow ${authd.task?.escrow_status}`);
75
78
  // 4. Poll until the human's proof is in (they submit in the app — human-only).
76
79
  const got = await call("get_task", { task_id: taskId });
77
80
  console.log(`get_task -> ${got.task.status} submissions ${got.submissions.length}`);
78
- // 5. If the proof is in, verify and release payment to the human.
81
+ // 5. If the proof is in, verify it and capture one unit to the human.
79
82
  const pending = (got.submissions ?? []).find((s) => s.status === "pending");
80
83
  if (pending) {
81
- const settled = await call("release_payment", { task_id: taskId, approve: true, score: 5 });
82
- console.log(`release_payment -> task ${settled.task?.status}`);
84
+ const settled = await call("review_submission", { submission_id: pending.id, approve: true, score: 5 });
85
+ console.log(`review_submission -> captured ${JSON.stringify(settled).slice(0, 80)}`);
83
86
  console.log("\n[agent] has a human verification it could not produce itself, and the contributor is paid.");
84
87
  }
85
88
  else {
86
- console.log("\n[agent] task is live and the hold is open; the human submits proof in the app, then the agent releases payment.");
89
+ console.log("\n[agent] bounty is live and the budget is frozen; humans submit proof in the app FCFS, then the agent reviews each.");
87
90
  }
88
91
  await client.close();