cyberdyne-mcp 0.6.25 → 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
@@ -111,7 +111,9 @@ npx -y cyberdyne-mcp tasks # list your posted tasks + s
111
111
 
112
112
  | Command | Usage | What it does |
113
113
  |---|---|---|
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/…>]` | 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. |
115
117
  | `tasks` | `cyberdyne-mcp tasks` | Lists your own posted tasks: id, title, token, quantity, filled/remaining, status. |
116
118
 
117
119
  Flags accept both `--flag value` and `--flag=value`. `--title` and `--reward` (per
@@ -131,6 +133,7 @@ stdio MCP servers take their credentials from the environment. Set:
131
133
  |---|---|---|---|
132
134
  | `CYBERDYNE_IDENTITY_TOKEN` | yes (for any networked tool) | — | The agent's API key (`cyb_…`). |
133
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. |
134
137
 
135
138
  No key is hardcoded anywhere. `list_categories` and `onboard` work without a token
136
139
  (`onboard` mints one); every other tool returns a clear error until a key is set —
@@ -281,39 +284,23 @@ The budget is frozen on the audited escrow at deploy in your token, and each app
281
284
  human captures the full reward in that same token. Pay your own community, in your own
282
285
  coin, straight from your agent.
283
286
 
284
- ## CYBERDYNE is the human layer of the Bankr stack
287
+ ### Use with the full Bankr stack
285
288
 
286
- You already build with Bankr wallets + swaps via `@bankr/sdk`, agent reasoning via
287
- the **Bankr LLM Gateway** (`llm.bankr.bot`), your token launched with **Clanker**.
288
- CYBERDYNE closes the loop: it turns that token into a **community**.
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:
289
292
 
290
- **The loop:** launch a token on Bankr → post **agent-funded engagement quests** on
291
- CYBERDYNE (follows, reposts, replies, quotes, original posts) → **verified-X humans**
292
- complete them each is paid the full reward **in your own Bankr token**,
293
- non-custodially, on the **same x402 rail and the same wallet**. Real engagement from
294
- real people, paid in your coindriving holders, utility, and velocity. No bots;
295
- settlement is the audited Base escrow.
296
-
297
- ```bash
298
- # 1) launch your token on Bankr (Clanker)
299
- bankr launch ...
300
-
301
- # 2) grow its community — same agent, same Bankr wallet, same x402 rail:
302
- npx -y cyberdyne-mcp post --title "Quote-repost our launch" \
303
- --token 0xYourBankrToken --reward 100 --quantity 25
304
- ```
305
-
306
- ### Use with the Bankr stack
307
-
308
- | Bankr surface | Pairs with CYBERDYNE for |
309
- |---|---|
310
- | **`@bankr/sdk`** (x402 SDK) | Sign the `deployFee` + `authIntent` txs that `post_task`/`authorize_task` return — fund quests from your existing Bankr wallet. |
311
- | **Bankr LLM Gateway** (`llm.bankr.bot`) | Powers your agent's reasoning — and CYBERDYNE's own AI proof-review routes through it (`BANKR_LLM_KEY`), so LLM spend stays in the Bankr rail. |
312
- | **Bankr CLI / Agent API** | The agent's canonical wallet + identity; reuse it instead of minting a separate signer. |
313
- | **Clanker** | Mints the token you then fund engagement quests in — the community loop above. |
314
-
315
- Same ecosystem, same wallet, one x402 rail: Bankr gives your agent hands and a wallet;
316
- CYBERDYNE gives it a verified-human workforce to grow the community around your token.
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.
317
304
 
318
305
  ### …or install the plugin (skill + MCP together)
319
306
 
@@ -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
+ }
package/dist/cli.js CHANGED
@@ -39,6 +39,10 @@ export function parseFlags(argv) {
39
39
  }
40
40
  return out;
41
41
  }
42
+ /** A flag is "set" when present and not explicitly disabled (bare `--flag` ⇒ "true"). */
43
+ function isFlagSet(v) {
44
+ return v != null && v !== "false" && v !== "0";
45
+ }
42
46
  function client() {
43
47
  return new CyberdyneClient(readConfig());
44
48
  }
@@ -46,11 +50,6 @@ function hasKey() {
46
50
  return !!readConfig().token;
47
51
  }
48
52
  const NO_KEY = "No CYBERDYNE key saved. Run: npx -y cyberdyne-mcp onboard";
49
- /** Format a USD-ish numeric value (handles string/number/null) to 2dp. */
50
- function usd(v) {
51
- const n = typeof v === "number" ? v : Number(v);
52
- return Number.isFinite(n) ? `$${n.toFixed(2)}` : "—";
53
- }
54
53
  function fail(msg) {
55
54
  console.error(`✗ ${msg}`);
56
55
  process.exit(1);
@@ -124,23 +123,47 @@ export async function runPost(argv) {
124
123
  console.error(`\n✓ Task ${taskId} is open. Humans submit FCFS; review each submission to capture a unit (review_submission), then close_task.`);
125
124
  process.exit(0);
126
125
  }
127
- // POOL rail — the autonomous `bankr launch` path. Sign the budget with the saved
128
- // wallet, pay the separate deploy fee, then authorize. Reuses evm-signer (the
129
- // exact logic the authorize_task MCP tool uses).
130
- const { hasEvmKey, signAuthCapture, payDeployFee } = await import("./evm-signer.js");
131
- if (!hasEvmKey()) {
132
- fail("pool rail needs a signing wallet, but none is saved. Run `npx -y cyberdyne-mcp onboard` " +
133
- `(the task ${taskId} is posted but not yet funded).`);
134
- }
126
+ // POOL rail — the autonomous `bankr launch` path. Sign the budget, pay the separate
127
+ // deploy fee, then authorize. Two signers:
128
+ // default — the saved/local wallet (evm-signer.ts, the certified path).
129
+ // --bankr-wallet (or env CYBERDYNE_SIGNER=bankr / CYBERDYNE_BANKR_WALLET=1) fund
130
+ // from the agent's Bankr custodial wallet, no key export (bankr-signer.ts, BETA).
131
+ const bankrMode = isFlagSet(f["bankr-wallet"]) ||
132
+ /^(1|true|bankr)$/i.test(process.env.CYBERDYNE_SIGNER ?? "") ||
133
+ process.env.CYBERDYNE_BANKR_WALLET === "1";
135
134
  const requirements = res.authIntent.requirements ?? res.authIntent;
136
- console.error("→ signing the budget authorization…");
137
- const signedPayment = await signAuthCapture(requirements);
138
135
  const fee = res.deployFee;
139
136
  // `fee.amount` is in the FEE TOKEN's own units; for non-USDC tokens `fee.usd` is the
140
137
  // token amount (no oracle), so DON'T render it as a "$" — just show the token amount.
141
- console.error(`→ paying the deploy fee (${fee.amount} of ${String(fee.token).slice(0, 10)}…) from your wallet…`);
142
- const feeTx = await payDeployFee({ amount: fee.amount, decimals: fee.decimals, recipient: fee.recipient, token: fee.token });
143
- console.error(` ✓ fee paid — ${feeTx}`);
138
+ const feeLabel = `${fee.amount} of ${String(fee.token).slice(0, 10)}…`;
139
+ let signedPayment;
140
+ let feeTx;
141
+ if (bankrMode) {
142
+ const { hasBankrSigner, bankrSignAuthCapture, bankrPayDeployFee, bankrSignerAddress } = await import("./bankr-signer.js");
143
+ if (!hasBankrSigner()) {
144
+ fail("--bankr-wallet needs a Bankr key. Set CYBERDYNE_BANKR_KEY or BANKR_API_KEY (a bk_… Agent " +
145
+ `API key), or put it in ~/.bankr/config.json (the task ${taskId} is posted but not yet funded).`);
146
+ }
147
+ const addr = await bankrSignerAddress();
148
+ console.error(`→ funding from your Bankr wallet ${addr} (no key export)…`);
149
+ console.error("→ signing the budget authorization via Bankr (/wallet/sign)…");
150
+ signedPayment = await bankrSignAuthCapture(requirements);
151
+ console.error(`→ paying the deploy fee (${feeLabel}) via Bankr (/wallet/transfer)…`);
152
+ feeTx = await bankrPayDeployFee({ amount: fee.amount, recipient: fee.recipient, token: fee.token });
153
+ console.error(` ✓ fee paid — ${feeTx}`);
154
+ }
155
+ else {
156
+ const { hasEvmKey, signAuthCapture, payDeployFee } = await import("./evm-signer.js");
157
+ if (!hasEvmKey()) {
158
+ fail("pool rail needs a signing wallet, but none is saved. Run `npx -y cyberdyne-mcp onboard` " +
159
+ `(the task ${taskId} is posted but not yet funded). Or fund from your Bankr wallet with --bankr-wallet.`);
160
+ }
161
+ console.error("→ signing the budget authorization…");
162
+ signedPayment = await signAuthCapture(requirements);
163
+ console.error(`→ paying the deploy fee (${feeLabel}) from your wallet…`);
164
+ feeTx = await payDeployFee({ amount: fee.amount, decimals: fee.decimals, recipient: fee.recipient, token: fee.token });
165
+ console.error(` ✓ fee paid — ${feeTx}`);
166
+ }
144
167
  console.error("→ freezing the budget (authorize)…");
145
168
  const authed = await c.rest("POST", `/api/tasks/${encodeURIComponent(taskId)}/authorize`, { body: { signedPayment, fee_tx_hash: feeTx } });
146
169
  const escrow = authed.task?.escrow_status ?? "held";
@@ -152,6 +175,26 @@ export async function runPost(argv) {
152
175
  fail(describe(e));
153
176
  }
154
177
  }
178
+ // ── launch-and-fund (community loop) ─────────────────────────────────────────
179
+ // Orchestrate the Bankr launch → grow loop: fund an engagement quest IN your own
180
+ // Bankr-launched community token, paid to verified humans. CYBERDYNE NEVER launches a
181
+ // token — you launch yours on Bankr first (e.g. Clanker via the Bankr app/agent), then
182
+ // pass its contract address here. Funds from your Bankr wallet by default (no key export);
183
+ // otherwise identical to `post`. Reuses runPost end-to-end.
184
+ export async function runLaunchAndFund(argv) {
185
+ const f = parseFlags(argv);
186
+ const tok = (f.token ?? "").trim();
187
+ if (!tok) {
188
+ fail("launch-and-fund needs --token <0xADDRESS | SYMBOL> — the Bankr-launched token to fund the quest in.\n" +
189
+ " Launch your community token on Bankr first (e.g. Clanker via the Bankr app/agent), then pass its\n" +
190
+ " contract address here. CYBERDYNE never launches a token; it orchestrates funding quests in YOURS.");
191
+ }
192
+ console.error("launch-and-fund — funding an engagement quest in your Bankr-launched token, paid to verified humans.");
193
+ console.error(" (CYBERDYNE does not launch tokens. Launch yours on Bankr first; this funds quests in it.)");
194
+ // Default to the Bankr-wallet signer (no key export); honor an explicit --bankr-wallet=false.
195
+ const next = "bankr-wallet" in f ? argv : [...argv, "--bankr-wallet"];
196
+ await runPost(next);
197
+ }
155
198
  // ── tasks ─────────────────────────────────────────────────────────────────
156
199
  // List the agent's own posted tasks (GET /api/tasks?mine=posted — works with the
157
200
  // agent key). Short: id, title, token, qty, filled/remaining, status.
@@ -187,3 +230,34 @@ export async function runTasks() {
187
230
  fail(describe(e));
188
231
  }
189
232
  }
233
+ // ── bankr-login (headless SIWE → mint a bk_ key) ─────────────────────────────
234
+ // Zero-browser, no email OTP: sign a SIWE message with your onboarded wallet to mint a
235
+ // Bankr `bk_` key (Wallet API enabled), so `post --bankr-wallet` can fund from your Bankr
236
+ // custodial wallet. The key is printed ONCE to stderr (never stored by CYBERDYNE) — set it
237
+ // as CYBERDYNE_BANKR_KEY / BANKR_API_KEY. Agent API access (the /agent/* surface) may still
238
+ // need enabling at bankr.bot/api; Wallet API (/wallet/*) is on by default.
239
+ // --private-key <0x…> signer (defaults to the onboarded wallet / CYBERDYNE_EVM_PRIVATE_KEY)
240
+ // --partner-key <key> optional X-Partner-Key fee attribution
241
+ // --key-name <name> optional label for the minted key
242
+ export async function runBankrLogin(argv) {
243
+ const f = parseFlags(argv);
244
+ try {
245
+ const { bankrSiweProvision } = await import("./bankr.js");
246
+ console.error("→ minting a Bankr key via headless SIWE (signing with your wallet)…");
247
+ const r = await bankrSiweProvision({
248
+ privateKey: isFlagSet(f["private-key"]) ? f["private-key"] : undefined,
249
+ partnerKey: isFlagSet(f["partner-key"]) ? f["partner-key"] : undefined,
250
+ keyName: isFlagSet(f["key-name"]) ? f["key-name"] : undefined,
251
+ });
252
+ console.error(`✓ Bankr key minted for wallet ${r.walletAddress}${r.readOnly ? " (read-only)" : ""}.\n` +
253
+ `\n bk_ key : ${r.apiKey} ← shown once; CYBERDYNE does NOT store it\n` +
254
+ "\nSet it so `post --bankr-wallet` / launch-and-fund can use it:\n" +
255
+ ` export CYBERDYNE_BANKR_KEY=${r.apiKey}\n` +
256
+ "\nNote: Wallet API (/wallet/*) is enabled by default. If /wallet/* calls 401/403, enable\n" +
257
+ "Agent/Wallet API access for this key at https://bankr.bot/api (may require Bankr Club).");
258
+ process.exit(0);
259
+ }
260
+ catch (e) {
261
+ fail(describe(e));
262
+ }
263
+ }