@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 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` — master signs approveAgent + ApproveBuilderFee **0.01%** once
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, call `hl_place_order` with `{coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}` — size/px auto-round; no feed required.
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.14",
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)` → master signs approveAgent + ApproveBuilderFee **1bp** once
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
- **First place (testnet):** after approve, call hl_place_order with coin BTC, isBuy true, sizeUsd 12, leverage 2 size/px auto-round; feed optional.
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; size/px auto-round; feed optional' }
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 (auto size/px rounding + lev defaults + cloid/status) then cancel/close as needed',
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: 'If place errors on missing key: hl_new_agent_wallet → HYPELENS_AGENT_PK → hl_approve_payloads → hl_place_order(first_place_example)',
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('HYPELENS_AGENT_PK not set — call hl_quickstart, then hl_new_agent_wallet → set HYPELENS_AGENT_PK, hl_approve_payloads (master signs 1bp), then hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) on testnet');
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('HYPELENS_AGENT_PK not set — hl_new_agent_wallet → export HYPELENS_AGENT_PK=<pk> before place/balances/cancel/close');
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
- note: 'Sign both with the MASTER wallet (EIP-712), POST each as {action, signature, nonce} to ' + actions.NET[net()].exchange
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 { privateKey: pk, address: signer.addressFromPrivateKey(pk), note: 'store as HYPELENS_AGENT_PK; approve via approvePayloads(address)' };
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
- if (!skipRiskCheck && leverage) {
136
- risk = await pretradeCheckFull({ coin, dir: isBuy ? 'long' : 'short', leverage, entryPx });
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
- const { assetIndex, szDecimals } = await assetMeta(coin);
153
- const action = actions.buildOrderAction({ assetIndex, szDecimals, isBuy, entryPx, size, slPx, tpPx });
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 { placed: posted.ok, net: posted.net, response: posted.response, risk, builderFeeAttached: posted.ok };
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.13' });
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 — install, agent wallet + ApproveBuilderFee 1bp once, then place/cancel/close/positions. 1bp on fills, no sub. Heat optional after.',
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 subscription.',
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 auto size/price rounding + leverage defaults. Heat feed optional (advisory). No subscription.',
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(), isBuy: z.boolean(), size: z.number().positive().describe('Size in coin units'),
50
- entryPx: z.number().positive(), slPx: z.number().positive().optional(), tpPx: z.number().positive().optional(),
51
- leverage: z.number().positive().optional().describe('Enables advisory pre-trade risk check'),
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)));