cyberdyne-mcp 0.6.25 → 0.7.1
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 +19 -32
- package/dist/bankr-signer.js +142 -0
- package/dist/bankr.js +273 -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 +5 -2
- package/src/bankr-signer.ts +151 -0
- package/src/bankr.ts +336 -0
- package/src/cli.ts +103 -22
- package/src/server.ts +14 -2
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
|
|
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
|
-
|
|
287
|
+
### Use with the full Bankr stack
|
|
285
288
|
|
|
286
|
-
|
|
287
|
-
|
|
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:
|
|
289
292
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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,273 @@
|
|
|
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
|
+
// The nonce endpoint sets AWS load-balancer stickiness cookies (AWSALB…); the nonce is
|
|
223
|
+
// instance-local, so /cli/siwe/verify MUST carry them to reach the same backend — without
|
|
224
|
+
// this it intermittently fails "Nonce expired or already used" depending on LB routing.
|
|
225
|
+
const cookie = (typeof nonceRes.headers.getSetCookie === "function" ? nonceRes.headers.getSetCookie() : [])
|
|
226
|
+
.map((c) => c.split(";")[0])
|
|
227
|
+
.filter(Boolean)
|
|
228
|
+
.join("; ");
|
|
229
|
+
const { nonce } = (await nonceRes.json());
|
|
230
|
+
if (!nonce)
|
|
231
|
+
throw new BankrApiError(200, "/cli/siwe/nonce", "no nonce in response");
|
|
232
|
+
const message = [
|
|
233
|
+
`${host} wants you to sign in with your Ethereum account:`,
|
|
234
|
+
account.address,
|
|
235
|
+
"",
|
|
236
|
+
"Sign in to Bankr",
|
|
237
|
+
"",
|
|
238
|
+
`URI: ${BANKR_API_URL}/cli/siwe/verify`,
|
|
239
|
+
"Version: 1",
|
|
240
|
+
// Bankr's SIWE verifier expects "Chain ID: 1" — this is the LOGIN signature, not a
|
|
241
|
+
// transaction; CYBERDYNE settles on Base (8453). Matches @bankr/cli's siwe flow.
|
|
242
|
+
"Chain ID: 1",
|
|
243
|
+
`Nonce: ${nonce}`,
|
|
244
|
+
`Issued At: ${new Date().toISOString()}`,
|
|
245
|
+
].join("\n");
|
|
246
|
+
const signature = await account.signMessage({ message });
|
|
247
|
+
const verifyRes = await fetch(`${BANKR_API_URL}/cli/siwe/verify`, {
|
|
248
|
+
method: "POST",
|
|
249
|
+
headers: {
|
|
250
|
+
"content-type": "application/json",
|
|
251
|
+
accept: "application/json",
|
|
252
|
+
"user-agent": "cyberdyne-mcp",
|
|
253
|
+
...(cookie ? { cookie } : {}),
|
|
254
|
+
},
|
|
255
|
+
body: JSON.stringify({
|
|
256
|
+
message,
|
|
257
|
+
signature,
|
|
258
|
+
partnerApiKey: opts.partnerKey ?? process.env.CYBERDYNE_BANKR_PARTNER_KEY?.trim(),
|
|
259
|
+
keyName: opts.keyName ?? `cyberdyne-${new Date().toISOString().slice(0, 10)}`,
|
|
260
|
+
readOnly: false,
|
|
261
|
+
walletApiEnabled: opts.walletApiEnabled ?? true,
|
|
262
|
+
agentApiEnabled: false,
|
|
263
|
+
tokenLaunchApiEnabled: false,
|
|
264
|
+
allowedRecipients: opts.allowedRecipients,
|
|
265
|
+
}),
|
|
266
|
+
signal: AbortSignal.timeout(30_000),
|
|
267
|
+
});
|
|
268
|
+
const j = (await verifyRes.json().catch(() => ({})));
|
|
269
|
+
if (!verifyRes.ok || !j.apiKey) {
|
|
270
|
+
throw new BankrApiError(verifyRes.status, "/cli/siwe/verify", String(j.message ?? j.error ?? "verify failed"));
|
|
271
|
+
}
|
|
272
|
+
return { apiKey: j.apiKey, walletAddress: j.walletAddress ?? account.address, readOnly: j.readOnly };
|
|
273
|
+
}
|
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
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
}
|