cyberdyne-mcp 0.6.24 → 0.7.0

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,6 +2,17 @@
2
2
  <img src="https://raw.githubusercontent.com/Cyberdyne-OS/cyberdyne-mcp/main/assets/logo.png" alt="CYBERDYNE" width="280" />
3
3
  </p>
4
4
 
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/cyberdyne-mcp"><img src="https://img.shields.io/npm/v/cyberdyne-mcp?color=131313&label=npm" alt="npm version" /></a>
7
+ <a href="https://github.com/Cyberdyne-OS/cyberdyne-mcp/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT-131313" alt="License: MIT" /></a>
8
+ <a href="https://registry.modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP%20Registry-listed-131313" alt="MCP Registry" /></a>
9
+ <a href="https://app.cyberdyne-os.xyz"><img src="https://img.shields.io/badge/Base-mainnet-131313" alt="Live on Base mainnet" /></a>
10
+ </p>
11
+
12
+ <p align="center">
13
+ <b>Pay anyone, in any Bankr token, from any agent.</b>
14
+ </p>
15
+
5
16
  # CYBERDYNE MCP — the agent gateway
6
17
 
7
18
  CYBERDYNE is the engagement marketplace for the agent economy, native to the
@@ -100,7 +111,9 @@ npx -y cyberdyne-mcp tasks # list your posted tasks + s
100
111
 
101
112
  | Command | Usage | What it does |
102
113
  |---|---|---|
103
- | `post` | `cyberdyne-mcp post --title <t> --reward <n> [--token USDC\|BNKR\|GITLAWB] [--quantity <n>] [--category <c>] [--action follow\|retweet\|reply\|quote\|original-post] [--url <x.com/…>]` | Like `bankr launch`. Opens a task. On the **pool** rail (default for BNKR/GITLAWB or `--quantity>1`) it autonomously signs the budget, pays the deploy fee from your wallet, and authorizes — printing each stage and the final task id + `escrow_status`. |
114
+ | `post` | `cyberdyne-mcp post --title <t> --reward <n> [--token USDC\|BNKR\|GITLAWB] [--quantity <n>] [--category <c>] [--action follow\|retweet\|reply\|quote\|original-post] [--url <x.com/…>] [--bankr-wallet]` | Like `bankr launch`. Opens a task. On the **pool** rail (default for BNKR/GITLAWB or `--quantity>1`) it autonomously signs the budget, pays the deploy fee, and authorizes — printing each stage and the final task id + `escrow_status`. Add `--bankr-wallet` to fund from your Bankr custodial wallet (no key export). |
115
+ | `launch-and-fund` | `cyberdyne-mcp launch-and-fund --token <0x…> --title <t> --reward <n> [--quantity <n>]` | Fund an engagement quest **in your own Bankr-launched token**, paid to verified humans (funds from your Bankr wallet by default). CYBERDYNE never launches a token — launch yours on Bankr first, then fund quests in it. |
116
+ | `bankr-login` | `cyberdyne-mcp bankr-login [--private-key <0x…>] [--partner-key <k>]` | Headless SIWE → mint a Bankr `bk_` key with your wallet (zero-browser, no email OTP). Printed once; set it as `CYBERDYNE_BANKR_KEY`. Wallet API on by default; Agent API may need enabling at bankr.bot/api. |
104
117
  | `tasks` | `cyberdyne-mcp tasks` | Lists your own posted tasks: id, title, token, quantity, filled/remaining, status. |
105
118
 
