@hypelens/hypelens-agent-rail 0.1.1 → 0.1.2
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 +10 -6
- package/package.json +6 -4
- package/src/core.js +55 -12
- package/src/exchange.js +3 -3
- package/src/index.js +1 -1
- package/src/mcp.js +7 -7
- package/vendor/hl-actions.js +2 -2
- package/.gitignore +0 -3
- package/ROLLOUT.md +0 -67
- package/package-lock.json +0 -1182
- package/scripts/bundle-vendor.mjs +0 -13
- package/server.json +0 -35
- package/smithery.yaml +0 -23
- package/test/exchange.test.mjs +0 -74
- package/test/risk.test.mjs +0 -86
|
@@ -1,13 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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/test/exchange.test.mjs
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
});
|
package/test/risk.test.mjs
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
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
|
-
});
|