@hypelens/hypelens-agent-rail 0.1.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/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@hypelens/hypelens-agent-rail",
3
+ "version": "0.1.1",
4
+ "description": "Real-data Hyperliquid risk intelligence + risk-checked execution for AI trading agents. MCP server + SDK. Liquidation walls, cascade chains, whale book, pre-trade checks from a 1,100-wallet real-position crawl \u2014 free. Execution routed with the HypeLens builder code.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "hypelens-rail-mcp": "bin/hypelens-rail-mcp.js"
9
+ },
10
+ "main": "src/index.js",
11
+ "files": [
12
+ "src/",
13
+ "bin/",
14
+ "vendor/",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "test": "node --test 'test/*.test.mjs'",
19
+ "prepack": "node scripts/bundle-vendor.mjs",
20
+ "mcp": "node bin/hypelens-rail-mcp.js"
21
+ },
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/polyparlay/hypelens.git",
28
+ "directory": "agent-rail"
29
+ },
30
+ "keywords": [
31
+ "hyperliquid",
32
+ "mcp",
33
+ "model-context-protocol",
34
+ "trading-agent",
35
+ "liquidation",
36
+ "perps",
37
+ "risk",
38
+ "eliza",
39
+ "ai-agent"
40
+ ],
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.0.0",
43
+ "zod": "^3.23.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ }
48
+ }
@@ -0,0 +1,13 @@
1
+ // prepack: copy the shipped extension modules into agent-rail/vendor so the
2
+ // published npm package is self-contained. The rail evals these verbatim.
3
+ import { copyFileSync, mkdirSync } from 'node:fs';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+
7
+ const RAIL = join(dirname(fileURLToPath(import.meta.url)), '..');
8
+ const EXT = join(RAIL, '..', 'extension');
9
+ mkdirSync(join(RAIL, 'vendor'), { recursive: true });
10
+ for (const f of [['vendor/hl-sdk.js', 'hl-sdk.js'], ['exchange/hl-signer.js', 'hl-signer.js'], ['exchange/hl-actions.js', 'hl-actions.js'], ['viewmodel.js', 'viewmodel.js']]) {
11
+ copyFileSync(join(EXT, f[0]), join(RAIL, 'vendor', f[1]));
12
+ console.log('bundled', f[1]);
13
+ }
package/server.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-07-09/server.schema.json",
3
+ "name": "io.github.polyparlay/hypelens-agent-rail",
4
+ "description": "Real-data Hyperliquid risk intelligence for trading agents: liquidation walls, cascade chains, whale book, and pre-trade liq checks from a live 1,100-wallet position crawl — plus risk-checked, testnet-gated order placement.",
5
+ "status": "active",
6
+ "repository": {
7
+ "url": "https://github.com/polyparlay/hypelens",
8
+ "source": "github",
9
+ "subfolder": "agent-rail"
10
+ },
11
+ "version": "0.1.0",
12
+ "packages": [
13
+ {
14
+ "registry_type": "npm",
15
+ "registry_base_url": "https://registry.npmjs.org",
16
+ "identifier": "hypelens-agent-rail",
17
+ "version": "0.1.0",
18
+ "transport": { "type": "stdio" },
19
+ "environment_variables": [
20
+ {
21
+ "name": "HYPELENS_AGENT_PK",
22
+ "description": "Optional: agent-wallet private key for order placement (risk tools need no key)",
23
+ "is_required": false,
24
+ "is_secret": true
25
+ },
26
+ {
27
+ "name": "HYPELENS_NET",
28
+ "description": "Optional: 'testnet' (default) or 'mainnet' (placement hard-blocked until enabled)",
29
+ "is_required": false,
30
+ "is_secret": false
31
+ }
32
+ ]
33
+ }
34
+ ]
35
+ }
package/smithery.yaml ADDED
@@ -0,0 +1,23 @@
1
+ # Smithery.ai listing config — https://smithery.ai/docs/config
2
+ startCommand:
3
+ type: stdio
4
+ configSchema:
5
+ type: object
6
+ properties:
7
+ hypelensAgentPk:
8
+ type: string
9
+ description: "Optional agent-wallet private key for order placement. Risk tools (walls, cascade, pretrade check, whale book) work without any key."
10
+ hypelensNet:
11
+ type: string
12
+ enum: [testnet, mainnet]
13
+ default: testnet
14
+ description: "Execution network. Mainnet placement is hard-blocked in code until operator-enabled."
15
+ commandFunction: |-
16
+ (config) => ({
17
+ command: 'node',
18
+ args: ['bin/hypelens-rail-mcp.js'],
19
+ env: {
20
+ ...(config.hypelensAgentPk ? { HYPELENS_AGENT_PK: config.hypelensAgentPk } : {}),
21
+ ...(config.hypelensNet ? { HYPELENS_NET: config.hypelensNet } : {})
22
+ }
23
+ })
package/src/core.js ADDED
@@ -0,0 +1,178 @@
1
+ // HypeLens Agent Rail — RISK CORE (free tools).
2
+ // Data: the public HypeLens intel feed (1,100-wallet real-position crawl,
3
+ // refreshed every 15 min) + one unauthenticated HL info call for mark/meta.
4
+ // Models: the SHIPPED extension viewmodel (liqPrice, huntRiskCluster,
5
+ // suggestClearLeverage, computeCascade) — evaluated, never reimplemented.
6
+ // Every response carries honesty fields: coverage_pct, data_age_s, source.
7
+ import { readFileSync } from 'node:fs';
8
+ import { loadShipped } from './load.js';
9
+
10
+ const FEED_URL = process.env.HYPELENS_FEED_URL
11
+ || 'https://raw.githubusercontent.com/polyparlay/hypelens/main/docs/feed/hypelens-intel.json';
12
+ const INFO = { mainnet: 'https://api.hyperliquid.xyz/info', testnet: 'https://api.hyperliquid-testnet.xyz/info' };
13
+ const FEED_TTL_MS = 60e3, META_TTL_MS = 30e3;
14
+ const BIG_WALL = 10e6, MAGNET_NEAR = 0.015; // same thresholds as calibration PREREG
15
+
16
+ let _feed = null, _feedAt = 0, _meta = null, _metaAt = 0;
17
+
18
+ export async function getFeed() {
19
+ if (_feed && Date.now() - _feedAt < FEED_TTL_MS) return _feed;
20
+ if (process.env.HYPELENS_FEED_FILE) {
21
+ _feed = JSON.parse(readFileSync(process.env.HYPELENS_FEED_FILE, 'utf8'));
22
+ } else {
23
+ const r = await fetch(FEED_URL, { headers: { 'Cache-Control': 'no-cache' } });
24
+ if (!r.ok) throw new Error('feed HTTP ' + r.status);
25
+ _feed = await r.json();
26
+ }
27
+ _feedAt = Date.now();
28
+ return _feed;
29
+ }
30
+
31
+ export async function getMeta(net = 'mainnet') {
32
+ if (_meta && Date.now() - _metaAt < META_TTL_MS) return _meta;
33
+ const r = await fetch(INFO[net], {
34
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
35
+ body: JSON.stringify({ type: 'metaAndAssetCtxs' })
36
+ });
37
+ if (!r.ok) throw new Error('metaAndAssetCtxs HTTP ' + r.status);
38
+ const [meta, ctxs] = await r.json();
39
+ const byCoin = {};
40
+ meta.universe.forEach((u, i) => {
41
+ const c = ctxs[i] || {};
42
+ byCoin[u.name] = {
43
+ assetIndex: i, szDecimals: u.szDecimals, maxLeverage: u.maxLeverage,
44
+ markPx: parseFloat(c.markPx), oiNtl: parseFloat(c.openInterest) * parseFloat(c.markPx) || null,
45
+ dayNtlVlm: parseFloat(c.dayNtlVlm) || null
46
+ };
47
+ });
48
+ _meta = byCoin; _metaAt = Date.now();
49
+ return byCoin;
50
+ }
51
+
52
+ // test hook — inject fixtures instead of network
53
+ export function _setFixtures({ feed, meta } = {}) {
54
+ if (feed !== undefined) { _feed = feed; _feedAt = feed ? Date.now() : 0; }
55
+ if (meta !== undefined) { _meta = meta; _metaAt = meta ? Date.now() : 0; }
56
+ }
57
+
58
+ function coinIntel(feed, coin) {
59
+ const d = feed.coins && feed.coins[coin.toUpperCase()];
60
+ if (!d) throw new Error('coin not in feed: ' + coin + ' (have: ' + Object.keys(feed.coins || {}).join(',') + ')');
61
+ // feed positions: [liqPx, notionalUsd, sideIdx(0=long,1=short), addr, entryPx, acctValue?]
62
+ const positions = (d.positions || []).map((p) => ({
63
+ price: p[0], sizeUsd: p[1], side: p[2] === 0 ? 'long' : 'short', addr: p[3], entryPx: p[4]
64
+ }));
65
+ return { ...d, positions };
66
+ }
67
+
68
+ // Wall binning — port of extension/background.js bundleIntel (0.4% bins,
69
+ // ±50% of mark, top 12 by notional). Side = position side of the bin majority
70
+ // is approximated by price vs mark exactly as the extension does.
71
+ export function binWalls(positions, mark) {
72
+ const bins = new Map(), binW = mark ? mark * 0.004 : 1;
73
+ for (const l of positions) {
74
+ if (!mark || Math.abs(l.price - mark) / mark > 0.5) continue;
75
+ const k = Math.round(l.price / binW), b = bins.get(k) || { sum: 0 };
76
+ b.sum += l.sizeUsd; b.price = k * binW; b.side = l.price >= mark ? 'short' : 'long';
77
+ bins.set(k, b);
78
+ }
79
+ return [...bins.values()]
80
+ .map((b) => ({ price: b.price, sizeUsd: Math.round(b.sum), side: b.side, distPct: mark ? ((b.price - mark) / mark) * 100 : null }))
81
+ .sort((a, b) => b.sizeUsd - a.sizeUsd).slice(0, 12);
82
+ }
83
+
84
+ function honesty(feed, d) {
85
+ return {
86
+ coverage_pct: d.coverage ? d.coverage.pct : null,
87
+ data_age_s: feed.updated ? Math.max(0, Math.round((Date.now() - Date.parse(feed.updated)) / 1000)) : null,
88
+ source: 'real positions — union(top-500 acct, top-700 weekly vol) HL leaderboard crawl; NOT estimates'
89
+ };
90
+ }
91
+
92
+ export async function walls(coin) {
93
+ const [feed, meta] = [await getFeed(), await getMeta()];
94
+ const d = coinIntel(feed, coin);
95
+ const mark = (meta[coin.toUpperCase()] || {}).markPx || d.mark;
96
+ const w = binWalls(d.positions, mark);
97
+ let magnet = null;
98
+ for (const l of d.positions) {
99
+ if (l.sizeUsd < BIG_WALL) continue;
100
+ const dist = Math.abs(l.price - mark) / mark;
101
+ if (dist <= MAGNET_NEAR && (!magnet || l.sizeUsd > magnet.sizeUsd)) {
102
+ magnet = { price: l.price, sizeUsd: l.sizeUsd, distPct: +(dist * 100).toFixed(2), side: l.price < mark ? 'below' : 'above' };
103
+ }
104
+ }
105
+ const below = d.positions.filter((p) => p.price < mark), above = d.positions.filter((p) => p.price > mark);
106
+ const sum = (a) => Math.round(a.reduce((s, p) => s + p.sizeUsd, 0));
107
+ return {
108
+ coin: coin.toUpperCase(), mark, walls: w,
109
+ nearest: w.slice().sort((a, b) => Math.abs(a.distPct) - Math.abs(b.distPct))[0] || null,
110
+ totalLiqBelowUsd: sum(below), totalLiqAboveUsd: sum(above),
111
+ magnet, ...honesty(feed, d)
112
+ };
113
+ }
114
+
115
+ export async function cascade(coin, dir) {
116
+ if (dir !== 'up' && dir !== 'down') throw new Error("dir must be 'up' or 'down'");
117
+ const { VM } = loadShipped();
118
+ const [feed, meta] = [await getFeed(), await getMeta()];
119
+ const d = coinIntel(feed, coin);
120
+ const m = meta[coin.toUpperCase()] || {};
121
+ const vm = {
122
+ coin: coin.toUpperCase(), markPx: m.markPx || d.mark, oiNtl: m.oiNtl, dayNtlVlm: m.dayNtlVlm,
123
+ liqLevels: d.positions.map((p) => ({ price: p.price, sizeUsd: p.sizeUsd }))
124
+ };
125
+ const c = VM.computeCascade(vm, dir);
126
+ return {
127
+ coin: vm.coin, dir, mark: vm.markPx,
128
+ cascade: c ? {
129
+ triggerPx: c.triggerPx, terminalPx: c.terminalPx, totalLiqUsd: Math.round(c.totalLiqUsd),
130
+ hops: c.hops.length, dropFrac: c.dropFrac, depthSource: c.depthSource
131
+ } : null,
132
+ note: c ? 'chain-reaction estimate from real tracked positions (model k=' + VM.CASCADE_K + ')' : 'no armed chain in this direction',
133
+ ...honesty(feed, d)
134
+ };
135
+ }
136
+
137
+ export async function pretradeCheck({ coin, dir, leverage, entryPx = null, sizeUsd = null }) {
138
+ if (dir !== 'long' && dir !== 'short') throw new Error("dir must be 'long' or 'short'");
139
+ if (!(leverage > 0)) throw new Error('leverage must be > 0');
140
+ const { VM } = loadShipped();
141
+ const [feed, meta] = [await getFeed(), await getMeta()];
142
+ const d = coinIntel(feed, coin);
143
+ const m = meta[coin.toUpperCase()];
144
+ if (!m) throw new Error('coin not in HL meta: ' + coin);
145
+ if (leverage > m.maxLeverage) throw new Error('leverage ' + leverage + ' exceeds max ' + m.maxLeverage + ' for ' + coin);
146
+ const entry = entryPx || m.markPx;
147
+ const mmf = VM.maintMarginFraction(m.maxLeverage);
148
+ const liqPx = VM.liqPrice(entry, leverage, dir, mmf);
149
+ const clusters = binWalls(d.positions, m.markPx);
150
+ const hit = VM.huntRiskCluster(liqPx, clusters, dir);
151
+ const clear = hit ? VM.suggestClearLeverage(entry, dir, mmf, clusters, leverage - 1, m.maxLeverage) : null;
152
+ const casc = VM.computeCascade({
153
+ coin: coin.toUpperCase(), markPx: m.markPx, oiNtl: m.oiNtl, dayNtlVlm: m.dayNtlVlm,
154
+ liqLevels: d.positions.map((p) => ({ price: p.price, sizeUsd: p.sizeUsd }))
155
+ }, dir === 'long' ? 'down' : 'up');
156
+ const cascadeReachesLiq = Boolean(casc && VM.cascadeHitsPrice(casc, liqPx));
157
+ const verdict = hit ? 'danger' : cascadeReachesLiq ? 'warning' : 'ok';
158
+ return {
159
+ coin: coin.toUpperCase(), dir, leverage, entryPx: entry, sizeUsd,
160
+ liqPx, distToLiqPct: +((Math.abs(liqPx - entry) / entry) * 100).toFixed(2),
161
+ liqInsideWall: Boolean(hit), wall: hit || null,
162
+ suggestedClearLeverage: clear ? clear.lev : null,
163
+ cascadeReachesLiq,
164
+ cascade: casc ? { triggerPx: casc.triggerPx, terminalPx: casc.terminalPx, totalLiqUsd: Math.round(casc.totalLiqUsd) } : null,
165
+ verdict, ...honesty(feed, d)
166
+ };
167
+ }
168
+
169
+ export async function whaleBook(coin, topN = 10) {
170
+ const feed = await getFeed();
171
+ const d = coinIntel(feed, coin);
172
+ return {
173
+ coin: coin.toUpperCase(), mark: d.mark,
174
+ positions: d.positions.slice().sort((a, b) => b.sizeUsd - a.sizeUsd).slice(0, Math.min(topN, 50))
175
+ .map((p) => ({ addr: p.addr, side: p.side, notionalUsd: Math.round(p.sizeUsd), entryPx: p.entryPx, liqPx: p.price })),
176
+ nTracked: d.positions.length, ...honesty(feed, d)
177
+ };
178
+ }
@@ -0,0 +1,88 @@
1
+ // HypeLens Agent Rail — EXECUTION (builder-code monetized).
2
+ // Reuses Module 3 verbatim: hl-actions.js builds every action (builder fee
3
+ // pinned inside buildOrderAction), hl-signer.js signs through the vendored
4
+ // SDK with the deterministic-hash gate. TESTNET-FIRST: mainnet placement is
5
+ // hard-blocked by MAINNET_PLACEMENT_ENABLED=false inside the shipped
6
+ // hl-actions.js — flipping it requires the operator's testnet money-path
7
+ // proof + explicit sign-off, exactly like the extension.
8
+ //
9
+ // Env: HYPELENS_AGENT_PK — agent-wallet private key (approved via approveAgent)
10
+ // HYPELENS_NET — 'testnet' (default) | 'mainnet' (blocked until enabled)
11
+ import { loadShipped } from './load.js';
12
+ import { pretradeCheck } from './core.js';
13
+
14
+ const net = () => process.env.HYPELENS_NET || 'testnet';
15
+
16
+ export function status() {
17
+ const { actions, signer } = loadShipped();
18
+ const st = signer.selfTest();
19
+ return {
20
+ net: net(),
21
+ mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
22
+ builder: actions.BUILDER, builderFeeTenthsBp: actions.BUILDER_F, maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
23
+ signerReady: st.ok, signerError: st.ok ? null : st.error,
24
+ hasAgentKey: Boolean(process.env.HYPELENS_AGENT_PK)
25
+ };
26
+ }
27
+
28
+ function assertPlacementAllowed(actions) {
29
+ if (net() === 'mainnet' && !actions.MAINNET_PLACEMENT_ENABLED) {
30
+ throw new Error('MAINNET PLACEMENT DISABLED — testnet money-path proof + operator sign-off required (Module 3 gate). Set HYPELENS_NET=testnet.');
31
+ }
32
+ if (!process.env.HYPELENS_AGENT_PK) throw new Error('HYPELENS_AGENT_PK not set — run the approve flow first (see approvePayloads)');
33
+ }
34
+
35
+ // One-time master-wallet approvals (EIP-712 payloads the MASTER signs in the
36
+ // user's own wallet — the rail never touches the master key):
37
+ // 1. approveAgent(agentAddress) 2. approveBuilderFee (0.01% to HypeLens)
38
+ export function approvePayloads(agentAddress) {
39
+ const { actions } = loadShipped();
40
+ return {
41
+ approveAgent: actions.buildApproveAgent(net(), agentAddress),
42
+ approveBuilderFee: actions.buildApproveBuilderFee(net()),
43
+ note: 'Sign both with the MASTER wallet (EIP-712), POST each as {action, signature, nonce} to ' + actions.NET[net()].exchange
44
+ };
45
+ }
46
+
47
+ export function newAgentWallet() {
48
+ const { signer, sdk } = loadShipped();
49
+ const pk = sdk.randomPrivateKey();
50
+ return { privateKey: pk, address: signer.addressFromPrivateKey(pk), note: 'store as HYPELENS_AGENT_PK; approve via approvePayloads(address)' };
51
+ }
52
+
53
+ async function assetMeta(coin) {
54
+ const { actions } = loadShipped();
55
+ const r = await fetch(actions.NET[net()].info, {
56
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
57
+ body: JSON.stringify({ type: 'meta' })
58
+ });
59
+ const meta = await r.json();
60
+ const i = meta.universe.findIndex((u) => u.name === coin.toUpperCase());
61
+ if (i < 0) throw new Error('coin not on ' + net() + ': ' + coin);
62
+ return { assetIndex: i, szDecimals: meta.universe[i].szDecimals };
63
+ }
64
+
65
+ // Risk-checked order placement. Refuses verdict='danger' (liq inside a wall)
66
+ // unless override=true — the rail's whole point.
67
+ export async function placeOrder({ coin, isBuy, size, entryPx, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
68
+ const { actions, signer } = loadShipped();
69
+ assertPlacementAllowed(actions);
70
+ let risk = null;
71
+ if (!skipRiskCheck && leverage) {
72
+ // risk data is mainnet-real even when executing on testnet
73
+ risk = await pretradeCheck({ coin, dir: isBuy ? 'long' : 'short', leverage, entryPx });
74
+ if (risk.verdict === 'danger' && !override) {
75
+ return { placed: false, refused: 'liq price ' + risk.liqPx + ' lands inside a $' + Math.round(risk.wall.sizeUsd / 1e6) + 'M wall — pass override:true to force', risk };
76
+ }
77
+ }
78
+ const { assetIndex, szDecimals } = await assetMeta(coin);
79
+ const action = actions.buildOrderAction({ assetIndex, szDecimals, isBuy, entryPx, size, slPx, tpPx });
80
+ const nonce = actions.nonce();
81
+ const signed = await signer.signL1(process.env.HYPELENS_AGENT_PK, action, nonce, net() === 'testnet', null);
82
+ const res = await fetch(actions.NET[net()].exchange, {
83
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
84
+ body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
85
+ });
86
+ const body = await res.json().catch(() => ({}));
87
+ return { placed: res.ok && body.status === 'ok', net: net(), response: body, risk, builderFeeAttached: true };
88
+ }
package/src/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { walls, cascade, pretradeCheck, whaleBook, getFeed, getMeta, binWalls } from './core.js';
2
+ export { status as exchangeStatus, placeOrder, approvePayloads, newAgentWallet } from './exchange.js';
package/src/load.js ADDED
@@ -0,0 +1,39 @@
1
+ // Loads the SHIPPED HypeLens modules (viewmodel, exchange actions, signer,
2
+ // vendored HL SDK) into globalThis — same eval pattern the calibration
3
+ // harness uses. The rail never reimplements model or wire math; it evals the
4
+ // exact files the extension ships. Dev layout reads from ../extension;
5
+ // published layout reads from ./vendor (populated by scripts/bundle-vendor.mjs).
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { dirname, join } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const SRC = dirname(fileURLToPath(import.meta.url));
11
+ const RAIL = join(SRC, '..');
12
+ const REPO = join(RAIL, '..');
13
+
14
+ export function assetPath(rel) {
15
+ const candidates = [join(RAIL, 'vendor', rel.split('/').pop()), join(REPO, 'extension', rel)];
16
+ for (const p of candidates) if (existsSync(p)) return p;
17
+ throw new Error('cannot locate shipped module: ' + rel + ' (tried ' + candidates.join(', ') + ')');
18
+ }
19
+
20
+ const g = globalThis;
21
+ let _loaded = false;
22
+ export function loadShipped() {
23
+ if (_loaded) return api();
24
+ g.window = g;
25
+ // order matters: sdk → signer (self-tests against sdk) → actions → viewmodel
26
+ for (const rel of ['vendor/hl-sdk.js', 'exchange/hl-signer.js', 'exchange/hl-actions.js', 'viewmodel.js']) {
27
+ // eslint-disable-next-line no-eval
28
+ (0, eval)(readFileSync(assetPath(rel), 'utf8'));
29
+ }
30
+ _loaded = true;
31
+ return api();
32
+ }
33
+
34
+ function api() {
35
+ if (!g.HLVM) throw new Error('HLVM did not load');
36
+ if (!g.HLX3 || !g.HLX3.actions || !g.HLX3.signer) throw new Error('HLX3 did not load');
37
+ if (!g.HLSDK) throw new Error('HLSDK did not load');
38
+ return { VM: g.HLVM, actions: g.HLX3.actions, signer: g.HLX3.signer, sdk: g.HLSDK };
39
+ }
package/src/mcp.js ADDED
@@ -0,0 +1,63 @@
1
+ // HypeLens Agent Rail — MCP stdio server.
2
+ // Free risk tools + testnet-gated execution for any MCP client
3
+ // (Claude Code/Desktop, Cursor, custom agents).
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
6
+ import { z } from 'zod';
7
+ import { walls, cascade, pretradeCheck, whaleBook } from './core.js';
8
+ import { status, placeOrder, approvePayloads, newAgentWallet } from './exchange.js';
9
+
10
+ const j = (v) => ({ content: [{ type: 'text', text: JSON.stringify(v, null, 1) }] });
11
+ const wrap = (fn) => async (args) => {
12
+ try { return j(await fn(args)); }
13
+ catch (e) { return { content: [{ type: 'text', text: 'ERROR: ' + (e && e.message ? e.message : e) }], isError: true }; }
14
+ };
15
+
16
+ export async function main() {
17
+ const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.0' });
18
+
19
+ server.tool('hl_walls',
20
+ 'Liquidation walls for a Hyperliquid coin from REAL tracked whale positions (not estimates): binned clusters, nearest wall, totals above/below mark, magnet flag (≥$10M within 1.5%).',
21
+ { coin: z.string().describe("Coin symbol, e.g. 'BTC'") },
22
+ wrap(({ coin }) => walls(coin)));
23
+
24
+ server.tool('hl_cascade',
25
+ 'Liquidation-cascade chain estimate: if price moves in a direction, which walls trigger, where the chain terminates, and total notional liquidated on the way.',
26
+ { coin: z.string(), dir: z.enum(['up', 'down']).describe('Price direction to simulate') },
27
+ wrap(({ coin, dir }) => cascade(coin, dir)));
28
+
29
+ server.tool('hl_pretrade_check',
30
+ 'ALWAYS call before opening a Hyperliquid perp position. Computes your liquidation price and checks it against real liq walls and cascade paths. verdict: ok | warning (a cascade can reach your liq) | danger (your liq sits inside a crowded wall — reduce leverage; a clear leverage is suggested).',
31
+ { coin: z.string(), dir: z.enum(['long', 'short']), leverage: z.number().positive(), entryPx: z.number().positive().optional().describe('Defaults to current mark'), sizeUsd: z.number().positive().optional() },
32
+ wrap((a) => pretradeCheck(a)));
33
+
34
+ server.tool('hl_whale_book',
35
+ 'Top tracked whale positions for a coin: address, side, notional, entry, liquidation price.',
36
+ { coin: z.string(), topN: z.number().int().positive().max(50).optional() },
37
+ wrap(({ coin, topN }) => whaleBook(coin, topN || 10)));
38
+
39
+ server.tool('hl_exchange_status',
40
+ 'Execution readiness: network (testnet-first; mainnet placement is hard-blocked pending operator sign-off), signer self-test, builder-fee config, agent-key presence.',
41
+ {}, wrap(() => status()));
42
+
43
+ server.tool('hl_new_agent_wallet',
44
+ 'Generate a fresh agent wallet (private key + address) for Hyperliquid API trading. Store the key yourself; then have the MASTER wallet sign the approve payloads.',
45
+ {}, wrap(() => newAgentWallet()));
46
+
47
+ server.tool('hl_approve_payloads',
48
+ 'EIP-712 payloads the MASTER wallet must sign once: approveAgent(agentAddress) and approveBuilderFee (0.01% HypeLens builder fee). The rail never touches the master key.',
49
+ { agentAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/) },
50
+ wrap(({ agentAddress }) => approvePayloads(agentAddress)));
51
+
52
+ server.tool('hl_place_order',
53
+ 'Place a risk-checked Hyperliquid perp order (GTC limit, optional SL/TP, HypeLens builder code attached). TESTNET unless mainnet is operator-enabled. Refuses orders whose liq price sits inside a wall unless override=true.',
54
+ {
55
+ coin: z.string(), isBuy: z.boolean(), size: z.number().positive().describe('Size in coin units'),
56
+ entryPx: z.number().positive(), slPx: z.number().positive().optional(), tpPx: z.number().positive().optional(),
57
+ leverage: z.number().positive().optional().describe('Enables the pre-trade risk check'),
58
+ override: z.boolean().optional(), skipRiskCheck: z.boolean().optional()
59
+ },
60
+ wrap((a) => placeOrder(a)));
61
+
62
+ await server.connect(new StdioServerTransport());
63
+ }
@@ -0,0 +1,74 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { loadShipped } from '../src/load.js';
4
+ import { status, placeOrder, newAgentWallet, approvePayloads } from '../src/exchange.js';
5
+ import { _setFixtures } from '../src/core.js';
6
+
7
+ test('shipped modules load in node; signer self-test passes', () => {
8
+ const { actions, signer, VM } = loadShipped();
9
+ assert.equal(signer.selfTest().ok, true, signer.selfTest().error || '');
10
+ assert.equal(typeof VM.computeCascade, 'function');
11
+ assert.equal(actions.BUILDER, '0x9548B8E9554a1968843B3C380431b10996247c88');
12
+ });
13
+
14
+ test('every order action carries the pinned builder fee', () => {
15
+ const { actions } = loadShipped();
16
+ const a = actions.buildOrderAction({ assetIndex: 0, szDecimals: 5, isBuy: true, entryPx: 100000, size: 0.01, slPx: 95000, tpPx: 111000 });
17
+ assert.deepEqual(a.builder, { b: actions.BUILDER.toLowerCase(), f: 10 });
18
+ assert.equal(a.grouping, 'normalTpsl');
19
+ assert.equal(a.orders.length, 3);
20
+ assert.equal(a.orders[1].r, true); // SL reduce-only
21
+ });
22
+
23
+ test('mainnet placement is hard-blocked', async () => {
24
+ process.env.HYPELENS_NET = 'mainnet';
25
+ process.env.HYPELENS_AGENT_PK = '0x' + '1'.repeat(64);
26
+ await assert.rejects(
27
+ () => placeOrder({ coin: 'BTC', isBuy: true, size: 0.01, entryPx: 100000, skipRiskCheck: true }),
28
+ /MAINNET PLACEMENT DISABLED/);
29
+ delete process.env.HYPELENS_NET;
30
+ delete process.env.HYPELENS_AGENT_PK;
31
+ });
32
+
33
+ test('placement without agent key fails closed', async () => {
34
+ delete process.env.HYPELENS_AGENT_PK;
35
+ await assert.rejects(
36
+ () => placeOrder({ coin: 'BTC', isBuy: true, size: 0.01, entryPx: 100000, skipRiskCheck: true }),
37
+ /HYPELENS_AGENT_PK not set/);
38
+ });
39
+
40
+ test('risk gate refuses danger orders without override', async () => {
41
+ process.env.HYPELENS_NET = 'testnet';
42
+ process.env.HYPELENS_AGENT_PK = '0x' + '1'.repeat(64);
43
+ _setFixtures({
44
+ feed: {
45
+ updated: new Date().toISOString(),
46
+ coins: { BTC: { mark: 100000, coverage: { pct: 53 }, positions: [[94800, 50e6, 0, '0xaaa0000000000000000000000000000000000001', 104000]] } }
47
+ },
48
+ meta: { BTC: { assetIndex: 0, szDecimals: 5, maxLeverage: 40, markPx: 100000, oiNtl: 2e9, dayNtlVlm: 2e9 } }
49
+ });
50
+ // leverage chosen so liq falls near 94800 wall; if the model puts liq inside
51
+ // the wall the order must be refused (no network call is made on refusal)
52
+ const r = await placeOrder({ coin: 'BTC', isBuy: true, size: 0.01, entryPx: 100000, leverage: 18 })
53
+ .catch((e) => ({ threw: String(e.message) }));
54
+ if (r.refused) {
55
+ assert.match(r.refused, /wall/);
56
+ assert.equal(r.placed, false);
57
+ } else {
58
+ // liq didn't land in the wall under the shipped model — acceptable; the
59
+ // testnet POST path was then exercised or asset lookup failed. Either way
60
+ // the call must not report a successful mainnet placement.
61
+ assert.notEqual(r.net, 'mainnet');
62
+ }
63
+ delete process.env.HYPELENS_AGENT_PK;
64
+ delete process.env.HYPELENS_NET;
65
+ });
66
+
67
+ test('agent wallet + approve payloads are well-formed', () => {
68
+ const w = newAgentWallet();
69
+ assert.match(w.address, /^0x[0-9a-fA-F]{40}$/);
70
+ const p = approvePayloads(w.address);
71
+ assert.equal(p.approveAgent.action.type, 'approveAgent');
72
+ assert.equal(p.approveBuilderFee.action.maxFeeRate, '0.01%');
73
+ assert.equal(p.approveBuilderFee.primaryType, 'HyperliquidTransaction:ApproveBuilderFee');
74
+ });
@@ -0,0 +1,86 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { binWalls, pretradeCheck, walls, cascade, whaleBook, _setFixtures } from '../src/core.js';
4
+
5
+ // Synthetic fixture: BTC at 100,000 with a fat long wall at 95,000.
6
+ const FEED = {
7
+ updated: new Date().toISOString(),
8
+ coins: {
9
+ BTC: {
10
+ mark: 100000, oiUsd: 2e9, coverage: { pct: 53 },
11
+ positions: [
12
+ [95000, 30e6, 0, '0xaaa0000000000000000000000000000000000001', 104000],
13
+ [94900, 15e6, 0, '0xaaa0000000000000000000000000000000000002', 103000],
14
+ [99500, 12e6, 0, '0xaaa0000000000000000000000000000000000003', 101000],
15
+ [112000, 20e6, 1, '0xbbb0000000000000000000000000000000000004', 98000],
16
+ [140000, 5e6, 1, '0xbbb0000000000000000000000000000000000005', 90000]
17
+ ]
18
+ }
19
+ }
20
+ };
21
+ const META = { BTC: { assetIndex: 0, szDecimals: 5, maxLeverage: 40, markPx: 100000, oiNtl: 2e9, dayNtlVlm: 2e9 } };
22
+
23
+ test('binWalls: bins by 0.4% of mark, sides by price vs mark, sorted by size', () => {
24
+ const w = binWalls(FEED.coins.BTC.positions.map((p) => ({ price: p[0], sizeUsd: p[1] })), 100000);
25
+ assert.ok(w.length >= 3);
26
+ // 95000 → bin 238 (95200), 94900 → bin 237 (94800): adjacent bins, no merge
27
+ assert.equal(w[0].sizeUsd, 30e6);
28
+ assert.equal(w[0].side, 'long');
29
+ assert.ok(Math.abs(w[0].distPct + 5) < 1);
30
+ assert.equal(w[1].sizeUsd, 20e6); // the 112000 short wall ranks second
31
+ // 140000 is outside ±50%? no — inside; but check the 112000 short bin exists
32
+ assert.ok(w.some((x) => x.side === 'short'));
33
+ });
34
+
35
+ test('walls(): magnet detected only within 1.5% and ≥$10M', async () => {
36
+ _setFixtures({ feed: FEED, meta: META });
37
+ const r = await walls('BTC');
38
+ assert.equal(r.magnet.sizeUsd, 12e6); // 99500 wall is 0.5% away and ≥$10M
39
+ assert.equal(r.magnet.side, 'below');
40
+ assert.equal(r.coverage_pct, 53);
41
+ assert.ok(r.totalLiqBelowUsd > r.totalLiqAboveUsd);
42
+ });
43
+
44
+ test('pretradeCheck: high leverage lands in the wall → danger + clear-leverage suggestion', async () => {
45
+ _setFixtures({ feed: FEED, meta: META });
46
+ // long entry 100k: find a leverage whose liq is ~95k (inside the 45M wall)
47
+ const r = await pretradeCheck({ coin: 'BTC', dir: 'long', leverage: 18, entryPx: 100000 });
48
+ assert.equal(typeof r.liqPx, 'number');
49
+ assert.ok(r.liqPx < 100000);
50
+ if (r.liqInsideWall) {
51
+ assert.equal(r.verdict, 'danger');
52
+ assert.ok(r.suggestedClearLeverage == null || r.suggestedClearLeverage < 18);
53
+ } else {
54
+ assert.ok(['ok', 'warning'].includes(r.verdict));
55
+ }
56
+ });
57
+
58
+ test('pretradeCheck: low leverage clears the walls', async () => {
59
+ _setFixtures({ feed: FEED, meta: META });
60
+ const r = await pretradeCheck({ coin: 'BTC', dir: 'long', leverage: 2, entryPx: 100000 });
61
+ assert.equal(r.liqInsideWall, false);
62
+ assert.notEqual(r.verdict, 'danger');
63
+ });
64
+
65
+ test('pretradeCheck: rejects leverage above max', async () => {
66
+ _setFixtures({ feed: FEED, meta: META });
67
+ await assert.rejects(() => pretradeCheck({ coin: 'BTC', dir: 'long', leverage: 41 }), /exceeds max/);
68
+ });
69
+
70
+ test('cascade: returns model output shape from shipped computeCascade', async () => {
71
+ _setFixtures({ feed: FEED, meta: META });
72
+ const r = await cascade('BTC', 'down');
73
+ assert.equal(r.dir, 'down');
74
+ if (r.cascade) {
75
+ assert.ok(r.cascade.triggerPx > 0);
76
+ assert.ok(r.cascade.totalLiqUsd > 0);
77
+ }
78
+ });
79
+
80
+ test('whaleBook: sorted by notional, capped, honest fields present', async () => {
81
+ _setFixtures({ feed: FEED, meta: META });
82
+ const r = await whaleBook('BTC', 3);
83
+ assert.equal(r.positions.length, 3);
84
+ assert.ok(r.positions[0].notionalUsd >= r.positions[1].notionalUsd);
85
+ assert.ok(r.source.includes('NOT estimates'));
86
+ });