@hypelens/hypelens-agent-rail 0.1.19 → 0.1.21
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 +5 -4
- package/package.json +2 -2
- package/skill/hypelens-agent-rail/SKILL.md +6 -7
- package/skill/hypelens-agent-rail/SKILL.md.bak-pre-conversion +59 -0
- package/src/core.js +57 -11
- package/src/exchange.js +68 -20
- package/src/mcp.js +6 -6
- package/vendor/hl-actions.js +5 -3
- package/src/exchange.js.base016 +0 -375
package/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
## Install → approve → place
|
|
4
4
|
```bash
|
|
5
5
|
# MCP stdio (Claude / Cursor / any MCP host)
|
|
6
|
-
npx
|
|
6
|
+
npx @hypelens/hypelens-agent-rail@0.1.21
|
|
7
7
|
|
|
8
8
|
# Claude Code one-liner
|
|
9
|
-
claude mcp add hypelens -- npx
|
|
9
|
+
claude mcp add hypelens -- npx @hypelens/hypelens-agent-rail@0.1.21
|
|
10
10
|
|
|
11
11
|
# OpenClaw / ClawHub skill (skill slug, not npm alone)
|
|
12
12
|
clawhub install hypelens-agent-rail
|
|
@@ -15,9 +15,10 @@ npx skills add polyparlay/hypelens -s hypelens-agent-rail -y
|
|
|
15
15
|
```
|
|
16
16
|
1. `hl_quickstart` (optional) → `hl_new_agent_wallet` → `HYPELENS_AGENT_PK`
|
|
17
17
|
2. `hl_approve_payloads(agentAddress)` — **MASTER** signs approveAgent + ApproveBuilderFee **0.01%** once, then POST both
|
|
18
|
-
3. `
|
|
18
|
+
3. `hl_balances` — if `equity=0`, ask user to deposit **USDC** to `fundHow.depositAddress` on [app.hyperliquid.xyz](https://app.hyperliquid.xyz) (~$15+), then recheck
|
|
19
|
+
4. `hl_place_order` / `hl_cancel_order` / `hl_close_position` / `hl_positions` — place only when the user asks to trade
|
|
19
20
|
|
|
20
|
-
**First place (mainnet default):** after approve, `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})` — **sizeUsd alone**
|
|
21
|
+
**First place (mainnet default):** after approve, `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})` — **sizeUsd alone** = **IOC / market** at mark ± ~0.5% slip (same pattern as close) so fills land and builder fees accrue. Pass explicit `entryPx` only for a **GTC limit**. No feed required. On reject after sizeUsd, response includes `error` + `next` (margin / approve / min notional).
|
|
21
22
|
|
|
22
23
|
Default **mainnet**. Practice: `HYPELENS_NET=testnet`. Fee: **1bp on fills** — no sub. Missing `HYPELENS_AGENT_PK` errors point at the same path.
|
|
23
24
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypelens/hypelens-agent-rail",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.21",
|
|
4
|
+
"description": "Mainnet place rail. nextTool: wallet→approve→hl_balances(+fundHow if equity=0)→hl_place_order sizeUsd on user trading intent. 1bp builder. npx @hypelens/hypelens-agent-rail@0.1.21",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -4,8 +4,8 @@ description: >-
|
|
|
4
4
|
Trade Hyperliquid perpetual futures: place, cancel, close orders; check
|
|
5
5
|
balances and positions with P&L. Use when the user asks about Hyperliquid
|
|
6
6
|
trading, portfolio status, crypto positions, or wants to execute trades.
|
|
7
|
-
|
|
8
|
-
1bp → hl_balances → place. Keywords: hyperliquid trading place cancel close
|
|
7
|
+
Place only when the user asks to trade. Wallet → approve
|
|
8
|
+
1bp → hl_balances → place on intent. Keywords: hyperliquid trading place cancel close
|
|
9
9
|
perps hl_place_order.
|
|
10
10
|
topics: [hyperliquid, trading, place, order, cancel]
|
|
11
11
|
metadata:
|
|
@@ -26,7 +26,7 @@ metadata:
|
|
|
26
26
|
|
|
27
27
|
**Use when:** Hyperliquid trading, portfolio/balances, positions with P&L, or place / cancel / close perps.
|
|
28
28
|
|
|
29
|
-
**
|
|
29
|
+
**Consent:** Only call `hl_place_order` when the user asks to **trade**, **place**, or **execute**. Prefer sizeUsd IOC path. Heat tools are optional for sizing.
|
|
30
30
|
|
|
31
31
|
## Install
|
|
32
32
|
|
|
@@ -34,21 +34,20 @@ metadata:
|
|
|
34
34
|
clawhub install hyperliquid-place
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
MCP: `npx
|
|
37
|
+
MCP: `npx @hypelens/hypelens-agent-rail@0.1.21`
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## Optional setup (when the user wants to trade)
|
|
40
40
|
|
|
41
41
|
0. `hl_quickstart` — read checklist, then call `nextTool`
|
|
42
42
|
1. `hl_new_agent_wallet` → `export HYPELENS_AGENT_PK=<pk>`
|
|
43
43
|
2. `hl_approve_payloads(agentAddress)` → MASTER signs+POST **both** approveAgent + ApproveBuilderFee 1bp
|
|
44
44
|
3. `hl_balances` — if equity=0, fund agent on **mainnet**, call `hl_balances` again
|
|
45
|
-
4. `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})` —
|
|
45
|
+
4. `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})` — IOC/market only after user trading intent
|
|
46
46
|
|
|
47
47
|
## $0-fee traps (avoid)
|
|
48
48
|
|
|
49
49
|
- `HYPELENS_NET=testnet` before first place
|
|
50
50
|
- Skipping ApproveBuilderFee (agent-only approve)
|
|
51
|
-
- Stopping at wallet / approve / balances without place
|
|
52
51
|
- Calling `hl_walls` / `hl_cascade` / `hl_whale_book` / `hl_pretrade_check` before first place
|
|
53
52
|
|
|
54
53
|
Default net **mainnet** (1bp builder). Paper only after first place: `HYPELENS_NET=testnet`.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Hyperliquid Place
|
|
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.
|
|
8
|
+
metadata:
|
|
9
|
+
openclaw:
|
|
10
|
+
emoji: "📈"
|
|
11
|
+
requires:
|
|
12
|
+
bins:
|
|
13
|
+
- npx
|
|
14
|
+
envVars:
|
|
15
|
+
- name: HYPELENS_NET
|
|
16
|
+
required: false
|
|
17
|
+
description: testnet (default) or mainnet
|
|
18
|
+
- name: HYPELENS_AGENT_PK
|
|
19
|
+
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)
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# HypeLens Agent Rail — Place / Cancel / Close Hyperliquid Perps
|
|
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.**
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx -y @hypelens/hypelens-agent-rail
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**ClawHub / OpenClaw:** `clawhub install hypelens-agent-rail` · **skills.sh:** `npx skills add polyparlay/hypelens`
|
|
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`
|
|
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.
|
|
44
|
+
|
|
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
|
+
|
|
47
|
+
## Sizing rule
|
|
48
|
+
|
|
49
|
+
Warn if notional `sizeUsd` is more than **20% of account equity**. Size from balance; confirm mark before place.
|
|
50
|
+
|
|
51
|
+
## Tools
|
|
52
|
+
|
|
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
|
|
58
|
+
|
|
59
|
+
hyperliquid · place · cancel · close · order · perps · hl_place_order · mcp · openclaw · builder
|
package/src/core.js
CHANGED
|
@@ -445,7 +445,7 @@ export async function pretradeCheckFull(args) {
|
|
|
445
445
|
|
|
446
446
|
/** First-discovery: single ordered checklist to first MAINNET paying place. No heat-first. */
|
|
447
447
|
export async function quickstart({ coin = 'BTC' } = {}) {
|
|
448
|
-
const { actions } = loadShipped();
|
|
448
|
+
const { actions, signer } = loadShipped();
|
|
449
449
|
const net = process.env.HYPELENS_NET || 'mainnet';
|
|
450
450
|
const hasPk = Boolean(process.env.HYPELENS_AGENT_PK);
|
|
451
451
|
const first_place_example = { coin: (coin || 'BTC').toUpperCase(), isBuy: true, sizeUsd: 12, leverage: 2 };
|
|
@@ -453,17 +453,61 @@ export async function quickstart({ coin = 'BTC' } = {}) {
|
|
|
453
453
|
const checklist = [
|
|
454
454
|
'hl_new_agent_wallet → export HYPELENS_AGENT_PK',
|
|
455
455
|
'hl_approve_payloads(agentAddress) → MASTER signs+POST approveAgent + ApproveBuilderFee once',
|
|
456
|
-
'hl_balances —
|
|
457
|
-
placeCall + ' on mainnet'
|
|
456
|
+
'hl_balances — if equity=0 ask user to deposit USDC to agent address on app.hyperliquid.xyz, then recheck',
|
|
457
|
+
placeCall + ' on mainnet — only when user asked to trade/place/execute'
|
|
458
458
|
];
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
459
|
+
let equity = null;
|
|
460
|
+
let agentAddress = null;
|
|
461
|
+
let fundHow = null;
|
|
462
|
+
if (hasPk) {
|
|
463
|
+
try {
|
|
464
|
+
agentAddress = signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK);
|
|
465
|
+
const r = await fetch(actions.NET[net].info, {
|
|
466
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
467
|
+
body: JSON.stringify({ type: 'clearinghouseState', user: agentAddress })
|
|
468
|
+
});
|
|
469
|
+
if (r.ok) {
|
|
470
|
+
const state = await r.json();
|
|
471
|
+
equity = Number((state.marginSummary || {}).accountValue || 0);
|
|
472
|
+
if (!(equity > 0)) {
|
|
473
|
+
fundHow = {
|
|
474
|
+
depositAddress: agentAddress,
|
|
475
|
+
asset: 'USDC',
|
|
476
|
+
venue: 'Hyperliquid ' + net,
|
|
477
|
+
app: net === 'testnet' ? 'https://app.hyperliquid-testnet.xyz' : 'https://app.hyperliquid.xyz',
|
|
478
|
+
minUsdSuggest: 15
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch (_) { /* probe optional */ }
|
|
483
|
+
}
|
|
484
|
+
let nextTool;
|
|
485
|
+
let next;
|
|
486
|
+
let funnel_step;
|
|
487
|
+
if (!hasPk) {
|
|
488
|
+
nextTool = 'hl_new_agent_wallet';
|
|
489
|
+
next = 'hl_new_agent_wallet → export HYPELENS_AGENT_PK (then call nextTool only)';
|
|
490
|
+
funnel_step = 1;
|
|
491
|
+
} else if (equity != null && equity > 0) {
|
|
492
|
+
// Funded → place path (still consent-gated by skill). ApproveBuilderFee should already be done.
|
|
493
|
+
nextTool = 'hl_place_order';
|
|
494
|
+
next = 'Funded (equity=' + equity + '). If ApproveBuilderFee not yet POSTed, do hl_approve_payloads first; else ' + placeCall + ' — only if user asked to trade/place/execute';
|
|
495
|
+
funnel_step = 4;
|
|
496
|
+
} else {
|
|
497
|
+
// Unknown or zero equity: keep ApproveBuilderFee in-path (skipping it = $0 builder fees), attach fundHow for the deposit dropoff.
|
|
498
|
+
nextTool = 'hl_approve_payloads';
|
|
499
|
+
next = 'hl_approve_payloads(agentAddress) → MASTER POST both → hl_balances'
|
|
500
|
+
+ (equity === 0 ? ' (equity=0 now — after approve ask user to deposit USDC to ' + agentAddress + '; see fundHow)' : '')
|
|
501
|
+
+ ' → ' + placeCall + ' on user trading intent';
|
|
502
|
+
funnel_step = equity === 0 ? 3 : 2;
|
|
503
|
+
}
|
|
463
504
|
return {
|
|
464
505
|
first_success: 'mainnet place',
|
|
465
506
|
net,
|
|
466
507
|
hasAgentKey: hasPk,
|
|
508
|
+
agentAddress,
|
|
509
|
+
equity,
|
|
510
|
+
fundHow,
|
|
467
511
|
mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
|
|
468
512
|
builder_fee: '1bp (0.01%) on fills',
|
|
469
513
|
package: '@hypelens/hypelens-agent-rail',
|
|
@@ -471,14 +515,16 @@ export async function quickstart({ coin = 'BTC' } = {}) {
|
|
|
471
515
|
checklist,
|
|
472
516
|
steps: checklist,
|
|
473
517
|
first_place_example,
|
|
474
|
-
funnel_step
|
|
518
|
+
funnel_step,
|
|
475
519
|
nextTool,
|
|
476
520
|
next,
|
|
477
521
|
next_if_blocked: !hasPk
|
|
478
522
|
? 'approve missing — ' + next
|
|
479
|
-
:
|
|
523
|
+
: (equity === 0
|
|
524
|
+
? 'needs_user_fund — deposit USDC to agentAddress then hl_balances'
|
|
525
|
+
: 'hl_balances (equity>0) → then ' + placeCall + ' on mainnet when user asks to trade'),
|
|
480
526
|
heat_optional_after_place: true,
|
|
481
|
-
dropoff_trap: 'testnet / skip ApproveBuilderFee / equity=0 / heat-first = $0 fees',
|
|
482
|
-
note: '
|
|
527
|
+
dropoff_trap: 'testnet / skip ApproveBuilderFee / equity=0 (no fundHow shown) / heat-first = $0 fees',
|
|
528
|
+
note: 'Call one nextTool. Place only on explicit user trading intent. No heat before first place.'
|
|
483
529
|
};
|
|
484
530
|
}
|
package/src/exchange.js
CHANGED
|
@@ -60,13 +60,21 @@ export function approvePayloads(agentAddressArg) {
|
|
|
60
60
|
steps: [
|
|
61
61
|
'MASTER signs approveAgent + ApproveBuilderFee (EIP-712) — not agent pk',
|
|
62
62
|
'POST both to ' + exchange,
|
|
63
|
-
'Then call ONLY hl_balances'
|
|
63
|
+
'Then call ONLY hl_balances',
|
|
64
|
+
'If equity=0: ask user to deposit USDC to agentAddress on Hyperliquid (app.hyperliquid.xyz), then hl_balances again',
|
|
65
|
+
'When equity>0 and user asked to trade: hl_place_order sizeUsd:12'
|
|
64
66
|
],
|
|
65
67
|
nextTool: 'hl_balances',
|
|
66
|
-
next: 'After MASTER POST both →
|
|
68
|
+
next: 'After MASTER POST both → hl_balances. If equity=0 show depositAddress and ask user to fund USDC, then hl_balances again.',
|
|
67
69
|
nextCalls: ['hl_balances'],
|
|
68
70
|
nextPlace,
|
|
69
|
-
|
|
71
|
+
fundAfterApprove: {
|
|
72
|
+
depositAddress: agentAddressArg,
|
|
73
|
+
asset: 'USDC',
|
|
74
|
+
app: net() === 'testnet' ? 'https://app.hyperliquid-testnet.xyz' : 'https://app.hyperliquid.xyz',
|
|
75
|
+
minUsdSuggest: 15
|
|
76
|
+
},
|
|
77
|
+
note: 'Skipping ApproveBuilderFee = $0 fees. After POST: hl_balances → fund if equity=0 → place on ' + net() + ' only with user trading intent.'
|
|
70
78
|
};
|
|
71
79
|
}
|
|
72
80
|
|
|
@@ -78,7 +86,7 @@ export function newAgentWallet() {
|
|
|
78
86
|
address: signer.addressFromPrivateKey(pk),
|
|
79
87
|
funnel_step: 2,
|
|
80
88
|
nextTool: 'hl_approve_payloads',
|
|
81
|
-
next: 'export HYPELENS_AGENT_PK=<privateKey>; call hl_approve_payloads(address)
|
|
89
|
+
next: 'export HYPELENS_AGENT_PK=<privateKey>; call hl_approve_payloads(address) — when user wants to trade',
|
|
82
90
|
note: 'Do not call heat tools. Next ONE tool = hl_approve_payloads.'
|
|
83
91
|
};
|
|
84
92
|
}
|
|
@@ -115,23 +123,39 @@ async function assetMetaAndMark(coin) {
|
|
|
115
123
|
|
|
116
124
|
/**
|
|
117
125
|
* Resolve coin size + entryPx. Preferred first-place path: sizeUsd only
|
|
118
|
-
* (fetches mark, size = sizeUsd/mark, rounds to szDecimals
|
|
126
|
+
* (fetches mark, size = sizeUsd/mark, rounds to szDecimals, IOC ~0.5% slip).
|
|
127
|
+
* Explicit entryPx = GTC limit intent (no slip).
|
|
128
|
+
* @param {{coin, size, entryPx, sizeUsd, isBuy}} opts
|
|
119
129
|
*/
|
|
120
|
-
async function resolveSizeAndPx({ coin, size, entryPx, sizeUsd }) {
|
|
130
|
+
async function resolveSizeAndPx({ coin, size, entryPx, sizeUsd, isBuy = true }) {
|
|
121
131
|
const { actions } = loadShipped();
|
|
122
|
-
const
|
|
132
|
+
const limitIntent = entryPx != null;
|
|
133
|
+
const needMark = !limitIntent || (size == null && sizeUsd != null);
|
|
123
134
|
const meta = needMark ? await assetMetaAndMark(coin) : await assetMeta(coin);
|
|
124
|
-
let
|
|
135
|
+
let markPx = meta.markPx ?? null;
|
|
136
|
+
let px;
|
|
137
|
+
let tif;
|
|
138
|
+
if (limitIntent) {
|
|
139
|
+
px = Number(entryPx);
|
|
140
|
+
tif = 'Gtc';
|
|
141
|
+
} else {
|
|
142
|
+
// Market/IOC path: same 0.5% slip through mark as closePosition
|
|
143
|
+
if (!(markPx > 0)) throw new Error('entryPx required (or omit and pass sizeUsd for auto mark)');
|
|
144
|
+
px = isBuy ? markPx * 1.005 : markPx * 0.995;
|
|
145
|
+
tif = 'Ioc';
|
|
146
|
+
}
|
|
125
147
|
if (!(px > 0)) throw new Error('entryPx required (or omit and pass sizeUsd for auto mark)');
|
|
126
148
|
let sz = size != null ? Number(size) : null;
|
|
127
149
|
let usedSizeUsd = sizeUsd != null ? Number(sizeUsd) : null;
|
|
150
|
+
// Size from notional uses mark (true market size), not slipped px
|
|
151
|
+
const sizePx = (!limitIntent && markPx > 0) ? markPx : px;
|
|
128
152
|
if (sz == null) {
|
|
129
153
|
if (!(usedSizeUsd > 0)) {
|
|
130
|
-
throw new Error('size or sizeUsd required — first place: {coin:"BTC", isBuy:true, sizeUsd:12, leverage:2} (
|
|
154
|
+
throw new Error('size or sizeUsd required — first place: {coin:"BTC", isBuy:true, sizeUsd:12, leverage:2} (IOC @ mark+0.5% slip; pass entryPx for GTC limit)');
|
|
131
155
|
}
|
|
132
|
-
sz = usedSizeUsd /
|
|
156
|
+
sz = usedSizeUsd / sizePx;
|
|
133
157
|
} else if (usedSizeUsd == null) {
|
|
134
|
-
usedSizeUsd = sz *
|
|
158
|
+
usedSizeUsd = sz * sizePx;
|
|
135
159
|
}
|
|
136
160
|
sz = actions.roundToDecimals(sz, meta.szDecimals);
|
|
137
161
|
// Validate wire size will not collapse to 0
|
|
@@ -146,8 +170,10 @@ async function resolveSizeAndPx({ coin, size, entryPx, sizeUsd }) {
|
|
|
146
170
|
size: sz,
|
|
147
171
|
entryPx: px,
|
|
148
172
|
sizeUsd: usedSizeUsd,
|
|
149
|
-
markPx
|
|
150
|
-
|
|
173
|
+
markPx,
|
|
174
|
+
tif,
|
|
175
|
+
limitIntent,
|
|
176
|
+
autoRounded: size == null || !limitIntent
|
|
151
177
|
};
|
|
152
178
|
}
|
|
153
179
|
|
|
@@ -179,7 +205,7 @@ function interpretExchangeResult(body) {
|
|
|
179
205
|
function hintFromHlError(msg) {
|
|
180
206
|
const m = String(msg || '').toLowerCase();
|
|
181
207
|
if (m.includes('insufficient') || m.includes('margin') || m.includes('not enough') || m.includes('balance')) {
|
|
182
|
-
return 'no margin —
|
|
208
|
+
return 'no margin — ask user to deposit USDC to the agent address on Hyperliquid mainnet (app.hyperliquid.xyz), hl_balances until equity>0, then retry place hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})';
|
|
183
209
|
}
|
|
184
210
|
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')))) {
|
|
185
211
|
return 'approve missing — hl_approve_payloads → MASTER POST approveAgent+ApproveBuilderFee → hl_balances → hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})';
|
|
@@ -223,6 +249,20 @@ export async function getBalances({ user } = {}) {
|
|
|
223
249
|
const equity = Number(ms.accountValue || 0);
|
|
224
250
|
const nextPlace = { coin: 'BTC', isBuy: true, sizeUsd: 12, leverage: 2 };
|
|
225
251
|
const funded = equity > 0;
|
|
252
|
+
const fundHow = funded ? null : {
|
|
253
|
+
depositAddress: u,
|
|
254
|
+
asset: 'USDC',
|
|
255
|
+
venue: 'Hyperliquid ' + net(),
|
|
256
|
+
app: net() === 'testnet' ? 'https://app.hyperliquid-testnet.xyz' : 'https://app.hyperliquid.xyz',
|
|
257
|
+
minUsdSuggest: 15,
|
|
258
|
+
steps: [
|
|
259
|
+
'Show depositAddress to the user — this is the agent wallet that needs USDC',
|
|
260
|
+
'Ask user to deposit USDC on Hyperliquid ' + net() + ' to depositAddress (app link above), or transfer from their master account',
|
|
261
|
+
'Minimum ~$15 USDC covers first place sizeUsd:12',
|
|
262
|
+
'After user confirms deposit landed, call hl_balances again',
|
|
263
|
+
'When equity>0 AND user asked to trade/place/execute: hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2})'
|
|
264
|
+
]
|
|
265
|
+
};
|
|
226
266
|
return {
|
|
227
267
|
net: net(),
|
|
228
268
|
user: u,
|
|
@@ -233,13 +273,17 @@ export async function getBalances({ user } = {}) {
|
|
|
233
273
|
sizing_rule: 'Warn if sizeUsd > 20% of account equity; size from balance; confirm mark before place.',
|
|
234
274
|
feeModel: '1bp on fills only — no subscription',
|
|
235
275
|
funnel_step: funded ? 4 : 3,
|
|
276
|
+
blocker: funded ? null : 'needs_user_fund',
|
|
277
|
+
fundHow,
|
|
236
278
|
nextTool: funded ? 'hl_place_order' : 'hl_balances',
|
|
237
279
|
next: funded
|
|
238
|
-
? 'hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) —
|
|
239
|
-
: 'no margin —
|
|
280
|
+
? 'hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) — only if user asked to trade/place/execute'
|
|
281
|
+
: 'no margin — ask user to deposit USDC to ' + u + ' on Hyperliquid ' + net() + ' (see fundHow), then hl_balances again',
|
|
240
282
|
nextPlace,
|
|
241
283
|
first_success: 'mainnet place',
|
|
242
|
-
note: funded
|
|
284
|
+
note: funded
|
|
285
|
+
? 'Funded. Place only on explicit user trading intent; then ONE next tool = hl_place_order.'
|
|
286
|
+
: 'Unfunded = $0 fees. Conversion blocker is deposit — show fundHow.depositAddress to user, then re-check hl_balances.'
|
|
243
287
|
};
|
|
244
288
|
}
|
|
245
289
|
|
|
@@ -274,14 +318,14 @@ export async function getPositions({ user, coin } = {}) {
|
|
|
274
318
|
next: positions.length
|
|
275
319
|
? 'optional hl_close_position / hl_cancel_order — or another ' + placeCall
|
|
276
320
|
: 'no positions yet — call hl_balances then ' + placeCall + ' (do not open heat tools)',
|
|
277
|
-
note: 'Paying path = mainnet place
|
|
321
|
+
note: 'Paying path = mainnet place on explicit user trading intent.'
|
|
278
322
|
};
|
|
279
323
|
}
|
|
280
324
|
|
|
281
325
|
export async function placeOrder({ coin, isBuy, size = null, entryPx = null, sizeUsd = null, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
|
|
282
326
|
const { actions } = loadShipped();
|
|
283
327
|
assertPlacementAllowed(actions);
|
|
284
|
-
const resolved = await resolveSizeAndPx({ coin, size, entryPx, sizeUsd });
|
|
328
|
+
const resolved = await resolveSizeAndPx({ coin, size, entryPx, sizeUsd, isBuy });
|
|
285
329
|
let risk = null;
|
|
286
330
|
const lev = leverage != null ? Number(leverage) : null;
|
|
287
331
|
if (!skipRiskCheck && lev) {
|
|
@@ -320,7 +364,8 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
|
|
|
320
364
|
entryPx: resolved.entryPx,
|
|
321
365
|
size: resolved.size,
|
|
322
366
|
slPx,
|
|
323
|
-
tpPx
|
|
367
|
+
tpPx,
|
|
368
|
+
tif: resolved.tif
|
|
324
369
|
});
|
|
325
370
|
const posted = await postL1(action);
|
|
326
371
|
const out = {
|
|
@@ -333,6 +378,9 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
|
|
|
333
378
|
size: resolved.size,
|
|
334
379
|
entryPx: resolved.entryPx,
|
|
335
380
|
sizeUsd: resolved.sizeUsd,
|
|
381
|
+
markPx: resolved.markPx,
|
|
382
|
+
tif: resolved.tif,
|
|
383
|
+
limitIntent: resolved.limitIntent,
|
|
336
384
|
autoRounded: resolved.autoRounded,
|
|
337
385
|
sizingWarn,
|
|
338
386
|
feeModel: '1bp on fills only — no subscription'
|
package/src/mcp.js
CHANGED
|
@@ -32,10 +32,10 @@ const heatAfterPlace = (fn) => async (args) => {
|
|
|
32
32
|
};
|
|
33
33
|
|
|
34
34
|
export async function main() {
|
|
35
|
-
const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.
|
|
35
|
+
const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.20' });
|
|
36
36
|
|
|
37
37
|
server.tool('hl_quickstart',
|
|
38
|
-
'CALL FIRST:
|
|
38
|
+
'CALL FIRST: checklist wallet→approve→hl_balances(equity>0 / fundHow if 0)→hl_place_order sizeUsd mainnet when user asks to trade. No heat-first.',
|
|
39
39
|
{ coin: z.string().optional().describe('Optional first-place coin (default BTC)') },
|
|
40
40
|
wrap(({ coin }) => quickstart({ coin })));
|
|
41
41
|
|
|
@@ -53,7 +53,7 @@ export async function main() {
|
|
|
53
53
|
wrap(({ agentAddress }) => approvePayloads(agentAddress)));
|
|
54
54
|
|
|
55
55
|
server.tool('hl_balances',
|
|
56
|
-
'Fund check before place.
|
|
56
|
+
'Fund check before place. If equity=0 returns fundHow.depositAddress — ask user to deposit USDC, then recheck. If equity>0 nextTool=hl_place_order (only with user trading intent).',
|
|
57
57
|
{ user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
|
|
58
58
|
wrap((a) => getBalances(a)));
|
|
59
59
|
|
|
@@ -63,13 +63,13 @@ export async function main() {
|
|
|
63
63
|
wrap((a) => getPositions(a)));
|
|
64
64
|
|
|
65
65
|
server.tool('hl_place_order',
|
|
66
|
-
'PLACE mainnet perps (paying path). Prefer sizeUsd:12. Errors: approve missing / no margin / wrong net / retry place.',
|
|
66
|
+
'PLACE mainnet perps (paying path). Prefer sizeUsd:12 (IOC/market). Pass entryPx for GTC limit. Errors: approve missing / no margin / wrong net / retry place.',
|
|
67
67
|
{
|
|
68
68
|
coin: z.string().describe('e.g. BTC'),
|
|
69
69
|
isBuy: z.boolean(),
|
|
70
|
-
sizeUsd: z.number().positive().optional().describe('Preferred first-place notional USD —
|
|
70
|
+
sizeUsd: z.number().positive().optional().describe('Preferred first-place notional USD — IOC @ mark±0.5% slip + size round (e.g. 12)'),
|
|
71
71
|
size: z.number().positive().optional().describe('Coin units; optional if sizeUsd set'),
|
|
72
|
-
entryPx: z.number().positive().optional().describe('
|
|
72
|
+
entryPx: z.number().positive().optional().describe('GTC limit px; omit for IOC market (~0.5% slip from mark)'),
|
|
73
73
|
slPx: z.number().positive().optional(), tpPx: z.number().positive().optional(),
|
|
74
74
|
leverage: z.number().positive().optional().describe('Optional; enables advisory pre-trade risk check (first place: 2)'),
|
|
75
75
|
override: z.boolean().optional(), skipRiskCheck: z.boolean().optional()
|
package/vendor/hl-actions.js
CHANGED
|
@@ -82,15 +82,17 @@
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// ==== L1 (agent-signed) ORDER action with normalTpsl grouping + builder ====
|
|
85
|
-
// plan: { assetIndex, szDecimals, isBuy, entryPx, size, slPx?, tpPx? }
|
|
85
|
+
// plan: { assetIndex, szDecimals, isBuy, entryPx, size, slPx?, tpPx?, tif? }
|
|
86
86
|
function buildOrderAction(plan) {
|
|
87
87
|
if (plan.assetIndex == null || plan.assetIndex < 0) throw new Error('bad assetIndex');
|
|
88
88
|
if (!(plan.size > 0)) throw new Error('bad size');
|
|
89
89
|
const szDec = plan.szDecimals | 0;
|
|
90
90
|
const s = sizeToWire(plan.size, szDec);
|
|
91
|
+
const rawTif = String(plan.tif || 'Gtc');
|
|
92
|
+
const tif = /^ioc$/i.test(rawTif) ? 'Ioc' : 'Gtc';
|
|
91
93
|
const orders = [];
|
|
92
|
-
// 1) entry — GTC limit
|
|
93
|
-
orders.push({ a: plan.assetIndex, b: !!plan.isBuy, p: priceToWire(plan.entryPx, szDec), s, r: false, t: { limit: { tif:
|
|
94
|
+
// 1) entry — IOC when plan.tif=Ioc (market/sizeUsd), else GTC limit
|
|
95
|
+
orders.push({ a: plan.assetIndex, b: !!plan.isBuy, p: priceToWire(plan.entryPx, szDec), s, r: false, t: { limit: { tif: tif } } });
|
|
94
96
|
// 2) SL — reduceOnly stop-market trigger (opposite side)
|
|
95
97
|
if (plan.slPx != null) {
|
|
96
98
|
orders.push({ a: plan.assetIndex, b: !plan.isBuy, p: priceToWire(plan.slPx, szDec), s, r: true,
|
package/src/exchange.js.base016
DELETED
|
@@ -1,375 +0,0 @@
|
|
|
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
|
-
}
|