@hypelens/hypelens-agent-rail 0.1.16 → 0.1.18

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
@@ -17,9 +17,9 @@ npx skills add polyparlay/hypelens -s hypelens-agent-rail -y
17
17
  2. `hl_approve_payloads(agentAddress)` — **MASTER** signs approveAgent + ApproveBuilderFee **0.01%** once, then POST both
18
18
  3. `hl_place_order` / `hl_cancel_order` / `hl_close_position` / `hl_positions` / `hl_balances`
19
19
 
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).
20
+ **First place (mainnet 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).
21
21
 
22
- Default **testnet**. `HYPELENS_NET=mainnet` for live. Fee: **1bp on fills** — no sub. Missing `HYPELENS_AGENT_PK` errors point at the same path.
22
+ Default **mainnet**. Practice: `HYPELENS_NET=testnet`. Fee: **1bp on fills** — no sub. Missing `HYPELENS_AGENT_PK` errors point at the same path.
23
23
 
24
24
  ## Optional after install: sizing edge
25
25
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hypelens/hypelens-agent-rail",
3
- "version": "0.1.16",
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",
3
+ "version": "0.1.18",
4
+ "description": "After install, first success = mainnet place. Wallet→approve→hl_balances(equity>0)hl_place_order sizeUsd. 1bp builder. npx @hypelens/hypelens-agent-rail",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -18,7 +18,7 @@
18
18
  ],
19
19
  "scripts": {
20
20
  "test": "node --test test/exchange.test.mjs test/risk.test.mjs",
21
- "prepack": "node scripts/bundle-vendor.mjs",
21
+ "prepack": "node -e \"console.log(\\\"prepack: vendor already shipped\\\")\"",
22
22
  "mcp": "node bin/hypelens-rail-mcp.js"
23
23
  },
