@hypelens/hypelens-agent-rail 0.1.14 → 0.1.16

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
@@ -2,15 +2,24 @@
2
2
  **Place, cancel, close Hyperliquid perps.** Agent wallet once · auto-round · **1bp on fills — no sub.**
3
3
  ## Install → approve → place
4
4
  ```bash
5
+ # MCP stdio (Claude / Cursor / any MCP host)
5
6
  npx -y @hypelens/hypelens-agent-rail
7
+
8
+ # Claude Code one-liner
9
+ claude mcp add hypelens -- npx -y @hypelens/hypelens-agent-rail
10
+
11
+ # OpenClaw / ClawHub skill (skill slug, not npm alone)
12
+ clawhub install hypelens-agent-rail
13
+ # skills.sh (needs public GH; polyparlay anon currently 404)
14
+ npx skills add polyparlay/hypelens -s hypelens-agent-rail -y
6
15
  ```
7
- 1. `hl_new_agent_wallet` → `HYPELENS_AGENT_PK`
8
- 2. `hl_approve_payloads` — master signs approveAgent + ApproveBuilderFee **0.01%** once
16
+ 1. `hl_quickstart` (optional) → `hl_new_agent_wallet` → `HYPELENS_AGENT_PK`
17
+ 2. `hl_approve_payloads(agentAddress)` — **MASTER** signs approveAgent + ApproveBuilderFee **0.01%** once, then POST both
9
18
  3. `hl_place_order` / `hl_cancel_order` / `hl_close_position` / `hl_positions` / `hl_balances`
10
19
 
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.
20
+ **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. On reject after sizeUsd, response includes `error` + `next` (margin / approve / min notional).
12
21
 
13
- Default **testnet**. `HYPELENS_NET=mainnet` for live. Fee: **1bp on fills** — no sub.
22
+ Default **testnet**. `HYPELENS_NET=mainnet` for live. Fee: **1bp on fills** — no sub. Missing `HYPELENS_AGENT_PK` errors point at the same path.
14
23
 
15
24
  ## Optional after install: sizing edge
16
25
 
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.16",
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",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: hypelens-agent-rail
2
+ name: Hyperliquid Place
3
3
  description: >-
4
4
  hyperliquid place cancel close — Hyperliquid perps rail for OpenClaw/MCP.
5
5
  Agent wallet once, auto-round, 1bp builder on fills (no sub). Keywords:
@@ -31,18 +31,18 @@ metadata:
31
31
  npx -y @hypelens/hypelens-agent-rail