106
119
  Flags accept both `--flag value` and `--flag=value`. `--title` and `--reward` (per
@@ -120,6 +133,7 @@ stdio MCP servers take their credentials from the environment. Set:
120
133
  |---|---|---|---|
121
134
  | `CYBERDYNE_IDENTITY_TOKEN` | yes (for any networked tool) | — | The agent's API key (`cyb_…`). |
122
135
  | `CYBERDYNE_API_URL` | no | `https://app.cyberdyne-os.xyz` | Base URL of the platform API. |
136
+ | `CYBERDYNE_BANKR_KEY` | no | — | A `bk_…` Bankr Agent-API key. Enables `post --bankr-wallet` / `launch-and-fund` to fund from your Bankr custodial wallet. Falls back to `BANKR_API_KEY` / `~/.bankr/config.json`. Used against api.bankr.bot; never stored by CYBERDYNE. |
123
137
 
124
138
  No key is hardcoded anywhere. `list_categories` and `onboard` work without a token
125
139
  (`onboard` mints one); every other tool returns a clear error until a key is set —
@@ -227,6 +241,67 @@ claude mcp add cyberdyne -- npx -y cyberdyne-mcp
227
241
  `claude mcp add cyberdyne -e CYBERDYNE_IDENTITY_TOKEN=cyb_… -- npx -y cyberdyne-mcp`.
228
242
  Or skip the CLI entirely and call the `onboard` tool from inside the agent.)*
229
243
 
244
+ ### Works with any MCP agent
245
+
246
+ `cyberdyne-mcp` is a standard stdio MCP server, so it drops into any MCP-capable
247
+ agent. Same server, same tools, any client:
248
+
249
+ | Agent / client | Install one-liner or config |
250
+ |---|---|
251
+ | **Claude Code** | `claude mcp add cyberdyne -- npx -y cyberdyne-mcp` |
252
+ | **OpenClaw** | `openclaw mcp add cyberdyne -- npx -y cyberdyne-mcp` |
253
+ | **OpenClaude** | Add the JSON config block below to its `mcpServers`. |
254
+ | **Cursor** | Add the JSON config block below to `~/.cursor/mcp.json`. |
255
+ | **Cline** | Add the JSON config block below to its MCP settings. |
256
+ | **Any MCP client** | Run `npx -y cyberdyne-mcp` over stdio, or add the JSON config block below. |
257
+
258
+ ```json
259
+ {
260
+ "mcpServers": {
261
+ "cyberdyne": {
262
+ "command": "npx",
263
+ "args": ["-y", "cyberdyne-mcp"],
264
+ "env": { "CYBERDYNE_IDENTITY_TOKEN": "cyb_YOURKEY" }
265
+ }
266
+ }
267
+ }
268
+ ```
269
+
270
+ Mint the key first with `npx -y cyberdyne-mcp onboard` (or the `onboard` tool). Same
271
+ server, same tools, any agent — your agent can now hire and pay verified humans.
272
+
273
+ ### Fund quests in your OWN Bankr token
274
+
275
+ Quests aren't limited to USDC or BNKR. `pay_token` accepts **any registered
276
+ Bankr-launched token** — pass it by `0x` contract address and the dynamic registry
277
+ resolves on-chain decimals, runs a fee-on-transfer probe, and applies a safety gate:
278
+
279
+ ```bash
280
+ npx -y cyberdyne-mcp post --title "Quote-repost our launch" --token 0xYourBankrToken --reward 100 --quantity 5
281
+ ```
282
+
283
+ The budget is frozen on the audited escrow at deploy in your token, and each approved
284
+ human captures the full reward in that same token. Pay your own community, in your own
285
+ coin, straight from your agent.
286
+
287
+ ### Use with the full Bankr stack
288
+
289
+ CYBERDYNE is native to the Bankr ecosystem and consumes it where it helps — the core
290
+ (non-custodial escrow + auth-capture signing) stays in CYBERDYNE; Bankr is a convenience
291
+ + distribution layer around it:
292
+
293
+ | Capability | How | Status |
294
+ |---|---|---|
295
+ | **Pay in any Bankr token** | `--token USDC\|BNKR\|0x…` — humans paid in-token from the audited escrow | **live** |
296
+ | **Fund from your Bankr wallet** | `post --bankr-wallet` — deploy fee via `/wallet/transfer`, escrow auth via `/wallet/sign` (`eth_signTypedData_v4`), no key export | BETA¹ |
297
+ | **Headless Bankr key** | `bankr-login` — SIWE mint a `bk_` with your wallet (no browser/OTP) | built |
298
+ | **Discoverable by Bankr agents** | an x402 "front door" published to Bankr's x402 Cloud discovery index | platform-side |
299
+ | **LLM proof-grading on Bankr** | submission grading routes through the Bankr LLM gateway (Anthropic-compatible, Anthropic fallback) | platform-side |
300
+
301
+ ¹ USDC (EIP-3009) funds custodially with no allowance; ecosystem tokens (BNKR/GITLAWB/
302
+ Permit2) also need a one-time ERC-20→Permit2 approval from the Bankr wallet first. BETA:
303
+ the local-wallet path (`post` without `--bankr-wallet`) is the certified default.
304
+
230
305
  ### …or install the plugin (skill + MCP together)