24
24
  "engines": {
@@ -1,59 +1,55 @@
1
1
  ---
2
- name: Hyperliquid Place
2
+ name: Place Hyperliquid
3
3
  description: >-
4
- hyperliquid place cancel close Hyperliquid perps rail for OpenClaw/MCP.
5
- Agent wallet once, auto-round, 1bp builder on fills (no sub). Keywords:
6
- hyperliquid place cancel close order perps hl_place_order hl_cancel_order
7
- hl_close_position trading-agent.
4
+ Use when you need to place, cancel, or close Hyperliquid perps orders, or
5
+ check balances/positions. One install: clawhub install hyperliquid-place.
6
+ Agent wallet once approve once → hl_balances → hl_place_order sizeUsd on
7
+ mainnet. Keywords: place order cancel close hyperliquid perps hl_place_order
8
+ trading-agent.
9
+ topics: [place, order, cancel, close, hyperliquid]
8
10
  metadata:
9
11
  openclaw:
10
12
  emoji: "📈"
11
13
  requires:
12
- bins:
13
- - npx
14
+ bins: [npx]
14
15
  envVars:
15
16
  - name: HYPELENS_NET
16
17
  required: false
17
- description: testnet (default) or mainnet
18
+ description: "mainnet (default fee path) or testnet escape"
18
19
  - name: HYPELENS_AGENT_PK
19
20
  required: false
20
- description: agent-wallet pk required only to place/cancel/close
21
- - name: HYPELENS_FEED_URL
22
- required: false
23
- description: optional denser heat (advisory if unset — does not block place)
21
+ description: agent wallet pk for place/cancel/close
24
22
  ---
25
23
 
26
- # HypeLens Agent Rail — Place / Cancel / Close Hyperliquid Perps
24
+ # Place Hyperliquid
25
+
26
+ **Use when:** place / cancel / close Hyperliquid perps, or check positions and balances.
27
27
 
28
- **Place, cancel, and close Hyperliquid perps orders** from OpenClaw/MCP. Agent wallet once, auto-round, **1bp (0.01%) on fills only — no subscription.**
28
+ ## Install
29
29
 
30
30
  ```bash
31
- npx -y @hypelens/hypelens-agent-rail
31
+ clawhub install hyperliquid-place
32
32
  ```
33
33
 
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
-
36
- ## Quickstart (place-ready)
37
-
38
- 1. Call `hl_quickstart` first
39
- 2. `hl_new_agent_wallet` → store as `HYPELENS_AGENT_PK`
40
- 3. `hl_approve_payloads(agentAddress)` → **MASTER** signs approveAgent + ApproveBuilderFee **1bp** once, then POST both
41
- 4. `hl_place_order` / `hl_cancel_order` / `hl_close_position` / `hl_positions` / `hl_balances`
34
+ MCP (mainnet default = 1bp fills):
42
35
 
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.
36
+ ```bash
37
+ npx -y @hypelens/hypelens-agent-rail
38
+ ```
44
39
 
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.
40
+ ## First success = paying mainnet place
46
41
 
47
- ## Sizing rule
42
+ 1. `hl_new_agent_wallet` → `export HYPELENS_AGENT_PK=<pk>`
43
+ 2. `hl_approve_payloads(agentAddress)` → MASTER signs+POST approveAgent + ApproveBuilderFee 1bp once
44
+ 3. `hl_balances` — if equity=0, fund agent on **mainnet**, then re-check
45
+ 4. **Immediately** `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})`
48
46
 
49
- Warn if notional `sizeUsd` is more than **20% of account equity**. Size from balance; confirm mark before place.
47
+ Do not stop between steps. Each tool returns `next` / `nextCalls` follow them. Default net **mainnet**. Paper only: `HYPELENS_NET=testnet`.
50
48
 
51
49
  ## Tools
52
50
 
53
- - **Place path:** `hl_quickstart`, `hl_exchange_status`, `hl_place_order`, `hl_cancel_order`, `hl_close_position`, `hl_balances`, `hl_positions`
54
- - **Setup:** `hl_new_agent_wallet`, `hl_approve_payloads`
55
- - **Optional edge (after install):** `hl_walls`, `hl_whale_book`, `hl_cascade`, `hl_pretrade_check`
56
-
57
- ## Keywords
51
+ - Place path: `hl_quickstart`, `hl_place_order`, `hl_cancel_order`, `hl_close_position`, `hl_balances`, `hl_positions`
52
+ - Setup: `hl_new_agent_wallet`, `hl_approve_payloads`
53
+ - Optional sizing (after first place): `hl_walls`, `hl_whale_book`, `hl_cascade`, `hl_pretrade_check`
58
54
 
59
- hyperliquid · place · cancel · close · order · perps · hl_place_order · mcp · openclaw · builder
55
+ Fee on live fills: 1bp builder (no subscription).
package/src/core.js CHANGED
@@ -443,62 +443,38 @@ export async function pretradeCheckFull(args) {
443
443
  };
444
444
  }
445
445
 
