@hypelens/hypelens-agent-rail 0.1.14 → 0.1.15
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 +4 -4
- package/package.json +1 -1
- package/skill/hypelens-agent-rail/SKILL.md +3 -3
- package/src/core.js +3 -3
- package/src/exchange.js +112 -11
- package/src/mcp.js +11 -7
package/README.md
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
```bash
|
|
5
5
|
npx -y @hypelens/hypelens-agent-rail
|
|
6
6
|
```
|
|
7
|
-
1. `hl_new_agent_wallet` → `HYPELENS_AGENT_PK`
|
|
8
|
-
2. `hl_approve_payloads` —
|
|
7
|
+
1. `hl_quickstart` (optional) → `hl_new_agent_wallet` → `HYPELENS_AGENT_PK`
|
|
8
|
+
2. `hl_approve_payloads(agentAddress)` — **MASTER** signs approveAgent + ApproveBuilderFee **0.01%** once, then POST both
|
|
9
9
|
3. `hl_place_order` / `hl_cancel_order` / `hl_close_position` / `hl_positions` / `hl_balances`
|
|
10
10
|
|
|
11
|
-
**First place (testnet):** after approve,
|
|
11
|
+
**First place (testnet default):** after approve, `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})` — **sizeUsd alone** auto-fetches mark and rounds size; `entryPx` optional. No feed required.
|
|
12
12
|
|
|
13
|
-
Default **testnet**. `HYPELENS_NET=mainnet` for live. Fee: **1bp on fills** — no sub.
|
|
13
|
+
Default **testnet**. `HYPELENS_NET=mainnet` for live. Fee: **1bp on fills** — no sub. Missing `HYPELENS_AGENT_PK` errors point at the same path.
|
|
14
14
|
|
|
15
15
|
## Optional after install: sizing edge
|
|
16
16
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypelens/hypelens-agent-rail",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "Place, cancel, and close Hyperliquid perps from OpenClaw/MCP. Agent wallet once, auto-round, 1bp on fills (no sub). First place: hl_place_order BTC sizeUsd 12. npx @hypelens/hypelens-agent-rail",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -37,12 +37,12 @@ npx -y @hypelens/hypelens-agent-rail
|
|
|
37
37
|
|
|
38
38
|
1. Call `hl_quickstart` first
|
|
39
39
|
2. `hl_new_agent_wallet` → store as `HYPELENS_AGENT_PK`
|
|
40
|
-
3. `hl_approve_payloads(agentAddress)` →
|
|
40
|
+
3. `hl_approve_payloads(agentAddress)` → **MASTER** signs approveAgent + ApproveBuilderFee **1bp** once, then POST both
|
|
41
41
|
4. `hl_place_order` / `hl_cancel_order` / `hl_close_position` / `hl_positions` / `hl_balances`
|
|
42
42
|
|
|
43
|
+
**First place (testnet default):** after approve, `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})` — **sizeUsd alone** auto-fetches mark and rounds size (`entryPx` optional). Feed optional.
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
Default net is **testnet**. Set `HYPELENS_NET=mainnet` for live. Missing intel feed is **advisory** — does **not** block place.
|
|
45
|
+
Default net is **testnet**. Set `HYPELENS_NET=mainnet` for live. Missing intel feed is **advisory** — does **not** block place. Missing PK errors name the same wallet → approve → sizeUsd path.
|
|
46
46
|
|
|
47
47
|
## Sizing rule
|
|
48
48
|
|
package/src/core.js
CHANGED
|
@@ -460,7 +460,7 @@ export async function quickstart({ coin = 'BTC' } = {}) {
|
|
|
460
460
|
],
|
|
461
461
|
fee: '1bp (0.01%) on fills only',
|
|
462
462
|
package: '@hypelens/hypelens-agent-rail',
|
|
463
|
-
first_place_example: { coin: 'BTC', isBuy: true, sizeUsd: 12, leverage: 2, note: 'testnet default;
|
|
463
|
+
first_place_example: { coin: 'BTC', isBuy: true, sizeUsd: 12, leverage: 2, note: 'testnet default; sizeUsd alone → auto mark + szDecimals round; entryPx optional; feed optional' }
|
|
464
464
|
};
|
|
465
465
|
let sample_walls = null;
|
|
466
466
|
let sample_error = null;
|
|
@@ -490,12 +490,12 @@ export async function quickstart({ coin = 'BTC' } = {}) {
|
|
|
490
490
|
'1. Install: npx -y @hypelens/hypelens-agent-rail',
|
|
491
491
|
'2. hl_new_agent_wallet → HYPELENS_AGENT_PK',
|
|
492
492
|
'3. hl_approve_payloads → master signs approveAgent + ApproveBuilderFee 1bp once',
|
|
493
|
-
'4. hl_place_order
|
|
493
|
+
'4. hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) — auto mark + size round; then cancel/close as needed',
|
|
494
494
|
'5. Optional after install: hl_walls / hl_whale_book / hl_cascade / hl_pretrade_check for sizing edge'
|
|
495
495
|
],
|
|
496
496
|
sizing_rule: 'Warn if sizeUsd > 20% of account equity; size from balance; confirm mark before place.',
|
|
497
497
|
first_place_example: bootstrap.first_place_example,
|
|
498
|
-
next_if_blocked: '
|
|
498
|
+
next_if_blocked: 'Missing PK: hl_new_agent_wallet → HYPELENS_AGENT_PK → hl_approve_payloads (MASTER signs) → hl_place_order(first_place_example). Wrong net: default is testnet; set HYPELENS_NET=mainnet only for live.',
|
|
499
499
|
why_route_volume_here: AGENT_VALUE,
|
|
500
500
|
sample_walls,
|
|
501
501
|
sample_error,
|
package/src/exchange.js
CHANGED
|
@@ -22,32 +22,46 @@ export function status() {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
const MISSING_PK = 'HYPELENS_AGENT_PK not set — hl_new_agent_wallet → export HYPELENS_AGENT_PK=<pk>, hl_approve_payloads (MASTER signs approveAgent + ApproveBuilderFee 1bp once), then hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) on testnet (default). Set HYPELENS_NET=mainnet only for live.';
|
|
26
|
+
|
|
25
27
|
function assertPlacementAllowed(actions) {
|
|
26
28
|
if (net() === 'mainnet' && !actions.MAINNET_PLACEMENT_ENABLED) {
|
|
27
29
|
throw new Error('MAINNET PLACEMENT DISABLED — Set HYPELENS_NET=testnet.');
|
|
28
30
|
}
|
|
29
|
-
if (!process.env.HYPELENS_AGENT_PK) throw new Error(
|
|
31
|
+
if (!process.env.HYPELENS_AGENT_PK) throw new Error(MISSING_PK);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
function agentAddress() {
|
|
33
35
|
const { signer } = loadShipped();
|
|
34
|
-
if (!process.env.HYPELENS_AGENT_PK) throw new Error(
|
|
36
|
+
if (!process.env.HYPELENS_AGENT_PK) throw new Error(MISSING_PK);
|
|
35
37
|
return signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK);
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export function approvePayloads(agentAddressArg) {
|
|
39
41
|
const { actions } = loadShipped();
|
|
42
|
+
const exchange = actions.NET[net()].exchange;
|
|
40
43
|
return {
|
|
44
|
+
net: net(),
|
|
41
45
|
approveAgent: actions.buildApproveAgent(net(), agentAddressArg),
|
|
42
46
|
approveBuilderFee: actions.buildApproveBuilderFee(net()),
|
|
43
|
-
|
|
47
|
+
fee: '1bp (0.01%) on fills only — no subscription',
|
|
48
|
+
steps: [
|
|
49
|
+
'Sign approveAgent + ApproveBuilderFee with the MASTER wallet (EIP-712) — never the agent pk',
|
|
50
|
+
'POST each {action, signature, nonce} to ' + exchange,
|
|
51
|
+
'Then hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) on testnet (size/px auto-round; no entryPx needed)'
|
|
52
|
+
],
|
|
53
|
+
note: 'MASTER signs both once → POST to ' + exchange + ' → first place with sizeUsd (auto mark + szDecimals round). Default net=testnet; HYPELENS_NET=mainnet for live.'
|
|
44
54
|
};
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
export function newAgentWallet() {
|
|
48
58
|
const { signer, sdk } = loadShipped();
|
|
49
59
|
const pk = sdk.randomPrivateKey();
|
|
50
|
-
return {
|
|
60
|
+
return {
|
|
61
|
+
privateKey: pk,
|
|
62
|
+
address: signer.addressFromPrivateKey(pk),
|
|
63
|
+
note: 'export HYPELENS_AGENT_PK=<privateKey>; then hl_approve_payloads(address) for MASTER to sign 1bp once; then hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})'
|
|
64
|
+
};
|
|
51
65
|
}
|
|
52
66
|
|
|
53
67
|
async function assetMeta(coin) {
|
|
@@ -63,6 +77,61 @@ async function assetMeta(coin) {
|
|
|
63
77
|
return { assetIndex: i, szDecimals: meta.universe[i].szDecimals, name };
|
|
64
78
|
}
|
|
65
79
|
|
|
80
|
+
/** Mark + meta for sizeUsd auto-round (one round-trip). */
|
|
81
|
+
async function assetMetaAndMark(coin) {
|
|
82
|
+
const { actions } = loadShipped();
|
|
83
|
+
const r = await fetch(actions.NET[net()].info, {
|
|
84
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
85
|
+
body: JSON.stringify({ type: 'metaAndAssetCtxs' })
|
|
86
|
+
});
|
|
87
|
+
if (!r.ok) throw new Error('metaAndAssetCtxs HTTP ' + r.status);
|
|
88
|
+
const [meta, ctxs] = await r.json();
|
|
89
|
+
const name = resolveCoin(coin).toUpperCase();
|
|
90
|
+
const i = meta.universe.findIndex((u) => u.name === name);
|
|
91
|
+
if (i < 0) throw new Error('coin not on ' + net() + ': ' + coin);
|
|
92
|
+
const markPx = Number(ctxs[i].markPx);
|
|
93
|
+
if (!(markPx > 0)) throw new Error('no markPx for ' + name + ' on ' + net());
|
|
94
|
+
return { assetIndex: i, szDecimals: meta.universe[i].szDecimals, name, markPx };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Resolve coin size + entryPx. Preferred first-place path: sizeUsd only
|
|
99
|
+
* (fetches mark, size = sizeUsd/mark, rounds to szDecimals). Explicit size+entryPx still works.
|
|
100
|
+
*/
|
|
101
|
+
async function resolveSizeAndPx({ coin, size, entryPx, sizeUsd }) {
|
|
102
|
+
const { actions } = loadShipped();
|
|
103
|
+
const needMark = entryPx == null || (size == null && sizeUsd != null);
|
|
104
|
+
const meta = needMark ? await assetMetaAndMark(coin) : await assetMeta(coin);
|
|
105
|
+
let px = entryPx != null ? Number(entryPx) : meta.markPx;
|
|
106
|
+
if (!(px > 0)) throw new Error('entryPx required (or omit and pass sizeUsd for auto mark)');
|
|
107
|
+
let sz = size != null ? Number(size) : null;
|
|
108
|
+
let usedSizeUsd = sizeUsd != null ? Number(sizeUsd) : null;
|
|
109
|
+
if (sz == null) {
|
|
110
|
+
if (!(usedSizeUsd > 0)) {
|
|
111
|
+
throw new Error('size or sizeUsd required — first place: {coin:"BTC", isBuy:true, sizeUsd:12, leverage:2} (auto mark + szDecimals round; entryPx optional)');
|
|
112
|
+
}
|
|
113
|
+
sz = usedSizeUsd / px;
|
|
114
|
+
} else if (usedSizeUsd == null) {
|
|
115
|
+
usedSizeUsd = sz * px;
|
|
116
|
+
}
|
|
117
|
+
sz = actions.roundToDecimals(sz, meta.szDecimals);
|
|
118
|
+
// Validate wire size will not collapse to 0
|
|
119
|
+
const wire = actions.sizeToWire(sz, meta.szDecimals);
|
|
120
|
+
if (wire === '0') {
|
|
121
|
+
throw new Error('size rounds to zero at ' + meta.szDecimals + ' decimals — increase sizeUsd (BTC first place: sizeUsd:12) or pass a larger size');
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
assetIndex: meta.assetIndex,
|
|
125
|
+
szDecimals: meta.szDecimals,
|
|
126
|
+
name: meta.name,
|
|
127
|
+
size: sz,
|
|
128
|
+
entryPx: px,
|
|
129
|
+
sizeUsd: usedSizeUsd,
|
|
130
|
+
markPx: meta.markPx ?? null,
|
|
131
|
+
autoRounded: size == null || entryPx == null
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
66
135
|
async function postL1(action) {
|
|
67
136
|
const { actions, signer } = loadShipped();
|
|
68
137
|
assertPlacementAllowed(actions);
|
|
@@ -128,12 +197,14 @@ export async function getPositions({ user, coin } = {}) {
|
|
|
128
197
|
return { net: net(), user: u, positions };
|
|
129
198
|
}
|
|
130
199
|
|
|
131
|
-
export async function placeOrder({ coin, isBuy, size, entryPx, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
|
|
200
|
+
export async function placeOrder({ coin, isBuy, size = null, entryPx = null, sizeUsd = null, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
|
|
132
201
|
const { actions } = loadShipped();
|
|
133
202
|
assertPlacementAllowed(actions);
|
|
203
|
+
const resolved = await resolveSizeAndPx({ coin, size, entryPx, sizeUsd });
|
|
134
204
|
let risk = null;
|
|
135
|
-
|
|
136
|
-
|
|
205
|
+
const lev = leverage != null ? Number(leverage) : null;
|
|
206
|
+
if (!skipRiskCheck && lev) {
|
|
207
|
+
risk = await pretradeCheckFull({ coin, dir: isBuy ? 'long' : 'short', leverage: lev, entryPx: resolved.entryPx, sizeUsd: resolved.sizeUsd });
|
|
137
208
|
if (risk.refuseReason === 'full_feed_unconfigured') {
|
|
138
209
|
risk = { ...risk, advisory: true, feedAdvisory: true, note: (risk.note || '') + ' Place allowed without full feed; heat is advisory.' };
|
|
139
210
|
}
|
|
@@ -145,14 +216,44 @@ export async function placeOrder({ coin, isBuy, size, entryPx, slPx = null, tpPx
|
|
|
145
216
|
placed: false,
|
|
146
217
|
refused: 'liq price ' + risk.liqPx + ' lands inside a $' + wallSz + 'M wall — pass override:true to force',
|
|
147
218
|
risk,
|
|
148
|
-
builderFeeAttached: false
|
|
219
|
+
builderFeeAttached: false,
|
|
220
|
+
size: resolved.size,
|
|
221
|
+
entryPx: resolved.entryPx,
|
|
222
|
+
sizeUsd: resolved.sizeUsd
|
|
149
223
|
};
|
|
150
224
|
}
|
|
151
225
|
}
|
|
152
|
-
|
|
153
|
-
|
|
226
|
+
let sizingWarn = null;
|
|
227
|
+
try {
|
|
228
|
+
const bal = await getBalances();
|
|
229
|
+
if (bal.equity > 0 && resolved.sizeUsd > bal.equity * 0.2) {
|
|
230
|
+
sizingWarn = 'sizeUsd ' + Math.round(resolved.sizeUsd) + ' > 20% of equity ' + Math.round(bal.equity) + ' — size down or confirm';
|
|
231
|
+
}
|
|
232
|
+
} catch (_) { /* balances optional for place */ }
|
|
233
|
+
const action = actions.buildOrderAction({
|
|
234
|
+
assetIndex: resolved.assetIndex,
|
|
235
|
+
szDecimals: resolved.szDecimals,
|
|
236
|
+
isBuy,
|
|
237
|
+
entryPx: resolved.entryPx,
|
|
238
|
+
size: resolved.size,
|
|
239
|
+
slPx,
|
|
240
|
+
tpPx
|
|
241
|
+
});
|
|
154
242
|
const posted = await postL1(action);
|
|
155
|
-
return {
|
|
243
|
+
return {
|
|
244
|
+
placed: posted.ok,
|
|
245
|
+
net: posted.net,
|
|
246
|
+
response: posted.response,
|
|
247
|
+
risk,
|
|
248
|
+
builderFeeAttached: posted.ok,
|
|
249
|
+
coin: resolved.name,
|
|
250
|
+
size: resolved.size,
|
|
251
|
+
entryPx: resolved.entryPx,
|
|
252
|
+
sizeUsd: resolved.sizeUsd,
|
|
253
|
+
autoRounded: resolved.autoRounded,
|
|
254
|
+
sizingWarn,
|
|
255
|
+
feeModel: '1bp on fills only — no subscription'
|
|
256
|
+
};
|
|
156
257
|
}
|
|
157
258
|
|
|
158
259
|
/** Cancel by oid and/or cloid. */
|
package/src/mcp.js
CHANGED
|
@@ -13,10 +13,10 @@ const wrap = (fn) => async (args) => {
|
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
export async function main() {
|
|
16
|
-
const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.
|
|
16
|
+
const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.15' });
|
|
17
17
|
|
|
18
18
|
server.tool('hl_quickstart',
|
|
19
|
-
'CALL FIRST: place-ready bootstrap —
|
|
19
|
+
'CALL FIRST: place-ready bootstrap — wallet + MASTER ApproveBuilderFee 1bp once, then hl_place_order sizeUsd:12. Default testnet. 1bp fills, no sub.',
|
|
20
20
|
{ coin: z.string().optional().describe("Optional coin for sample walls (default BTC)") },
|
|
21
21
|
wrap(({ coin }) => quickstart({ coin })));
|
|
22
22
|
|
|
@@ -29,7 +29,7 @@ export async function main() {
|
|
|
29
29
|
{}, wrap(() => newAgentWallet()));
|
|
30
30
|
|
|
31
31
|
server.tool('hl_approve_payloads',
|
|
32
|
-
'Setup: EIP-712 payloads for MASTER to sign once — approveAgent + ApproveBuilderFee 0.01% (1bp). No
|
|
32
|
+
'Setup: EIP-712 payloads for MASTER to sign once — approveAgent + ApproveBuilderFee 0.01% (1bp). Then POST both; then sizeUsd place. No sub.',
|
|
33
33
|
{ agentAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/) },
|
|
34
34
|
wrap(({ agentAddress }) => approvePayloads(agentAddress)));
|
|
35
35
|
|
|
@@ -44,11 +44,15 @@ export async function main() {
|
|
|
44
44
|
wrap((a) => getPositions(a)));
|
|
45
45
|
|
|
46
46
|
server.tool('hl_place_order',
|
|
47
|
-
'PLACE Hyperliquid perps @ 1bp on fills
|
|
47
|
+
'PLACE Hyperliquid perps @ 1bp on fills. Preferred: sizeUsd (auto mark + szDecimals round; entryPx optional). Or pass size+entryPx. Heat advisory. No sub.',
|
|
48
48
|
{
|
|
49
|
-
coin: z.string()
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
coin: z.string().describe('e.g. BTC'),
|
|
50
|
+
isBuy: z.boolean(),
|
|
51
|
+
sizeUsd: z.number().positive().optional().describe('Preferred first-place notional USD — auto mark + size round (e.g. 12)'),
|
|
52
|
+
size: z.number().positive().optional().describe('Coin units; optional if sizeUsd set'),
|
|
53
|
+
entryPx: z.number().positive().optional().describe('Limit px; omit with sizeUsd to use mark'),
|
|
54
|
+
slPx: z.number().positive().optional(), tpPx: z.number().positive().optional(),
|
|
55
|
+
leverage: z.number().positive().optional().describe('Optional; enables advisory pre-trade risk check (first place: 2)'),
|
|
52
56
|
override: z.boolean().optional(), skipRiskCheck: z.boolean().optional()
|
|
53
57
|
},
|
|
54
58
|
wrap((a) => placeOrder(a)));
|