logos-layer 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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +44 -0
  3. package/dist/index.js +303 -0
  4. package/package.json +33 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dtzrr
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 ADDED
@@ -0,0 +1,44 @@
1
+ # logos-layer
2
+
3
+ An MCP server that lets an AI agent trade inside a mandate-guarded vault on Robinhood Chain. The vault's on-chain guard enforces the mandate on every order: compliant orders execute, violations revert with the exact rule they broke. The guard does not care how smart the agent is.
4
+
5
+ ## Tools
6
+
7
+ - `get_portfolio`: the vault's current cash, positions, prices, and mandate thresholds
8
+ - `check_order`: preview an order against the mandate before sending it
9
+ - `execute_order`: submit an order; the chain reverts anything the mandate forbids
10
+
11
+ ## Setup
12
+
13
+ Add to your MCP client config (Claude Desktop, Claude Code, or any MCP client):
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "logos-layer": {
19
+ "command": "npx",
20
+ "args": ["-y", "logos-layer"],
21
+ "env": {
22
+ "VAULT_ADDRESS": "<your-vault-address>",
23
+ "RPC_URL": "https://rpc.testnet.chain.robinhood.com",
24
+ "PRIVATE_KEY": "<your-agent-wallet-key>"
25
+ }
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ Requires Node 20+.
32
+
33
+ | Env | Required | Meaning |
34
+ | --- | --- | --- |
35
+ | `VAULT_ADDRESS` | yes | The deployed vault the agent trades inside |
36
+ | `RPC_URL` | yes | Robinhood Chain RPC endpoint |
37
+ | `PRIVATE_KEY` | yes | The agent wallet key; must be the vault owner to trade |
38
+ | `EXPLORER_URL` | no | Explorer base URL for transaction links |
39
+
40
+ ## Safety
41
+
42
+ The agent wallet must be the vault owner to trade, so use the key you deployed the vault with. Never use a key that holds real funds. Testnet instruments only; this is a research demo, not investment advice.
43
+
44
+ This package ships as a compiled artifact.
package/dist/index.js ADDED
@@ -0,0 +1,303 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ createPublicClient,
8
+ createWalletClient,
9
+ defineChain,
10
+ http,
11
+ parseAbi
12
+ } from "viem";
13
+ import { privateKeyToAccount } from "viem/accounts";
14
+ import { z } from "zod";
15
+
16
+ // ../mandate-schema/src/index.ts
17
+ var ASSETS = ["AMZN", "TSLA", "NFLX", "GOOG", "AAPL"];
18
+ var RULES = [
19
+ {
20
+ id: 1,
21
+ name: "Concentration limit",
22
+ english: "The market value of any single security shall not exceed 20% of the total Account value."
23
+ },
24
+ {
25
+ id: 2,
26
+ name: "Minimum cash reserve",
27
+ english: "The Account shall at all times maintain a cash balance of no less than 5% of the total Account value."
28
+ },
29
+ {
30
+ id: 3,
31
+ name: "Prohibition on leverage",
32
+ english: "The cash balance shall not at any time be negative."
33
+ },
34
+ {
35
+ id: 4,
36
+ name: "Prohibition on short sales",
37
+ english: "The quantity held of each security shall at all times be greater than or equal to zero."
38
+ },
39
+ {
40
+ id: 5,
41
+ name: "Order size limitation",
42
+ english: "No single order shall have a notional value exceeding 10% of the total Account value immediately prior to the order."
43
+ }
44
+ ];
45
+
46
+ // ../checker-ts/src/rat.ts
47
+ function gcd(a, b) {
48
+ a = a < 0n ? -a : a;
49
+ b = b < 0n ? -b : b;
50
+ while (b !== 0n) [a, b] = [b, a % b];
51
+ return a;
52
+ }
53
+ function rat(n, d = 1n) {
54
+ if (d === 0n) throw new Error("rat: zero denominator");
55
+ if (d < 0n) {
56
+ n = -n;
57
+ d = -d;
58
+ }
59
+ const g = gcd(n, d);
60
+ return g === 0n ? { n: 0n, d: 1n } : { n: n / g, d: d / g };
61
+ }
62
+ var ZERO = rat(0n);
63
+ function fromInt(n) {
64
+ return rat(BigInt(n));
65
+ }
66
+ function add(a, b) {
67
+ return rat(a.n * b.d + b.n * a.d, a.d * b.d);
68
+ }
69
+ function sub(a, b) {
70
+ return rat(a.n * b.d - b.n * a.d, a.d * b.d);
71
+ }
72
+ function mul(a, b) {
73
+ return rat(a.n * b.n, a.d * b.d);
74
+ }
75
+ function abs(a) {
76
+ return { n: a.n < 0n ? -a.n : a.n, d: a.d };
77
+ }
78
+ function le(a, b) {
79
+ return a.n * b.d <= b.n * a.d;
80
+ }
81
+ function toNumber(a) {
82
+ return Number(a.n) / Number(a.d);
83
+ }
84
+
85
+ // ../checker-ts/src/checker.ts
86
+ var BPS_DEN = fromInt(1e4);
87
+ function totalValue(p, prices) {
88
+ let acc = p.cash;
89
+ for (const a of ASSETS) acc = add(acc, mul(p.positions[a], prices[a]));
90
+ return acc;
91
+ }
92
+ function applyOrder(p, o, prices) {
93
+ const positions = { ...p.positions };
94
+ positions[o.asset] = add(positions[o.asset], o.qty);
95
+ return {
96
+ cash: sub(p.cash, mul(o.qty, prices[o.asset])),
97
+ positions
98
+ };
99
+ }
100
+ function checkRules(m, prices, p, o) {
101
+ const tv = totalValue(p, prices);
102
+ const post = applyOrder(p, o, prices);
103
+ const postTv = totalValue(post, prices);
104
+ return {
105
+ // rule 1: single-asset cap — 10000·(pos·price) ≤ maxAssetBps·postTv
106
+ 1: ASSETS.every(
107
+ (a) => le(mul(BPS_DEN, mul(post.positions[a], prices[a])), mul(m.maxAssetBps, postTv))
108
+ ),
109
+ // rule 2: cash floor — minCashBps·postTv ≤ 10000·cash
110
+ 2: le(mul(m.minCashBps, postTv), mul(BPS_DEN, post.cash)),
111
+ // rule 3: no leverage — 0 ≤ cash
112
+ 3: le(ZERO, post.cash),
113
+ // rule 4: no shorts — 0 ≤ position, all assets
114
+ 4: ASSETS.every((a) => le(ZERO, post.positions[a])),
115
+ // rule 5: throttle — 10000·|qty·price| ≤ maxOrderBps·tv (pre-trade)
116
+ 5: le(mul(BPS_DEN, abs(mul(o.qty, prices[o.asset]))), mul(m.maxOrderBps, tv))
117
+ };
118
+ }
119
+ function firstViolation(m, prices, p, o) {
120
+ const r = checkRules(m, prices, p, o);
121
+ for (const id of [1, 2, 3, 4, 5]) if (!r[id]) return id;
122
+ return null;
123
+ }
124
+
125
+ // src/index.ts
126
+ var VAULT = requireEnv("VAULT_ADDRESS");
127
+ var RPC_URL = requireEnv("RPC_URL");
128
+ var PRIVATE_KEY = requireEnv("PRIVATE_KEY");
129
+ var EXPLORER_URL = process.env.EXPLORER_URL;
130
+ function requireEnv(name) {
131
+ const v = process.env[name];
132
+ if (!v) {
133
+ console.error(`Missing required env var ${name}`);
134
+ process.exit(1);
135
+ }
136
+ return v;
137
+ }
138
+ var vaultAbi = parseAbi([
139
+ "struct Portfolio { int256 cash; int256[5] positions; }",
140
+ "function portfolio() view returns (Portfolio)",
141
+ "function mandate() view returns (int256 maxAssetBps, int256 minCashBps, int256 maxOrderBps)",
142
+ "function prices(uint256) view returns (int256)",
143
+ "function preview(uint8 asset, int256 qty) view returns (bool ok, uint8 ruleId)",
144
+ "function execute(uint8 asset, int256 qty) returns (uint256)",
145
+ "error MandateViolation(uint8 ruleId)"
146
+ ]);
147
+ var account = privateKeyToAccount(
148
+ PRIVATE_KEY.startsWith("0x") ? PRIVATE_KEY : `0x${PRIVATE_KEY}`
149
+ );
150
+ async function makeClients() {
151
+ const probe = createPublicClient({ transport: http(RPC_URL) });
152
+ const chainId = await probe.getChainId();
153
+ const chain = defineChain({
154
+ id: chainId,
155
+ name: `chain-${chainId}`,
156
+ nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
157
+ rpcUrls: { default: { http: [RPC_URL] } }
158
+ });
159
+ return {
160
+ publicClient: createPublicClient({ chain, transport: http(RPC_URL) }),
161
+ walletClient: createWalletClient({ account, chain, transport: http(RPC_URL) })
162
+ };
163
+ }
164
+ var { publicClient, walletClient } = await makeClients();
165
+ async function readVault() {
166
+ const [portfolio, mandate, ...priceResults] = await Promise.all([
167
+ publicClient.readContract({ address: VAULT, abi: vaultAbi, functionName: "portfolio" }),
168
+ publicClient.readContract({ address: VAULT, abi: vaultAbi, functionName: "mandate" }),
169
+ ...ASSETS.map(
170
+ (_, i) => publicClient.readContract({
171
+ address: VAULT,
172
+ abi: vaultAbi,
173
+ functionName: "prices",
174
+ args: [BigInt(i)]
175
+ })
176
+ )
177
+ ]);
178
+ const positions = Object.fromEntries(
179
+ ASSETS.map((a, i) => [a, Number(portfolio.positions[i])])
180
+ );
181
+ const prices = Object.fromEntries(
182
+ ASSETS.map((a, i) => [a, Number(priceResults[i])])
183
+ );
184
+ const nav = Number(portfolio.cash) + ASSETS.reduce((acc, a) => acc + positions[a] * prices[a], 0);
185
+ return {
186
+ cash: Number(portfolio.cash),
187
+ positions,
188
+ prices,
189
+ mandate: {
190
+ maxAssetPct: Number(mandate[0]) / 100,
191
+ minCashPct: Number(mandate[1]) / 100,
192
+ maxOrderPct: Number(mandate[2]) / 100
193
+ },
194
+ nav
195
+ };
196
+ }
197
+ function localVerdict(state, asset, signedQty) {
198
+ const m = {
199
+ maxAssetBps: fromInt(Math.round(state.mandate.maxAssetPct * 100)),
200
+ minCashBps: fromInt(Math.round(state.mandate.minCashPct * 100)),
201
+ maxOrderBps: fromInt(Math.round(state.mandate.maxOrderPct * 100))
202
+ };
203
+ const p = {
204
+ cash: fromInt(state.cash),
205
+ positions: Object.fromEntries(
206
+ ASSETS.map((a) => [a, fromInt(state.positions[a])])
207
+ )
208
+ };
209
+ const pr = Object.fromEntries(
210
+ ASSETS.map((a) => [a, fromInt(state.prices[a])])
211
+ );
212
+ const o = { asset, qty: fromInt(signedQty) };
213
+ const rules = checkRules(m, pr, p, o);
214
+ const post = applyOrder(p, o, pr);
215
+ return {
216
+ rules,
217
+ firstViolation: firstViolation(m, pr, p, o),
218
+ postNav: toNumber(totalValue(post, pr))
219
+ };
220
+ }
221
+ var orderShape = {
222
+ asset: z.enum(ASSETS).describe("Asset symbol to trade"),
223
+ side: z.enum(["buy", "sell"]).describe("Order side"),
224
+ quantity: z.number().int().positive().describe("Number of shares")
225
+ };
226
+ function describeVerdict(state, asset, side, quantity) {
227
+ const signedQty = side === "buy" ? quantity : -quantity;
228
+ const v = localVerdict(state, asset, signedQty);
229
+ const lines = RULES.map((r) => {
230
+ const pass = v.rules[r.id];
231
+ return ` rule ${r.id} ${pass ? "PASS" : "FAIL"} \u2014 ${r.name}`;
232
+ });
233
+ return { signedQty, verdict: v, ruleLines: lines.join("\n") };
234
+ }
235
+ var server = new McpServer({ name: "logos-layer", version: "0.1.0" });
236
+ server.tool(
237
+ "get_portfolio",
238
+ "Read the vault's on-chain portfolio: cash, positions, prices, NAV, and the mandate thresholds that every trade must satisfy.",
239
+ {},
240
+ async () => {
241
+ const s = await readVault();
242
+ const text = [
243
+ `Vault ${VAULT}`,
244
+ `NAV $${s.nav.toLocaleString("en-US")} \xB7 cash $${s.cash.toLocaleString("en-US")}`,
245
+ ...ASSETS.map(
246
+ (a) => ` ${a}: ${s.positions[a]} shares @ $${s.prices[a]} = $${(s.positions[a] * s.prices[a]).toLocaleString("en-US")}`
247
+ ),
248
+ "",
249
+ `Mandate (enforced on-chain, proven in Lean):`,
250
+ ` concentration cap ${s.mandate.maxAssetPct}% of NAV per asset`,
251
+ ` cash floor ${s.mandate.minCashPct}% of NAV`,
252
+ ` order throttle ${s.mandate.maxOrderPct}% of pre-trade NAV`,
253
+ ` no leverage, no short sales`
254
+ ].join("\n");
255
+ return { content: [{ type: "text", text }] };
256
+ }
257
+ );
258
+ server.tool(
259
+ "check_order",
260
+ "Dry-run an order against the formally verified compliance checker without sending a transaction. Returns the verdict of each mandate rule.",
261
+ orderShape,
262
+ async ({ asset, side, quantity }) => {
263
+ const s = await readVault();
264
+ const { verdict, ruleLines } = describeVerdict(s, asset, side, quantity);
265
+ const ok = verdict.firstViolation === null;
266
+ const text = [
267
+ `${side.toUpperCase()} ${quantity} ${asset} \u2192 ${ok ? `PASS. Trade would execute; post-trade NAV $${verdict.postNav.toLocaleString("en-US")}.` : `FAIL. On-chain this reverts MandateViolation(${verdict.firstViolation}).`}`,
268
+ ruleLines
269
+ ].join("\n");
270
+ return { content: [{ type: "text", text }] };
271
+ }
272
+ );
273
+ server.tool(
274
+ "execute_order",
275
+ "Execute an order on-chain through the mandate guard. Compliant orders execute and update the portfolio; non-compliant orders are refused before sending (the contract would revert them regardless).",
276
+ orderShape,
277
+ async ({ asset, side, quantity }) => {
278
+ const s = await readVault();
279
+ const { signedQty, verdict, ruleLines } = describeVerdict(s, asset, side, quantity);
280
+ if (verdict.firstViolation !== null) {
281
+ const rule = RULES.find((r) => r.id === verdict.firstViolation);
282
+ const text2 = [
283
+ `REFUSED: ${side.toUpperCase()} ${quantity} ${asset} violates rule ${verdict.firstViolation} (${rule?.name}).`,
284
+ `The on-chain guard would revert this with MandateViolation(${verdict.firstViolation}); not sending the transaction.`,
285
+ ruleLines
286
+ ].join("\n");
287
+ return { content: [{ type: "text", text: text2 }] };
288
+ }
289
+ const hash = await walletClient.writeContract({
290
+ address: VAULT,
291
+ abi: vaultAbi,
292
+ functionName: "execute",
293
+ args: [ASSETS.indexOf(asset), BigInt(signedQty)]
294
+ });
295
+ const receipt = await publicClient.waitForTransactionReceipt({ hash });
296
+ const link = EXPLORER_URL ? `
297
+ ${EXPLORER_URL}/tx/${hash}` : "";
298
+ const text = receipt.status === "success" ? `EXECUTED: ${side.toUpperCase()} ${quantity} ${asset} in block ${receipt.blockNumber}. Post-trade NAV $${verdict.postNav.toLocaleString("en-US")}.${link}` : `REVERTED on-chain (block ${receipt.blockNumber}). The guard refused the trade.${link}`;
299
+ return { content: [{ type: "text", text }] };
300
+ }
301
+ );
302
+ await server.connect(new StdioServerTransport());
303
+ console.error(`aristotem-vault MCP server ready \xB7 vault ${VAULT} \xB7 agent ${account.address}`);
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "logos-layer",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Logos Layer: an MCP server that lets AI agents trade inside a mandate-guarded vault on Robinhood Chain. Compliant orders execute; violations revert on-chain.",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "logos-layer": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "scripts": {
19
+ "build": "tsup",
20
+ "start": "node dist/index.js",
21
+ "prepublishOnly": "pnpm build"
22
+ },
23
+ "dependencies": {
24
+ "@modelcontextprotocol/sdk": "^1.29.0",
25
+ "viem": "^2.55.8",
26
+ "zod": "^4.4.3"
27
+ },
28
+ "devDependencies": {
29
+ "@pct/checker-ts": "workspace:*",
30
+ "@pct/mandate-schema": "workspace:*",
31
+ "tsup": "^8.5.0"
32
+ }
33
+ }