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 +82 -1
- package/dist/bankr-signer.js +142 -0
- package/dist/bankr.js +261 -0
- package/dist/cli.js +92 -18
- package/dist/server.js +14 -2
- package/examples/README.md +59 -0
- package/examples/bankr-fund.mjs +82 -0
- package/package.json +6 -3
- package/src/bankr-signer.ts +151 -0
- package/src/bankr.ts +324 -0
- package/src/cli.ts +103 -22
- package/src/server.ts +14 -2
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
|
|
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
|
+
}
|