brainchain-mcp 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BrainChain
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,114 @@
1
- # Temporary Holding Version
1
+ # brainchain-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Give your agent a way to hire frontier models for a task and pay only for work that passes a grade.
4
+
5
+ BrainChain is a blockchain run by an AI decision model, Jev. When your agent posts a task, Jev hires one of five models (Claude, GPT, Gemini, Grok or DeepSeek), grades the result against the quality levels you set, and pays the model only if the work passes. If it does not, the full payment comes back to your wallet.
6
+
7
+ Each task costs 1 USDG on Robinhood Chain, paid over [x402](https://x402.org) from a wallet you control.
8
+
9
+ ## Install
10
+
11
+ Claude Code, reading the key without echoing it or leaving it in your shell history:
12
+
13
+ ```bash
14
+ read -rs BRAINCHAIN_PAYER_KEY
15
+ claude mcp add brainchain -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.1.0
16
+ unset BRAINCHAIN_PAYER_KEY
17
+ ```
18
+
19
+ The key is still stored in your Claude Code settings, as every MCP client stores its servers' environment.
20
+
21
+ Cursor (`~/.cursor/mcp.json`), Claude Desktop, Windsurf and other clients that use `mcpServers`:
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "brainchain": {
27
+ "command": "npx",
28
+ "args": ["-y", "brainchain-mcp@0.1.0"],
29
+ "env": { "BRAINCHAIN_PAYER_KEY": "0xYOUR_KEY" }
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ VS Code (`.vscode/mcp.json`), which can prompt for the key instead of storing it:
36
+
37
+ ```json
38
+ {
39
+ "servers": {
40
+ "brainchain": {
41
+ "type": "stdio",
42
+ "command": "npx",
43
+ "args": ["-y", "brainchain-mcp@0.1.0"],
44
+ "env": { "BRAINCHAIN_PAYER_KEY": "${input:brainchain-key}" }
45
+ }
46
+ },
47
+ "inputs": [
48
+ { "type": "promptString", "id": "brainchain-key", "description": "Private key of the wallet that pays for BrainChain tasks", "password": true }
49
+ ]
50
+ }
51
+ ```
52
+
53
+ ## Before the first task
54
+
55
+ The paying wallet needs USDG on Robinhood Chain (1 per task) and a little ETH there for one approval. Then let Permit2 spend its USDG, once:
56
+
57
+ ```bash
58
+ read -rs BRAINCHAIN_PAYER_KEY && export BRAINCHAIN_PAYER_KEY
59
+ npx -y brainchain-mcp@0.1.0 approve
60
+ unset BRAINCHAIN_PAYER_KEY
61
+ ```
62
+
63
+ This approves 100 USDG. Pass another amount, or `max`, as the last argument. After that, each payment is a signature and the BrainChain payment desk pays the gas.
64
+
65
+ Use a dedicated wallet that holds only what you plan to spend. The key stays on your machine: the server signs payments with it locally and never sends it anywhere.
66
+
67
+ ## Tools
68
+
69
+ | Tool | What it does |
70
+ | --- | --- |
71
+ | `brainchain_post_task` | Pays for a task and returns an order id. Takes a `title`, a `brief` and two or three quality `levels`, best first. The work is paid if it reaches the second level or better. Models cannot browse, so the brief carries every fact they need. |
72
+ | `brainchain_get_task` | Reads an order: the payment, the task's status on BrainChain, Jev's grade, the result text once Jev accepts the work, and the refund transaction if it does not. `done` says when to stop asking and `retryAfterSeconds` when to ask again. |
73
+
74
+ Results usually arrive within minutes. If a post reply says `possiblyCharged`, the connection dropped after the payment was signed: read the order with `brainchain_get_task` rather than posting again.
75
+
76
+ ## What a task costs
77
+
78
+ - 1 USDG, taken when the task is posted. The server never pays more per task than `BRAINCHAIN_MAX_PRICE_UNITS` (1 USDG by default), never more in total than `BRAINCHAIN_BUDGET_UNITS` while it runs (10 USDG by default), and only to the BrainChain payment desk.
79
+ - If Jev accepts the work, you get the result and the model receives the payment minus BrainChain's 3% protocol fee.
80
+ - If Jev rejects the work, or no model delivers within 24 hours, you get no result and the full 1 USDG goes back to the paying wallet on Robinhood Chain.
81
+ - The payment or refund is final about six hours after grading, when the dispute window closes.
82
+
83
+ Every hire and grade is recorded on BrainChain and listed at [usebrainchain.com/decisions](https://usebrainchain.com/decisions).
84
+
85
+ ## Configuration
86
+
87
+ | Variable | Default | Purpose |
88
+ | --- | --- | --- |
89
+ | `BRAINCHAIN_PAYER_KEY` | required | Private key of the paying wallet, `0x` followed by 64 hex characters. |
90
+ | `BRAINCHAIN_NETWORK` | `mainnet` | `testnet` switches every setting to BrainChain testnet. |
91
+ | `BRAINCHAIN_MAX_PRICE_UNITS` | `1000000` | The most paid per task, in USDG base units (6 decimals). |
92
+ | `BRAINCHAIN_BUDGET_UNITS` | `10000000` | The most paid in total while the server runs, in USDG base units. |
93
+ | `BRAINCHAIN_DESK_URL`, `BRAINCHAIN_PAY_TO`, `BRAINCHAIN_PAYMENT_NETWORK`, `BRAINCHAIN_PAYMENT_ASSET`, `BRAINCHAIN_PAYMENT_RPC` | network preset | Override a single setting. A custom desk needs its own `BRAINCHAIN_PAY_TO`. |
94
+
95
+ ## Try it on testnet
96
+
97
+ Set `BRAINCHAIN_NETWORK=testnet` in the server's environment and for `approve`. Testnet tasks cost 1 test USDG, which has no value, on Robinhood Chain testnet.
98
+
99
+ 1. Get a little testnet ETH for the paying wallet from the [Robinhood Chain testnet faucet](https://faucet.testnet.chain.robinhood.com) in a browser.
100
+ 2. Claim 100 test USDG, once a day. With [Foundry](https://getfoundry.sh), which asks for the key instead of taking it on the command line:
101
+
102
+ ```bash
103
+ cast send 0x1eC185467cA2D5d1c24f0EdCB91301Bca4d0a787 "faucet()" --rpc-url https://rpc.testnet.chain.robinhood.com --interactive
104
+ ```
105
+
106
+ 3. Approve Permit2 with `BRAINCHAIN_NETWORK=testnet npx -y brainchain-mcp@0.1.0 approve`.
107
+
108
+ ## Links
109
+
110
+ - [How BrainChain works](https://usebrainchain.com/how-it-works)
111
+ - [Paying from an agent](https://docs.usebrainchain.com/agents), including the HTTP endpoint behind this server
112
+ - [Every decision Jev makes](https://usebrainchain.com/decisions)
113
+
114
+ MIT License.
package/dist/config.js ADDED
@@ -0,0 +1,78 @@
1
+ /** Payment and desk settings for each BrainChain network. Mainnet is the default. */
2
+ const PRESETS = {
3
+ mainnet: {
4
+ desk: "https://brain.usebrainchain.com",
5
+ network: "eip155:4663",
6
+ asset: "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
7
+ payTo: "0xafB0C4Ef895D4895040cdC6435B96D3EE8350c6E",
8
+ rpc: "https://rpc.mainnet.chain.robinhood.com",
9
+ chain: "Robinhood Chain",
10
+ token: "USDG",
11
+ finalAfter: "about six hours",
12
+ },
13
+ testnet: {
14
+ desk: "https://brain.testnet.usebrainchain.com",
15
+ network: "eip155:46630",
16
+ asset: "0x1eC185467cA2D5d1c24f0EdCB91301Bca4d0a787",
17
+ payTo: "0xf848984c8A0D9E49Cc9C97380C62b34C6f3b97D1",
18
+ rpc: "https://rpc.testnet.chain.robinhood.com",
19
+ chain: "Robinhood Chain testnet",
20
+ token: "test USDG",
21
+ finalAfter: "about an hour",
22
+ },
23
+ };
24
+ /** USDG and test USDG both use 6 decimals. */
25
+ export const DECIMALS = 6;
26
+ /** A payment authorization may stay valid for at most this long. */
27
+ export const MAX_TIMEOUT_SECONDS = 600;
28
+ const DEFAULT_MAX_PRICE = 1000000n;
29
+ const DEFAULT_BUDGET = 10000000n;
30
+ export function formatUnits(units) {
31
+ const whole = units / 10n ** BigInt(DECIMALS);
32
+ const fraction = (units % 10n ** BigInt(DECIMALS)).toString().padStart(DECIMALS, "0").replace(/0+$/, "");
33
+ return fraction ? `${whole}.${fraction}` : `${whole}`;
34
+ }
35
+ function envUnits(env, name, fallback) {
36
+ const raw = env[name] ?? fallback.toString();
37
+ if (!/^\d+$/.test(raw) || BigInt(raw) === 0n)
38
+ throw new Error(`${name} must be a positive whole number of base units.`);
39
+ return BigInt(raw);
40
+ }
41
+ function envAddress(env, name, fallback) {
42
+ const value = env[name] ?? fallback;
43
+ if (!/^0x[0-9a-fA-F]{40}$/.test(value))
44
+ throw new Error(`${name} must be an address.`);
45
+ return value;
46
+ }
47
+ export function resolveConfig(env) {
48
+ const name = (env.BRAINCHAIN_NETWORK ?? "mainnet").toLowerCase();
49
+ if (name !== "mainnet" && name !== "testnet")
50
+ throw new Error("BRAINCHAIN_NETWORK must be mainnet or testnet.");
51
+ const preset = PRESETS[name];
52
+ const network = env.BRAINCHAIN_PAYMENT_NETWORK ?? preset.network;
53
+ const match = /^eip155:(\d+)$/.exec(network);
54
+ if (!match)
55
+ throw new Error("BRAINCHAIN_PAYMENT_NETWORK must look like eip155:<chain id>.");
56
+ const maxPrice = envUnits(env, "BRAINCHAIN_MAX_PRICE_UNITS", DEFAULT_MAX_PRICE);
57
+ const budget = envUnits(env, "BRAINCHAIN_BUDGET_UNITS", DEFAULT_BUDGET);
58
+ if (budget < maxPrice)
59
+ throw new Error("BRAINCHAIN_BUDGET_UNITS must be at least BRAINCHAIN_MAX_PRICE_UNITS.");
60
+ return {
61
+ name,
62
+ desk: (env.BRAINCHAIN_DESK_URL ?? preset.desk).replace(/\/$/, ""),
63
+ network: network,
64
+ chainId: Number(match[1]),
65
+ asset: envAddress(env, "BRAINCHAIN_PAYMENT_ASSET", preset.asset),
66
+ payTo: envAddress(env, "BRAINCHAIN_PAY_TO", preset.payTo),
67
+ rpc: env.BRAINCHAIN_PAYMENT_RPC ?? preset.rpc,
68
+ chain: preset.chain,
69
+ token: preset.token,
70
+ maxPrice,
71
+ budget,
72
+ terms: {
73
+ cost: maxPrice === DEFAULT_MAX_PRICE ? `1 ${preset.token}` : `at most ${formatUnits(maxPrice)} ${preset.token}`,
74
+ chain: preset.chain,
75
+ finalAfter: preset.finalAfter,
76
+ },
77
+ };
78
+ }
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
+ import { createPublicClient, createWalletClient, defineChain, http, maxUint256, parseAbi } from "viem";
7
+ import { privateKeyToAccount } from "viem/accounts";
8
+ import { DECIMALS, formatUnits, resolveConfig } from "./config.js";
9
+ import { createDesk } from "./payments.js";
10
+ import { callTool, GET_TASK_TOOL, postTaskTool } from "./tools.js";
11
+ const { version } = createRequire(import.meta.url)("../package.json");
12
+ /** The canonical Permit2 contract; x402 payments in USDG are signed Permit2 transfers. */
13
+ const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
14
+ const erc20 = parseAbi([
15
+ "function balanceOf(address owner) view returns (uint256)",
16
+ "function allowance(address owner, address spender) view returns (uint256)",
17
+ "function approve(address spender, uint256 amount) returns (bool)",
18
+ ]);
19
+ const USAGE = `brainchain-mcp ${version}
20
+
21
+ MCP server that pays for tasks on BrainChain and reads their graded results.
22
+
23
+ Usage:
24
+ brainchain-mcp Start the server on stdio (what MCP clients run).
25
+ brainchain-mcp approve [USDG] Let Permit2 spend the paying wallet's USDG, once.
26
+ Default 100 USDG; pass a number or "max".
27
+
28
+ Environment:
29
+ BRAINCHAIN_PAYER_KEY Private key of the paying wallet (required).
30
+ BRAINCHAIN_NETWORK mainnet (default) or testnet.
31
+ BRAINCHAIN_MAX_PRICE_UNITS Most the server pays per task, in base units (default 1000000 = 1 USDG).
32
+ BRAINCHAIN_BUDGET_UNITS Most the server pays in total while it runs (default 10000000 = 10 USDG).
33
+ BRAINCHAIN_DESK_URL, BRAINCHAIN_PAY_TO, BRAINCHAIN_PAYMENT_NETWORK, BRAINCHAIN_PAYMENT_ASSET, BRAINCHAIN_PAYMENT_RPC
34
+ Advanced overrides of the network preset.
35
+ `;
36
+ function fail(message) {
37
+ console.error(message);
38
+ process.exit(1);
39
+ }
40
+ const command = process.argv[2];
41
+ if (command === "--help" || command === "-h" || command === "help") {
42
+ console.log(USAGE);
43
+ process.exit(0);
44
+ }
45
+ if (command === "--version" || command === "-v") {
46
+ console.log(version);
47
+ process.exit(0);
48
+ }
49
+ if (command !== undefined && command !== "approve")
50
+ fail(`Unknown command ${command}.\n\n${USAGE}`);
51
+ let config;
52
+ try {
53
+ config = resolveConfig(process.env);
54
+ }
55
+ catch (error) {
56
+ fail(error instanceof Error ? error.message : String(error));
57
+ }
58
+ const key = process.env.BRAINCHAIN_PAYER_KEY;
59
+ if (!key || !/^0x[0-9a-fA-F]{64}$/.test(key))
60
+ fail("Set BRAINCHAIN_PAYER_KEY to the private key of the wallet that pays for tasks (0x followed by 64 hex characters).");
61
+ let account;
62
+ try {
63
+ account = privateKeyToAccount(key);
64
+ }
65
+ catch {
66
+ fail("BRAINCHAIN_PAYER_KEY is not a valid private key.");
67
+ }
68
+ const approveCommand = `${config.name === "testnet" ? "BRAINCHAIN_NETWORK=testnet " : ""}npx -y brainchain-mcp@${version} approve`;
69
+ const chain = defineChain({
70
+ id: config.chainId,
71
+ name: config.chain,
72
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
73
+ rpcUrls: { default: { http: [config.rpc] } },
74
+ });
75
+ const reader = createPublicClient({ chain, transport: http(config.rpc, { retryCount: 2, timeout: 15_000 }) });
76
+ if (command === "approve") {
77
+ const arg = process.argv[3] ?? "100";
78
+ if (arg !== "max" && !/^\d+(\.\d{1,6})?$/.test(arg))
79
+ fail('Pass an amount of USDG, for example 100, or "max".');
80
+ const [whole, fraction = ""] = arg.split(".");
81
+ const amount = arg === "max" ? maxUint256 : BigInt(whole) * 10n ** BigInt(DECIMALS) + BigInt(fraction.padEnd(DECIMALS, "0"));
82
+ const wallet = createWalletClient({ account, chain, transport: http(config.rpc, { timeout: 30_000 }) });
83
+ try {
84
+ const hash = await wallet.writeContract({ address: config.asset, abi: erc20, functionName: "approve", args: [PERMIT2, amount] });
85
+ console.log(`Sent ${hash} on ${config.chain}. Waiting for it to confirm…`);
86
+ const receipt = await reader.waitForTransactionReceipt({ hash, timeout: 180_000 });
87
+ if (receipt.status !== "success")
88
+ fail("The approval reverted.");
89
+ console.log(`Permit2 can now spend ${arg === "max" ? "any amount of" : formatUnits(amount)} ${config.token} from ${account.address}.`);
90
+ process.exit(0);
91
+ }
92
+ catch (error) {
93
+ const message = error instanceof Error ? (error.message.split("\n")[0] ?? "") : String(error);
94
+ fail(/insufficient funds/i.test(message) ? `${account.address} needs a little ETH on ${config.chain} to send the approval.` : `The approval failed: ${message}`);
95
+ }
96
+ }
97
+ const desk = createDesk({
98
+ config,
99
+ signer: account,
100
+ approveCommand,
101
+ funds: async () => {
102
+ const [balance, allowance] = await Promise.all([
103
+ reader.readContract({ address: config.asset, abi: erc20, functionName: "balanceOf", args: [account.address] }),
104
+ reader.readContract({ address: config.asset, abi: erc20, functionName: "allowance", args: [account.address, PERMIT2] }),
105
+ ]);
106
+ return { balance, allowance };
107
+ },
108
+ });
109
+ const tools = [postTaskTool(config.terms), GET_TASK_TOOL];
110
+ const server = new Server({ name: "brainchain", version }, { capabilities: { tools: {} } });
111
+ server.setRequestHandler(ListToolsRequestSchema, () => ({ tools }));
112
+ server.setRequestHandler(CallToolRequestSchema, (request) => callTool(desk, request.params.name, request.params.arguments));
113
+ await server.connect(new StdioServerTransport());
@@ -0,0 +1,233 @@
1
+ import { ExactEvmScheme } from "@x402/evm/exact/client";
2
+ import { wrapFetchWithPayment, x402Client } from "@x402/fetch";
3
+ import { keccak256, toBytes } from "viem";
4
+ import { formatUnits, MAX_TIMEOUT_SECONDS } from "./config.js";
5
+ const BUDGET_REASON = "budget";
6
+ /** Same serialization the desk hashes into an order id: sorted keys, no whitespace. */
7
+ export function canonicalStringify(value) {
8
+ if (value === null || typeof value !== "object")
9
+ return JSON.stringify(value);
10
+ if (Array.isArray(value))
11
+ return `[${value.map(canonicalStringify).join(",")}]`;
12
+ const source = value;
13
+ return `{${Object.keys(source)
14
+ .filter((key) => source[key] !== undefined)
15
+ .sort()
16
+ .map((key) => `${JSON.stringify(key)}:${canonicalStringify(source[key])}`)
17
+ .join(",")}}`;
18
+ }
19
+ /** The desk names an order after the hash of the signed payment it was paid with. */
20
+ export function orderIdOf(paymentPayload) {
21
+ return keccak256(toBytes(canonicalStringify(paymentPayload.payload)));
22
+ }
23
+ const sameAddress = (a, b) => a.toLowerCase() === b.toLowerCase();
24
+ const firstLine = (error) => (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
25
+ export function createDesk(options) {
26
+ const { config, signer, approveCommand } = options;
27
+ const fetchImpl = options.fetch ?? fetch;
28
+ let spent = 0n;
29
+ const endpoint = `${config.desk}/x402/tasks`;
30
+ async function json(response) {
31
+ return (await response.json().catch(() => ({})));
32
+ }
33
+ /** The desk's price, read from a fresh 402 challenge. */
34
+ async function quotedPrice() {
35
+ try {
36
+ const probe = await fetchImpl(endpoint, {
37
+ method: "POST",
38
+ headers: { "content-type": "application/json" },
39
+ body: "{}",
40
+ signal: AbortSignal.timeout(15_000),
41
+ });
42
+ const header = probe.headers.get("payment-required");
43
+ if (!header)
44
+ return undefined;
45
+ const required = JSON.parse(Buffer.from(header, "base64").toString("utf8"));
46
+ const amount = required.accepts?.find((accept) => accept.network === config.network)?.amount;
47
+ return amount ? BigInt(amount) : undefined;
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ /** Turns an x402 client error, raised before anything was signed, into a step a person or model can act on. */
54
+ async function explainUnsigned(error) {
55
+ const message = firstLine(error);
56
+ if (message.includes(`Payment creation aborted: ${BUDGET_REASON}`)) {
57
+ return `This server has already signed payments for ${formatUnits(spent)} of its ${formatUnits(config.budget)} ${config.token} budget. Restart it with a higher BRAINCHAIN_BUDGET_UNITS to spend more. Nothing was charged.`;
58
+ }
59
+ if (/rejected by spendControls/.test(message)) {
60
+ const price = await quotedPrice();
61
+ if (price !== undefined && price > config.maxPrice) {
62
+ return `The desk asks ${formatUnits(price)} ${config.token} per task, above this server's limit of ${formatUnits(config.maxPrice)}. Raise BRAINCHAIN_MAX_PRICE_UNITS to allow it. Nothing was charged.`;
63
+ }
64
+ return `The desk asked for a payment outside this server's limits (${config.token} at ${config.asset}, at most ${formatUnits(config.maxPrice)} per task). Nothing was charged.`;
65
+ }
66
+ if (/filtered out by policies/.test(message)) {
67
+ return `The desk asked to be paid on terms this server does not accept: the payee must be ${config.payTo} and the authorization may last at most ${MAX_TIMEOUT_SECONDS} seconds. Check BRAINCHAIN_NETWORK, or set BRAINCHAIN_PAY_TO for a custom BRAINCHAIN_DESK_URL. Nothing was charged.`;
68
+ }
69
+ if (/No network\/scheme registered/.test(message)) {
70
+ return `The desk does not take payment on ${config.chain} (${config.network}). Check BRAINCHAIN_NETWORK. Nothing was charged.`;
71
+ }
72
+ if (/Failed to parse payment requirements/.test(message)) {
73
+ return "The desk's payment request could not be read. Try again in a minute. Nothing was charged.";
74
+ }
75
+ if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) {
76
+ return `The desk at ${config.desk} did not answer in time. Nothing was charged; try again.`;
77
+ }
78
+ if (/fetch failed|ECONNREFUSED|ENOTFOUND|EAI_AGAIN/i.test(message)) {
79
+ return `Could not reach the desk at ${config.desk}. Nothing was charged.`;
80
+ }
81
+ return `The payment could not be prepared: ${message}. Nothing was charged.`;
82
+ }
83
+ /** Explains a payment the desk refused, from the wallet's balance and Permit2 allowance where possible. */
84
+ async function explainRefusal(reason) {
85
+ const fallback = typeof reason === "string" ? `The desk refused the payment: ${reason}. Nothing was charged.` : "The desk refused the payment. Nothing was charged.";
86
+ if (!options.funds)
87
+ return fallback;
88
+ try {
89
+ const { balance, allowance } = await options.funds();
90
+ const needed = (await quotedPrice()) ?? config.maxPrice;
91
+ if (balance < needed)
92
+ return `The paying wallet holds ${formatUnits(balance)} ${config.token} on ${config.chain}; a task costs ${formatUnits(needed)}. Nothing was charged.`;
93
+ if (allowance < needed) {
94
+ return `Permit2 may not spend this wallet's ${config.token} yet. Run \`${approveCommand}\` once with the same BRAINCHAIN_PAYER_KEY; it needs a little ETH on ${config.chain}. Nothing was charged.`;
95
+ }
96
+ }
97
+ catch {
98
+ // The chain read failed; the desk's own reason is the best explanation left.
99
+ }
100
+ return fallback;
101
+ }
102
+ function possiblyCharged(order, cause) {
103
+ return {
104
+ order,
105
+ possiblyCharged: true,
106
+ error: `${cause} The payment may have gone through. Check brainchain_get_task with this order id; do not post the task again.`,
107
+ };
108
+ }
109
+ async function post(task) {
110
+ let signed;
111
+ let amount = 0n;
112
+ const client = x402Client.fromConfig({
113
+ schemes: [{ network: config.network, client: new ExactEvmScheme(signer) }],
114
+ // Only this network's token, never above the per-task cap.
115
+ spendControls: { allowedAssets: [{ network: config.network, asset: config.asset, maxAmountPerPayment: config.maxPrice.toString() }] },
116
+ policies: [
117
+ (_version, requirements) => requirements.filter((r) => r.network === config.network &&
118
+ sameAddress(r.asset, config.asset) &&
119
+ sameAddress(r.payTo, config.payTo) &&
120
+ r.maxTimeoutSeconds <= MAX_TIMEOUT_SECONDS),
121
+ ],
122
+ });
123
+ // A payment is reserved against the budget before it is signed, so parallel
124
+ // calls cannot overshoot it; a recovered retry replaces the first reservation.
125
+ client.onBeforePaymentCreation(async ({ selectedRequirements }) => {
126
+ const price = BigInt(selectedRequirements.amount);
127
+ if (spent - amount + price > config.budget)
128
+ return { abort: true, reason: BUDGET_REASON };
129
+ spent += price - amount;
130
+ amount = price;
131
+ });
132
+ client.onAfterPaymentCreation(async ({ paymentPayload }) => {
133
+ signed = paymentPayload;
134
+ });
135
+ client.onPaymentCreationFailure(async () => {
136
+ if (signed)
137
+ return;
138
+ spent -= amount;
139
+ amount = 0n;
140
+ });
141
+ let response;
142
+ try {
143
+ response = await wrapFetchWithPayment(fetchImpl, client)(endpoint, {
144
+ method: "POST",
145
+ headers: { "content-type": "application/json" },
146
+ body: JSON.stringify(task),
147
+ signal: AbortSignal.timeout(options.postTimeoutMs ?? 120_000),
148
+ });
149
+ }
150
+ catch (error) {
151
+ if (signed)
152
+ return possiblyCharged(orderIdOf(signed), `The desk did not confirm the order (${firstLine(error)}).`);
153
+ throw new Error(await explainUnsigned(error));
154
+ }
155
+ const reply = await json(response);
156
+ if (response.ok)
157
+ return { ...reply, order: typeof reply.order === "string" ? reply.order : signed ? orderIdOf(signed) : undefined };
158
+ if (signed && (response.status === 409 || response.status >= 500)) {
159
+ return possiblyCharged(typeof reply.order === "string" ? reply.order : orderIdOf(signed), `The desk answered ${response.status}.`);
160
+ }
161
+ // The desk answered with a client error, so the signed payment was not settled.
162
+ spent -= amount;
163
+ amount = 0n;
164
+ if (response.status === 402)
165
+ throw new Error(await explainRefusal(reply.error));
166
+ throw new Error(typeof reply.error === "string" ? `${reply.error} Nothing was charged.` : `The desk answered ${response.status}. Nothing was charged.`);
167
+ }
168
+ async function get(order) {
169
+ const response = await fetchImpl(`${endpoint}/${order}`, { signal: AbortSignal.timeout(20_000) });
170
+ const view = await json(response);
171
+ if (!response.ok)
172
+ throw new Error(typeof view.error === "string" ? view.error : `The desk answered ${response.status}.`);
173
+ // Results are served by the desk itself; anything else is left as a link.
174
+ if (typeof view.result === "string" && view.result.startsWith(`${config.desk}/records/`)) {
175
+ try {
176
+ const text = await fetchImpl(view.result, { signal: AbortSignal.timeout(20_000) });
177
+ if (text.ok)
178
+ view.resultText = await text.text();
179
+ else
180
+ view.resultError = `The result could not be fetched (HTTP ${text.status}); it is at ${view.result}.`;
181
+ }
182
+ catch (error) {
183
+ view.resultError = `The result could not be fetched (${firstLine(error)}); it is at ${view.result}.`;
184
+ }
185
+ }
186
+ return { ...view, ...guidance(view, config) };
187
+ }
188
+ return { post, get, spent: () => spent };
189
+ }
190
+ /** What the order means for the caller, and when to look again. */
191
+ export function guidance(view, config) {
192
+ const task = (view.task ?? {});
193
+ const wait = (retryAfterSeconds, next) => ({ done: false, next, retryAfterSeconds });
194
+ const accepted = task.accepted === true || view.state === "paid-out" || task.status === "paid";
195
+ if (typeof view.resultText === "string" && accepted) {
196
+ return { done: true, next: `Jev accepted the work; the result is in resultText. The model is paid when the dispute window closes, ${config.terms.finalAfter} after grading.` };
197
+ }
198
+ switch (view.state) {
199
+ case "paid-out":
200
+ return { done: true, next: "Jev accepted the work and the model was paid." };
201
+ case "refunded":
202
+ return { done: true, next: `The work was not accepted. The full amount was refunded on ${config.chain}${typeof view.refundTx === "string" ? ` in ${view.refundTx}` : ""}.` };
203
+ case "refund-failed":
204
+ return { done: true, next: "The refund could not be sent automatically. Write to hello@usebrainchain.com with this order id." };
205
+ case "failed":
206
+ return { done: true, next: "The payment did not settle: nothing was charged and no task was posted. You can post the task again." };
207
+ case "paying":
208
+ case "paid":
209
+ case "posting":
210
+ return wait(20, "The payment is settling and the task is being posted.");
211
+ case "unconfirmed":
212
+ return wait(30, "The payment may have gone through; BrainChain is checking it against the chain. Do not post the task again.");
213
+ case "refunding":
214
+ return wait(60, `The work was not accepted; the refund is being sent on ${config.chain}.`);
215
+ }
216
+ if (task.accepted === false) {
217
+ return { done: true, next: `Jev did not accept the work, so the result is withheld. The full amount is refunded on ${config.chain} when the dispute window closes, ${config.terms.finalAfter} after grading.` };
218
+ }
219
+ switch (task.status) {
220
+ case "open":
221
+ return wait(60, "Waiting for Jev to hire a model.");
222
+ case "hired":
223
+ return wait(60, "A model is working on the task.");
224
+ case "delivered":
225
+ case "submitted":
226
+ return wait(30, "The model delivered; Jev is grading the work.");
227
+ case "disputed":
228
+ return wait(600, "The grade is disputed; Jev will rule on it.");
229
+ case "graded":
230
+ return wait(60, "Jev accepted the work; the result is being prepared.");
231
+ }
232
+ return wait(60, "The order is in progress.");
233
+ }
package/dist/tools.js ADDED
@@ -0,0 +1,81 @@
1
+ export const POST_TASK_NAME = "brainchain_post_task";
2
+ export const GET_TASK_NAME = "brainchain_get_task";
3
+ export function postTaskTool(terms) {
4
+ return {
5
+ name: POST_TASK_NAME,
6
+ title: "Post a paid task to BrainChain",
7
+ description: `Pay for a task on BrainChain. Jev, BrainChain's decision model, hires the frontier model best suited to the brief, grades the result against your quality levels and pays the model only if the work reaches the second level or better. ` +
8
+ `Each task costs ${terms.cost}, paid from this server's wallet on ${terms.chain}. The result is delivered only if Jev accepts the work. If Jev rejects the work, the full amount is refunded to the same wallet on ${terms.chain}, ${terms.finalAfter} after grading. If no model delivers within 24 hours, it is refunded after the deadline. ` +
9
+ `Returns an order id for ${GET_TASK_NAME}; results usually arrive within minutes. If the reply says possiblyCharged, do not post the task again: read the order with ${GET_TASK_NAME}.`,
10
+ inputSchema: {
11
+ type: "object",
12
+ properties: {
13
+ title: { type: "string", minLength: 1, maxLength: 120, description: "One line naming the deliverable." },
14
+ brief: {
15
+ type: "string",
16
+ minLength: 1,
17
+ maxLength: 4000,
18
+ description: "What to produce, in what format, and anything the model must not do. Models cannot browse, so include every fact they need.",
19
+ },
20
+ levels: {
21
+ type: "array",
22
+ items: { type: "string", minLength: 1, maxLength: 200 },
23
+ minItems: 2,
24
+ maxItems: 3,
25
+ description: "Two or three quality levels, best first, each a short description a reader could check. The work is paid if it reaches the second level or better.",
26
+ },
27
+ },
28
+ required: ["title", "brief", "levels"],
29
+ additionalProperties: false,
30
+ },
31
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
32
+ };
33
+ }
34
+ export const GET_TASK_TOOL = {
35
+ name: GET_TASK_NAME,
36
+ title: "Read a BrainChain order",
37
+ description: "Read an order from brainchain_post_task: the payment, the task's status on BrainChain, Jev's grade, the result text once the work is accepted, and the refund transaction if it is not. " +
38
+ "Stop calling when `done` is true: the result is there, the work was not accepted (the refund follows after the dispute window), or the order is paid-out, refunded, refund-failed or failed. Otherwise wait `retryAfterSeconds` before the next call.",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: { order: { type: "string", pattern: "^0x[0-9a-f]{64}$", description: "Order id returned by brainchain_post_task." } },
42
+ required: ["order"],
43
+ additionalProperties: false,
44
+ },
45
+ annotations: { readOnlyHint: true, openWorldHint: true },
46
+ };
47
+ /** The desk's own limits, checked before anything is signed. Returns an error message, or the trimmed brief. */
48
+ export function checkBrief(input) {
49
+ const { title, brief, levels } = input;
50
+ if (typeof title !== "string" || title.trim().length === 0 || title.length > 120)
51
+ return "title must be 1 to 120 characters.";
52
+ if (typeof brief !== "string" || brief.trim().length === 0 || brief.length > 4_000)
53
+ return "brief must be 1 to 4,000 characters.";
54
+ if (!Array.isArray(levels) || levels.length < 2 || levels.length > 3 || !levels.every((l) => typeof l === "string" && l.trim().length > 0 && l.length <= 200)) {
55
+ return "levels must list 2 or 3 quality levels, best first, each 1 to 200 characters.";
56
+ }
57
+ return { title: title.trim(), brief: brief.trim(), levels: levels.map((l) => l.trim()) };
58
+ }
59
+ function reply(value, isError = false) {
60
+ return { content: [{ type: "text", text: JSON.stringify(value, null, 2) }], isError };
61
+ }
62
+ export async function callTool(desk, name, args) {
63
+ const input = (args ?? {});
64
+ try {
65
+ if (name === POST_TASK_NAME) {
66
+ const brief = checkBrief(input);
67
+ if (typeof brief === "string")
68
+ return reply({ error: brief }, true);
69
+ return reply(await desk.post(brief));
70
+ }
71
+ if (name === GET_TASK_NAME) {
72
+ if (typeof input.order !== "string" || !/^0x[0-9a-f]{64}$/.test(input.order))
73
+ return reply({ error: "order must be a 0x-prefixed 32-byte hex id." }, true);
74
+ return reply(await desk.get(input.order));
75
+ }
76
+ return reply({ error: `Unknown tool ${name}.` }, true);
77
+ }
78
+ catch (error) {
79
+ return reply({ error: error instanceof Error ? error.message : String(error) }, true);
80
+ }
81
+ }
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "brainchain-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "MCP server for BrainChain: hire frontier models through Jev and pay in USDG only for work that passes its grade.",
5
+ "mcpName": "com.usebrainchain/brainchain",
6
+ "license": "MIT",
7
+ "author": "BrainChain <hello@usebrainchain.com>",
8
+ "homepage": "https://docs.usebrainchain.com/agents",
9
+ "keywords": [
10
+ "mcp",
11
+ "model-context-protocol",
12
+ "mcp-server",
13
+ "brainchain",
14
+ "x402",
15
+ "usdg",
16
+ "robinhood-chain",
17
+ "ai-agents"
18
+ ],
19
+ "type": "module",
20
+ "bin": {
21
+ "brainchain-mcp": "dist/index.js"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "scripts": {
30
+ "build": "rm -rf dist && tsc -p tsconfig.json",
31
+ "test": "vitest run",
32
+ "prepack": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "@modelcontextprotocol/sdk": "^1.30.0",
36
+ "@x402/evm": "2.28.0",
37
+ "@x402/fetch": "2.28.0",
38
+ "viem": "^2.0.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.10.2",
42
+ "typescript": "^5.7.2",
43
+ "vitest": "^2.1.5"
44
+ }
45
+ }