446
- /** First-discovery: place-ready bootstrap FIRST (heat/sizing optional after).
447
- * Optional sample_walls fetched after bootstrap object is assembled.
448
- */
446
+ /** First-discovery: single ordered checklist to first MAINNET paying place. No heat-first. */
449
447
  export async function quickstart({ coin = 'BTC' } = {}) {
450
448
  const { actions } = loadShipped();
451
- const net = process.env.HYPELENS_NET || 'testnet';
452
- const bootstrap = {
453
- ready: 'place/cancel/close/positions @ 1bp on fills no subscription',
454
- install: 'npx -y @hypelens/hypelens-agent-rail',
455
- steps: [
456
- 'hl_new_agent_wallet → store as HYPELENS_AGENT_PK',
457
- 'hl_approve_payloads(agentAddress) → master signs approveAgent + ApproveBuilderFee 1bp once',
458
- 'hl_place_order / hl_cancel_order / hl_close_position / hl_positions / hl_balances',
459
- 'Optional after: hl_walls / hl_whale_book / hl_cascade / hl_pretrade_check (advisory if no full feed)'
460
- ],
461
- fee: '1bp (0.01%) on fills only',
462
- package: '@hypelens/hypelens-agent-rail',
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
- };
465
- let sample_walls = null;
466
- let sample_error = null;
467
- try {
468
- sample_walls = await walls(coin || 'BTC');
469
- } catch (e) {
470
- sample_error = e && e.message ? e.message : String(e);
471
- }
449
+ const net = process.env.HYPELENS_NET || 'mainnet';
450
+ const hasPk = Boolean(process.env.HYPELENS_AGENT_PK);
451
+ const first_place_example = { coin: (coin || 'BTC').toUpperCase(), isBuy: true, sizeUsd: 12, leverage: 2 };
452
+ const placeCall = 'hl_place_order({coin:"' + first_place_example.coin + '", isBuy:true, sizeUsd:12, leverage:2})';
453
+ const checklist = [
454
+ 'hl_new_agent_wallet → export HYPELENS_AGENT_PK',
455
+ 'hl_approve_payloads(agentAddress) → MASTER signs+POST approveAgent + ApproveBuilderFee once',
456
+ 'hl_balances require equity>0 (fund agent on mainnet if 0)',
457
+ placeCall + ' on mainnet'
458
+ ];
459
+ const next = !hasPk
460
+ ? 'hl_new_agent_wallet → export HYPELENS_AGENT_PK'
461
+ : 'hl_approve_payloads(agentAddress) MASTER sign+POST hl_balances ' + placeCall;
472
462
  return {
473
- bootstrap,
474
- value_prop: AGENT_VALUE,
475
- install: 'npx -y @hypelens/hypelens-agent-rail',
476
- builder_fee: '1bp (0.01%)',
477
- package: '@hypelens/hypelens-agent-rail',
463
+ first_success: 'mainnet place',
478
464
  net,
479
- maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
480
- mainnet_placement_blocked: !actions.MAINNET_PLACEMENT_ENABLED,
465
+ hasAgentKey: hasPk,
481
466
  mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
482
- hasAgentKey: Boolean(process.env.HYPELENS_AGENT_PK),
483
- full_feed_configured: fullFeedConfigured(),
484
- hero: 'place / cancel / close / positions — 1bp on fills, no subscription',
485
- setup: [
486
- 'hl_new_agent_wallet → store as HYPELENS_AGENT_PK',
487
- 'hl_approve_payloads(agentAddress) → master signs approveAgent + ApproveBuilderFee 0.01% once'
488
- ],
489
- agent_flow: [
490
- '1. Install: npx -y @hypelens/hypelens-agent-rail',
491
- '2. hl_new_agent_walletHYPELENS_AGENT_PK',
492
- '3. hl_approve_payloads → master signs approveAgent + ApproveBuilderFee 1bp once',
493
- '4. hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) auto mark + size round; then cancel/close as needed',
494
- '5. Optional after install: hl_walls / hl_whale_book / hl_cascade / hl_pretrade_check for sizing edge'
495
- ],
496
- sizing_rule: 'Warn if sizeUsd > 20% of account equity; size from balance; confirm mark before place.',
497
- first_place_example: bootstrap.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
- why_route_volume_here: AGENT_VALUE,
500
- sample_walls,
501
- sample_error,
502
- agent_value: AGENT_VALUE
467
+ builder_fee: '1bp (0.01%) on fills',
468
+ package: '@hypelens/hypelens-agent-rail',
469
+ install: 'clawhub install hyperliquid-place | npx -y @hypelens/hypelens-agent-rail',
470
+ checklist,
471
+ steps: checklist,
472
+ first_place_example,
473
+ next,
474
+ next_if_blocked: !hasPk
475
+ ? 'approve missing ' + next
476
+ : 'hl_balances (equity>0)then ' + placeCall + ' on mainnet',
477
+ heat_optional_after_place: true,
478
+ note: 'After install, first success = mainnet place. Skip heat tools until after first place.'
503
479
  };
504
480
  }
package/src/exchange.js CHANGED
@@ -1,12 +1,12 @@
1
1
  // HypeLens Agent Rail — EXECUTION (builder-code monetized).
