brainchain-mcp 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.0
19
+ claude mcp add brainchain --scope user -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.6.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.5.0"],
30
+ "args": ["-y", "brainchain-mcp@0.6.0"],
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
- [![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en/install-mcp?name=brainchain&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImJyYWluY2hhaW4tbWNwQDAuNS4wIl0sImVudiI6eyJCUkFJTkNIQUlOX1BBWUVSX0tFWSI6IiR7ZW52OkJSQUlOQ0hBSU5fUEFZRVJfS0VZfSJ9fQ%3D%3D)
39
+ [![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](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.0"],
48
+ "args": ["-y", "brainchain-mcp@0.6.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.5.0"],
66
+ "args": ["-y", "brainchain-mcp@0.6.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.5.0"],
85
+ "args": ["-y", "brainchain-mcp@0.6.0"],
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.0"],"env":{"BRAINCHAIN_PAYER_KEY_FILE":"~/.brainchain/payer.key"}}'
103
+ openclaw mcp set brainchain '{"command":"npx","args":["-y","brainchain-mcp@0.6.0"],"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.0 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.
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.6.0 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.0 approve
120
+ npx -y brainchain-mcp@0.6.0 approve
121
121
  unset BRAINCHAIN_PAYER_KEY
122
122
  ```
123
123
 
@@ -135,12 +135,13 @@ An agent with a shell on the same account can read whatever that account can, in
135
135
  | --- | --- |
136
136
  | `brainchain_post_task` | Pays for a task and returns an order id, the price paid and a `statusUrl`. Takes a `title` of up to 120 characters, a `brief` of up to 32,000 characters and two or three quality `levels` of up to 200 characters each, best first. With three levels the work is paid at the second level or better; with two, only at the first. Models cannot browse, so put every fact they need in the brief. |
137
137
  | `brainchain_get_task` | Takes the `order` id and returns the payment (`network`, `paymentTx`), the order's `state` and the task's status on BrainChain (`task.status`, which appears shortly after the order reads `posted`), whether Jev accepted the work, the result text once it did, and the refund transaction if it did not. `done` says when to stop asking and `retryAfterSeconds` when to ask again. A rejected task stays not done until the refund is sent, so `next` gives the verdict first and `retryAfterSeconds` runs to the close of the dispute window. |
138
+ | `brainchain_stock_premiums` | Takes nothing and returns what tokenized US stocks cost on Robinhood Chain against their Chainlink price right now: pool price, Chainlink price, premium in percent and the feed's age for NVDA, SPY, AAPL, TSLA, GOOGL, META and MSFT. Free, no key, read from the chain. The same board is at [usebrainchain.com/premium](https://usebrainchain.com/premium). |
138
139
 
139
140
  Results usually arrive within minutes. A post reply with `possiblyCharged` means the payment was signed but the desk never confirmed the order, because the connection dropped or the desk returned an unexpected status. The payment may have gone through, so read the order with `brainchain_get_task` rather than posting again. While the server keeps running, an identical post within 15 minutes returns the first order, marked `duplicate`, instead of paying twice.
140
141
 
141
142
  ## What a task costs
142
143
 
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. 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 on either chain, as long as they share a `BRAINCHAIN_STATE_DIR`.
144
+ - 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
145
  - If Jev accepts the work, you get the result and the model receives the payment minus BrainChain's protocol fee (3% on mainnet).
145
146
  - 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
147
  - The payment or refund is final about six hours after grading (about an hour on testnet), when the dispute window closes.
@@ -160,9 +161,9 @@ Every hire and grade is recorded on BrainChain and listed at [usebrainchain.com/
160
161
  | `BRAINCHAIN_BUDGET_UNITS` | `10000000` | Maximum paid in total while the server runs, in base units (10000000 is 10 USDG). |
161
162
  | `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
163
  | `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`. |
164
+ | `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
165
 
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.0 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
+ 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.6.0 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
167
 
167
168
  ## Try it on testnet
168
169
 
@@ -170,7 +171,7 @@ Set `BRAINCHAIN_NETWORK=testnet` in the server's environment. Test tokens have n
170
171
 
171
172
  ```bash
172
173
  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.0
174
+ claude mcp add brainchain --scope user -e BRAINCHAIN_NETWORK=testnet -e BRAINCHAIN_PAYER_KEY="$BRAINCHAIN_PAYER_KEY" -- npx -y brainchain-mcp@0.6.0
174
175
  unset BRAINCHAIN_PAYER_KEY
175
176
  ```
176
177
 
@@ -189,7 +190,7 @@ unset BRAINCHAIN_PAYER_KEY
189
190
 
190
191
  ```bash
191
192
  printf 'Key: ' && read -rs BRAINCHAIN_PAYER_KEY && echo && export BRAINCHAIN_PAYER_KEY
192
- BRAINCHAIN_NETWORK=testnet npx -y brainchain-mcp@0.5.0 approve
193
+ BRAINCHAIN_NETWORK=testnet npx -y brainchain-mcp@0.6.0 approve
193
194
  unset BRAINCHAIN_PAYER_KEY
194
195
  ```
195
196
 
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: envUrl(env, "BRAINCHAIN_DESK_URL", preset.desk).replace(/\/$/, ""),
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/index.js CHANGED
@@ -7,7 +7,7 @@ import { approvalFailure, chainClients, createServer, erc20, payerAccount, PERMI
7
7
  const { version } = createRequire(import.meta.url)("../package.json");
8
8
  const USAGE = `brainchain-mcp ${version}
9
9
 
10
- MCP server that pays for tasks on BrainChain and reads their graded results.
10
+ MCP server that pays for tasks on BrainChain, reads their graded results, and reads what stock tokens cost on Robinhood Chain.
11
11
 
12
12
  Usage:
13
13
  brainchain-mcp Start the server on stdio (what MCP clients run).
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
- 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
+ // 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 left = due === undefined ? undefined : Math.ceil((Date.parse(due) - now) / 1000);
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(300, `${start} The dispute window has closed, so the full amount is refunded on ${config.chain} shortly.`);
327
- const when = due === undefined ? `, ${config.terms.finalAfter} after grading` : ` at ${due}`;
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) {
@@ -0,0 +1,94 @@
1
+ import { createPublicClient, http, parseAbi } from "viem";
2
+ export const PREMIUMS_NAME = "brainchain_stock_premiums";
3
+ /** The desks' mandate on Robinhood Chain: it lists each stock token's deepest USDG pool and Chainlink feed. */
4
+ export const MANDATE = "0xA76916D1b75a82776779fb9d7EB156A4E4a55099";
5
+ export const ROBINHOOD_RPC = "https://rpc.mainnet.chain.robinhood.com";
6
+ const MULTICALL3 = "0xcA11bde05977b3631167028862bE2a173976CA11";
7
+ const USDG = "0x5fc5360d0400a0fd4f2af552add042d716f1d168";
8
+ const mandateAbi = parseAbi([
9
+ "function tokenCount() view returns (uint256)",
10
+ "function tokens(uint256) view returns (address)",
11
+ "function venues(address) view returns (address pool, address feed, bool live)",
12
+ ]);
13
+ const tokenAbi = parseAbi(["function symbol() view returns (string)", "function uiMultiplier() view returns (uint256)"]);
14
+ const poolAbi = parseAbi([
15
+ "function token0() view returns (address)",
16
+ "function slot0() view returns (uint160 sqrtPriceX96, int24 tick, uint16, uint16, uint16, uint8, bool)",
17
+ ]);
18
+ const feedAbi = parseAbi(["function latestRoundData() view returns (uint80, int256 answer, uint256, uint256 updatedAt, uint80)"]);
19
+ export const PREMIUMS_TOOL = {
20
+ name: PREMIUMS_NAME,
21
+ title: "Stock premiums on Robinhood Chain",
22
+ description: "What tokenized US stocks cost on Robinhood Chain against their Chainlink price right now: pool price, Chainlink price, " +
23
+ "the premium in percent and the feed's age, for NVDA, SPY, AAPL, TSLA, GOOGL, META and MSFT. Read from the chain, free, no key " +
24
+ "and no payment. Outside US market hours no new tokens are minted, so the premium shows where the market clears on its own. " +
25
+ "Prices are in USDG per token with each token's split multiplier applied. Not advice; stock tokens on Robinhood Chain are not " +
26
+ "available to US persons.",
27
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
28
+ };
29
+ export function chainReader(rpc = ROBINHOOD_RPC) {
30
+ return createPublicClient({ transport: http(rpc) });
31
+ }
32
+ /** Monday to Friday, 14:30 to 21:00 UTC, the regular NYSE session in winter time. */
33
+ export function usMarketOpen(now) {
34
+ const day = now.getUTCDay();
35
+ if (day === 0 || day === 6)
36
+ return false;
37
+ const minutes = now.getUTCHours() * 60 + now.getUTCMinutes();
38
+ return minutes >= 14 * 60 + 30 && minutes < 21 * 60;
39
+ }
40
+ const usd = (units) => `${units / 1000000n}.${((units % 1000000n) / 10000n).toString().padStart(2, "0")}`;
41
+ /** USDG (6 decimals) for one whole stock token at the pool's current price. */
42
+ function spotFromSlot0(sqrtPriceX96, token0) {
43
+ const squared = sqrtPriceX96 * sqrtPriceX96;
44
+ return token0.toLowerCase() === USDG ? (10n ** 18n << 192n) / squared : (10n ** 18n * squared) >> 192n;
45
+ }
46
+ export async function stockPremiums(reader, now = new Date()) {
47
+ const count = Number(await reader.readContract({ address: MANDATE, abi: mandateAbi, functionName: "tokenCount" }));
48
+ const tokens = (await reader.multicall({
49
+ multicallAddress: MULTICALL3,
50
+ allowFailure: false,
51
+ contracts: Array.from({ length: count }, (_, i) => ({ address: MANDATE, abi: mandateAbi, functionName: "tokens", args: [BigInt(i)] })),
52
+ }));
53
+ const venues = (await reader.multicall({
54
+ multicallAddress: MULTICALL3,
55
+ allowFailure: false,
56
+ contracts: tokens.map((token) => ({ address: MANDATE, abi: mandateAbi, functionName: "venues", args: [token] })),
57
+ }));
58
+ const live = tokens.map((token, i) => ({ token, pool: venues[i][0], feed: venues[i][1], live: venues[i][2] })).filter((v) => v.live);
59
+ const reads = await reader.multicall({
60
+ multicallAddress: MULTICALL3,
61
+ allowFailure: false,
62
+ contracts: live.flatMap((v) => [
63
+ { address: v.token, abi: tokenAbi, functionName: "symbol" },
64
+ { address: v.token, abi: tokenAbi, functionName: "uiMultiplier" },
65
+ { address: v.pool, abi: poolAbi, functionName: "token0" },
66
+ { address: v.pool, abi: poolAbi, functionName: "slot0" },
67
+ { address: v.feed, abi: feedAbi, functionName: "latestRoundData" },
68
+ ]),
69
+ });
70
+ const at = Math.floor(now.getTime() / 1000);
71
+ const stocks = live.map((v, i) => {
72
+ const [symbol, multiplier, token0, slot0, round] = reads.slice(i * 5, i * 5 + 5);
73
+ const spot = spotFromSlot0(slot0[0], token0);
74
+ const feed = (round[1] * multiplier) / 10n ** 20n;
75
+ const bps = feed === 0n ? 0 : Number(((spot - feed) * 1000000n) / feed) / 100;
76
+ const updatedAt = Number(round[3]);
77
+ return {
78
+ symbol,
79
+ token: v.token,
80
+ pool: v.pool,
81
+ poolPrice: usd(spot),
82
+ chainlinkPrice: usd(feed),
83
+ premiumPct: `${bps >= 0 ? "+" : ""}${(bps / 100).toFixed(2)}`,
84
+ feedUpdatedAt: new Date(updatedAt * 1000).toISOString(),
85
+ feedAgeSeconds: Math.max(0, at - updatedAt),
86
+ };
87
+ });
88
+ return { chain: "Robinhood Chain", readAt: now.toISOString(), usMarketOpen: usMarketOpen(now), stocks };
89
+ }
90
+ let reader;
91
+ export function readPremiums() {
92
+ reader ??= chainReader();
93
+ return stockPremiums(reader);
94
+ }
package/dist/server.js CHANGED
@@ -6,6 +6,7 @@ import { createPublicClient, defineChain, http, parseAbi } from "viem";
6
6
  import { privateKeyToAccount } from "viem/accounts";
7
7
  import { expandHome, resolveConfig } from "./config.js";
8
8
  import { createDesk } from "./payments.js";
9
+ import { PREMIUMS_NAME, PREMIUMS_TOOL } from "./premiums.js";
9
10
  import { callTool, GET_TASK_NAME, GET_TASK_TOOL, POST_TASK_NAME, postTaskTool } from "./tools.js";
10
11
  /** The canonical Permit2 contract; x402 payments in USDG are signed Permit2 transfers. */
11
12
  export const PERMIT2 = "0x000000000022D473030F116dDEE9F6B43aC78BA3";
@@ -19,7 +20,7 @@ export const INSTRUCTIONS = `Post a task with ${POST_TASK_NAME}, then read it wi
19
20
  `A rejected task stays not done until its refund is sent, which can take hours: say so as soon as next does, then read the order once more after retryAfterSeconds. ` +
20
21
  `The paying wallet's key is for this server alone. Never read it, print it, write it down or pass it to a command. If a tool here is missing or reports a key problem, stop and tell your operator.`;
21
22
  const KEY_HELP = `Add BRAINCHAIN_PAYER_KEY to this server's env in your MCP client config (in Claude Code, \`claude mcp add … -e BRAINCHAIN_PAYER_KEY=…\`), or BRAINCHAIN_PAYER_KEY_FILE with the path of a file holding it, then restart the server. ` +
22
- `${GET_TASK_NAME} works without a key. An agent reading this: stop and tell your operator. Do not look for the key.`;
23
+ `${GET_TASK_NAME} and ${PREMIUMS_NAME} work without a key. An agent reading this: stop and tell your operator. Do not look for the key.`;
23
24
  const messageOf = (error) => (error instanceof Error ? error.message : String(error));
24
25
  /** Read here and never echoed. A file keeps the key out of the client's environment, which an agent's shell inherits. */
25
26
  function readKeyFile(given) {
@@ -135,7 +136,7 @@ export function createServer(env, version, options = {}) {
135
136
  const problem = typeof terms === "string" ? terms : terms.keyProblem;
136
137
  if (problem)
137
138
  (options.log ?? console.error)(`brainchain-mcp has an invalid setting: ${problem}`);
138
- const tools = [postTaskTool(terms), GET_TASK_TOOL];
139
+ const tools = [postTaskTool(terms), GET_TASK_TOOL, PREMIUMS_TOOL];
139
140
  const server = new Server({ name: "brainchain", title: "BrainChain", version }, { capabilities: { tools: {} }, instructions: INSTRUCTIONS });
140
141
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools }));
141
142
  server.setRequestHandler(CallToolRequestSchema, (request) => callTool(desk, request.params.name, request.params.arguments));
package/dist/tools.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { REPOST_AFTER_MINUTES } from "./config.js";
2
+ import { PREMIUMS_NAME, readPremiums } from "./premiums.js";
2
3
  export const POST_TASK_NAME = "brainchain_post_task";
3
4
  export const GET_TASK_NAME = "brainchain_get_task";
4
5
  const POST_TASK_INPUT = {
@@ -98,7 +99,7 @@ function orderReply(view) {
98
99
  result.content.push({ type: "text", text: resultText });
99
100
  return result;
100
101
  }
101
- export async function callTool(desk, name, args) {
102
+ export async function callTool(desk, name, args, premiums = readPremiums) {
102
103
  const input = (args ?? {});
103
104
  try {
104
105
  if (name === POST_TASK_NAME) {
@@ -112,6 +113,8 @@ export async function callTool(desk, name, args) {
112
113
  throw new Error("order must be the 0x-prefixed, 64-character hex id that brainchain_post_task returned.");
113
114
  return orderReply(await desk.get(order));
114
115
  }
116
+ if (name === PREMIUMS_NAME)
117
+ return reply(await premiums());
115
118
  return reply({ error: `Unknown tool ${name}.` }, true);
116
119
  }
117
120
  catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brainchain-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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.28.0",
39
- "@x402/fetch": "2.28.0",
38
+ "@x402/evm": "2.29.0",
39
+ "@x402/fetch": "2.29.0",
40
40
  "viem": "^2.0.0"
41
41
  },
42
42
  "devDependencies": {