brainchain-mcp 0.5.0 → 0.5.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 +14 -14
- package/dist/config.js +10 -1
- package/dist/daily.js +36 -8
- package/dist/payments.js +23 -11
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Read the key without echoing it or leaving it in your shell history, then add th
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
printf 'Key: ' && read -rs BRAINCHAIN_PAYER_KEY && echo
|
|
19
|
-
claude mcp add brainchain --scope user -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.5.
|
|
19
|
+
claude mcp add brainchain --scope user -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.5.1
|
|
20
20
|
unset BRAINCHAIN_PAYER_KEY
|
|
21
21
|
```
|
|
22
22
|
|
|
@@ -27,7 +27,7 @@ unset BRAINCHAIN_PAYER_KEY
|
|
|
27
27
|
"mcpServers": {
|
|
28
28
|
"brainchain": {
|
|
29
29
|
"command": "npx",
|
|
30
|
-
"args": ["-y", "brainchain-mcp@0.5.
|
|
30
|
+
"args": ["-y", "brainchain-mcp@0.5.1"],
|
|
31
31
|
"env": { "BRAINCHAIN_PAYER_KEY": "${BRAINCHAIN_PAYER_KEY}" }
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -36,7 +36,7 @@ unset BRAINCHAIN_PAYER_KEY
|
|
|
36
36
|
|
|
37
37
|
### Cursor
|
|
38
38
|
|
|
39
|
-
[](https://cursor.com/en/install-mcp?name=brainchain&config=
|
|
39
|
+
[](https://cursor.com/en/install-mcp?name=brainchain&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImJyYWluY2hhaW4tbWNwQDAuNS4xIl0sImVudiI6eyJCUkFJTkNIQUlOX1BBWUVSX0tFWSI6IiR7ZW52OkJSQUlOQ0hBSU5fUEFZRVJfS0VZfSJ9fQ%3D%3D)
|
|
40
40
|
|
|
41
41
|
Or add the server to a project's `.cursor/mcp.json` (`~/.cursor/mcp.json` for every project):
|
|
42
42
|
|
|
@@ -45,7 +45,7 @@ Or add the server to a project's `.cursor/mcp.json` (`~/.cursor/mcp.json` for ev
|
|
|
45
45
|
"mcpServers": {
|
|
46
46
|
"brainchain": {
|
|
47
47
|
"command": "npx",
|
|
48
|
-
"args": ["-y", "brainchain-mcp@0.5.
|
|
48
|
+
"args": ["-y", "brainchain-mcp@0.5.1"],
|
|
49
49
|
"env": { "BRAINCHAIN_PAYER_KEY": "${env:BRAINCHAIN_PAYER_KEY}" }
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -63,7 +63,7 @@ Add the server to the client's `mcpServers` configuration. For Claude Desktop th
|
|
|
63
63
|
"mcpServers": {
|
|
64
64
|
"brainchain": {
|
|
65
65
|
"command": "npx",
|
|
66
|
-
"args": ["-y", "brainchain-mcp@0.5.
|
|
66
|
+
"args": ["-y", "brainchain-mcp@0.5.1"],
|
|
67
67
|
"env": { "BRAINCHAIN_PAYER_KEY": "0xYOUR_KEY" }
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -82,7 +82,7 @@ These clients keep the key in that file as plain text.
|
|
|
82
82
|
"brainchain": {
|
|
83
83
|
"type": "stdio",
|
|
84
84
|
"command": "npx",
|
|
85
|
-
"args": ["-y", "brainchain-mcp@0.5.
|
|
85
|
+
"args": ["-y", "brainchain-mcp@0.5.1"],
|
|
86
86
|
"env": { "BRAINCHAIN_PAYER_KEY": "${input:brainchain-key}" }
|
|
87
87
|
}
|
|
88
88
|
},
|
|
@@ -100,10 +100,10 @@ Save the key in a file only your account can read, then point the server at it:
|
|
|
100
100
|
mkdir -p ~/.brainchain
|
|
101
101
|
(umask 077; printf 'Key: ' && read -rs key && echo && printf '%s\n' "$key" > ~/.brainchain/payer.key)
|
|
102
102
|
chmod 600 ~/.brainchain/payer.key
|
|
103
|
-
openclaw mcp set brainchain '{"command":"npx","args":["-y","brainchain-mcp@0.5.
|
|
103
|
+
openclaw mcp set brainchain '{"command":"npx","args":["-y","brainchain-mcp@0.5.1"],"env":{"BRAINCHAIN_PAYER_KEY_FILE":"~/.brainchain/payer.key"}}'
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
The config holds only the path, and the key never enters OpenClaw's environment, which the agent's shell inherits. A key saved in `skills.entries.brainchain.apiKey` does not reach the server. `BRAINCHAIN_PAYER_KEY_FILE=~/.brainchain/payer.key npx -y brainchain-mcp@0.5.
|
|
106
|
+
The config holds only the path, and the key never enters OpenClaw's environment, which the agent's shell inherits. A key saved in `skills.entries.brainchain.apiKey` does not reach the server. `BRAINCHAIN_PAYER_KEY_FILE=~/.brainchain/payer.key npx -y brainchain-mcp@0.5.1 wallet` checks the file and prints the wallet it holds. [`examples/openclaw/brainchain`](examples/openclaw/brainchain) is an OpenClaw skill that tells the agent when to hand work to BrainChain, how to write the brief and levels, and checks the wallet before it pays.
|
|
107
107
|
|
|
108
108
|
Each setup above pays in USDG on Robinhood Chain. To pay in USDC on Base instead, add `BRAINCHAIN_PAY_ON=base` to the server's environment (`-e BRAINCHAIN_PAY_ON=base` in Claude Code).
|
|
109
109
|
|
|
@@ -117,7 +117,7 @@ Each brief is posted on BrainChain and each result is served at a public link. K
|
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
119
|
printf 'Key: ' && read -rs BRAINCHAIN_PAYER_KEY && echo && export BRAINCHAIN_PAYER_KEY
|
|
120
|
-
npx -y brainchain-mcp@0.5.
|
|
120
|
+
npx -y brainchain-mcp@0.5.1 approve
|
|
121
121
|
unset BRAINCHAIN_PAYER_KEY
|
|
122
122
|
```
|
|
123
123
|
|
|
@@ -140,7 +140,7 @@ Results usually arrive within minutes. A post reply with `possiblyCharged` means
|
|
|
140
140
|
|
|
141
141
|
## What a task costs
|
|
142
142
|
|
|
143
|
-
- 1 USDG or 1 USDC, taken when the task is posted. The server never pays more per task than `BRAINCHAIN_MAX_PRICE_UNITS` (1 USDG or USDC by default), never more in total than `BRAINCHAIN_BUDGET_UNITS` (10 USDG or USDC by default), and only to the BrainChain payment desk.
|
|
143
|
+
- 1 USDG or 1 USDC, taken when the task is posted. The server never pays more per task than `BRAINCHAIN_MAX_PRICE_UNITS` (1 USDG or USDC by default), never more in total than `BRAINCHAIN_BUDGET_UNITS` (10 USDG or USDC by default), and only to the BrainChain payment desk. Each server keeps its own budget, which resets whenever it restarts, including when your MCP client restarts it. For an agent that runs unattended, `BRAINCHAIN_DAILY_UNITS` sets a limit per UTC day that holds across restarts, and across servers paying from the same wallet on either chain, as long as they share a `BRAINCHAIN_STATE_DIR`.
|
|
144
144
|
- If Jev accepts the work, you get the result and the model receives the payment minus BrainChain's protocol fee (3% on mainnet).
|
|
145
145
|
- If Jev rejects the work, or no model delivers within 24 hours, you get no result and the full amount goes back to the paying wallet on the chain it paid on.
|
|
146
146
|
- The payment or refund is final about six hours after grading (about an hour on testnet), when the dispute window closes.
|
|
@@ -160,9 +160,9 @@ Every hire and grade is recorded on BrainChain and listed at [usebrainchain.com/
|
|
|
160
160
|
| `BRAINCHAIN_BUDGET_UNITS` | `10000000` | Maximum paid in total while the server runs, in base units (10000000 is 10 USDG). |
|
|
161
161
|
| `BRAINCHAIN_DAILY_UNITS` | no limit | Maximum the wallet pays per UTC day, in base units. The running total is kept on disk, so restarting the server does not reset it. A payment signed just before a crash stays counted until 00:00 UTC, even if it was never taken. |
|
|
162
162
|
| `BRAINCHAIN_STATE_DIR` | `~/.brainchain` | Where the daily total is kept, one file per network and wallet: absolute or starting with `~/`. |
|
|
163
|
-
| `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
|
|
163
|
+
| `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`, and an https URL unless it is on localhost or 127.0.0.1. |
|
|
164
164
|
|
|
165
|
-
If a setting is invalid, the server says so once on stderr when it starts, and both tools return the error. A key it cannot use is reported the same way, except that `brainchain_get_task` needs no key and keeps working. `npx -y brainchain-mcp@0.5.
|
|
165
|
+
If a setting is invalid, the server says so once on stderr when it starts, and both tools return the error. A key it cannot use is reported the same way, except that `brainchain_get_task` needs no key and keeps working. `npx -y brainchain-mcp@0.5.1 wallet` checks the settings and the key, and prints the wallet that pays. `--help` lists the commands and variables, and `--version` prints the version.
|
|
166
166
|
|
|
167
167
|
## Try it on testnet
|
|
168
168
|
|
|
@@ -170,7 +170,7 @@ Set `BRAINCHAIN_NETWORK=testnet` in the server's environment. Test tokens have n
|
|
|
170
170
|
|
|
171
171
|
```bash
|
|
172
172
|
printf 'Key: ' && read -rs BRAINCHAIN_PAYER_KEY && echo
|
|
173
|
-
claude mcp add brainchain --scope user -e BRAINCHAIN_NETWORK=testnet -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.5.
|
|
173
|
+
claude mcp add brainchain --scope user -e BRAINCHAIN_NETWORK=testnet -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.5.1
|
|
174
174
|
unset BRAINCHAIN_PAYER_KEY
|
|
175
175
|
```
|
|
176
176
|
|
|
@@ -189,7 +189,7 @@ unset BRAINCHAIN_PAYER_KEY
|
|
|
189
189
|
|
|
190
190
|
```bash
|
|
191
191
|
printf 'Key: ' && read -rs BRAINCHAIN_PAYER_KEY && echo && export BRAINCHAIN_PAYER_KEY
|
|
192
|
-
BRAINCHAIN_NETWORK=testnet npx -y brainchain-mcp@0.5.
|
|
192
|
+
BRAINCHAIN_NETWORK=testnet npx -y brainchain-mcp@0.5.1 approve
|
|
193
193
|
unset BRAINCHAIN_PAYER_KEY
|
|
194
194
|
```
|
|
195
195
|
|
package/dist/config.js
CHANGED
|
@@ -99,6 +99,15 @@ function envUrl(env, name, fallback) {
|
|
|
99
99
|
throw new Error(`${name} must be an http or https URL.`);
|
|
100
100
|
return value;
|
|
101
101
|
}
|
|
102
|
+
/** Signed payments travel to the desk, so plain http is allowed only where they never leave the machine. */
|
|
103
|
+
function envDesk(env, fallback) {
|
|
104
|
+
const desk = envUrl(env, "BRAINCHAIN_DESK_URL", fallback).replace(/\/+$/, "");
|
|
105
|
+
const { protocol, hostname } = new URL(desk);
|
|
106
|
+
if (protocol === "http:" && !["localhost", "127.0.0.1", "[::1]"].includes(hostname)) {
|
|
107
|
+
throw new Error("BRAINCHAIN_DESK_URL must be an https URL, or http on localhost or 127.0.0.1.");
|
|
108
|
+
}
|
|
109
|
+
return desk;
|
|
110
|
+
}
|
|
102
111
|
export function resolveConfig(env) {
|
|
103
112
|
const name = (envString(env, "BRAINCHAIN_NETWORK") ?? "mainnet").toLowerCase();
|
|
104
113
|
if (name !== "mainnet" && name !== "testnet")
|
|
@@ -122,7 +131,7 @@ export function resolveConfig(env) {
|
|
|
122
131
|
name,
|
|
123
132
|
payOn,
|
|
124
133
|
permit2: preset.permit2,
|
|
125
|
-
desk:
|
|
134
|
+
desk: envDesk(env, preset.desk),
|
|
126
135
|
network: network,
|
|
127
136
|
chainId: Number(match[1]),
|
|
128
137
|
asset: envAddress(env, "BRAINCHAIN_PAYMENT_ASSET", preset.asset),
|
package/dist/daily.js
CHANGED
|
@@ -4,10 +4,41 @@ import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/prom
|
|
|
4
4
|
import { dirname } from "node:path";
|
|
5
5
|
const LOCK_STALE_MS = 10_000;
|
|
6
6
|
const LOCK_WAIT_MS = 5_000;
|
|
7
|
+
/** Clearing a lock takes a few system calls, so a marker older than this was left by a crash. */
|
|
8
|
+
const TAKEOVER_STALE_MS = 5_000;
|
|
7
9
|
/** The UTC date, which is when the limit resets. */
|
|
8
10
|
export function utcDay(now) {
|
|
9
11
|
return new Date(now).toISOString().slice(0, 10);
|
|
10
12
|
}
|
|
13
|
+
/** How long ago a file was last written, or 0 when it is gone. */
|
|
14
|
+
const ageOf = (file) => stat(file).then((s) => Date.now() - s.mtimeMs, () => 0);
|
|
15
|
+
/**
|
|
16
|
+
* Removes a lock left by a process that died mid-write. The marker lets one
|
|
17
|
+
* process at a time judge and remove it, since removing a lock another process
|
|
18
|
+
* has just taken would give the lock to both.
|
|
19
|
+
*/
|
|
20
|
+
async function clearStale(lockPath) {
|
|
21
|
+
const marker = `${lockPath}.takeover`;
|
|
22
|
+
try {
|
|
23
|
+
await (await open(marker, "wx")).close();
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
if (error.code !== "EEXIST")
|
|
27
|
+
throw error;
|
|
28
|
+
if ((await ageOf(marker)) >= TAKEOVER_STALE_MS)
|
|
29
|
+
await rm(marker, { force: true });
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
if ((await ageOf(lockPath)) <= LOCK_STALE_MS)
|
|
34
|
+
return false;
|
|
35
|
+
await rm(lockPath, { force: true });
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await rm(marker, { force: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
11
42
|
export function dailyLedger(path, limit, now = Date.now) {
|
|
12
43
|
const lockPath = `${path}.lock`;
|
|
13
44
|
async function locked(work) {
|
|
@@ -21,15 +52,12 @@ export function dailyLedger(path, limit, now = Date.now) {
|
|
|
21
52
|
catch (error) {
|
|
22
53
|
if (error.code !== "EEXIST")
|
|
23
54
|
throw error;
|
|
24
|
-
// A lock left by a process that died mid-write.
|
|
25
|
-
const age = await stat(lockPath).then((s) => Date.now() - s.mtimeMs, () => 0);
|
|
26
|
-
if (age > LOCK_STALE_MS)
|
|
27
|
-
await rm(lockPath, { force: true });
|
|
28
|
-
else if (Date.now() > deadline)
|
|
29
|
-
throw new Error(`The daily spending record at ${path} is locked by another process; try again`);
|
|
30
|
-
else
|
|
31
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
32
55
|
}
|
|
56
|
+
if ((await ageOf(lockPath)) > LOCK_STALE_MS && (await clearStale(lockPath)))
|
|
57
|
+
continue;
|
|
58
|
+
if (Date.now() > deadline)
|
|
59
|
+
throw new Error(`The daily spending record at ${path} is locked by another process; try again`);
|
|
60
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
33
61
|
}
|
|
34
62
|
try {
|
|
35
63
|
return await work();
|
package/dist/payments.js
CHANGED
|
@@ -87,7 +87,8 @@ export function createDesk(options) {
|
|
|
87
87
|
}
|
|
88
88
|
if (message.includes(`Payment creation aborted: ${DAILY_REASON}`)) {
|
|
89
89
|
const today = await daily().spentToday().catch(() => undefined);
|
|
90
|
-
|
|
90
|
+
// One limit covers both chains, so it is named without a token.
|
|
91
|
+
return `This wallet has already signed payments for ${today === undefined ? "most" : formatUnits(today)} of its daily limit of ${formatUnits(config.daily)}, counting USDG and USDC together. The limit resets at 00:00 UTC; raise BRAINCHAIN_DAILY_UNITS to allow more. Nothing was charged.`;
|
|
91
92
|
}
|
|
92
93
|
if (/rejected by spendControls/.test(message)) {
|
|
93
94
|
const price = await quotedPrice();
|
|
@@ -191,19 +192,28 @@ export function createDesk(options) {
|
|
|
191
192
|
],
|
|
192
193
|
});
|
|
193
194
|
// A payment is reserved against the budget and the daily limit before it is
|
|
194
|
-
// signed, so parallel calls cannot overshoot either.
|
|
195
|
+
// signed, so parallel calls cannot overshoot either. The budget is counted before
|
|
196
|
+
// the first await, so no other call can pass the check in between.
|
|
195
197
|
client.onBeforePaymentCreation(async ({ selectedRequirements }) => {
|
|
196
198
|
const price = BigInt(selectedRequirements.amount);
|
|
197
199
|
if (spent + price > config.budget)
|
|
198
200
|
return { abort: true, reason: BUDGET_REASON };
|
|
199
|
-
const today = daily();
|
|
200
|
-
if (today) {
|
|
201
|
-
reservation = await today.reserve(price);
|
|
202
|
-
if (!reservation)
|
|
203
|
-
return { abort: true, reason: DAILY_REASON };
|
|
204
|
-
}
|
|
205
201
|
spent += price;
|
|
206
202
|
amount = price;
|
|
203
|
+
let today;
|
|
204
|
+
try {
|
|
205
|
+
today = daily();
|
|
206
|
+
if (today)
|
|
207
|
+
reservation = await today.reserve(price);
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
await unreserve();
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
if (today && !reservation) {
|
|
214
|
+
await unreserve();
|
|
215
|
+
return { abort: true, reason: DAILY_REASON };
|
|
216
|
+
}
|
|
207
217
|
});
|
|
208
218
|
client.onAfterPaymentCreation(async ({ paymentPayload }) => {
|
|
209
219
|
signed = paymentPayload;
|
|
@@ -321,10 +331,12 @@ export function guidance(view, config, now = Date.now()) {
|
|
|
321
331
|
// The caller hears the verdict now and comes back once, when the window has closed.
|
|
322
332
|
const rejected = () => {
|
|
323
333
|
const start = "Jev did not accept the work, so the result is withheld.";
|
|
324
|
-
const
|
|
334
|
+
const closes = due === undefined ? NaN : Date.parse(due);
|
|
335
|
+
const left = Number.isFinite(closes) ? Math.ceil((closes - now) / 1000) : undefined;
|
|
336
|
+
// The desk sends the refund within a minute or two of the window closing.
|
|
325
337
|
if (left !== undefined && left <= 0)
|
|
326
|
-
return wait(
|
|
327
|
-
const when =
|
|
338
|
+
return wait(60, `${start} The dispute window has closed, so the full amount is refunded on ${config.chain} shortly.`);
|
|
339
|
+
const when = left === undefined ? `, ${config.terms.finalAfter} after grading` : ` at ${due}`;
|
|
328
340
|
return wait(left === undefined ? 900 : left + 60, `${start} The full amount is refunded on ${config.chain} when the dispute window closes${when}, unless the model disputes the grade and a ruling pays it. Report this now, then read the order again after that time for the refund transaction.`);
|
|
329
341
|
};
|
|
330
342
|
switch (view.state) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainchain-mcp",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "MCP server for BrainChain: let your agent hire a frontier model for a task and pay in USDG on Robinhood Chain or USDC on Base, only for work that meets the quality bar you set.",
|
|
5
5
|
"mcpName": "com.usebrainchain/brainchain",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,8 +35,8 @@
|
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
38
|
-
"@x402/evm": "2.
|
|
39
|
-
"@x402/fetch": "2.
|
|
38
|
+
"@x402/evm": "2.29.0",
|
|
39
|
+
"@x402/fetch": "2.29.0",
|
|
40
40
|
"viem": "^2.0.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|