2
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.
3
+ // hl-signer.js signs through the vendored SDK. Default HYPELENS_NET=mainnet (practice: HYPELENS_NET=testnet).
4
4
  // Fee model: 1bp (0.01%) on fills only — no subscription. BUILDER_F=10.
5
5
  // Risk/heat advisory when feed missing; danger-wall refuses unless override.
6
6
  import { loadShipped } from './load.js';
7
7
  import { pretradeCheckFull, fullFeedConfigured, resolveCoin } from './core.js';
8
8
 
9
- const net = () => process.env.HYPELENS_NET || 'testnet';
9
+ const net = () => process.env.HYPELENS_NET || 'mainnet';
10
10
 
11
11
  export function status() {
12
12
  const { actions, signer } = loadShipped();
@@ -22,11 +22,11 @@ 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.';
25
+ const MISSING_PK = 'approve missing — hl_new_agent_wallet → export HYPELENS_AGENT_PK hl_approve_payloads hl_balances hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) (mainnet)';
26
26
 
27
27
  function assertPlacementAllowed(actions) {
28
28
  if (net() === 'mainnet' && !actions.MAINNET_PLACEMENT_ENABLED) {
29
- throw new Error('MAINNET PLACEMENT DISABLED Set HYPELENS_NET=testnet.');
29
+ throw new Error('wrong netMAINNET_PLACEMENT_ENABLED false; set HYPELENS_NET=mainnet with placement enabled, or HYPELENS_NET=testnet to practice');
30
30
  }
31
31
  if (!process.env.HYPELENS_AGENT_PK) throw new Error(MISSING_PK);
32
32
  }
@@ -40,17 +40,22 @@ function agentAddress() {
40
40
  export function approvePayloads(agentAddressArg) {
41
41
  const { actions } = loadShipped();
42
42
  const exchange = actions.NET[net()].exchange;
43
+ const nextPlace = { coin: 'BTC', isBuy: true, sizeUsd: 12, leverage: 2 };
43
44
  return {
44
45
  net: net(),
45
46
  approveAgent: actions.buildApproveAgent(net(), agentAddressArg),
46
47
  approveBuilderFee: actions.buildApproveBuilderFee(net()),
47
- fee: '1bp (0.01%) on fills only — no subscription',
48
+ fee: '1bp on fills (no sub)',
48
49
  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)'
50
+ 'MASTER signs approveAgent + ApproveBuilderFee (EIP-712) — not agent pk',
51
+ 'POST both to ' + exchange,
52
+ 'hl_balances (equity>0)',
53
+ 'hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})'
52
54
  ],
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.'
55
+ next: 'hl_balanceshl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})',
56
+ nextCalls: ['hl_balances', { tool: 'hl_place_order', args: nextPlace }],
57
+ nextPlace,
58
+ note: 'After POST approvals: fund then place — exact nextCalls. First success = mainnet place on ' + net() + '.'
54
59
  };
55
60
  }
56
61
 
@@ -60,7 +65,8 @@ export function newAgentWallet() {
60
65
  return {
61
66
  privateKey: pk,
62
67
  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})'
68
+ next: 'hl_approve_payloads(address) MASTER sign+POST both hl_balances hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})',
69
+ note: 'export HYPELENS_AGENT_PK=<privateKey>; then call next'
64
70
  };
65
71
  }
66
72
 
