@hypelens/hypelens-agent-rail 0.1.15 → 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 +11 -2
- package/package.json +3 -3
- package/skill/hypelens-agent-rail/SKILL.md +29 -33
- package/src/core.js +28 -52
- package/src/exchange.js +81 -15
- package/src/exchange.js.base016 +375 -0
- package/src/mcp.js +7 -7
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
16
|
1. `hl_quickstart` (optional) → `hl_new_agent_wallet` → `HYPELENS_AGENT_PK`
|
|
8
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 (
|
|
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).
|
|
12
21
|
|
|
13
|
-
Default **
|
|
22
|
+
Default **mainnet**. Practice: `HYPELENS_NET=testnet`. 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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypelens/hypelens-agent-rail",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
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
|
|
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:
|
|
2
|
+
name: Place Hyperliquid
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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:
|
|
18
|
+
description: "mainnet (default fee path) or testnet escape"
|
|
18
19
|
- name: HYPELENS_AGENT_PK
|
|
19
20
|
required: false
|
|
20
|
-
description: agent
|
|
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
|
-
#
|
|
24
|
+
# Place Hyperliquid
|
|
25
|
+
|
|
26
|
+
**Use when:** place / cancel / close Hyperliquid perps, or check positions and balances.
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
## Install
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
|
|
31
|
+
clawhub install hyperliquid-place
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
|
|
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
|
-
|
|
36
|
+
```bash
|
|
37
|
+
npx -y @hypelens/hypelens-agent-rail
|
|
38
|
+
```
|
|
44
39
|
|
|
45
|
-
|
|
40
|
+
## First success = paying mainnet place
|
|
46
41
|
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
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
|
-
|
|
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:
|
|
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 || '
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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
|
-
|
|
480
|
-
mainnet_placement_blocked: !actions.MAINNET_PLACEMENT_ENABLED,
|
|
465
|
+
hasAgentKey: hasPk,
|
|
481
466
|
mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
'
|
|
491
|
-
'
|
|
492
|
-
|
|
493
|
-
|
|
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 || '
|
|
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 = '
|
|
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('
|
|
29
|
+
throw new Error('wrong net — MAINNET_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
|
|
48
|
+
fee: '1bp on fills (no sub)',
|
|
48
49
|
steps: [
|
|
49
|
-
'
|
|
50
|
-
'POST
|
|
51
|
-
'
|
|
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
|
-
|
|
55
|
+
next: 'hl_balances → hl_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
|
-
|
|
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
|
|
|
@@ -132,6 +138,45 @@ async function resolveSizeAndPx({ coin, size, entryPx, sizeUsd }) {
|
|
|
132
138
|
};
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
/** Extract HL place/cancel wire error + actionable next after sizeUsd resolve. */
|
|
142
|
+
function interpretExchangeResult(body) {
|
|
143
|
+
if (!body || typeof body !== 'object') {
|
|
144
|
+
return { ok: false, error: 'empty exchange response', next: 'retry place — hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) on mainnet' };
|
|
145
|
+
}
|
|
146
|
+
if (body.status === 'err' || body.status === 'error') {
|
|
147
|
+
const raw = typeof body.response === 'string' ? body.response : JSON.stringify(body.response || body);
|
|
148
|
+
return { ok: false, error: raw, next: hintFromHlError(raw) };
|
|
149
|
+
}
|
|
150
|
+
const statuses = body?.response?.data?.statuses;
|
|
151
|
+
if (Array.isArray(statuses)) {
|
|
152
|
+
for (const s of statuses) {
|
|
153
|
+
if (s && typeof s.error === 'string' && s.error) {
|
|
154
|
+
return { ok: false, error: s.error, next: hintFromHlError(s.error), statuses };
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// resting / filled / etc
|
|
158
|
+
const resting = statuses.find((s) => s && (s.resting || s.filled));
|
|
159
|
+
if (resting) return { ok: true, error: null, next: null, statuses };
|
|
160
|
+
if (statuses.length) return { ok: true, error: null, next: null, statuses };
|
|
161
|
+
}
|
|
162
|
+
if (body.status === 'ok') return { ok: true, error: null, next: null };
|
|
163
|
+
return { ok: false, error: JSON.stringify(body), next: hintFromHlError(JSON.stringify(body)) };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function hintFromHlError(msg) {
|
|
167
|
+
const m = String(msg || '').toLowerCase();
|
|
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})';
|
|
170
|
+
}
|
|
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})';
|
|
173
|
+
}
|
|
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';
|
|
176
|
+
}
|
|
177
|
+
return 'retry place — hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) on mainnet (omit entryPx; sizeUsd auto-round)';
|
|
178
|
+
}
|
|
179
|
+
|
|
135
180
|
async function postL1(action) {
|
|
136
181
|
const { actions, signer } = loadShipped();
|
|
137
182
|
assertPlacementAllowed(actions);
|
|
@@ -142,7 +187,9 @@ async function postL1(action) {
|
|
|
142
187
|
body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
|
|
143
188
|
});
|
|
144
189
|
const body = await res.json().catch(() => ({}));
|
|
145
|
-
|
|
190
|
+
const interpreted = interpretExchangeResult(body);
|
|
191
|
+
const ok = res.ok && interpreted.ok;
|
|
192
|
+
return { ok, net: net(), response: body, error: interpreted.error, next: interpreted.next, statuses: interpreted.statuses || null };
|
|
146
193
|
}
|
|
147
194
|
|
|
148
195
|
async function clearinghouse(user) {
|
|
@@ -161,6 +208,8 @@ export async function getBalances({ user } = {}) {
|
|
|
161
208
|
const state = await clearinghouse(u);
|
|
162
209
|
const ms = state.marginSummary || {};
|
|
163
210
|
const equity = Number(ms.accountValue || 0);
|
|
211
|
+
const nextPlace = { coin: 'BTC', isBuy: true, sizeUsd: 12, leverage: 2 };
|
|
212
|
+
const funded = equity > 0;
|
|
164
213
|
return {
|
|
165
214
|
net: net(),
|
|
166
215
|
user: u,
|
|
@@ -169,7 +218,12 @@ export async function getBalances({ user } = {}) {
|
|
|
169
218
|
equity,
|
|
170
219
|
sizingWarnThresholdUsd: equity * 0.2,
|
|
171
220
|
sizing_rule: 'Warn if sizeUsd > 20% of account equity; size from balance; confirm mark before place.',
|
|
172
|
-
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'
|
|
173
227
|
};
|
|
174
228
|
}
|
|
175
229
|
|
|
@@ -214,12 +268,14 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
|
|
|
214
268
|
: (risk.wall && risk.wall.sizeUsdCoarse != null ? Math.round(risk.wall.sizeUsdCoarse / 1e6) : '?');
|
|
215
269
|
return {
|
|
216
270
|
placed: false,
|
|
271
|
+
error: 'liq price ' + risk.liqPx + ' lands inside a $' + wallSz + 'M wall',
|
|
217
272
|
refused: 'liq price ' + risk.liqPx + ' lands inside a $' + wallSz + 'M wall — pass override:true to force',
|
|
218
273
|
risk,
|
|
219
274
|
builderFeeAttached: false,
|
|
220
275
|
size: resolved.size,
|
|
221
276
|
entryPx: resolved.entryPx,
|
|
222
|
-
sizeUsd: resolved.sizeUsd
|
|
277
|
+
sizeUsd: resolved.sizeUsd,
|
|
278
|
+
next: 'hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2, override:true})'
|
|
223
279
|
};
|
|
224
280
|
}
|
|
225
281
|
}
|
|
@@ -240,7 +296,7 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
|
|
|
240
296
|
tpPx
|
|
241
297
|
});
|
|
242
298
|
const posted = await postL1(action);
|
|
243
|
-
|
|
299
|
+
const out = {
|
|
244
300
|
placed: posted.ok,
|
|
245
301
|
net: posted.net,
|
|
246
302
|
response: posted.response,
|
|
@@ -254,6 +310,14 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
|
|
|
254
310
|
sizingWarn,
|
|
255
311
|
feeModel: '1bp on fills only — no subscription'
|
|
256
312
|
};
|
|
313
|
+
if (!posted.ok) {
|
|
314
|
+
out.error = posted.error || 'place rejected by exchange';
|
|
315
|
+
out.next = posted.next || hintFromHlError(posted.error || '');
|
|
316
|
+
if (posted.statuses) out.statuses = posted.statuses;
|
|
317
|
+
} else {
|
|
318
|
+
out.next = 'placed on ' + posted.net + ' — builder 1bp on fills; optional hl_cancel_order / hl_close_position';
|
|
319
|
+
}
|
|
320
|
+
return out;
|
|
257
321
|
}
|
|
258
322
|
|
|
259
323
|
/** Cancel by oid and/or cloid. */
|
|
@@ -266,7 +330,9 @@ export async function cancelOrder({ coin, oid = null, cloid = null }) {
|
|
|
266
330
|
else if (oid != null) action = actions.buildCancelAction([{ assetIndex, oid: Number(oid) }]);
|
|
267
331
|
else throw new Error('oid or cloid required — pass oid or cloid to hl_cancel_order');
|
|
268
332
|
const posted = await postL1(action);
|
|
269
|
-
|
|
333
|
+
const out = { cancelled: posted.ok, net: posted.net, response: posted.response, coin: resolveCoin(coin).toUpperCase(), oid, cloid };
|
|
334
|
+
if (!posted.ok) { out.error = posted.error || 'cancel rejected'; out.next = posted.next || hintFromHlError(posted.error || ''); }
|
|
335
|
+
return out;
|
|
270
336
|
}
|
|
271
337
|
|
|
272
338
|
/** IOC reduce-only close for a coin (uses position size if size omitted). */
|
|
@@ -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
|
+
const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.18' });
|
|
17
17
|
|
|
18
18
|
server.tool('hl_quickstart',
|
|
19
|
-
'CALL FIRST:
|
|
20
|
-
{ coin: z.string().optional().describe(
|
|
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:
|
|
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:
|
|
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
|
-
'
|
|
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
|
|
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(),
|