32
32
  ```
33
33
 
34
- **ClawHub / OpenClaw:** `clawhub install hypelens-agent-rail` · **skills.sh:** `npx skills add polyparlay/hypelens`
34
+ **ClawHub / OpenClaw:** `clawhub install hypelens-agent-rail` · **skills.sh:** `npx skills add polyparlay/hypelens -s hypelens-agent-rail -y` · **MCP:** `npx -y @hypelens/hypelens-agent-rail`
35
35
 
36
36
  ## Quickstart (place-ready)
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,112 @@ 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
+
135
+ /** Extract HL place/cancel wire error + actionable next after sizeUsd resolve. */
136
+ function interpretExchangeResult(body) {
137
+ if (!body || typeof body !== 'object') {
138
+ return { ok: false, error: 'empty exchange response', next: 'retry hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) on testnet' };
139
+ }
140
+ if (body.status === 'err' || body.status === 'error') {
141
+ const raw = typeof body.response === 'string' ? body.response : JSON.stringify(body.response || body);
142
+ return { ok: false, error: raw, next: hintFromHlError(raw) };
143
+ }
144
+ const statuses = body?.response?.data?.statuses;
145
+ if (Array.isArray(statuses)) {
146
+ for (const s of statuses) {
147
+ if (s && typeof s.error === 'string' && s.error) {
148
+ return { ok: false, error: s.error, next: hintFromHlError(s.error), statuses };
149
+ }
150
+ }
151
+ // resting / filled / etc
152
+ const resting = statuses.find((s) => s && (s.resting || s.filled));
153
+ if (resting) return { ok: true, error: null, next: null, statuses };
154
+ if (statuses.length) return { ok: true, error: null, next: null, statuses };
155
+ }
156
+ if (body.status === 'ok') return { ok: true, error: null, next: null };
157
+ return { ok: false, error: JSON.stringify(body), next: hintFromHlError(JSON.stringify(body)) };
158
+ }
159
+
160
+ function hintFromHlError(msg) {
161
+ const m = String(msg || '').toLowerCase();
162
+ if (m.includes('insufficient') || m.includes('margin') || m.includes('not enough')) {
163
+ return 'Fund agent on this net (default testnet) or lower sizeUsd; check hl_balances.equity then retry sizeUsd place';
164
+ }
165
+ if (m.includes('does not exist') || m.includes('user or api wallet') || m.includes('unknown user')) {
166
+ return 'MASTER must POST approveAgent + ApproveBuilderFee 1bp (hl_approve_payloads) before first place; then sizeUsd:12';
167
+ }
168
+ if (m.includes('builder') && (m.includes('fee') || m.includes('approval') || m.includes('approve'))) {
169
+ return 'MASTER must sign ApproveBuilderFee 0.01% (1bp) via hl_approve_payloads and POST it; then retry sizeUsd place';
170
+ }
171
+ if (m.includes('minimum') || m.includes('min ') || m.includes('$10') || m.includes('too small')) {
172
+ return 'Increase sizeUsd (BTC first place: sizeUsd:12) — HL min notional; auto-round may shrink tiny sizes to zero';
173
+ }
174
+ if (m.includes('oracle') || m.includes('price') && m.includes('far')) {
175
+ return 'Omit entryPx and pass sizeUsd only (uses mark), or set entryPx near mark; retry hl_place_order sizeUsd';
176
+ }
177
+ if (m.includes('leverage') || m.includes('max lev')) {
178
+ return 'Lower leverage (first place: leverage:2) or set coin max leverage on HL, then retry sizeUsd place';
179
+ }
180
+ if (m.includes('permit') || m.includes('agent') && m.includes('not')) {
181
+ return 'Run hl_approve_payloads(agentAddress) — MASTER signs approveAgent once, POST, then sizeUsd place';
182
+ }
183
+ return 'Check hl_exchange_status + hl_balances; ensure MASTER approved agent+1bp builder; retry hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) on testnet';
184
+ }
185
+
66
186
  async function postL1(action) {
67
187
  const { actions, signer } = loadShipped();
68
188
  assertPlacementAllowed(actions);
@@ -73,7 +193,9 @@ async function postL1(action) {
73
193
  body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
74
194
  });
75
195
  const body = await res.json().catch(() => ({}));
76
- return { ok: res.ok && body.status === 'ok', net: net(), response: body };
196
+ const interpreted = interpretExchangeResult(body);
197
+ const ok = res.ok && interpreted.ok;
198
+ return { ok, net: net(), response: body, error: interpreted.error, next: interpreted.next, statuses: interpreted.statuses || null };
77
199
  }
78
200
 
79
201
  async function clearinghouse(user) {
@@ -128,12 +250,14 @@ export async function getPositions({ user, coin } = {}) {
128
250
  return { net: net(), user: u, positions };
129
251
  }
130
252
 
131
- export async function placeOrder({ coin, isBuy, size, entryPx, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
253
+ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, sizeUsd = null, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
132
254
  const { actions } = loadShipped();
133
255
  assertPlacementAllowed(actions);
256
+ const resolved = await resolveSizeAndPx({ coin, size, entryPx, sizeUsd });
134
257
  let risk = null;
135
- if (!skipRiskCheck && leverage) {
136
- risk = await pretradeCheckFull({ coin, dir: isBuy ? 'long' : 'short', leverage, entryPx });
258
+ const lev = leverage != null ? Number(leverage) : null;
259
+ if (!skipRiskCheck && lev) {
260
+ risk = await pretradeCheckFull({ coin, dir: isBuy ? 'long' : 'short', leverage: lev, entryPx: resolved.entryPx, sizeUsd: resolved.sizeUsd });
137
261
  if (risk.refuseReason === 'full_feed_unconfigured') {
138
262
  risk = { ...risk, advisory: true, feedAdvisory: true, note: (risk.note || '') + ' Place allowed without full feed; heat is advisory.' };
139
263
  }
@@ -145,14 +269,52 @@ export async function placeOrder({ coin, isBuy, size, entryPx, slPx = null, tpPx
145
269
  placed: false,
146
270
  refused: 'liq price ' + risk.liqPx + ' lands inside a $' + wallSz + 'M wall — pass override:true to force',
147
271
  risk,
148
- builderFeeAttached: false
272
+ builderFeeAttached: false,
273
+ size: resolved.size,
274
+ entryPx: resolved.entryPx,
275
+ sizeUsd: resolved.sizeUsd
149
276
  };
150
277
  }
151
278
  }
152
- const { assetIndex, szDecimals } = await assetMeta(coin);
153
- const action = actions.buildOrderAction({ assetIndex, szDecimals, isBuy, entryPx, size, slPx, tpPx });
279
+ let sizingWarn = null;
280
+ try {
281
+ const bal = await getBalances();
282
+ if (bal.equity > 0 && resolved.sizeUsd > bal.equity * 0.2) {
283
+ sizingWarn = 'sizeUsd ' + Math.round(resolved.sizeUsd) + ' > 20% of equity ' + Math.round(bal.equity) + ' — size down or confirm';
284
+ }
285
+ } catch (_) { /* balances optional for place */ }
286
+ const action = actions.buildOrderAction({
287
+ assetIndex: resolved.assetIndex,
288
+ szDecimals: resolved.szDecimals,
289
+ isBuy,
290
+ entryPx: resolved.entryPx,
291
+ size: resolved.size,
292
+ slPx,
293
+ tpPx
294
+ });
154
295
  const posted = await postL1(action);
155
- return { placed: posted.ok, net: posted.net, response: posted.response, risk, builderFeeAttached: posted.ok };
296
+ const out = {
297
+ placed: posted.ok,
298
+ net: posted.net,
299
+ response: posted.response,
300
+ risk,
301
+ builderFeeAttached: posted.ok,
302
+ coin: resolved.name,
303
+ size: resolved.size,
304
+ entryPx: resolved.entryPx,
305
+ sizeUsd: resolved.sizeUsd,
306
+ autoRounded: resolved.autoRounded,
307
+ sizingWarn,
308
+ feeModel: '1bp on fills only — no subscription'
309
+ };
310
+ if (!posted.ok) {
311
+ out.error = posted.error || 'place rejected by exchange';
312
+ out.next = posted.next || hintFromHlError(posted.error || '');
313
+ if (posted.statuses) out.statuses = posted.statuses;
314
+ } else {
315
+ out.next = 'placed — cancel via hl_cancel_order; close via hl_close_position; fees 1bp on fills only';
316
+ }
317
+ return out;
156
318
  }
157
319
 
158
320
  /** Cancel by oid and/or cloid. */
@@ -165,7 +327,9 @@ export async function cancelOrder({ coin, oid = null, cloid = null }) {
165
327
  else if (oid != null) action = actions.buildCancelAction([{ assetIndex, oid: Number(oid) }]);
166
328
  else throw new Error('oid or cloid required — pass oid or cloid to hl_cancel_order');
167
329
  const posted = await postL1(action);
168
- return { cancelled: posted.ok, net: posted.net, response: posted.response, coin: resolveCoin(coin).toUpperCase(), oid, cloid };
330
+ const out = { cancelled: posted.ok, net: posted.net, response: posted.response, coin: resolveCoin(coin).toUpperCase(), oid, cloid };
331
+ if (!posted.ok) { out.error = posted.error || 'cancel rejected'; out.next = posted.next || hintFromHlError(posted.error || ''); }
332
+ return out;
169
333
  }
170
334
 
171
335
  /** IOC reduce-only close for a coin (uses position size if size omitted). */
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.16' });
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)));