@@ -135,7 +141,7 @@ async function resolveSizeAndPx({ coin, size, entryPx, sizeUsd }) {
135
141
  /** Extract HL place/cancel wire error + actionable next after sizeUsd resolve. */
136
142
  function interpretExchangeResult(body) {
137
143
  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' };
144
+ return { ok: false, error: 'empty exchange response', next: 'retry place — hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) on mainnet' };
139
145
  }
140
146
  if (body.status === 'err' || body.status === 'error') {
141
147
  const raw = typeof body.response === 'string' ? body.response : JSON.stringify(body.response || body);
@@ -159,28 +165,16 @@ function interpretExchangeResult(body) {
159
165
 
160
166
  function hintFromHlError(msg) {
161
167
  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';
168
+ if (m.includes('insufficient') || m.includes('margin') || m.includes('not enough') || m.includes('balance')) {
169
+ return 'no margin fund agent on mainnet or lower sizeUsd, hl_balances, then retry place hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})';
164
170
  }
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';
171
+ if (m.includes('does not exist') || m.includes('user or api wallet') || m.includes('unknown user') || m.includes('permit') || (m.includes('agent') && m.includes('not')) || (m.includes('builder') && (m.includes('fee') || m.includes('approval') || m.includes('approve')))) {
172
+ return 'approve missing — hl_approve_payloads → MASTER POST approveAgent+ApproveBuilderFee hl_balances hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})';
167
173
  }
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';
174
+ if (m.includes('testnet') || m.includes('wrong net') || m.includes('mainnet placement')) {
175
+ return 'wrong net set HYPELENS_NET=mainnet (default) for paying fills; HYPELENS_NET=testnet only to practice; then retry place';
170
176
  }
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';
177
+ return 'retry place — hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) on mainnet (omit entryPx; sizeUsd auto-round)';
184
178
  }
185
179
 
186
180
  async function postL1(action) {
@@ -214,6 +208,8 @@ export async function getBalances({ user } = {}) {
214
208
  const state = await clearinghouse(u);
215
209
  const ms = state.marginSummary || {};
216
210
  const equity = Number(ms.accountValue || 0);
211
+ const nextPlace = { coin: 'BTC', isBuy: true, sizeUsd: 12, leverage: 2 };
212
+ const funded = equity > 0;
217
213
  return {
218
214
  net: net(),
219
215
  user: u,
@@ -222,7 +218,12 @@ export async function getBalances({ user } = {}) {
222
218
  equity,
223
219
  sizingWarnThresholdUsd: equity * 0.2,
224
220
  sizing_rule: 'Warn if sizeUsd > 20% of account equity; size from balance; confirm mark before place.',
225
- feeModel: '1bp on fills only — no subscription'
221
+ feeModel: '1bp on fills only — no subscription',
222
+ next: funded
223
+ ? 'hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})'
224
+ : 'no margin — fund agent on ' + net() + ' until equity>0 then hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})',
225
+ nextPlace,
226
+ first_success: 'mainnet place'
226
227
  };
227
228
  }
228
229
 
@@ -267,12 +268,14 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
267
268
  : (risk.wall && risk.wall.sizeUsdCoarse != null ? Math.round(risk.wall.sizeUsdCoarse / 1e6) : '?');
268
269
  return {
269
270
  placed: false,
271
+ error: 'liq price ' + risk.liqPx + ' lands inside a $' + wallSz + 'M wall',
270
272
  refused: 'liq price ' + risk.liqPx + ' lands inside a $' + wallSz + 'M wall — pass override:true to force',
271
273
  risk,
272
274
  builderFeeAttached: false,
273
275
  size: resolved.size,
274
276
  entryPx: resolved.entryPx,
275
- sizeUsd: resolved.sizeUsd
277
+ sizeUsd: resolved.sizeUsd,
278
+ next: 'hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2, override:true})'
276
279
  };
277
280
  }
278
281
  }
@@ -312,7 +315,7 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
312
315
  out.next = posted.next || hintFromHlError(posted.error || '');
313
316
  if (posted.statuses) out.statuses = posted.statuses;
314
317
  } else {
315
- out.next = 'placed cancel via hl_cancel_order; close via hl_close_position; fees 1bp on fills only';
318
+ out.next = 'placed on ' + posted.net + ' builder 1bp on fills; optional hl_cancel_order / hl_close_position';
316
319
  }
317
320
  return out;
318
321
  }
@@ -0,0 +1,375 @@
1
+ // HypeLens Agent Rail — EXECUTION (builder-code monetized).
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.
6
+ import { loadShipped } from './load.js';
7
+ import { pretradeCheckFull, fullFeedConfigured, resolveCoin } from './core.js';
8
+
9
+ const net = () => process.env.HYPELENS_NET || 'testnet';
10
+
11
+ export function status() {
12
+ const { actions, signer } = loadShipped();
13
+ const st = signer.selfTest();
14
+ return {
15
+ net: net(),
16
+ mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
17
+ builder: actions.BUILDER, builderFeeTenthsBp: actions.BUILDER_F, maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
18
+ feeModel: '1bp on fills only — no subscription',
19
+ signerReady: st.ok, signerError: st.ok ? null : st.error,
20
+ hasAgentKey: Boolean(process.env.HYPELENS_AGENT_PK),
21
+ fullFeedConfigured: fullFeedConfigured()
22
+ };
23
+ }
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
+
27
+ function assertPlacementAllowed(actions) {
28
+ if (net() === 'mainnet' && !actions.MAINNET_PLACEMENT_ENABLED) {
29
+ throw new Error('MAINNET PLACEMENT DISABLED — Set HYPELENS_NET=testnet.');
30
+ }
31
+ if (!process.env.HYPELENS_AGENT_PK) throw new Error(MISSING_PK);
32
+ }
33
+
34
+ function agentAddress() {
35
+ const { signer } = loadShipped();
36
+ if (!process.env.HYPELENS_AGENT_PK) throw new Error(MISSING_PK);
37
+ return signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK);
38
+ }
39
+
40
+ export function approvePayloads(agentAddressArg) {
41
+ const { actions } = loadShipped();
42
+ const exchange = actions.NET[net()].exchange;
43
+ return {
44
+ net: net(),
45
+ approveAgent: actions.buildApproveAgent(net(), agentAddressArg),
46
+ approveBuilderFee: actions.buildApproveBuilderFee(net()),
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.'
54
+ };
55
+ }
56
+
57
+ export function newAgentWallet() {
58
+ const { signer, sdk } = loadShipped();
59
+ const pk = sdk.randomPrivateKey();
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
+ };
65
+ }
66
+
67
+ async function assetMeta(coin) {
68
+ const { actions } = loadShipped();
69
+ const r = await fetch(actions.NET[net()].info, {
70
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ type: 'meta' })
72
+ });
73
+ const meta = await r.json();
74
+ const name = resolveCoin(coin).toUpperCase();
75
+ const i = meta.universe.findIndex((u) => u.name === name);
76
+ if (i < 0) throw new Error('coin not on ' + net() + ': ' + coin);
77
+ return { assetIndex: i, szDecimals: meta.universe[i].szDecimals, name };
78
+ }
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
+
186
+ async function postL1(action) {
187
+ const { actions, signer } = loadShipped();
188
+ assertPlacementAllowed(actions);
189
+ const nonce = actions.nonce();
190
+ const signed = await signer.signL1(process.env.HYPELENS_AGENT_PK, action, nonce, net() === 'testnet', null);
191
+ const res = await fetch(actions.NET[net()].exchange, {
192
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
193
+ body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
194
+ });
195
+ const body = await res.json().catch(() => ({}));
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 };
199
+ }
200
+
201
+ async function clearinghouse(user) {
202
+ const { actions } = loadShipped();
203
+ const r = await fetch(actions.NET[net()].info, {
204
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
205
+ body: JSON.stringify({ type: 'clearinghouseState', user })
206
+ });
207
+ if (!r.ok) throw new Error('clearinghouseState HTTP ' + r.status);
208
+ return r.json();
209
+ }
210
+
211
+ /** Balances / margin summary for the agent (or explicit user). */
212
+ export async function getBalances({ user } = {}) {
213
+ const u = user || agentAddress();
214
+ const state = await clearinghouse(u);
215
+ const ms = state.marginSummary || {};
216
+ const equity = Number(ms.accountValue || 0);
217
+ return {
218
+ net: net(),
219
+ user: u,
220
+ marginSummary: ms,
221
+ withdrawable: state.withdrawable ?? null,
222
+ equity,
223
+ sizingWarnThresholdUsd: equity * 0.2,
224
+ sizing_rule: 'Warn if sizeUsd > 20% of account equity; size from balance; confirm mark before place.',
225
+ feeModel: '1bp on fills only — no subscription'
226
+ };
227
+ }
228
+
229
+ /** Open perp positions (assetPositions). */
230
+ export async function getPositions({ user, coin } = {}) {
231
+ const u = user || agentAddress();
232
+ const state = await clearinghouse(u);
233
+ let positions = (state.assetPositions || []).map((ap) => {
234
+ const p = ap.position || ap;
235
+ return {
236
+ coin: p.coin,
237
+ szi: Number(p.szi || 0),
238
+ entryPx: p.entryPx != null ? Number(p.entryPx) : null,
239
+ positionValue: p.positionValue != null ? Number(p.positionValue) : null,
240
+ unrealizedPnl: p.unrealizedPnl != null ? Number(p.unrealizedPnl) : null,
241
+ leverage: p.leverage || null,
242
+ liquidationPx: p.liquidationPx != null ? Number(p.liquidationPx) : null,
243
+ marginUsed: p.marginUsed != null ? Number(p.marginUsed) : null
244
+ };
245
+ }).filter((p) => p.szi !== 0);
246
+ if (coin) {
247
+ const c = resolveCoin(coin).toUpperCase();
248
+ positions = positions.filter((p) => (p.coin || '').toUpperCase() === c);
249
+ }
250
+ return { net: net(), user: u, positions };
251
+ }
252
+
253
+ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, sizeUsd = null, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
254
+ const { actions } = loadShipped();
255
+ assertPlacementAllowed(actions);
256
+ const resolved = await resolveSizeAndPx({ coin, size, entryPx, sizeUsd });
257
+ let risk = null;
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 });
261
+ if (risk.refuseReason === 'full_feed_unconfigured') {
262
+ risk = { ...risk, advisory: true, feedAdvisory: true, note: (risk.note || '') + ' Place allowed without full feed; heat is advisory.' };
263
+ }
264
+ if (risk.verdict === 'danger' && risk.refuseReason !== 'full_feed_unconfigured' && !override) {
265
+ const wallSz = risk.wall && risk.wall.sizeUsd != null
266
+ ? Math.round(risk.wall.sizeUsd / 1e6)
267
+ : (risk.wall && risk.wall.sizeUsdCoarse != null ? Math.round(risk.wall.sizeUsdCoarse / 1e6) : '?');
268
+ return {
269
+ placed: false,
270
+ refused: 'liq price ' + risk.liqPx + ' lands inside a $' + wallSz + 'M wall — pass override:true to force',
271
+ risk,
272
+ builderFeeAttached: false,
273
+ size: resolved.size,
274
+ entryPx: resolved.entryPx,
275
+ sizeUsd: resolved.sizeUsd
276
+ };
277
+ }
278
+ }
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
+ });
295
+ const posted = await postL1(action);
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;
318
+ }
319
+
320
+ /** Cancel by oid and/or cloid. */
321
+ export async function cancelOrder({ coin, oid = null, cloid = null }) {
322
+ const { actions } = loadShipped();
323
+ assertPlacementAllowed(actions);
324
+ const { assetIndex } = await assetMeta(coin);
325
+ let action;
326
+ if (cloid) action = actions.buildCancelByCloidAction([{ assetIndex, cloid }]);
327
+ else if (oid != null) action = actions.buildCancelAction([{ assetIndex, oid: Number(oid) }]);
328
+ else throw new Error('oid or cloid required — pass oid or cloid to hl_cancel_order');
329
+ const posted = await postL1(action);
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;
333
+ }
334
+
335
+ /** IOC reduce-only close for a coin (uses position size if size omitted). */
336
+ export async function closePosition({ coin, size = null, px = null, user } = {}) {
337
+ const { actions } = loadShipped();
338
+ assertPlacementAllowed(actions);
339
+ const name = resolveCoin(coin).toUpperCase();
340
+ const { positions } = await getPositions({ user, coin: name });
341
+ const pos = positions[0];
342
+ if (!pos || !pos.szi) throw new Error('no open position for ' + name + ' — check hl_positions first');
343
+ const absSz = Math.abs(pos.szi);
344
+ const closeSz = size != null ? Number(size) : absSz;
345
+ if (!(closeSz > 0) || closeSz > absSz + 1e-12) throw new Error('bad close size — omit size for full close, or size <= abs(szi)');
346
+ const isBuy = pos.szi < 0; // short -> buy to close
347
+ let entryPx = px;
348
+ if (entryPx == null) {
349
+ const { actions: a2 } = loadShipped();
350
+ const r = await fetch(a2.NET[net()].info, {
351
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
352
+ body: JSON.stringify({ type: 'metaAndAssetCtxs' })
353
+ });
354
+ const [meta, ctxs] = await r.json();
355
+ const i = meta.universe.findIndex((u) => u.name === name);
356
+ if (i < 0) throw new Error('coin not in meta: ' + name);
357
+ entryPx = Number(ctxs[i].markPx);
358
+ // slip 0.5% through for IOC fill
359
+ entryPx = isBuy ? entryPx * 1.005 : entryPx * 0.995;
360
+ }
361
+ const { assetIndex, szDecimals } = await assetMeta(name);
362
+ const action = actions.buildCloseAction({ assetIndex, szDecimals, isBuy, entryPx, size: closeSz });
363
+ const posted = await postL1(action);
364
+ return {
365
+ closed: posted.ok,
366
+ net: posted.net,
367
+ response: posted.response,
368
+ coin: name,
369
+ size: closeSz,
370
+ isBuy,
371
+ entryPx,
372
+ builderFeeAttached: posted.ok,
373
+ feeModel: '1bp on fills only — no subscription'
374
+ };
375
+ }
package/src/mcp.js CHANGED
@@ -13,11 +13,11 @@ 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' });
16
+ const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.18' });
17
17
 