231
306
 
232
307
  ```
@@ -247,6 +322,12 @@ close_task` on its own. If CYBERDYNE's operator is ever down, it can `reclaim` t
247
322
  unfilled budget directly from the escrow after the authorization deadline. There is
248
323
  **no direct hire** — every task is an open FCFS pool bounty.
249
324
 
325
+ ## Star History
326
+
327
+ <a href="https://star-history.com/#Cyberdyne-OS/cyberdyne-mcp&Date">
328
+ <img src="https://api.star-history.com/svg?repos=Cyberdyne-OS/cyberdyne-mcp&type=Date" alt="Star History Chart" width="600" />
329
+ </a>
330
+
250
331
  ## Honesty / accuracy
251
332
 
252
333
  State only what is independently verifiable. This repository, its code, and the
@@ -0,0 +1,142 @@
1
+ /**
2
+ * BETA — Bankr-wallet-native funding for CYBERDYNE (no private-key export).
3
+ *
4
+ * Lets an agent fund a quest entirely from its Bankr-managed (Privy) custodial wallet:
5
+ * - the deploy fee → POST /wallet/transfer (a plain ERC-20 send)
6
+ * - the auth-capture authorization → signed via POST /wallet/sign (eth_signTypedData_v4)
7
+ *
8
+ * The auth-capture TYPED DATA is built by the SAME audited @x402/evm AuthCaptureEvmScheme
9
+ * the local path uses (src/evm-signer.ts) — we only swap the SIGNER: a viem custom account
10
+ * whose signTypedData/signMessage forward to Bankr's custodial signer. So the payload shape
11
+ * is identical to the certified local path; only the signature source differs.
12
+ *
13
+ * STATUS: this path requires a valid bk_ key with Bankr Agent API access and has NOT yet
14
+ * been certified end-to-end on mainnet (the proven default remains the local-wallet path in
15
+ * evm-signer.ts). It is opt-in only (`post --bankr-wallet` / CYBERDYNE_SIGNER=bankr).
16
+ *
17
+ * Permit2-method tokens (BNKR/GITLAWB/non-EIP-3009) need a one-time ERC-20→Permit2 approval
18
+ * sent from the wallet before the budget can freeze. Sending that approval from a custodial
19
+ * Bankr wallet is not automated here, so we READ the allowance first and fail with a clear,
20
+ * actionable error if it's missing — rather than producing a signature that would revert at
21
+ * authorize. EIP-3009 tokens (USDC) need no approval and work directly.
22
+ */
23
+ import { createPublicClient, getAddress, http, isAddress } from "viem";
24
+ import { toAccount } from "viem/accounts";
25
+ import { base, baseSepolia } from "viem/chains";
26
+ import { AuthCaptureEvmScheme, toClientEvmSigner } from "@x402/evm";
27
+ import { bankrSignTypedData, bankrSignMessage, bankrTransfer, bankrWalletAddress, resolveBankrKey, MissingBankrKeyError } from "./bankr.js";
28
+ function chain() {
29
+ return Number(process.env.CYBERDYNE_CHAIN_ID ?? 8453) === 8453 ? base : baseSepolia;
30
+ }
31
+ const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
32
+ const ERC20_ALLOWANCE_ABI = [
33
+ { name: "allowance", type: "function", stateMutability: "view", inputs: [{ name: "owner", type: "address" }, { name: "spender", type: "address" }], outputs: [{ type: "uint256" }] },
34
+ ];
35
+ /** True when a bk_ key is discoverable (so callers can offer the Bankr-wallet path). */
36
+ export function hasBankrSigner() {
37
+ return !!resolveBankrKey();
38
+ }
39
+ /**
40
+ * A viem custom account backed by the Bankr custodial wallet. Its address is the
41
+ * canonical Bankr EVM wallet; its signTypedData/signMessage call POST /wallet/sign.
42
+ * signTransaction is unsupported (the custodial wallet broadcasts via /wallet/transfer
43
+ * | /wallet/submit, not via a locally-serialized signed tx).
44
+ */
45
+ export async function bankrAccount(key) {
46
+ const bk = key ?? resolveBankrKey();
47
+ if (!bk)
48
+ throw new MissingBankrKeyError();
49
+ const address = getAddress(await bankrWalletAddress(bk));
50
+ return toAccount({
51
+ address,
52
+ // viem passes the full EIP-712 definition { domain, types, primaryType, message };
53
+ // we forward it verbatim to Bankr's eth_signTypedData_v4 signer (correct shape).
54
+ async signTypedData(typedData) {
55
+ return bankrSignTypedData(typedData, bk);
56
+ },
57
+ async signMessage({ message }) {
58
+ const text = typeof message === "string"
59
+ ? message
60
+ : typeof message?.raw === "string"
61
+ ? (message.raw)
62
+ : JSON.stringify(message);
63
+ return bankrSignMessage(text, bk);
64
+ },
65
+ async signTransaction() {
66
+ throw new Error("the Bankr custodial wallet does not export raw signed transactions — use /wallet/transfer (fees) or /wallet/submit");
67
+ },
68
+ });
69
+ }
70
+ /** The agent's Bankr wallet address (the on-chain payer the platform will verify). */
71
+ export async function bankrSignerAddress(key) {
72
+ return bankrWalletAddress(key);
73
+ }
74
+ /**
75
+ * Guard Permit2-method tokens: the on-chain freeze needs an ERC-20→Permit2 allowance
76
+ * the signature alone can't set. Read it; if insufficient, fail with a clear next step
77
+ * instead of producing a payload that would revert at authorize.
78
+ */
79
+ async function assertPermit2Ready(requirements, owner) {
80
+ const r = (requirements ?? {});
81
+ if (r.extra?.assetTransferMethod !== "permit2")
82
+ return; // USDC/EIP-3009: nothing to approve
83
+ const token = r.asset;
84
+ if (!token || !isAddress(token))
85
+ return;
86
+ const need = (() => { try {
87
+ return BigInt(r.amount ?? 0);
88
+ }
89
+ catch {
90
+ return 0n;
91
+ } })();
92
+ const pub = createPublicClient({ chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
93
+ if (need === 0n)
94
+ return; // nothing to freeze → no approval needed
95
+ const allowance = (await pub.readContract({
96
+ address: getAddress(token), abi: ERC20_ALLOWANCE_ABI, functionName: "allowance", args: [getAddress(owner), PERMIT2_ADDRESS],
97
+ }));
98
+ if (allowance >= need)
99
+ return;
100
+ throw new Error(`Bankr-wallet funding for this Permit2 token (${token}) needs a one-time ERC-20→Permit2 approval ` +
101
+ `from your Bankr wallet (${owner}) that isn't in place yet. Approve Permit2 (spender ` +
102
+ `${PERMIT2_ADDRESS}) from the Bankr wallet first, or fund this token from a local wallet. ` +
103
+ "USDC (EIP-3009) needs no approval and works directly.");
104
+ }
105
+ /**
106
+ * Sign the auth-capture requirements with the Bankr custodial wallet → base64 payload
107
+ * the platform's /authorize route consumes as `signedPayment`. Same scheme + payload
108
+ * shape as the local path; only the signer is Bankr.
109
+ */
110
+ export async function bankrSignAuthCapture(requirements, key) {
111
+ const bk = key ?? resolveBankrKey();
112
+ if (!bk)
113
+ throw new MissingBankrKeyError();
114
+ const account = await bankrAccount(bk);
115
+ await assertPermit2Ready(requirements, account.address);
116
+ const pub = createPublicClient({ chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
117
+ const scheme = new AuthCaptureEvmScheme(toClientEvmSigner(account, pub));
118
+ const result = await scheme.createPaymentPayload(2, requirements);
119
+ return Buffer.from(JSON.stringify(result)).toString("base64");
120
+ }
121
+ /**
122
+ * Pay the SEPARATE deploy fee from the Bankr custodial wallet via /wallet/transfer.
123
+ * `amount` is the fee in the FEE TOKEN's own (human-readable) units, as post_task pins it
124
+ * (Bankr resolves on-chain decimals server-side from the token address). Returns the tx
125
+ * hash to pass to authorize as `fee_tx_hash`.
126
+ *
127
+ * Like the local payDeployFee, we WAIT for on-chain inclusion (2 confs) + a success status
128
+ * before returning: authorize runs immediately after and the platform's verifyFeePayment
129
+ * reads the tx on-chain, so returning an un-mined hash would make authorize fail spuriously.
130
+ */
131
+ export async function bankrPayDeployFee(fee, key) {
132
+ if (!isAddress(fee.token) || !isAddress(fee.recipient)) {
133
+ throw new Error("deploy_fee.token/recipient is not a valid address — pass the full deployFee object from post_task");
134
+ }
135
+ const hash = await bankrTransfer({ tokenAddress: getAddress(fee.token), recipientAddress: getAddress(fee.recipient), amount: fee.amount, isNativeToken: false, chain: "base" }, key);
136
+ const pub = createPublicClient({ chain: chain(), transport: http(process.env.CYBERDYNE_RPC_URL) });
137
+ const receipt = await pub.waitForTransactionReceipt({ hash: hash, confirmations: 2 });
138
+ if (receipt.status !== "success") {
139
+ throw new Error(`Bankr deploy-fee tx ${hash} reverted on-chain — fee NOT paid (no budget will freeze)`);
140
+ }
141
+ return hash;
142
+ }
package/dist/bankr.js ADDED
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Thin, typed client for the LIVE Bankr Agent API (https://api.bankr.bot).
3
+ *
4
+ * This is how CYBERDYNE consumes the Bankr stack so an agent can act through its
5
+ * Bankr-managed (Privy) custodial wallet WITHOUT exporting a private key:
6
+ * - GET /wallet/me → the agent's canonical Bankr wallet address
7
+ * - POST /wallet/transfer → send an ERC-20 (e.g. the deploy fee) → txHash
8
+ * - POST /wallet/sign → custodial EIP-712 / personal_sign → signature
9
+ * - GET /_health → API-key liveness
10
+ * - GET /tokens/search?query= → resolve a symbol ↔ address (public)
11
+ *
12
+ * Endpoint shapes verified against the published `@bankr/cli` (0.3.1) source. Auth is
13
+ * the `bk_…` Agent API key in the `X-API-Key` header (Bankr Club / Agent API access
14
+ * required — a key without it returns 401 on /wallet/*). NOTHING here is x402-specific:
15
+ * CYBERDYNE's own auth-capture signing stays in src/evm-signer.ts (@x402/evm) — this
16
+ * module only forwards a typed-data struct to Bankr's signer and returns the signature.
17
+ *
18
+ * The bk_ key is read fresh from the env / ~/.bankr each call and is NEVER persisted by
19
+ * CYBERDYNE and NEVER logged.
20
+ */
21
+ import { readFileSync } from "node:fs";
22
+ import { homedir } from "node:os";
23
+ import { join } from "node:path";
24
+ import { readSavedWalletKey } from "./client.js";
25
+ export const BANKR_API_URL = "https://api.bankr.bot";
26
+ /**
27
+ * Resolve the agent's Bankr `bk_` key, most-explicit first:
28
+ * CYBERDYNE_BANKR_KEY → BANKR_API_KEY (Bankr's own standard env var) →
29
+ * any bk_-prefixed value in ~/.bankr/config.json (the file Bankr's CLI/SDK use).
30
+ * Returns undefined when Bankr isn't configured (callers then skip Bankr cleanly).
31
+ */
32
+ export function resolveBankrKey(env = process.env) {
33
+ const fromEnv = env.CYBERDYNE_BANKR_KEY?.trim() || env.BANKR_API_KEY?.trim();
34
+ if (fromEnv?.startsWith("bk_"))
35
+ return fromEnv;
36
+ try {
37
+ const cfg = JSON.parse(readFileSync(join(homedir(), ".bankr", "config.json"), "utf8"));
38
+ for (const v of Object.values(cfg)) {
39
+ if (typeof v === "string" && v.startsWith("bk_"))
40
+ return v.trim();
41
+ }
42
+ }
43
+ catch {
44
+ /* no ~/.bankr/config.json — Bankr just isn't configured here */
45
+ }
46
+ return undefined;
47
+ }
48
+ /** Thrown when a Bankr call is attempted without a discoverable bk_ key. */
49
+ export class MissingBankrKeyError extends Error {
50
+ constructor() {
51
+ super("no Bankr key — set CYBERDYNE_BANKR_KEY or BANKR_API_KEY (a bk_… Agent API key from " +
52
+ "https://bankr.bot, Agent API access enabled), or put it in ~/.bankr/config.json.");
53
+ this.name = "MissingBankrKeyError";
54
+ }
55
+ }
56
+ /** A Bankr API error carrying the HTTP status + the API's message. */
57
+ export class BankrApiError extends Error {
58
+ status;
59
+ path;
60
+ constructor(status, path, message) {
61
+ super(`Bankr ${path} → ${status}: ${message}`);
62
+ this.status = status;
63
+ this.path = path;
64
+ this.name = "BankrApiError";
65
+ }
66
+ }
67
+ function authHeaders(key) {
68
+ const h = { "X-API-Key": key, accept: "application/json", "user-agent": "cyberdyne-mcp" };
69
+ const partner = process.env.CYBERDYNE_BANKR_PARTNER_KEY?.trim();
70
+ if (partner)
71
+ h["X-Partner-Key"] = partner;
72
+ return h;
73
+ }
74
+ async function call(method, path, opts = {}) {
75
+ const key = opts.key ?? resolveBankrKey();
76
+ if (!key)
77
+ throw new MissingBankrKeyError();
78
+ const res = await fetch(`${BANKR_API_URL}${path}`, {
79
+ method,
80
+ headers: {
81
+ ...authHeaders(key),
82
+ ...(opts.body !== undefined ? { "content-type": "application/json" } : {}),
83
+ },
84
+ ...(opts.body !== undefined ? { body: JSON.stringify(opts.body) } : {}),
85
+ signal: AbortSignal.timeout(20_000),
86
+ });
87
+ const json = (await res.json().catch(() => ({})));
88
+ if (!res.ok) {
89
+ const msg = String(json?.message ?? json?.error ?? `http_${res.status}`);
90
+ throw new BankrApiError(res.status, path, msg);
91
+ }
92
+ return json;
93
+ }
94
+ /** GET /wallet/me — the agent's Bankr identity, incl. its managed wallet address. */
95
+ export async function bankrWalletMe(key) {
96
+ return call("GET", "/wallet/me", { key });
97
+ }
98
+ /** Resolve the canonical Bankr EVM wallet address (walletAddress | evmAddress | address). */
99
+ export async function bankrWalletAddress(key) {
100
+ const me = await bankrWalletMe(key);
101
+ const addr = me.walletAddress || me.evmAddress || me.address;
102
+ if (typeof addr !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(addr)) {
103
+ throw new BankrApiError(200, "/wallet/me", "no EVM wallet address in response");
104
+ }
105
+ return addr;
106
+ }
107
+ /**
108
+ * POST /wallet/transfer — send an ERC-20 (or native) from the Bankr-managed wallet.
109
+ * `amount` is the HUMAN-readable token amount (the API resolves decimals), matching
110
+ * the `bankr transfer --amount` CLI. Returns the broadcast tx hash.
111
+ */
112
+ export async function bankrTransfer(params, key) {
113
+ const r = await call("POST", "/wallet/transfer", {
114
+ key,
115
+ body: {
116
+ tokenAddress: params.tokenAddress,
117
+ recipientAddress: params.recipientAddress,
118
+ amount: String(params.amount),
119
+ isNativeToken: params.isNativeToken ?? false,
120
+ chain: params.chain ?? "base",
121
+ },
122
+ });
123
+ if (!r.txHash)
124
+ throw new BankrApiError(200, "/wallet/transfer", "no txHash in response");
125
+ return r.txHash;
126
+ }
127
+ /**
128
+ * POST /wallet/sign with signatureType "eth_signTypedData_v4" — custodial EIP-712.
129
+ * `typedData` is the full { domain, types, primaryType, message } struct. Returns the
130
+ * 0x signature. This is what lets the Bankr wallet sign CYBERDYNE's auth-capture
131
+ * authorization without exporting a key (see src/bankr-signer.ts).
132
+ */
133
+ export async function bankrSignTypedData(typedData, key) {
134
+ const r = await call("POST", "/wallet/sign", {
135
+ key,
136
+ body: { signatureType: "eth_signTypedData_v4", typedData },
137
+ });
138
+ if (!r.signature?.startsWith("0x"))
139
+ throw new BankrApiError(200, "/wallet/sign", "no signature in response");
140
+ return r.signature;
141
+ }
142
+ /** POST /wallet/sign with signatureType "personal_sign". Returns the 0x signature. */
143
+ export async function bankrSignMessage(message, key) {
144
+ const r = await call("POST", "/wallet/sign", {
145
+ key,
146
+ body: { signatureType: "personal_sign", message },
147
+ });
148
+ if (!r.signature?.startsWith("0x"))
149
+ throw new BankrApiError(200, "/wallet/sign", "no signature in response");
150
+ return r.signature;
151
+ }
152
+ // ── health / discovery ────────────────────────────────────────────────────────
153
+ /** GET /_health — cheap key/liveness probe. Returns true on a 2xx. */
154
+ export async function bankrHealth(key) {
155
+ try {
156
+ await call("GET", "/_health", { key });
157
+ return true;
158
+ }
159
+ catch {
160
+ return false;
161
+ }
162
+ }
163
+ /** GET /tokens/search?query= — public symbol↔address resolver (the one /wallet/transfer uses). */
164
+ export async function bankrSearchTokens(query, chainId) {
165
+ const params = new URLSearchParams({ query });
166
+ if (chainId)
167
+ params.set("chainId", String(chainId));
168
+ const res = await fetch(`${BANKR_API_URL}/tokens/search?${params}`, {
169
+ headers: { accept: "application/json", "user-agent": "cyberdyne-mcp" },
170
+ signal: AbortSignal.timeout(15_000),
171
+ });
172
+ const json = (await res.json().catch(() => ({})));
173
+ if (!res.ok)
174
+ throw new BankrApiError(res.status, "/tokens/search", "search failed");
175
+ return Array.isArray(json) ? json : (json.tokens ?? []);
176
+ }
177
+ /** GET /wallet/portfolio — multi-chain balances/holdings (cheap treasury/earnings view). */
178
+ export async function bankrPortfolio(opts = {}, key) {
179
+ const q = new URLSearchParams();
180
+ if (opts.chains?.length)
181
+ q.set("chains", opts.chains.join(","));
182
+ if (opts.showLowValueTokens)
183
+ q.set("showLowValueTokens", "true");
184
+ const qs = q.toString();
185
+ return call("GET", `/wallet/portfolio${qs ? `?${qs}` : ""}`, { key });
186
+ }
187
+ /**
188
+ * POST /wallet/x402-pay — pay any x402-priced URL custodially from the Bankr wallet,
189
+ * Bankr handling the 402 handshake server-side, capped by `maxPaymentUsd`. Useful for an
190
+ * agent to call a paid discovery/quote endpoint (e.g. CYBERDYNE's x402 Cloud front door)
191
+ * with one authenticated POST instead of a client-side wallet signature.
192
+ */
193
+ export async function bankrX402Pay(params, key) {
194
+ return call("POST", "/wallet/x402-pay", {
195
+ key,
196
+ body: { url: params.url, method: params.method ?? "GET", body: params.body, maxPaymentUsd: params.maxPaymentUsd },
197
+ });
198
+ }
199
+ /**
200
+ * Headless `bankr login --siwe`: mint a `bk_` key by signing a SIWE message with the
201
+ * agent's own wallet — zero-browser, no email OTP. Flow (verified against @bankr/cli):
202
+ * GET /cli/siwe/nonce → build the SIWE message → personal_sign → POST /cli/siwe/verify.
203
+ * Mints with Wallet API enabled (so /wallet/* works); Agent API + Token-Launch API stay
204
+ * OFF (Agent API is separately gated at bankr.bot/api; we never launch tokens). The
205
+ * private key never leaves this process and is never logged. Defaults the signer to the
206
+ * onboarded wallet (CYBERDYNE_EVM_PRIVATE_KEY / ~/.cyberdyne config).
207
+ */
208
+ export async function bankrSiweProvision(opts = {}) {
209
+ const pk = (opts.privateKey ?? process.env.CYBERDYNE_EVM_PRIVATE_KEY?.trim() ?? readSavedWalletKey() ?? "").trim();
210
+ if (!pk) {
211
+ throw new Error("no signing wallet — pass a private key, set CYBERDYNE_EVM_PRIVATE_KEY, or run `cyberdyne-mcp onboard` first");
212
+ }
213
+ const { privateKeyToAccount } = await import("viem/accounts");
214
+ const account = privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`));
215
+ const host = new URL(BANKR_API_URL).host;
216
+ const nonceRes = await fetch(`${BANKR_API_URL}/cli/siwe/nonce`, {
217
+ headers: { accept: "application/json", "user-agent": "cyberdyne-mcp" },
218
+ signal: AbortSignal.timeout(20_000),
219
+ });
220
+ if (!nonceRes.ok)
221
+ throw new BankrApiError(nonceRes.status, "/cli/siwe/nonce", "nonce request failed");
222
+ const { nonce } = (await nonceRes.json());
223
+ if (!nonce)
224
+ throw new BankrApiError(200, "/cli/siwe/nonce", "no nonce in response");
225
+ const message = [
226
+ `${host} wants you to sign in with your Ethereum account:`,
227
+ account.address,
228
+ "",
229
+ "Sign in to Bankr",
230
+ "",
231
+ `URI: ${BANKR_API_URL}/cli/siwe/verify`,
232
+ "Version: 1",
233
+ // Bankr's SIWE verifier expects "Chain ID: 1" — this is the LOGIN signature, not a
234
+ // transaction; CYBERDYNE settles on Base (8453). Matches @bankr/cli's siwe flow.
235
+ "Chain ID: 1",
236
+ `Nonce: ${nonce}`,
237
+ `Issued At: ${new Date().toISOString()}`,
238
+ ].join("\n");
239
+ const signature = await account.signMessage({ message });
240
+ const verifyRes = await fetch(`${BANKR_API_URL}/cli/siwe/verify`, {
241
+ method: "POST",
242
+ headers: { "content-type": "application/json", accept: "application/json", "user-agent": "cyberdyne-mcp" },
243
+ body: JSON.stringify({
244
+ message,
245
+ signature,
246
+ partnerApiKey: opts.partnerKey ?? process.env.CYBERDYNE_BANKR_PARTNER_KEY?.trim(),
247
+ keyName: opts.keyName ?? `cyberdyne-${new Date().toISOString().slice(0, 10)}`,
248
+ readOnly: false,
249
+ walletApiEnabled: opts.walletApiEnabled ?? true,
250
+ agentApiEnabled: false,
251
+ tokenLaunchApiEnabled: false,
252
+ allowedRecipients: opts.allowedRecipients,
253
+ }),
254
+ signal: AbortSignal.timeout(30_000),
255
+ });
256
+ const j = (await verifyRes.json().catch(() => ({})));
257
+ if (!verifyRes.ok || !j.apiKey) {
258
+ throw new BankrApiError(verifyRes.status, "/cli/siwe/verify", String(j.message ?? j.error ?? "verify failed"));
259
+ }
260
+ return { apiKey: j.apiKey, walletAddress: j.walletAddress ?? account.address, readOnly: j.readOnly };
261
+ }