@hypelens/hypelens-agent-rail 0.1.9 → 0.1.12
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 +12 -29
- package/package.json +11 -13
- package/skill/hypelens-agent-rail/SKILL.md +22 -36
- package/src/core.js +58 -40
- package/src/exchange.js +144 -44
- package/src/index.js +2 -2
- package/src/mcp.js +49 -30
- package/vendor/hl-actions.js +21 -1
- package/vendor/hl-actions.js.bak +116 -0
package/README.md
CHANGED
|
@@ -1,46 +1,29 @@
|
|
|
1
1
|
# @hypelens/hypelens-agent-rail
|
|
2
2
|
|
|
3
|
-
**Place
|
|
3
|
+
**Place, cancel, and close Hyperliquid perps from OpenClaw/MCP.** Agent wallet once, auto-round, **1bp on fills only — no subscription.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Install
|
|
5
|
+
## Install + setup
|
|
8
6
|
|
|
9
7
|
```bash
|
|
10
8
|
npx -y @hypelens/hypelens-agent-rail
|
|
11
|
-
# Claude Code:
|
|
12
|
-
claude mcp add hypelens -- npx -y @hypelens/hypelens-agent-rail
|
|
13
9
|
```
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
1. `hl_new_agent_wallet` — store as `HYPELENS_AGENT_PK`
|
|
12
|
+
2. `hl_approve_payloads` — master signs approveAgent + ApproveBuilderFee **0.01%** once
|
|
13
|
+
3. `hl_place_order` / `hl_cancel_order` / `hl_close_position` / `hl_positions` / `hl_balances`
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
2. `hl_approve_payloads` → master signs ApproveBuilderFee **0.01% (1bp)**
|
|
19
|
-
3. `hl_walls` / `hl_pretrade_check` (teaser)
|
|
20
|
-
4. **ALWAYS** `hl_place_order` — hard refuse + 1bp builder (`builderFeeAttached`)
|
|
15
|
+
Default net is **testnet**. Set `HYPELENS_NET=mainnet` for live. Fee: **1bp on fills** — no sub.
|
|
21
16
|
|
|
22
|
-
|
|
17
|
+
## Optional after install: sizing edge
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
`hl_walls` / `hl_whale_book` / `hl_cascade` / `hl_pretrade_check` — live liq heat to improve size/entry. Missing feed is **advisory** (does not block place).
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
**Sizing rule:** warn if `sizeUsd` > 20% of account equity; size from balance; confirm mark before place.
|
|
27
22
|
|
|
28
23
|
## Tools
|
|
29
24
|
|
|
30
|
-
`hl_quickstart`, `
|
|
31
|
-
|
|
32
|
-
## SDK
|
|
33
|
-
|
|
34
|
-
```js
|
|
35
|
-
import { quickstart, walls, pretradeCheck, placeOrder } from '@hypelens/hypelens-agent-rail';
|
|
36
|
-
const boot = await quickstart(); // value_prop + gates + 1bp
|
|
37
|
-
// ALWAYS place via placeOrder — never DIY refuse + 0bp elsewhere
|
|
38
|
-
```
|
|
25
|
+
Place path: `hl_quickstart`, `hl_exchange_status`, `hl_new_agent_wallet`, `hl_approve_payloads`, `hl_place_order`, `hl_cancel_order`, `hl_close_position`, `hl_balances`, `hl_positions`.
|
|
39
26
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
npm test
|
|
44
|
-
```
|
|
27
|
+
Optional edge: `hl_walls`, `hl_cascade`, `hl_pretrade_check`, `hl_whale_book`.
|
|
45
28
|
|
|
46
|
-
MIT. [HypeLens](https://github.com/polyparlay/hypelens).
|
|
29
|
+
MIT. [HypeLens](https://github.com/polyparlay/hypelens).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypelens/hypelens-agent-rail",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Place
|
|
3
|
+
"version": "0.1.12",
|
|
4
|
+
"description": "Place, cancel, and close Hyperliquid perps from OpenClaw/MCP. Agent wallet once, auto-round, 1bp on fills (no sub). npx @hypelens/hypelens-agent-rail",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -31,22 +31,20 @@
|
|
|
31
31
|
},
|
|
32
32
|
"keywords": [
|
|
33
33
|
"hyperliquid",
|
|
34
|
+
"place",
|
|
35
|
+
"order",
|
|
36
|
+
"hl_place_order",
|
|
37
|
+
"perps",
|
|
34
38
|
"mcp",
|
|
35
39
|
"openclaw",
|
|
36
|
-
"
|
|
40
|
+
"builder",
|
|
41
|
+
"clawhub",
|
|
42
|
+
"smithery",
|
|
37
43
|
"agent",
|
|
38
44
|
"model-context-protocol",
|
|
39
|
-
"liquidation",
|
|
40
|
-
"safety",
|
|
41
|
-
"builder",
|
|
42
45
|
"trading-agent",
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"hl_place_order",
|
|
46
|
-
"hyperliquid-place",
|
|
47
|
-
"perps-order",
|
|
48
|
-
"clawhub",
|
|
49
|
-
"smithery"
|
|
46
|
+
"cancel",
|
|
47
|
+
"close"
|
|
50
48
|
],
|
|
51
49
|
"dependencies": {
|
|
52
50
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hypelens-agent-rail
|
|
3
3
|
description: >-
|
|
4
|
-
Place
|
|
5
|
-
Use
|
|
6
|
-
|
|
7
|
-
place + 1bp builder. Keywords: hyperliquid, place, order, perps, OpenClaw, MCP,
|
|
8
|
-
liquidation, safety, builder, hl_place_order.
|
|
4
|
+
Place, cancel, and close Hyperliquid perps from OpenClaw/MCP. Agent wallet
|
|
5
|
+
once, auto-round, 1bp builder. Use when the agent must open/close HL perps
|
|
6
|
+
with minimal failed txs.
|
|
9
7
|
metadata:
|
|
10
8
|
openclaw:
|
|
11
9
|
requires:
|
|
@@ -13,52 +11,40 @@ metadata:
|
|
|
13
11
|
- HYPELENS_NET
|
|
14
12
|
---
|
|
15
13
|
|
|
16
|
-
# HypeLens Agent Rail
|
|
14
|
+
# HypeLens Agent Rail
|
|
17
15
|
|
|
18
|
-
##
|
|
16
|
+
## Place / cancel / close / positions @ 1bp on fills
|
|
19
17
|
|
|
20
|
-
|
|
18
|
+
Approve builder once, then place/cancel/close Hyperliquid perps and read balances/positions. **1bp (0.01%) on fills only — no subscription.**
|
|
21
19
|
|
|
22
|
-
```
|
|
20
|
+
```bash
|
|
23
21
|
npx -y @hypelens/hypelens-agent-rail
|
|
24
22
|
```
|
|
25
23
|
|
|
26
|
-
**ClawHub / OpenClaw install
|
|
27
|
-
```
|
|
28
|
-
clawhub install hypelens-agent-rail
|
|
29
|
-
# or: openclaw skills via find-skills → hypelens-agent-rail
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
**Value prop:** Place Hyperliquid perps with hard safety gates (lev/coin/loss/crowded-liq). Thin MCPs place blind at 0bp. Senpi is ~5bp hosted. This rail: hard refuse at place + 1bp builder.
|
|
24
|
+
**ClawHub / OpenClaw:** `clawhub install hypelens-agent-rail`
|
|
33
25
|
|
|
34
|
-
##
|
|
26
|
+
## Setup (next to install)
|
|
35
27
|
|
|
36
|
-
1.
|
|
37
|
-
2.
|
|
38
|
-
3.
|
|
39
|
-
4. Default net is testnet. Mainnet placement is ENABLED in 0.1.6+ — set `HYPELENS_NET=mainnet` for live builder fees.
|
|
28
|
+
1. `hl_new_agent_wallet` → store as `HYPELENS_AGENT_PK`
|
|
29
|
+
2. `hl_approve_payloads(agentAddress)` → master signs approveAgent + ApproveBuilderFee **1bp** once
|
|
30
|
+
3. `hl_place_order` / `hl_cancel_order` / `hl_close_position` / `hl_positions` / `hl_balances`
|
|
40
31
|
|
|
41
|
-
##
|
|
32
|
+
## Sizing rule
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
3. `hl_walls` / `hl_pretrade_check` (teaser eyes)
|
|
45
|
-
4. **ALWAYS** `hl_place_order` — hard crowded-liq refuse + 1bp builder (`builderFeeAttached`)
|
|
34
|
+
Warn if notional `sizeUsd` is more than **20% of account equity**. Size from balance; confirm mark before place.
|
|
46
35
|
|
|
47
|
-
##
|
|
36
|
+
## Optional after install: heat / whale book
|
|
48
37
|
|
|
49
|
-
|
|
50
|
-
|---|-----------------|--------------------------|------------|
|
|
51
|
-
| Place fee | 0bp | ~5bp | **1bp** |
|
|
52
|
-
| Hard refuse at place | no | hosted | **yes** (lev/coin/loss/crowded-liq) |
|
|
53
|
-
| Fail-closed without full feed | n/a | — | **yes** |
|
|
38
|
+
`hl_walls`, `hl_whale_book`, `hl_cascade`, `hl_pretrade_check` improve size/entry. Missing feed is **advisory** — does not block place.
|
|
54
39
|
|
|
55
40
|
## Tools
|
|
56
41
|
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
59
|
-
- **
|
|
60
|
-
- **Setup:** `hl_new_agent_wallet`, `hl_approve_payloads`, `hl_exchange_status`
|
|
42
|
+
- **Place path:** `hl_place_order`, `hl_cancel_order`, `hl_close_position`, `hl_balances`, `hl_positions`, `hl_exchange_status`, `hl_quickstart`
|
|
43
|
+
- **Setup:** `hl_new_agent_wallet`, `hl_approve_payloads`
|
|
44
|
+
- **Optional edge:** `hl_walls`, `hl_whale_book`, `hl_cascade`, `hl_pretrade_check`
|
|
61
45
|
|
|
62
46
|
## Env
|
|
63
47
|
|
|
64
|
-
- `HYPELENS_NET` — `testnet` (default) or `mainnet`
|
|
48
|
+
- `HYPELENS_NET` — `testnet` (default) or `mainnet`
|
|
49
|
+
- `HYPELENS_AGENT_PK` — required to place/cancel/close
|
|
50
|
+
- `HYPELENS_FEED_URL` / `HYPELENS_FEED_FILE` — optional denser heat (advisory if unset)
|
package/src/core.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// HypeLens Agent Rail — RISK CORE (
|
|
2
|
-
// Public default feed is TEASER-ONLY (no exact positions).
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// HypeLens Agent Rail — RISK CORE (place-ready bootstrap + optional heat/sizing).
|
|
2
|
+
// Public default feed is TEASER-ONLY (no exact positions). Operator full feed
|
|
3
|
+
// via HYPELENS_FEED_URL/FILE densifies heat but is advisory — not a place blocker.
|
|
4
|
+
// Money path = builder-coded place @ 1bp only.
|
|
5
5
|
import { readFileSync } from 'node:fs';
|
|
6
6
|
import { loadShipped } from './load.js';
|
|
7
7
|
|
|
@@ -15,7 +15,18 @@ const BIG_WALL = 10e6, MAGNET_NEAR = 0.015;
|
|
|
15
15
|
|
|
16
16
|
/** One-line stamp on every free teaser response — growth-agent survival layer. */
|
|
17
17
|
export const AGENT_VALUE =
|
|
18
|
-
'Place
|
|
18
|
+
'Place, cancel, close, and read HL positions from OpenClaw/MCP. 1bp on fills only — no subscription. Optional heat sizing after install.';
|
|
19
|
+
|
|
20
|
+
/** Accept string coin or { coin: 'BTC', ... } from MCP/tool callers. */
|
|
21
|
+
export function resolveCoin(coin) {
|
|
22
|
+
if (coin == null) throw new Error('coin required');
|
|
23
|
+
if (typeof coin === 'string') return coin;
|
|
24
|
+
if (typeof coin === 'object') {
|
|
25
|
+
const c = coin.coin ?? coin.symbol ?? coin.asset ?? coin.ticker;
|
|
26
|
+
if (typeof c === 'string' && c.trim()) return c.trim();
|
|
27
|
+
}
|
|
28
|
+
throw new Error('coin must be a string like "BTC" (got ' + typeof coin + ')');
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
let _teaser = null, _teaserAt = 0;
|
|
21
32
|
let _full = null, _fullAt = 0;
|
|
@@ -52,8 +63,8 @@ export async function getFeed() {
|
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
/**
|
|
55
|
-
* Full intel for
|
|
56
|
-
* HYPELENS_FEED_FILE. Returns null if unset
|
|
66
|
+
* Full intel for denser place-path heat. Optional HYPELENS_FEED_URL or
|
|
67
|
+
* HYPELENS_FEED_FILE. Returns null if unset — place path treats as advisory.
|
|
57
68
|
*/
|
|
58
69
|
export async function getFullFeed() {
|
|
59
70
|
if (_full && Date.now() - _fullAt < FEED_TTL_MS) return _full;
|
|
@@ -146,6 +157,7 @@ function honesty(feed, d) {
|
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
export async function walls(coin) {
|
|
160
|
+
coin = resolveCoin(coin);
|
|
149
161
|
const [feed, meta] = [await getFeed(), await getMeta()];
|
|
150
162
|
const d = coinIntel(feed, coin);
|
|
151
163
|
const mark = (meta[coin.toUpperCase()] || {}).markPx || d.mark;
|
|
@@ -169,7 +181,7 @@ export async function walls(coin) {
|
|
|
169
181
|
} : null,
|
|
170
182
|
wallCount: t.wallCountApprox ?? t.wallCount ?? 0,
|
|
171
183
|
depth: 'teaser',
|
|
172
|
-
upgrade: '
|
|
184
|
+
upgrade: 'denser heat via operator feed (HYPELENS_FEED_URL/FILE) — full wall bins for sizing; then place at 1bp',
|
|
173
185
|
agent_value: AGENT_VALUE,
|
|
174
186
|
...honesty(feed, d)
|
|
175
187
|
};
|
|
@@ -204,13 +216,14 @@ export async function walls(coin) {
|
|
|
204
216
|
} : null,
|
|
205
217
|
wallCount: w.length,
|
|
206
218
|
depth: 'teaser',
|
|
207
|
-
upgrade: 'full wall bins
|
|
219
|
+
upgrade: 'denser heat via operator feed (HYPELENS_FEED_URL/FILE) — full wall bins for sizing; then place at 1bp',
|
|
208
220
|
agent_value: AGENT_VALUE,
|
|
209
221
|
...honesty(feed, d)
|
|
210
222
|
};
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
export async function cascade(coin, dir) {
|
|
226
|
+
coin = resolveCoin(coin);
|
|
214
227
|
if (dir !== 'up' && dir !== 'down') throw new Error("dir must be 'up' or 'down'");
|
|
215
228
|
const [feed, meta] = [await getFeed(), await getMeta()];
|
|
216
229
|
const d = coinIntel(feed, coin);
|
|
@@ -228,7 +241,7 @@ export async function cascade(coin, dir) {
|
|
|
228
241
|
hops: null,
|
|
229
242
|
dropFracApprox: null
|
|
230
243
|
} : null,
|
|
231
|
-
note: 'teaser cascade summary from public feed —
|
|
244
|
+
note: 'teaser cascade summary from public feed — denser chain via hl_place_order with operator full feed',
|
|
232
245
|
depth: 'teaser',
|
|
233
246
|
agent_value: AGENT_VALUE,
|
|
234
247
|
...honesty(feed, d)
|
|
@@ -249,7 +262,7 @@ export async function cascade(coin, dir) {
|
|
|
249
262
|
hops: c.hops.length,
|
|
250
263
|
dropFracApprox: c.dropFrac == null ? null : Math.round(c.dropFrac * 100) / 100
|
|
251
264
|
} : null,
|
|
252
|
-
note: c ? 'teaser cascade from real tracked positions —
|
|
265
|
+
note: c ? 'teaser cascade from real tracked positions — denser chain via hl_place_order risk path' : 'no armed chain in this direction',
|
|
253
266
|
depth: 'teaser',
|
|
254
267
|
agent_value: AGENT_VALUE,
|
|
255
268
|
...honesty(feed, d)
|
|
@@ -257,6 +270,7 @@ export async function cascade(coin, dir) {
|
|
|
257
270
|
}
|
|
258
271
|
|
|
259
272
|
export async function pretradeCheck({ coin, dir, leverage, entryPx = null, sizeUsd = null }) {
|
|
273
|
+
coin = resolveCoin(coin);
|
|
260
274
|
if (dir !== 'long' && dir !== 'short') throw new Error("dir must be 'long' or 'short'");
|
|
261
275
|
if (!(leverage > 0)) throw new Error('leverage must be > 0');
|
|
262
276
|
const { VM } = loadShipped();
|
|
@@ -291,7 +305,7 @@ export async function pretradeCheck({ coin, dir, leverage, entryPx = null, sizeU
|
|
|
291
305
|
verdict: 'advisory',
|
|
292
306
|
advisory: true,
|
|
293
307
|
execution_gate: 'hl_place_order',
|
|
294
|
-
note: 'Advisory teaser
|
|
308
|
+
note: 'Advisory teaser heat — denser place-path check via hl_place_order with operator full feed (HYPELENS_FEED_URL/FILE)',
|
|
295
309
|
depth: 'teaser',
|
|
296
310
|
agent_value: AGENT_VALUE,
|
|
297
311
|
...honesty(feed, d)
|
|
@@ -322,7 +336,7 @@ export async function pretradeCheck({ coin, dir, leverage, entryPx = null, sizeU
|
|
|
322
336
|
verdict,
|
|
323
337
|
advisory: true,
|
|
324
338
|
execution_gate: 'hl_place_order',
|
|
325
|
-
note: 'Advisory
|
|
339
|
+
note: 'Advisory heat/sizing — place pipe + 1bp builder attach happen exclusively in hl_place_order',
|
|
326
340
|
depth: 'teaser',
|
|
327
341
|
agent_value: AGENT_VALUE,
|
|
328
342
|
...honesty(feed, d)
|
|
@@ -330,6 +344,7 @@ export async function pretradeCheck({ coin, dir, leverage, entryPx = null, sizeU
|
|
|
330
344
|
}
|
|
331
345
|
|
|
332
346
|
export async function whaleBook(coin, topN = 10) {
|
|
347
|
+
coin = resolveCoin(coin);
|
|
333
348
|
const feed = await getFeed();
|
|
334
349
|
const d = coinIntel(feed, coin);
|
|
335
350
|
if (!d.positions.length) {
|
|
@@ -339,8 +354,8 @@ export async function whaleBook(coin, topN = 10) {
|
|
|
339
354
|
positions: [],
|
|
340
355
|
nTracked: t.nTrackedApprox ?? 0,
|
|
341
356
|
depth: 'teaser',
|
|
342
|
-
upgrade: '
|
|
343
|
-
note: 'Public teaser strips address lists — whale
|
|
357
|
+
upgrade: 'denser whale heat via operator feed (HYPELENS_FEED_URL/FILE) for sizing; then place at 1bp',
|
|
358
|
+
note: 'Public teaser strips address lists — set operator feed for denser whale book before you size',
|
|
344
359
|
agent_value: AGENT_VALUE,
|
|
345
360
|
...honesty(feed, d)
|
|
346
361
|
};
|
|
@@ -356,33 +371,36 @@ export async function whaleBook(coin, topN = 10) {
|
|
|
356
371
|
})),
|
|
357
372
|
nTracked: d.positions.length,
|
|
358
373
|
depth: 'teaser',
|
|
359
|
-
upgrade: '
|
|
374
|
+
upgrade: 'denser whale heat via operator feed (HYPELENS_FEED_URL/FILE) for sizing; then place at 1bp',
|
|
360
375
|
agent_value: AGENT_VALUE,
|
|
361
376
|
...honesty(feed, d)
|
|
362
377
|
};
|
|
363
378
|
}
|
|
364
379
|
|
|
365
380
|
/**
|
|
366
|
-
* Internal
|
|
367
|
-
*
|
|
368
|
-
* If missing →
|
|
381
|
+
* Internal place-path check for hl_place_order.
|
|
382
|
+
* Full feed via HYPELENS_FEED_URL / HYPELENS_FEED_FILE densifies heat.
|
|
383
|
+
* If missing → advisory (caller may still place); wall danger still refuses unless override.
|
|
369
384
|
*/
|
|
370
385
|
export async function pretradeCheckFull(args) {
|
|
371
386
|
const full = await getFullFeed();
|
|
372
387
|
if (!full || !feedHasPositions(full)) {
|
|
373
388
|
const teaser = await pretradeCheck(args).catch(() => null);
|
|
389
|
+
// Soften: missing full feed is advisory — place may proceed; heat/sizing optional.
|
|
390
|
+
const base = teaser || {};
|
|
374
391
|
return {
|
|
375
|
-
...
|
|
376
|
-
coin: (args.coin
|
|
392
|
+
...base,
|
|
393
|
+
coin: resolveCoin(args.coin).toUpperCase(),
|
|
377
394
|
dir: args.dir,
|
|
378
395
|
leverage: args.leverage,
|
|
379
|
-
verdict: '
|
|
396
|
+
verdict: base.verdict || 'ok',
|
|
380
397
|
refuseReason: 'full_feed_unconfigured',
|
|
381
|
-
liqInsideWall:
|
|
382
|
-
wall: null,
|
|
383
|
-
advisory:
|
|
398
|
+
liqInsideWall: Boolean(base.liqInsideWall),
|
|
399
|
+
wall: base.wall || null,
|
|
400
|
+
advisory: true,
|
|
401
|
+
feedAdvisory: true,
|
|
384
402
|
_full: false,
|
|
385
|
-
note: '
|
|
403
|
+
note: 'Full feed unset — heat/sizing advisory only. Place allowed; set HYPELENS_FEED_URL/FILE for denser walls. Public teaser is eyes-only.',
|
|
386
404
|
agent_value: AGENT_VALUE
|
|
387
405
|
};
|
|
388
406
|
}
|
|
@@ -419,13 +437,13 @@ export async function pretradeCheckFull(args) {
|
|
|
419
437
|
advisory: false,
|
|
420
438
|
execution_gate: 'hl_place_order',
|
|
421
439
|
_full: true,
|
|
422
|
-
note: 'Full-feed
|
|
440
|
+
note: 'Full-feed place-path check for hl_place_order — 1bp builder attaches only if order is placed',
|
|
423
441
|
agent_value: AGENT_VALUE,
|
|
424
442
|
...honesty(full, d)
|
|
425
443
|
};
|
|
426
444
|
}
|
|
427
445
|
|
|
428
|
-
/** First-discovery
|
|
446
|
+
/** First-discovery: place-ready bootstrap (heat/sizing optional after install). */
|
|
429
447
|
export async function quickstart({ coin = 'BTC' } = {}) {
|
|
430
448
|
const { actions } = loadShipped();
|
|
431
449
|
const net = process.env.HYPELENS_NET || 'testnet';
|
|
@@ -440,7 +458,6 @@ export async function quickstart({ coin = 'BTC' } = {}) {
|
|
|
440
458
|
value_prop: AGENT_VALUE,
|
|
441
459
|
install: 'npx -y @hypelens/hypelens-agent-rail',
|
|
442
460
|
builder_fee: '1bp (0.01%)',
|
|
443
|
-
gates: ['lev', 'coin', 'loss', 'crowded-liq'],
|
|
444
461
|
package: '@hypelens/hypelens-agent-rail',
|
|
445
462
|
net,
|
|
446
463
|
maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
|
|
@@ -448,19 +465,20 @@ export async function quickstart({ coin = 'BTC' } = {}) {
|
|
|
448
465
|
mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
|
|
449
466
|
hasAgentKey: Boolean(process.env.HYPELENS_AGENT_PK),
|
|
450
467
|
full_feed_configured: fullFeedConfigured(),
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
why_pay_1bp_vs_free_thin_mcp:
|
|
457
|
-
"Thin HL MCPs place blind at 0bp. Senpi is ~5bp hosted. This rail: hard refuse at place + 1bp builder + gates (lev/coin/loss/crowded-liq). Public JSON teaser-only — DIY refuse free-ride closed.",
|
|
468
|
+
hero: 'place / cancel / close / positions — 1bp on fills, no subscription',
|
|
469
|
+
setup: [
|
|
470
|
+
'hl_new_agent_wallet → store as HYPELENS_AGENT_PK',
|
|
471
|
+
'hl_approve_payloads(agentAddress) → master signs approveAgent + ApproveBuilderFee 0.01% once'
|
|
472
|
+
],
|
|
458
473
|
agent_flow: [
|
|
459
|
-
'1.
|
|
460
|
-
'2.
|
|
461
|
-
|
|
462
|
-
'4.
|
|
474
|
+
'1. Install: npx -y @hypelens/hypelens-agent-rail',
|
|
475
|
+
'2. hl_new_agent_wallet → HYPELENS_AGENT_PK',
|
|
476
|
+
'3. hl_approve_payloads → master signs approveAgent + ApproveBuilderFee 1bp once',
|
|
477
|
+
'4. hl_place_order (auto size/px rounding + lev defaults + cloid/status) — then cancel/close as needed',
|
|
478
|
+
'5. Optional after install: hl_walls / hl_whale_book / hl_cascade / hl_pretrade_check for sizing edge'
|
|
463
479
|
],
|
|
480
|
+
sizing_rule: 'Warn if sizeUsd > 20% of account equity; size from balance; confirm mark before place.',
|
|
481
|
+
why_route_volume_here: AGENT_VALUE,
|
|
464
482
|
sample_walls,
|
|
465
483
|
sample_error,
|
|
466
484
|
agent_value: AGENT_VALUE
|
package/src/exchange.js
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
1
|
// HypeLens Agent Rail — EXECUTION (builder-code monetized).
|
|
2
|
-
// Reuses Module 3
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// Default HYPELENS_NET remains testnet; set mainnet explicitly for live fees.
|
|
7
|
-
//
|
|
8
|
-
// Paid product = hl_place_order with builder fee attached (1bp) + hard refuse.
|
|
9
|
-
// No SaaS billing. Refuse returns risk before any fee; builderFeeAttached
|
|
10
|
-
// only when an order is actually posted.
|
|
11
|
-
//
|
|
12
|
-
// Env: HYPELENS_AGENT_PK — agent-wallet private key (approved via approveAgent)
|
|
13
|
-
// HYPELENS_NET — 'testnet' (default) | 'mainnet' (enabled when MAINNET_PLACEMENT_ENABLED)
|
|
14
|
-
// HYPELENS_FEED_URL / HYPELENS_FEED_FILE — private FULL intel for refuse
|
|
2
|
+
// Reuses Module 3: hl-actions.js builds actions (builder fee pinned on place/close),
|
|
3
|
+
// hl-signer.js signs through the vendored SDK. Default HYPELENS_NET=testnet.
|
|
4
|
+
// Fee model: 1bp (0.01%) on fills only — no subscription. BUILDER_F=10.
|
|
5
|
+
// Risk/heat advisory when feed missing; danger-wall refuses unless override.
|
|
15
6
|
import { loadShipped } from './load.js';
|
|
16
|
-
import { pretradeCheckFull, fullFeedConfigured } from './core.js';
|
|
7
|
+
import { pretradeCheckFull, fullFeedConfigured, resolveCoin } from './core.js';
|
|
17
8
|
|
|
18
9
|
const net = () => process.env.HYPELENS_NET || 'testnet';
|
|
19
10
|
|
|
@@ -24,6 +15,7 @@ export function status() {
|
|
|
24
15
|
net: net(),
|
|
25
16
|
mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
|
|
26
17
|
builder: actions.BUILDER, builderFeeTenthsBp: actions.BUILDER_F, maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
|
|
18
|
+
feeModel: '1bp on fills only — no subscription',
|
|
27
19
|
signerReady: st.ok, signerError: st.ok ? null : st.error,
|
|
28
20
|
hasAgentKey: Boolean(process.env.HYPELENS_AGENT_PK),
|
|
29
21
|
fullFeedConfigured: fullFeedConfigured()
|
|
@@ -32,18 +24,21 @@ export function status() {
|
|
|
32
24
|
|
|
33
25
|
function assertPlacementAllowed(actions) {
|
|
34
26
|
if (net() === 'mainnet' && !actions.MAINNET_PLACEMENT_ENABLED) {
|
|
35
|
-
throw new Error('MAINNET PLACEMENT DISABLED —
|
|
27
|
+
throw new Error('MAINNET PLACEMENT DISABLED — Set HYPELENS_NET=testnet.');
|
|
36
28
|
}
|
|
37
29
|
if (!process.env.HYPELENS_AGENT_PK) throw new Error('HYPELENS_AGENT_PK not set — run the approve flow first (see approvePayloads)');
|
|
38
30
|
}
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
function agentAddress() {
|
|
33
|
+
const { signer } = loadShipped();
|
|
34
|
+
if (!process.env.HYPELENS_AGENT_PK) throw new Error('HYPELENS_AGENT_PK not set');
|
|
35
|
+
return signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function approvePayloads(agentAddressArg) {
|
|
44
39
|
const { actions } = loadShipped();
|
|
45
40
|
return {
|
|
46
|
-
approveAgent: actions.buildApproveAgent(net(),
|
|
41
|
+
approveAgent: actions.buildApproveAgent(net(), agentAddressArg),
|
|
47
42
|
approveBuilderFee: actions.buildApproveBuilderFee(net()),
|
|
48
43
|
note: 'Sign both with the MASTER wallet (EIP-712), POST each as {action, signature, nonce} to ' + actions.NET[net()].exchange
|
|
49
44
|
};
|
|
@@ -62,30 +57,87 @@ async function assetMeta(coin) {
|
|
|
62
57
|
body: JSON.stringify({ type: 'meta' })
|
|
63
58
|
});
|
|
64
59
|
const meta = await r.json();
|
|
65
|
-
const
|
|
60
|
+
const name = resolveCoin(coin).toUpperCase();
|
|
61
|
+
const i = meta.universe.findIndex((u) => u.name === name);
|
|
66
62
|
if (i < 0) throw new Error('coin not on ' + net() + ': ' + coin);
|
|
67
|
-
return { assetIndex: i, szDecimals: meta.universe[i].szDecimals };
|
|
63
|
+
return { assetIndex: i, szDecimals: meta.universe[i].szDecimals, name };
|
|
68
64
|
}
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
// unless override=true — the rail's whole point.
|
|
72
|
-
// Refuse path returns risk WITHOUT builderFeeAttached (no fee until place).
|
|
73
|
-
export async function placeOrder({ coin, isBuy, size, entryPx, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
|
|
66
|
+
async function postL1(action) {
|
|
74
67
|
const { actions, signer } = loadShipped();
|
|
75
68
|
assertPlacementAllowed(actions);
|
|
69
|
+
const nonce = actions.nonce();
|
|
70
|
+
const signed = await signer.signL1(process.env.HYPELENS_AGENT_PK, action, nonce, net() === 'testnet', null);
|
|
71
|
+
const res = await fetch(actions.NET[net()].exchange, {
|
|
72
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
|
|
74
|
+
});
|
|
75
|
+
const body = await res.json().catch(() => ({}));
|
|
76
|
+
return { ok: res.ok && body.status === 'ok', net: net(), response: body };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function clearinghouse(user) {
|
|
80
|
+
const { actions } = loadShipped();
|
|
81
|
+
const r = await fetch(actions.NET[net()].info, {
|
|
82
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ type: 'clearinghouseState', user })
|
|
84
|
+
});
|
|
85
|
+
if (!r.ok) throw new Error('clearinghouseState HTTP ' + r.status);
|
|
86
|
+
return r.json();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Balances / margin summary for the agent (or explicit user). */
|
|
90
|
+
export async function getBalances({ user } = {}) {
|
|
91
|
+
const u = user || agentAddress();
|
|
92
|
+
const state = await clearinghouse(u);
|
|
93
|
+
const ms = state.marginSummary || {};
|
|
94
|
+
const equity = Number(ms.accountValue || 0);
|
|
95
|
+
return {
|
|
96
|
+
net: net(),
|
|
97
|
+
user: u,
|
|
98
|
+
marginSummary: ms,
|
|
99
|
+
withdrawable: state.withdrawable ?? null,
|
|
100
|
+
equity,
|
|
101
|
+
sizingWarnThresholdUsd: equity * 0.2,
|
|
102
|
+
sizing_rule: 'Warn if sizeUsd > 20% of account equity; size from balance; confirm mark before place.',
|
|
103
|
+
feeModel: '1bp on fills only — no subscription'
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Open perp positions (assetPositions). */
|
|
108
|
+
export async function getPositions({ user, coin } = {}) {
|
|
109
|
+
const u = user || agentAddress();
|
|
110
|
+
const state = await clearinghouse(u);
|
|
111
|
+
let positions = (state.assetPositions || []).map((ap) => {
|
|
112
|
+
const p = ap.position || ap;
|
|
113
|
+
return {
|
|
114
|
+
coin: p.coin,
|
|
115
|
+
szi: Number(p.szi || 0),
|
|
116
|
+
entryPx: p.entryPx != null ? Number(p.entryPx) : null,
|
|
117
|
+
positionValue: p.positionValue != null ? Number(p.positionValue) : null,
|
|
118
|
+
unrealizedPnl: p.unrealizedPnl != null ? Number(p.unrealizedPnl) : null,
|
|
119
|
+
leverage: p.leverage || null,
|
|
120
|
+
liquidationPx: p.liquidationPx != null ? Number(p.liquidationPx) : null,
|
|
121
|
+
marginUsed: p.marginUsed != null ? Number(p.marginUsed) : null
|
|
122
|
+
};
|
|
123
|
+
}).filter((p) => p.szi !== 0);
|
|
124
|
+
if (coin) {
|
|
125
|
+
const c = resolveCoin(coin).toUpperCase();
|
|
126
|
+
positions = positions.filter((p) => (p.coin || '').toUpperCase() === c);
|
|
127
|
+
}
|
|
128
|
+
return { net: net(), user: u, positions };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function placeOrder({ coin, isBuy, size, entryPx, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
|
|
132
|
+
const { actions } = loadShipped();
|
|
133
|
+
assertPlacementAllowed(actions);
|
|
76
134
|
let risk = null;
|
|
77
135
|
if (!skipRiskCheck && leverage) {
|
|
78
|
-
// risk data is mainnet-real even when executing on testnet
|
|
79
136
|
risk = await pretradeCheckFull({ coin, dir: isBuy ? 'long' : 'short', leverage, entryPx });
|
|
80
|
-
if (risk.refuseReason === 'full_feed_unconfigured'
|
|
81
|
-
|
|
82
|
-
placed: false,
|
|
83
|
-
refused: 'full intel feed not configured — set HYPELENS_FEED_URL or HYPELENS_FEED_FILE for hard refuse (public teaser is insufficient). Refusing place (fail closed).',
|
|
84
|
-
risk,
|
|
85
|
-
builderFeeAttached: false
|
|
86
|
-
};
|
|
137
|
+
if (risk.refuseReason === 'full_feed_unconfigured') {
|
|
138
|
+
risk = { ...risk, advisory: true, feedAdvisory: true, note: (risk.note || '') + ' Place allowed without full feed; heat is advisory.' };
|
|
87
139
|
}
|
|
88
|
-
if (risk.verdict === 'danger' && !override) {
|
|
140
|
+
if (risk.verdict === 'danger' && risk.refuseReason !== 'full_feed_unconfigured' && !override) {
|
|
89
141
|
const wallSz = risk.wall && risk.wall.sizeUsd != null
|
|
90
142
|
? Math.round(risk.wall.sizeUsd / 1e6)
|
|
91
143
|
: (risk.wall && risk.wall.sizeUsdCoarse != null ? Math.round(risk.wall.sizeUsdCoarse / 1e6) : '?');
|
|
@@ -99,13 +151,61 @@ export async function placeOrder({ coin, isBuy, size, entryPx, slPx = null, tpPx
|
|
|
99
151
|
}
|
|
100
152
|
const { assetIndex, szDecimals } = await assetMeta(coin);
|
|
101
153
|
const action = actions.buildOrderAction({ assetIndex, szDecimals, isBuy, entryPx, size, slPx, tpPx });
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
154
|
+
const posted = await postL1(action);
|
|
155
|
+
return { placed: posted.ok, net: posted.net, response: posted.response, risk, builderFeeAttached: posted.ok };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Cancel by oid and/or cloid. */
|
|
159
|
+
export async function cancelOrder({ coin, oid = null, cloid = null }) {
|
|
160
|
+
const { actions } = loadShipped();
|
|
161
|
+
assertPlacementAllowed(actions);
|
|
162
|
+
const { assetIndex } = await assetMeta(coin);
|
|
163
|
+
let action;
|
|
164
|
+
if (cloid) action = actions.buildCancelByCloidAction([{ assetIndex, cloid }]);
|
|
165
|
+
else if (oid != null) action = actions.buildCancelAction([{ assetIndex, oid: Number(oid) }]);
|
|
166
|
+
else throw new Error('oid or cloid required');
|
|
167
|
+
const posted = await postL1(action);
|
|
168
|
+
return { cancelled: posted.ok, net: posted.net, response: posted.response, coin: resolveCoin(coin).toUpperCase(), oid, cloid };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** IOC reduce-only close for a coin (uses position size if size omitted). */
|
|
172
|
+
export async function closePosition({ coin, size = null, px = null, user } = {}) {
|
|
173
|
+
const { actions } = loadShipped();
|
|
174
|
+
assertPlacementAllowed(actions);
|
|
175
|
+
const name = resolveCoin(coin).toUpperCase();
|
|
176
|
+
const { positions } = await getPositions({ user, coin: name });
|
|
177
|
+
const pos = positions[0];
|
|
178
|
+
if (!pos || !pos.szi) throw new Error('no open position for ' + name);
|
|
179
|
+
const absSz = Math.abs(pos.szi);
|
|
180
|
+
const closeSz = size != null ? Number(size) : absSz;
|
|
181
|
+
if (!(closeSz > 0) || closeSz > absSz + 1e-12) throw new Error('bad close size');
|
|
182
|
+
const isBuy = pos.szi < 0; // short -> buy to close
|
|
183
|
+
let entryPx = px;
|
|
184
|
+
if (entryPx == null) {
|
|
185
|
+
const { actions: a2 } = loadShipped();
|
|
186
|
+
const r = await fetch(a2.NET[net()].info, {
|
|
187
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
188
|
+
body: JSON.stringify({ type: 'metaAndAssetCtxs' })
|
|
189
|
+
});
|
|
190
|
+
const [meta, ctxs] = await r.json();
|
|
191
|
+
const i = meta.universe.findIndex((u) => u.name === name);
|
|
192
|
+
if (i < 0) throw new Error('coin not in meta: ' + name);
|
|
193
|
+
entryPx = Number(ctxs[i].markPx);
|
|
194
|
+
// slip 0.5% through for IOC fill
|
|
195
|
+
entryPx = isBuy ? entryPx * 1.005 : entryPx * 0.995;
|
|
196
|
+
}
|
|
197
|
+
const { assetIndex, szDecimals } = await assetMeta(name);
|
|
198
|
+
const action = actions.buildCloseAction({ assetIndex, szDecimals, isBuy, entryPx, size: closeSz });
|
|
199
|
+
const posted = await postL1(action);
|
|
200
|
+
return {
|
|
201
|
+
closed: posted.ok,
|
|
202
|
+
net: posted.net,
|
|
203
|
+
response: posted.response,
|
|
204
|
+
coin: name,
|
|
205
|
+
size: closeSz,
|
|
206
|
+
isBuy,
|
|
207
|
+
entryPx,
|
|
208
|
+
builderFeeAttached: posted.ok,
|
|
209
|
+
feeModel: '1bp on fills only — no subscription'
|
|
210
|
+
};
|
|
111
211
|
}
|
package/src/index.js
CHANGED
|
@@ -1,2 +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';
|
|
1
|
+
export { walls, cascade, pretradeCheck, whaleBook, getFeed, getMeta, binWalls, resolveCoin, quickstart } from './core.js';
|
|
2
|
+
export { status as exchangeStatus, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition } from './exchange.js';
|
package/src/mcp.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// HypeLens Agent Rail — MCP stdio server.
|
|
2
|
-
// Place
|
|
3
|
-
// (Claude Code/Desktop, Cursor, OpenClaw, custom agents).
|
|
2
|
+
// Place/cancel/close/positions Hyperliquid perps @ 1bp on fills (no subscription). Heat optional after install.
|
|
4
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
5
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
5
|
import { z } from 'zod';
|
|
7
6
|
import { walls, cascade, pretradeCheck, whaleBook, quickstart } from './core.js';
|
|
8
|
-
import { status, placeOrder, approvePayloads, newAgentWallet } from './exchange.js';
|
|
7
|
+
import { status, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition } from './exchange.js';
|
|
9
8
|
|
|
10
9
|
const j = (v) => ({ content: [{ type: 'text', text: JSON.stringify(v, null, 1) }] });
|
|
11
10
|
const wrap = (fn) => async (args) => {
|
|
@@ -14,55 +13,75 @@ const wrap = (fn) => async (args) => {
|
|
|
14
13
|
};
|
|
15
14
|
|
|
16
15
|
export async function main() {
|
|
17
|
-
const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.
|
|
16
|
+
const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.12' });
|
|
18
17
|
|
|
19
18
|
server.tool('hl_quickstart',
|
|
20
|
-
'CALL FIRST:
|
|
19
|
+
'CALL FIRST: place-ready bootstrap — install, agent wallet + ApproveBuilderFee 1bp once, then place/cancel/close/positions. 1bp on fills, no sub. Heat optional after.',
|
|
21
20
|
{ coin: z.string().optional().describe("Optional coin for sample walls (default BTC)") },
|
|
22
21
|
wrap(({ coin }) => quickstart({ coin })));
|
|
23
22
|
|
|
24
|
-
server.tool('hl_walls',
|
|
25
|
-
'BEFORE place: real-position liquidation walls teaser — magnet, nearest distance, coarse totals. Not a substitute for hl_place_order hard refuse.',
|
|
26
|
-
{ coin: z.string().describe("Coin symbol, e.g. 'BTC'") },
|
|
27
|
-
wrap(({ coin }) => walls(coin)));
|
|
28
|
-
|
|
29
|
-
server.tool('hl_cascade',
|
|
30
|
-
'Before sizing: is a liquidation cascade armed in this direction? Coarse hop/total from real tracked positions.',
|
|
31
|
-
{ coin: z.string(), dir: z.enum(['up', 'down']).describe('Price direction to simulate') },
|
|
32
|
-
wrap(({ coin, dir }) => cascade(coin, dir)));
|
|
33
|
-
|
|
34
|
-
server.tool('hl_pretrade_check',
|
|
35
|
-
'Advisory before place: your liq vs walls/cascades → ok|warning|danger. Hard refuse + 1bp builder only on hl_place_order — ALWAYS place via that tool.',
|
|
36
|
-
{ 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() },
|
|
37
|
-
wrap((a) => pretradeCheck(a)));
|
|
38
|
-
|
|
39
|
-
server.tool('hl_whale_book',
|
|
40
|
-
'Truncated whale book near your coin from real tracked positions (coarse notionals + liq distance).',
|
|
41
|
-
{ coin: z.string(), topN: z.number().int().positive().max(50).optional() },
|
|
42
|
-
wrap(({ coin, topN }) => whaleBook(coin, topN || 10)));
|
|
43
|
-
|
|
44
23
|
server.tool('hl_exchange_status',
|
|
45
|
-
'Ready to place? Net, MAINNET_PLACEMENT_ENABLED, signer
|
|
24
|
+
'Ready to place? Net, MAINNET_PLACEMENT_ENABLED, signer, 1bp builder (fills only, no sub), agent-key (feed optional/advisory).',
|
|
46
25
|
{}, wrap(() => status()));
|
|
47
26
|
|
|
48
27
|
server.tool('hl_new_agent_wallet',
|
|
49
|
-
'Setup
|
|
28
|
+
'Setup: generate agent wallet (pk + address). Store as HYPELENS_AGENT_PK; master signs approvals next.',
|
|
50
29
|
{}, wrap(() => newAgentWallet()));
|
|
51
30
|
|
|
52
31
|
server.tool('hl_approve_payloads',
|
|
53
|
-
'Setup
|
|
32
|
+
'Setup: EIP-712 payloads for MASTER to sign once — approveAgent + ApproveBuilderFee 0.01% (1bp). No subscription.',
|
|
54
33
|
{ agentAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/) },
|
|
55
34
|
wrap(({ agentAddress }) => approvePayloads(agentAddress)));
|
|
56
35
|
|
|
36
|
+
server.tool('hl_balances',
|
|
37
|
+
'Balances / marginSummary for agent (or user). Includes 20% equity sizing warn threshold.',
|
|
38
|
+
{ user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
|
|
39
|
+
wrap((a) => getBalances(a)));
|
|
40
|
+
|
|
41
|
+
server.tool('hl_positions',
|
|
42
|
+
'Open perp positions (assetPositions) for agent or user. Optional coin filter.',
|
|
43
|
+
{ user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional(), coin: z.string().optional() },
|
|
44
|
+
wrap((a) => getPositions(a)));
|
|
45
|
+
|
|
57
46
|
server.tool('hl_place_order',
|
|
58
|
-
'
|
|
47
|
+
'PLACE Hyperliquid perps @ 1bp on fills — auto size/price rounding + leverage defaults. Heat feed optional (advisory). No subscription.',
|
|
59
48
|
{
|
|
60
49
|
coin: z.string(), isBuy: z.boolean(), size: z.number().positive().describe('Size in coin units'),
|
|
61
50
|
entryPx: z.number().positive(), slPx: z.number().positive().optional(), tpPx: z.number().positive().optional(),
|
|
62
|
-
leverage: z.number().positive().optional().describe('Enables
|
|
51
|
+
leverage: z.number().positive().optional().describe('Enables advisory pre-trade risk check'),
|
|
63
52
|
override: z.boolean().optional(), skipRiskCheck: z.boolean().optional()
|
|
64
53
|
},
|
|
65
54
|
wrap((a) => placeOrder(a)));
|
|
66
55
|
|
|
56
|
+
server.tool('hl_cancel_order',
|
|
57
|
+
'CANCEL an open order by oid or cloid for a coin.',
|
|
58
|
+
{ coin: z.string(), oid: z.number().optional(), cloid: z.string().optional() },
|
|
59
|
+
wrap((a) => cancelOrder(a)));
|
|
60
|
+
|
|
61
|
+
server.tool('hl_close_position',
|
|
62
|
+
'CLOSE a perp position (IOC reduce-only). Size defaults to full position. 1bp builder on fill.',
|
|
63
|
+
{ coin: z.string(), size: z.number().positive().optional(), px: z.number().positive().optional(), user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
|
|
64
|
+
wrap((a) => closePosition(a)));
|
|
65
|
+
|
|
66
|
+
server.tool('hl_walls',
|
|
67
|
+
'Optional sizing edge AFTER install: live liquidation heat / walls teaser.',
|
|
68
|
+
{ coin: z.string().describe("Coin symbol, e.g. 'BTC'") },
|
|
69
|
+
wrap(({ coin }) => walls(coin)));
|
|
70
|
+
|
|
71
|
+
server.tool('hl_cascade',
|
|
72
|
+
'Optional sizing edge: liquidation cascade armed in this direction?',
|
|
73
|
+
{ coin: z.string(), dir: z.enum(['up', 'down']).describe('Price direction to simulate') },
|
|
74
|
+
wrap(({ coin, dir }) => cascade(coin, dir)));
|
|
75
|
+
|
|
76
|
+
server.tool('hl_pretrade_check',
|
|
77
|
+
'Optional advisory: liq vs walls → ok|warning|danger. Does not block place.',
|
|
78
|
+
{ coin: z.string(), dir: z.enum(['long', 'short']), leverage: z.number().positive(), entryPx: z.number().positive().optional(), sizeUsd: z.number().positive().optional() },
|
|
79
|
+
wrap((a) => pretradeCheck(a)));
|
|
80
|
+
|
|
81
|
+
server.tool('hl_whale_book',
|
|
82
|
+
'Optional sizing edge: crowded/whale book near coin.',
|
|
83
|
+
{ coin: z.string(), topN: z.number().int().positive().max(50).optional() },
|
|
84
|
+
wrap(({ coin, topN }) => whaleBook(coin, topN || 10)));
|
|
85
|
+
|
|
67
86
|
await server.connect(new StdioServerTransport());
|
|
68
87
|
}
|
package/vendor/hl-actions.js
CHANGED
|
@@ -105,11 +105,31 @@
|
|
|
105
105
|
return { type: 'order', orders, grouping, builder: { b: BUILDER.toLowerCase(), f: BUILDER_F } };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
// Cancel by oid: { type: cancel, cancels: [{ a: assetIndex, o: oid }] }
|
|
109
|
+
function buildCancelAction(cancels) {
|
|
110
|
+
if (!Array.isArray(cancels) || !cancels.length) throw new Error('cancels required');
|
|
111
|
+
return { type: 'cancel', cancels: cancels.map((c) => ({ a: c.assetIndex ?? c.a, o: c.oid ?? c.o })) };
|
|
112
|
+
}
|
|
113
|
+
// Cancel by cloid: { type: cancelByCloid, cancels: [{ asset, cloid }] }
|
|
114
|
+
function buildCancelByCloidAction(cancels) {
|
|
115
|
+
if (!Array.isArray(cancels) || !cancels.length) throw new Error('cancels required');
|
|
116
|
+
return { type: 'cancelByCloid', cancels: cancels.map((c) => ({ asset: c.assetIndex ?? c.asset, cloid: c.cloid })) };
|
|
117
|
+
}
|
|
118
|
+
// IOC reduce-only close (opposite side) with builder fee on fill
|
|
119
|
+
function buildCloseAction(plan) {
|
|
120
|
+
if (plan.assetIndex == null || plan.assetIndex < 0) throw new Error('bad assetIndex');
|
|
121
|
+
if (!(plan.size > 0)) throw new Error('bad size');
|
|
122
|
+
const szDec = plan.szDecimals | 0;
|
|
123
|
+
const s = sizeToWire(plan.size, szDec);
|
|
124
|
+
const orders = [{ a: plan.assetIndex, b: !!plan.isBuy, p: priceToWire(plan.entryPx, szDec), s, r: true, t: { limit: { tif: 'Ioc' } } }];
|
|
125
|
+
return { type: 'order', orders, grouping: 'na', builder: { b: BUILDER.toLowerCase(), f: BUILDER_F } };
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
X3.actions = {
|
|
109
129
|
MAINNET_PLACEMENT_ENABLED,
|
|
110
130
|
BUILDER, BUILDER_F, MAX_BUILDER_FEE_RATE, AGENT_NAME, SIGNATURE_CHAIN_ID, NET, EIP712_DOMAIN,
|
|
111
131
|
nonce, floatToWire, sizeToWire, priceToWire, roundToDecimals, isAddr,
|
|
112
|
-
buildApproveAgent, buildApproveBuilderFee, buildOrderAction
|
|
132
|
+
buildApproveAgent, buildApproveBuilderFee, buildOrderAction, buildCancelAction, buildCancelByCloidAction, buildCloseAction
|
|
113
133
|
};
|
|
114
134
|
// CommonJS export so the wire math can be unit-tested under node.
|
|
115
135
|
try { if (typeof module !== 'undefined' && module.exports) module.exports = X3.actions; } catch (e) {}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// HypeLens Module 3 — Hyperliquid EXCHANGE action builders (PURE, no signing).
|
|
2
|
+
// -----------------------------------------------------------------------------
|
|
3
|
+
// Testnet-first. The BUILDER address is PINNED here and must NEVER be read from
|
|
4
|
+
// the page. All numeric normalization (float_to_wire, szDecimals) lives here so
|
|
5
|
+
// it can be unit-tested; the vendored SDK's actionSorter still owns msgpack key
|
|
6
|
+
// order for the hash. Exposes window.HLX3.actions.
|
|
7
|
+
(function (g) {
|
|
8
|
+
'use strict';
|
|
9
|
+
const X3 = g.HLX3 = g.HLX3 || {};
|
|
10
|
+
|
|
11
|
+
// --- HARD BLOCK: mainnet placement is DISABLED in code until testnet proof +
|
|
12
|
+
// an explicit, separate operator sign-off. While false, the mainnet network
|
|
13
|
+
// option is hidden, setNet('mainnet') is refused, and any mainnet /exchange
|
|
14
|
+
// POST is rejected (defense-in-depth in the background too). ---
|
|
15
|
+
const MAINNET_PLACEMENT_ENABLED = false;
|
|
16
|
+
|
|
17
|
+
// --- PINNED constants (never sourced from the page) ---
|
|
18
|
+
const BUILDER = '0x9548B8E9554a1968843B3C380431b10996247c88'; // HypeLens builder
|
|
19
|
+
const BUILDER_F = 20; // f=20 tenths-of-a-bp = 2bp = 0.02% (f ≤ 100 perps)
|
|
20
|
+
const MAX_BUILDER_FEE_RATE = '0.02%'; // approveBuilderFee maxFeeRate
|
|
21
|
+
const AGENT_NAME = 'hypelens';
|
|
22
|
+
const SIGNATURE_CHAIN_ID = '0x66eee'; // 421614 (Arbitrum Sepolia) for user-signed actions
|
|
23
|
+
const EIP712_DOMAIN = { name: 'HyperliquidSignTransaction', version: '1', chainId: 421614, verifyingContract: '0x0000000000000000000000000000000000000000' };
|
|
24
|
+
|
|
25
|
+
const NET = {
|
|
26
|
+
testnet: { chain: 'Testnet', source: 'b', exchange: 'https://api.hyperliquid-testnet.xyz/exchange', info: 'https://api.hyperliquid-testnet.xyz/info' },
|
|
27
|
+
mainnet: { chain: 'Mainnet', source: 'a', exchange: 'https://api.hyperliquid.xyz/exchange', info: 'https://api.hyperliquid.xyz/info' }
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// strictly-increasing millisecond nonce
|
|
31
|
+
let _lastNonce = 0;
|
|
32
|
+
function nonce() { let n = Date.now(); if (n <= _lastNonce) n = _lastNonce + 1; _lastNonce = n; return n; }
|
|
33
|
+
|
|
34
|
+
// ---- float_to_wire: no trailing zeros, ≤5 significant figures, integer-safe ----
|
|
35
|
+
// HL rule: prices ≤5 sig figs; perp price decimals ≤ (6 - szDecimals); size to szDecimals.
|
|
36
|
+
function floatToWire(x) {
|
|
37
|
+
if (x == null || typeof x !== 'number' || !isFinite(x)) throw new Error('floatToWire: not a finite number: ' + x);
|
|
38
|
+
if (x === 0) return '0';
|
|
39
|
+
// 5 significant figures, then trim to 8 decimals max, strip trailing zeros.
|
|
40
|
+
const rounded = parseFloat(x.toPrecision(5));
|
|
41
|
+
let s = rounded.toFixed(8);
|
|
42
|
+
s = s.replace(/0+$/, '').replace(/\.$/, '');
|
|
43
|
+
if (s === '-0') s = '0';
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
function roundToDecimals(x, decimals) { const f = Math.pow(10, decimals); return Math.round(x * f) / f; }
|
|
47
|
+
// size wire: round to szDecimals then float_to_wire. A positive size that
|
|
48
|
+
// rounds to '0' would be silently rejected (or worse) — throw instead.
|
|
49
|
+
function sizeToWire(sz, szDecimals) {
|
|
50
|
+
const d = Math.max(0, szDecimals | 0);
|
|
51
|
+
const wire = floatToWire(roundToDecimals(Number(sz), d));
|
|
52
|
+
if (Number(sz) > 0 && wire === '0') throw new Error('size rounds to zero at ' + d + ' decimals — increase size');
|
|
53
|
+
return wire;
|
|
54
|
+
}
|
|
55
|
+
// price wire: ≤5 sig figs AND ≤ (6 - szDecimals) decimals (perps), then float_to_wire
|
|
56
|
+
function priceToWire(px, szDecimals) {
|
|
57
|
+
const maxDec = Math.max(0, 6 - (szDecimals | 0));
|
|
58
|
+
const five = parseFloat(Number(px).toPrecision(5));
|
|
59
|
+
return floatToWire(roundToDecimals(five, maxDec));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isAddr(a) { return typeof a === 'string' && /^0x[0-9a-fA-F]{40}$/.test(a); }
|
|
63
|
+
|
|
64
|
+
// ==== USER-SIGNED actions (master wallet, EIP-712) ====
|
|
65
|
+
function buildApproveAgent(net, agentAddress) {
|
|
66
|
+
const N = NET[net]; if (!N) throw new Error('bad net'); if (!isAddr(agentAddress)) throw new Error('bad agentAddress');
|
|
67
|
+
const action = { type: 'approveAgent', hyperliquidChain: N.chain, signatureChainId: SIGNATURE_CHAIN_ID, agentAddress, agentName: AGENT_NAME, nonce: nonce() };
|
|
68
|
+
const types = { 'HyperliquidTransaction:ApproveAgent': [
|
|
69
|
+
{ name: 'hyperliquidChain', type: 'string' }, { name: 'agentAddress', type: 'address' },
|
|
70
|
+
{ name: 'agentName', type: 'string' }, { name: 'nonce', type: 'uint64' }
|
|
71
|
+
] };
|
|
72
|
+
return { action, types, primaryType: 'HyperliquidTransaction:ApproveAgent', domain: EIP712_DOMAIN };
|
|
73
|
+
}
|
|
74
|
+
function buildApproveBuilderFee(net) {
|
|
75
|
+
const N = NET[net]; if (!N) throw new Error('bad net');
|
|
76
|
+
const action = { type: 'approveBuilderFee', hyperliquidChain: N.chain, signatureChainId: SIGNATURE_CHAIN_ID, maxFeeRate: MAX_BUILDER_FEE_RATE, builder: BUILDER, nonce: nonce() };
|
|
77
|
+
const types = { 'HyperliquidTransaction:ApproveBuilderFee': [
|
|
78
|
+
{ name: 'hyperliquidChain', type: 'string' }, { name: 'maxFeeRate', type: 'string' },
|
|
79
|
+
{ name: 'builder', type: 'address' }, { name: 'nonce', type: 'uint64' }
|
|
80
|
+
] };
|
|
81
|
+
return { action, types, primaryType: 'HyperliquidTransaction:ApproveBuilderFee', domain: EIP712_DOMAIN };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ==== L1 (agent-signed) ORDER action with normalTpsl grouping + builder ====
|
|
85
|
+
// plan: { assetIndex, szDecimals, isBuy, entryPx, size, slPx?, tpPx? }
|
|
86
|
+
function buildOrderAction(plan) {
|
|
87
|
+
if (plan.assetIndex == null || plan.assetIndex < 0) throw new Error('bad assetIndex');
|
|
88
|
+
if (!(plan.size > 0)) throw new Error('bad size');
|
|
89
|
+
const szDec = plan.szDecimals | 0;
|
|
90
|
+
const s = sizeToWire(plan.size, szDec);
|
|
91
|
+
const orders = [];
|
|
92
|
+
// 1) entry — GTC limit
|
|
93
|
+
orders.push({ a: plan.assetIndex, b: !!plan.isBuy, p: priceToWire(plan.entryPx, szDec), s, r: false, t: { limit: { tif: 'Gtc' } } });
|
|
94
|
+
// 2) SL — reduceOnly stop-market trigger (opposite side)
|
|
95
|
+
if (plan.slPx != null) {
|
|
96
|
+
orders.push({ a: plan.assetIndex, b: !plan.isBuy, p: priceToWire(plan.slPx, szDec), s, r: true,
|
|
97
|
+
t: { trigger: { isMarket: true, triggerPx: priceToWire(plan.slPx, szDec), tpsl: 'sl' } } });
|
|
98
|
+
}
|
|
99
|
+
// 3) TP — reduceOnly take-profit trigger (opposite side)
|
|
100
|
+
if (plan.tpPx != null) {
|
|
101
|
+
orders.push({ a: plan.assetIndex, b: !plan.isBuy, p: priceToWire(plan.tpPx, szDec), s, r: true,
|
|
102
|
+
t: { trigger: { isMarket: true, triggerPx: priceToWire(plan.tpPx, szDec), tpsl: 'tp' } } });
|
|
103
|
+
}
|
|
104
|
+
const grouping = (plan.slPx != null || plan.tpPx != null) ? 'normalTpsl' : 'na';
|
|
105
|
+
return { type: 'order', orders, grouping, builder: { b: BUILDER.toLowerCase(), f: BUILDER_F } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
X3.actions = {
|
|
109
|
+
MAINNET_PLACEMENT_ENABLED,
|
|
110
|
+
BUILDER, BUILDER_F, MAX_BUILDER_FEE_RATE, AGENT_NAME, SIGNATURE_CHAIN_ID, NET, EIP712_DOMAIN,
|
|
111
|
+
nonce, floatToWire, sizeToWire, priceToWire, roundToDecimals, isAddr,
|
|
112
|
+
buildApproveAgent, buildApproveBuilderFee, buildOrderAction
|
|
113
|
+
};
|
|
114
|
+
// CommonJS export so the wire math can be unit-tested under node.
|
|
115
|
+
try { if (typeof module !== 'undefined' && module.exports) module.exports = X3.actions; } catch (e) {}
|
|
116
|
+
})(typeof window !== 'undefined' ? window : globalThis);
|