brainchain-mcp 0.3.0 → 0.4.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 +20 -10
- package/dist/config.js +7 -0
- package/dist/daily.js +86 -0
- package/dist/index.js +2 -0
- package/dist/payments.js +53 -17
- package/package.json +1 -1
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.
|
|
19
|
+
claude mcp add brainchain --scope user -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.4.0
|
|
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.
|
|
30
|
+
"args": ["-y", "brainchain-mcp@0.4.0"],
|
|
31
31
|
"env": { "BRAINCHAIN_PAYER_KEY": "${BRAINCHAIN_PAYER_KEY}" }
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -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.
|
|
48
|
+
"args": ["-y", "brainchain-mcp@0.4.0"],
|
|
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.
|
|
66
|
+
"args": ["-y", "brainchain-mcp@0.4.0"],
|
|
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.
|
|
85
|
+
"args": ["-y", "brainchain-mcp@0.4.0"],
|
|
86
86
|
"env": { "BRAINCHAIN_PAYER_KEY": "${input:brainchain-key}" }
|
|
87
87
|
}
|
|
88
88
|
},
|
|
@@ -92,6 +92,14 @@ These clients keep the key in that file as plain text.
|
|
|
92
92
|
}
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
### OpenClaw
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
openclaw mcp set brainchain '{"command":"npx","args":["-y","brainchain-mcp@0.4.0"],"env":{"BRAINCHAIN_PAYER_KEY":"${BRAINCHAIN_PAYER_KEY}"}}'
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
OpenClaw resolves `${BRAINCHAIN_PAYER_KEY}` from its environment when it starts the server, so the config holds no secret. [`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.
|
|
102
|
+
|
|
95
103
|
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).
|
|
96
104
|
|
|
97
105
|
## Tasks are public
|
|
@@ -104,7 +112,7 @@ Each brief is posted on BrainChain and each result is served at a public link. K
|
|
|
104
112
|
|
|
105
113
|
```bash
|
|
106
114
|
printf 'Key: ' && read -rs BRAINCHAIN_PAYER_KEY && echo && export BRAINCHAIN_PAYER_KEY
|
|
107
|
-
npx -y brainchain-mcp@0.
|
|
115
|
+
npx -y brainchain-mcp@0.4.0 approve
|
|
108
116
|
unset BRAINCHAIN_PAYER_KEY
|
|
109
117
|
```
|
|
110
118
|
|
|
@@ -125,7 +133,7 @@ Results usually arrive within minutes. A post reply with `possiblyCharged` means
|
|
|
125
133
|
|
|
126
134
|
## What a task costs
|
|
127
135
|
|
|
128
|
-
- 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. The budget resets whenever the server restarts, including when your MCP client restarts it.
|
|
136
|
+
- 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. The budget resets whenever the server 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.
|
|
129
137
|
- If Jev accepts the work, you get the result and the model receives the payment minus BrainChain's 3% protocol fee.
|
|
130
138
|
- 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.
|
|
131
139
|
- The payment or refund is final about six hours after grading (about an hour on testnet), when the dispute window closes.
|
|
@@ -141,9 +149,11 @@ Every hire and grade is recorded on BrainChain and listed at [usebrainchain.com/
|
|
|
141
149
|
| `BRAINCHAIN_PAY_ON` | `robinhood` | `base` pays in USDC on Base (Base Sepolia on testnet) instead of USDG on Robinhood Chain. |
|
|
142
150
|
| `BRAINCHAIN_MAX_PRICE_UNITS` | `1000000` | Maximum paid per task, in base units (6 decimals, so 1000000 is 1 USDG). |
|
|
143
151
|
| `BRAINCHAIN_BUDGET_UNITS` | `10000000` | Maximum paid in total while the server runs, in base units (10000000 is 10 USDG). |
|
|
152
|
+
| `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. |
|
|
153
|
+
| `BRAINCHAIN_STATE_DIR` | `~/.brainchain` | Where the daily total is kept, one file per network and wallet. |
|
|
144
154
|
| `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`. |
|
|
145
155
|
|
|
146
|
-
If a setting is invalid, the server says so once on stderr when it starts, and both tools return the error. `npx -y brainchain-mcp@0.
|
|
156
|
+
If a setting is invalid, the server says so once on stderr when it starts, and both tools return the error. `npx -y brainchain-mcp@0.4.0 --help` lists the commands and variables, and `--version` prints the version.
|
|
147
157
|
|
|
148
158
|
## Try it on testnet
|
|
149
159
|
|
|
@@ -151,7 +161,7 @@ Set `BRAINCHAIN_NETWORK=testnet` in the server's environment. Test tokens have n
|
|
|
151
161
|
|
|
152
162
|
```bash
|
|
153
163
|
printf 'Key: ' && read -rs BRAINCHAIN_PAYER_KEY && echo
|
|
154
|
-
claude mcp add brainchain --scope user -e BRAINCHAIN_NETWORK=testnet -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.
|
|
164
|
+
claude mcp add brainchain --scope user -e BRAINCHAIN_NETWORK=testnet -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.4.0
|
|
155
165
|
unset BRAINCHAIN_PAYER_KEY
|
|
156
166
|
```
|
|
157
167
|
|
|
@@ -166,7 +176,7 @@ unset BRAINCHAIN_PAYER_KEY
|
|
|
166
176
|
cast send 0x1eC185467cA2D5d1c24f0EdCB91301Bca4d0a787 "faucet()" --rpc-url https://rpc.testnet.chain.robinhood.com --interactive
|
|
167
177
|
```
|
|
168
178
|
|
|
169
|
-
3. Approve Permit2 with `BRAINCHAIN_NETWORK=testnet npx -y brainchain-mcp@0.
|
|
179
|
+
3. Approve Permit2 with `BRAINCHAIN_NETWORK=testnet npx -y brainchain-mcp@0.4.0 approve`.
|
|
170
180
|
|
|
171
181
|
## Links
|
|
172
182
|
|
package/dist/config.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
const MAINNET_DESK = { desk: "https://brain.usebrainchain.com", payTo: "0xafB0C4Ef895D4895040cdC6435B96D3EE8350c6E", finalAfter: "about six hours" };
|
|
2
4
|
const TESTNET_DESK = { desk: "https://brain.testnet.usebrainchain.com", payTo: "0xf848984c8A0D9E49Cc9C97380C62b34C6f3b97D1", finalAfter: "about an hour" };
|
|
3
5
|
const PRESETS = {
|
|
@@ -92,6 +94,9 @@ export function resolveConfig(env) {
|
|
|
92
94
|
const budget = envUnits(env, "BRAINCHAIN_BUDGET_UNITS", DEFAULT_BUDGET);
|
|
93
95
|
if (budget < maxPrice)
|
|
94
96
|
throw new Error("BRAINCHAIN_BUDGET_UNITS must be at least BRAINCHAIN_MAX_PRICE_UNITS.");
|
|
97
|
+
const daily = envString(env, "BRAINCHAIN_DAILY_UNITS") === undefined ? undefined : envUnits(env, "BRAINCHAIN_DAILY_UNITS", 0n);
|
|
98
|
+
if (daily !== undefined && daily < maxPrice)
|
|
99
|
+
throw new Error("BRAINCHAIN_DAILY_UNITS must be at least BRAINCHAIN_MAX_PRICE_UNITS.");
|
|
95
100
|
return {
|
|
96
101
|
name,
|
|
97
102
|
payOn,
|
|
@@ -106,6 +111,8 @@ export function resolveConfig(env) {
|
|
|
106
111
|
token: preset.token,
|
|
107
112
|
maxPrice,
|
|
108
113
|
budget,
|
|
114
|
+
...(daily !== undefined ? { daily } : {}),
|
|
115
|
+
stateDir: envString(env, "BRAINCHAIN_STATE_DIR") ?? join(homedir(), ".brainchain"),
|
|
109
116
|
terms: {
|
|
110
117
|
cost: maxPrice === DEFAULT_MAX_PRICE ? `1 ${preset.token}` : `at most ${formatUnits(maxPrice)} ${preset.token}`,
|
|
111
118
|
chain: preset.chain,
|
package/dist/daily.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// The daily limit is kept on disk, so restarting the server, or a second server
|
|
2
|
+
// paying from the same wallet, does not reset what the wallet may spend today.
|
|
3
|
+
import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
const LOCK_STALE_MS = 10_000;
|
|
6
|
+
const LOCK_WAIT_MS = 5_000;
|
|
7
|
+
/** The UTC date, which is when the limit resets. */
|
|
8
|
+
export function utcDay(now) {
|
|
9
|
+
return new Date(now).toISOString().slice(0, 10);
|
|
10
|
+
}
|
|
11
|
+
export function dailyLedger(path, limit, now = Date.now) {
|
|
12
|
+
const lockPath = `${path}.lock`;
|
|
13
|
+
async function locked(work) {
|
|
14
|
+
await mkdir(dirname(path), { recursive: true });
|
|
15
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
16
|
+
for (;;) {
|
|
17
|
+
try {
|
|
18
|
+
await (await open(lockPath, "wx")).close();
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code !== "EEXIST")
|
|
23
|
+
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
|
+
}
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return await work();
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await rm(lockPath, { force: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function read() {
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = await readFile(path, "utf8");
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error.code === "ENOENT")
|
|
48
|
+
return 0n;
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
// An unreadable record must not reset the limit to zero.
|
|
52
|
+
const record = (() => {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
if (typeof record?.day !== "string" || typeof record.units !== "string" || !/^\d+$/.test(record.units)) {
|
|
61
|
+
throw new Error(`The daily spending record at ${path} cannot be read; fix or delete it to pay again`);
|
|
62
|
+
}
|
|
63
|
+
return record.day === utcDay(now()) ? BigInt(record.units) : 0n;
|
|
64
|
+
}
|
|
65
|
+
async function write(units) {
|
|
66
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
67
|
+
await writeFile(temporary, JSON.stringify({ day: utcDay(now()), units: units.toString() }), { mode: 0o600 });
|
|
68
|
+
await rename(temporary, path);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
reserve: (amount) => locked(async () => {
|
|
72
|
+
const spent = await read();
|
|
73
|
+
if (spent + amount > limit)
|
|
74
|
+
return undefined;
|
|
75
|
+
await write(spent + amount);
|
|
76
|
+
return { day: utcDay(now()), amount };
|
|
77
|
+
}),
|
|
78
|
+
release: (reservation) => locked(async () => {
|
|
79
|
+
if (reservation.day !== utcDay(now()))
|
|
80
|
+
return;
|
|
81
|
+
const spent = await read();
|
|
82
|
+
await write(spent > reservation.amount ? spent - reservation.amount : 0n);
|
|
83
|
+
}),
|
|
84
|
+
spentToday: () => locked(read),
|
|
85
|
+
};
|
|
86
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,8 @@ Environment:
|
|
|
23
23
|
base pays in USDC on Base (Base Sepolia on testnet).
|
|
24
24
|
BRAINCHAIN_MAX_PRICE_UNITS Maximum paid per task, in base units (default 1000000 = 1 USDG or USDC).
|
|
25
25
|
BRAINCHAIN_BUDGET_UNITS Maximum paid in total while the server runs (default 10000000 = 10 USDG or USDC).
|
|
26
|
+
BRAINCHAIN_DAILY_UNITS Maximum the wallet pays per UTC day, kept across restarts (default: no daily limit).
|
|
27
|
+
BRAINCHAIN_STATE_DIR Where the daily limit's record is kept (default ~/.brainchain).
|
|
26
28
|
BRAINCHAIN_DESK_URL, BRAINCHAIN_PAY_TO, BRAINCHAIN_PAYMENT_NETWORK, BRAINCHAIN_PAYMENT_ASSET, BRAINCHAIN_PAYMENT_RPC
|
|
27
29
|
Advanced overrides of the network preset.
|
|
28
30
|
`;
|
package/dist/payments.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { ExactEvmScheme } from "@x402/evm/exact/client";
|
|
2
2
|
import { wrapFetchWithPayment, x402Client } from "@x402/fetch";
|
|
3
|
+
import { join } from "node:path";
|
|
3
4
|
import { keccak256, toBytes } from "viem";
|
|
4
5
|
import { formatUnits, MAX_TIMEOUT_SECONDS, REPOST_AFTER_MINUTES } from "./config.js";
|
|
6
|
+
import { dailyLedger } from "./daily.js";
|
|
5
7
|
const BUDGET_REASON = "budget";
|
|
8
|
+
const DAILY_REASON = "daily limit";
|
|
6
9
|
const RESULT_LIMIT = 20_000;
|
|
7
10
|
/** Same serialization the desk hashes into an order id: sorted keys, no whitespace. */
|
|
8
11
|
export function canonicalStringify(value) {
|
|
@@ -45,6 +48,12 @@ export function createDesk(options) {
|
|
|
45
48
|
const { config, signer, approveCommand } = options;
|
|
46
49
|
const fetchImpl = options.fetch ?? fetch;
|
|
47
50
|
let spent = 0n;
|
|
51
|
+
const account = () => (typeof signer === "function" ? signer() : signer);
|
|
52
|
+
let ledger;
|
|
53
|
+
// Kept per network and wallet, so a testnet wallet never spends from the mainnet limit.
|
|
54
|
+
const daily = () => config.daily === undefined
|
|
55
|
+
? undefined
|
|
56
|
+
: (ledger ??= dailyLedger(join(config.stateDir, `spend-${config.name}-${config.payOn}-${account().address.toLowerCase()}.json`), config.daily));
|
|
48
57
|
const endpoint = `${config.desk}/x402/tasks`;
|
|
49
58
|
const recent = new Map();
|
|
50
59
|
/** When this process paid, or may have paid, for each order; the desk can take minutes to know one. */
|
|
@@ -75,6 +84,10 @@ export function createDesk(options) {
|
|
|
75
84
|
if (message.includes(`Payment creation aborted: ${BUDGET_REASON}`)) {
|
|
76
85
|
return `This server has already signed payments for ${formatUnits(spent)} of its ${formatUnits(config.budget)} ${config.token} budget. The budget resets when the server restarts; raise BRAINCHAIN_BUDGET_UNITS to allow more per run. Nothing was charged.`;
|
|
77
86
|
}
|
|
87
|
+
if (message.includes(`Payment creation aborted: ${DAILY_REASON}`)) {
|
|
88
|
+
const today = await daily().spentToday().catch(() => undefined);
|
|
89
|
+
return `This wallet has already signed payments for ${today === undefined ? "most" : formatUnits(today)} of its ${formatUnits(config.daily)} ${config.token} daily limit. The limit resets at 00:00 UTC; raise BRAINCHAIN_DAILY_UNITS to allow more. Nothing was charged.`;
|
|
90
|
+
}
|
|
78
91
|
if (/rejected by spendControls/.test(message)) {
|
|
79
92
|
const price = await quotedPrice();
|
|
80
93
|
if (price !== undefined && price > config.maxPrice) {
|
|
@@ -110,7 +123,7 @@ export function createDesk(options) {
|
|
|
110
123
|
const [{ balance, allowance }, quoted] = await until(deadline, Promise.all([options.funds(), quotedPrice(deadline)]));
|
|
111
124
|
const needed = quoted ?? config.maxPrice;
|
|
112
125
|
if (balance < needed) {
|
|
113
|
-
const { address } =
|
|
126
|
+
const { address } = account();
|
|
114
127
|
return `The paying wallet holds ${formatUnits(balance)} ${config.token} on ${config.chain}; a task costs ${formatUnits(needed)}. Nothing was charged. Send at least ${formatUnits(needed - balance)} ${config.token} to ${address} on ${config.chain}, then post again.`;
|
|
115
128
|
}
|
|
116
129
|
if (config.permit2 && allowance !== undefined && allowance < needed) {
|
|
@@ -157,9 +170,17 @@ export function createDesk(options) {
|
|
|
157
170
|
async function pay(task) {
|
|
158
171
|
let signed;
|
|
159
172
|
let amount = 0n;
|
|
173
|
+
let reservation;
|
|
174
|
+
const unreserve = async () => {
|
|
175
|
+
spent -= amount;
|
|
176
|
+
amount = 0n;
|
|
177
|
+
if (reservation)
|
|
178
|
+
await daily().release(reservation);
|
|
179
|
+
reservation = undefined;
|
|
180
|
+
};
|
|
160
181
|
const receipt = (order) => ({ order, price: `${formatUnits(amount)} ${config.token}`, statusUrl: `${endpoint}/${order}` });
|
|
161
182
|
const client = x402Client.fromConfig({
|
|
162
|
-
schemes: [{ network: config.network, client: new ExactEvmScheme(
|
|
183
|
+
schemes: [{ network: config.network, client: new ExactEvmScheme(account()) }],
|
|
163
184
|
spendControls: { allowedAssets: [{ network: config.network, asset: config.asset, maxAmountPerPayment: config.maxPrice.toString() }] },
|
|
164
185
|
policies: [
|
|
165
186
|
(_version, requirements) => requirements.filter((r) => r.network === config.network &&
|
|
@@ -168,12 +189,18 @@ export function createDesk(options) {
|
|
|
168
189
|
r.maxTimeoutSeconds <= MAX_TIMEOUT_SECONDS),
|
|
169
190
|
],
|
|
170
191
|
});
|
|
171
|
-
// A payment is reserved against the budget before it is
|
|
172
|
-
// calls cannot overshoot
|
|
192
|
+
// A payment is reserved against the budget and the daily limit before it is
|
|
193
|
+
// signed, so parallel calls cannot overshoot either.
|
|
173
194
|
client.onBeforePaymentCreation(async ({ selectedRequirements }) => {
|
|
174
195
|
const price = BigInt(selectedRequirements.amount);
|
|
175
196
|
if (spent + price > config.budget)
|
|
176
197
|
return { abort: true, reason: BUDGET_REASON };
|
|
198
|
+
const today = daily();
|
|
199
|
+
if (today) {
|
|
200
|
+
reservation = await today.reserve(price);
|
|
201
|
+
if (!reservation)
|
|
202
|
+
return { abort: true, reason: DAILY_REASON };
|
|
203
|
+
}
|
|
177
204
|
spent += price;
|
|
178
205
|
amount = price;
|
|
179
206
|
});
|
|
@@ -181,10 +208,8 @@ export function createDesk(options) {
|
|
|
181
208
|
signed = paymentPayload;
|
|
182
209
|
});
|
|
183
210
|
client.onPaymentCreationFailure(async () => {
|
|
184
|
-
if (signed)
|
|
185
|
-
|
|
186
|
-
spent -= amount;
|
|
187
|
-
amount = 0n;
|
|
211
|
+
if (!signed)
|
|
212
|
+
await unreserve();
|
|
188
213
|
});
|
|
189
214
|
let response;
|
|
190
215
|
try {
|
|
@@ -214,8 +239,7 @@ export function createDesk(options) {
|
|
|
214
239
|
if (signed && response.status !== 400 && response.status !== 402) {
|
|
215
240
|
return possiblyCharged(receipt(typeof reply.order === "string" ? reply.order : orderIdOf(signed)), `The desk answered ${response.status}.`);
|
|
216
241
|
}
|
|
217
|
-
|
|
218
|
-
amount = 0n;
|
|
242
|
+
await unreserve();
|
|
219
243
|
if (response.status === 402)
|
|
220
244
|
throw new Error(await explainRefusal(reply.error));
|
|
221
245
|
throw new Error(typeof reply.error === "string" ? `${reply.error} Nothing was charged.` : `The desk answered ${response.status}. Nothing was charged.`);
|
|
@@ -281,6 +305,9 @@ export function createDesk(options) {
|
|
|
281
305
|
export function guidance(view, config) {
|
|
282
306
|
const task = (view.task ?? {});
|
|
283
307
|
const wait = (retryAfterSeconds, next) => ({ done: false, next, retryAfterSeconds });
|
|
308
|
+
const why = typeof view.refundReason === "string" ? view.refundReason : "The work was not accepted.";
|
|
309
|
+
// The board refunds a step that misses its deadline; saying when lets the caller stop waiting at the right time.
|
|
310
|
+
const unless = (missed) => (typeof task.due?.by === "string" ? ` If ${missed} by ${task.due.by}, the full amount is refunded.` : "");
|
|
284
311
|
const accepted = task.accepted === true || view.state === "paid-out" || task.status === "paid";
|
|
285
312
|
if (typeof view.resultText === "string" && accepted) {
|
|
286
313
|
return { done: true, next: `Jev accepted the work; the result follows this summary. The model is paid when the dispute window closes, ${config.terms.finalAfter} after grading.` };
|
|
@@ -289,7 +316,7 @@ export function guidance(view, config) {
|
|
|
289
316
|
case "paid-out":
|
|
290
317
|
return { done: true, next: "Jev accepted the work and the model was paid." };
|
|
291
318
|
case "refunded":
|
|
292
|
-
return { done: true, next:
|
|
319
|
+
return { done: true, next: `${why} The full amount was refunded on ${config.chain}${typeof view.refundTx === "string" ? ` in ${view.refundTx}` : ""}.` };
|
|
293
320
|
case "refund-failed":
|
|
294
321
|
return { done: true, next: "The refund could not be sent automatically. Write to hello@usebrainchain.com with this order id." };
|
|
295
322
|
case "failed":
|
|
@@ -301,21 +328,30 @@ export function guidance(view, config) {
|
|
|
301
328
|
case "unconfirmed":
|
|
302
329
|
return wait(30, "The payment may have gone through; BrainChain is checking it against the chain. Do not post the task again.");
|
|
303
330
|
case "refunding":
|
|
304
|
-
return wait(60,
|
|
331
|
+
return wait(60, `${why} The refund is being sent on ${config.chain}.`);
|
|
332
|
+
}
|
|
333
|
+
// A dispute, a ruling under its appeal window and an appeal can each still change a grade, accepted or not.
|
|
334
|
+
const due = typeof task.due?.by === "string" ? task.due.by : undefined;
|
|
335
|
+
const by = due ? ` by ${due}` : "";
|
|
336
|
+
switch (task.status) {
|
|
337
|
+
case "disputed":
|
|
338
|
+
return wait(600, `The grade is disputed; Jev rules on it${by}. If no ruling comes, the grade stands.`);
|
|
339
|
+
case "ruled":
|
|
340
|
+
return wait(900, `Jev ruled on the dispute. The ruling settles when its appeal window closes${due ? ` at ${due}` : ""}, unless the side it went against appeals.`);
|
|
341
|
+
case "appealed":
|
|
342
|
+
return wait(600, `The ruling was appealed; Jev rules again${by}. If no second ruling comes, the first one stands.`);
|
|
305
343
|
}
|
|
306
344
|
if (task.accepted === false) {
|
|
307
345
|
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.` };
|
|
308
346
|
}
|
|
309
347
|
switch (task.status) {
|
|
310
348
|
case "open":
|
|
311
|
-
return wait(60,
|
|
349
|
+
return wait(60, `Waiting for Jev to hire a model.${unless("the work is not delivered")}`);
|
|
312
350
|
case "hired":
|
|
313
|
-
return wait(60,
|
|
351
|
+
return wait(60, `A model is working on the task.${unless("the work is not delivered")}`);
|
|
314
352
|
case "delivered":
|
|
315
353
|
case "submitted":
|
|
316
|
-
return wait(30,
|
|
317
|
-
case "disputed":
|
|
318
|
-
return wait(600, "The grade is disputed; Jev will rule on it.");
|
|
354
|
+
return wait(30, `The model delivered; Jev is grading the work.${unless("Jev has not graded it")}`);
|
|
319
355
|
case "graded":
|
|
320
356
|
return wait(60, "Jev accepted the work; the result is being prepared.");
|
|
321
357
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "brainchain-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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",
|