18
18
  server.tool('hl_quickstart',
19
- 'CALL FIRST: place-ready bootstrap wallet + MASTER ApproveBuilderFee 1bp once, then hl_place_order sizeUsd:12. Default testnet. 1bp fills, no sub.',
20
- { coin: z.string().optional().describe("Optional coin for sample walls (default BTC)") },
19
+ 'CALL FIRST: single checklist wallet→approve→hl_balances(equity>0)→hl_place_order sizeUsd mainnet. No heat-first.',
20
+ { coin: z.string().optional().describe('Optional first-place coin (default BTC)') },
21
21
  wrap(({ coin }) => quickstart({ coin })));
22
22
 
23
23
  server.tool('hl_exchange_status',
@@ -25,16 +25,16 @@ export async function main() {
25
25
  {}, wrap(() => status()));
26
26
 
27
27
  server.tool('hl_new_agent_wallet',
28
- 'Setup: generate agent wallet (pk + address). Store as HYPELENS_AGENT_PK; master signs approvals next.',
28
+ 'Setup: new agent wallet export HYPELENS_AGENT_PK next hl_approve_payloads.',
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). Then POST both; then sizeUsd place. No sub.',
32
+ 'Setup: MASTER signs+POST approveAgent + ApproveBuilderFee once next hl_balances then hl_place_order sizeUsd:12.',
33
33
  { agentAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/) },
34
34
  wrap(({ agentAddress }) => approvePayloads(agentAddress)));
35
35
 
36
36
  server.tool('hl_balances',
37
- 'Balances / marginSummary for agent (or user). Includes 20% equity sizing warn threshold.',
37
+ 'Fund check before place. next = hl_place_order sizeUsd:12 if equity>=12, else fund agent on mainnet.',
38
38
  { user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
39
39
  wrap((a) => getBalances(a)));
40
40
 
@@ -44,7 +44,7 @@ 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. Preferred: sizeUsd (auto mark + szDecimals round; entryPx optional). Or pass size+entryPx. Heat advisory. No sub.',
47
+ 'PLACE mainnet perps (paying path). Prefer sizeUsd:12. Errors: approve missing / no margin / wrong net / retry place.',
48
48
  {
49
49
  coin: z.string().describe('e.g. BTC'),
50
50
  isBuy: z.boolean(),