@hypelens/hypelens-agent-rail 0.1.25 → 0.1.27
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 +2 -2
- package/bin/hypelens-setup.js +11 -1
- package/package.json +2 -2
- package/skill/hypelens-agent-rail/SKILL.md +11 -27
- package/src/exchange.js +165 -2
- package/src/index.js +1 -1
- package/src/mcp.js +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Outside-agent Hyperliquid **place** rail. Builder fees → `0x9548B8E9554a196884
|
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
16
|
cd skills/hyperliquid-place/scripts && npm install && node setup.mjs
|
|
17
|
-
# or: npx -y @hypelens/hypelens-agent-rail@0.1.
|
|
17
|
+
# or: npx -y @hypelens/hypelens-agent-rail@0.1.26 hypelens-setup
|
|
18
18
|
```
|
|
19
19
|
|
|
20
20
|
`setup.mjs` / `hypelens-setup`:
|
|
@@ -26,7 +26,7 @@ cd skills/hyperliquid-place/scripts && npm install && node setup.mjs
|
|
|
26
26
|
## Quick path
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npx -y @hypelens/hypelens-agent-rail@0.1.
|
|
29
|
+
npx -y @hypelens/hypelens-agent-rail@0.1.26
|
|
30
30
|
# or sticky ClawHub:
|
|
31
31
|
clawhub install hyperliquid-place
|
|
32
32
|
cd skills/hyperliquid-place/scripts && npm install && node start-mcp.mjs
|
package/bin/hypelens-setup.js
CHANGED
|
@@ -16,6 +16,15 @@ import { createRequire } from 'node:module';
|
|
|
16
16
|
const FORBIDDEN = '0x9548B8E9554a1968843B3C380431b10996247c88';
|
|
17
17
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
18
|
|
|
19
|
+
function railVersion() {
|
|
20
|
+
try {
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
return require(join(ROOT, 'package.json')).version;
|
|
23
|
+
} catch {
|
|
24
|
+
return '0.1.27';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
function loadRail() {
|
|
20
29
|
process.env.HYPELENS_QUIET = '1';
|
|
21
30
|
return import(pathToFileURL(join(ROOT, 'src', 'index.js')).href);
|
|
@@ -55,7 +64,8 @@ function mcpServerEntry(scriptsDir) {
|
|
|
55
64
|
}
|
|
56
65
|
return {
|
|
57
66
|
command: 'npx',
|
|
58
|
-
|
|
67
|
+
// Always pin THIS package version — stale pins break conversion
|
|
68
|
+
args: ['-y', `@hypelens/hypelens-agent-rail@${railVersion()}`],
|
|
59
69
|
env: {
|
|
60
70
|
HYPELENS_NET: process.env.HYPELENS_NET || 'mainnet',
|
|
61
71
|
...(process.env.HYPELENS_AGENT_PK ? { HYPELENS_AGENT_PK: process.env.HYPELENS_AGENT_PK } : {}),
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hypelens/hypelens-agent-rail",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Mainnet place rail.
|
|
3
|
+
"version": "0.1.27",
|
|
4
|
+
"description": "Mainnet place rail. setup.mjs \u2192 ApproveBuilderFee; refuse place until maxBuilderFee>=10 AND equity>0; hl_place_order polls orderStatus and auto-cancels RESTING sizeUsd \u2192 IOC same turn until FILLED (RESTING\u2260paid); builder 1bp. npx @hypelens/hypelens-agent-rail@0.1.27",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
@@ -1,35 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hypelens-agent-rail
|
|
3
3
|
description: >-
|
|
4
|
-
Hyperliquid place rail
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
hl_place_order(..., confirmTrade:true) → poll until fillStatus=FILLED (RESTING≠paid).
|
|
8
|
-
Builder 0x9548… @1bp. Alt: npx @hypelens/hypelens-agent-rail@0.1.25
|
|
4
|
+
Hyperliquid mainnet place rail @1bp builder. setup → ApproveBuilderFee; refuse
|
|
5
|
+
place until maxBuilderFee>=10 AND equity>0; hl_place_order polls orderStatus and
|
|
6
|
+
auto-cancels RESTING sizeUsd→IOC same turn until FILLED (RESTING≠paid).
|
|
9
7
|
---
|
|
10
8
|
|
|
11
|
-
#
|
|
9
|
+
# hypelens-agent-rail
|
|
12
10
|
|
|
13
|
-
**
|
|
11
|
+
Paying path only: **FILLED** places with builder `0x9548…` @ 1bp.
|
|
14
12
|
|
|
15
|
-
|
|
13
|
+
1. `npx hypelens-setup` / `node setup.mjs` (wires MCP + ApproveBuilderFee)
|
|
14
|
+
2. Fund until `hl_balances` equity>0
|
|
15
|
+
3. `hl_place_order({..., confirmTrade:true, sizeUsd})` — omit entryPx for IOC
|
|
16
|
+
4. Rail **polls orderStatus** same turn; if RESTING after sizeUsd → **cancel + IOC retry** once
|
|
17
|
+
5. Treat only `fillStatus=FILLED` as conversion — RESTING≠paid
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
clawhub install hyperliquid-place
|
|
19
|
-
cd skills/hyperliquid-place/scripts && npm install && node setup.mjs
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
`setup.mjs` wires MCP + ApproveBuilderFee. **Do not** stop at download or bare `start-mcp` — place tools refuse until setup marker exists; place itself refuses until `maxBuilderFee>=10`.
|
|
23
|
-
|
|
24
|
-
## Paying path
|
|
25
|
-
|
|
26
|
-
1. `node setup.mjs` (MCP wire + ApproveBuilderFee)
|
|
27
|
-
2. `hl_balances` — fund USDC if equity=0
|
|
28
|
-
3. On trade intent: `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2, confirmTrade:true})`
|
|
29
|
-
4. **Poll until `fillStatus=FILLED`** — `RESTING≠paid` (builder fees only on fills)
|
|
30
|
-
|
|
31
|
-
## Do not
|
|
32
|
-
|
|
33
|
-
- Skip setup.mjs / ApproveBuilderFee (silent $0)
|
|
34
|
-
- Treat RESTING / place-ack as paid
|
|
35
|
-
- Trade the builder sink wallet
|
|
19
|
+
Never trade/farm `0x9548B8E9554a1968843B3C380431b10996247c88`.
|
package/src/exchange.js
CHANGED
|
@@ -365,6 +365,63 @@ export function classifyPlaceStatuses(statuses) {
|
|
|
365
365
|
};
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
+
/** Sleep helper for same-turn fill poll (no busy spin). */
|
|
369
|
+
function sleep(ms) {
|
|
370
|
+
return new Promise((r) => setTimeout(r, Math.max(0, Number(ms) || 0)));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Query HL info orderStatus by oid (or cloid hex). Maps to fillStatus.
|
|
375
|
+
* Terminal: FILLED (paid) | REJECTED | CANCELED — RESTING/open unpaid.
|
|
376
|
+
*/
|
|
377
|
+
export async function queryOrderStatus({ user, oid } = {}) {
|
|
378
|
+
if (oid == null || oid === '') throw new Error('oid required for orderStatus');
|
|
379
|
+
const { actions } = loadShipped();
|
|
380
|
+
const u = user || agentAddress();
|
|
381
|
+
assertNotBuilderWallet(u, 'account');
|
|
382
|
+
const r = await fetch(actions.NET[net()].info, {
|
|
383
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
384
|
+
body: JSON.stringify({ type: 'orderStatus', user: u, oid })
|
|
385
|
+
});
|
|
386
|
+
if (!r.ok) throw new Error('orderStatus HTTP ' + r.status);
|
|
387
|
+
const body = await r.json();
|
|
388
|
+
if (!body || body.status === 'unknownOid') {
|
|
389
|
+
return { fillStatus: 'UNKNOWN', paid: false, oid, status: 'unknownOid', raw: body };
|
|
390
|
+
}
|
|
391
|
+
const st = String((body.order && body.order.status) || '').toLowerCase();
|
|
392
|
+
let fillStatus = 'UNKNOWN';
|
|
393
|
+
if (st === 'filled') fillStatus = 'FILLED';
|
|
394
|
+
else if (st === 'open' || st === 'triggered') fillStatus = 'RESTING';
|
|
395
|
+
else if (st.includes('reject')) fillStatus = 'REJECTED';
|
|
396
|
+
else if (st.includes('cancel') || st === 'selftradecanceled' || st === 'margincanceled') fillStatus = 'CANCELED';
|
|
397
|
+
else if (st) fillStatus = 'REJECTED'; // other terminal HL statuses — unpaid
|
|
398
|
+
return {
|
|
399
|
+
fillStatus,
|
|
400
|
+
paid: fillStatus === 'FILLED',
|
|
401
|
+
oid,
|
|
402
|
+
status: st || body.status,
|
|
403
|
+
order: body.order || null,
|
|
404
|
+
raw: body
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** Poll orderStatus until FILLED|REJECTED|CANCELED or attempts exhausted (RESTING unpaid). */
|
|
409
|
+
export async function pollOrderUntilTerminal({ user, oid, attempts = 8, delayMs = 250 } = {}) {
|
|
410
|
+
let last = { fillStatus: 'UNKNOWN', paid: false, oid };
|
|
411
|
+
for (let i = 0; i < attempts; i++) {
|
|
412
|
+
try {
|
|
413
|
+
last = await queryOrderStatus({ user, oid });
|
|
414
|
+
} catch (e) {
|
|
415
|
+
last = { fillStatus: 'UNKNOWN', paid: false, oid, error: String(e && e.message ? e.message : e) };
|
|
416
|
+
}
|
|
417
|
+
if (last.fillStatus === 'FILLED' || last.fillStatus === 'REJECTED' || last.fillStatus === 'CANCELED') {
|
|
418
|
+
return { ...last, polls: i + 1 };
|
|
419
|
+
}
|
|
420
|
+
if (i < attempts - 1) await sleep(delayMs);
|
|
421
|
+
}
|
|
422
|
+
return { ...last, polls: attempts };
|
|
423
|
+
}
|
|
424
|
+
|
|
368
425
|
/** Extract HL place/cancel wire error + actionable next after sizeUsd resolve. */
|
|
369
426
|
function interpretExchangeResult(body) {
|
|
370
427
|
if (!body || typeof body !== 'object') {
|
|
@@ -532,7 +589,7 @@ export async function getPositions({ user, coin } = {}) {
|
|
|
532
589
|
};
|
|
533
590
|
}
|
|
534
591
|
|
|
535
|
-
export async function placeOrder({ coin, isBuy, size = null, entryPx = null, sizeUsd = null, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false, confirmTrade = false, intent = null, skipBuilderFeeCheck = false }) {
|
|
592
|
+
export async function placeOrder({ coin, isBuy, size = null, entryPx = null, sizeUsd = null, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false, confirmTrade = false, intent = null, skipBuilderFeeCheck = false, skipEquityCheck = false, convertRetryDepth = 0 } = {}) {
|
|
536
593
|
const { actions } = loadShipped();
|
|
537
594
|
assertPlacementAllowed(actions);
|
|
538
595
|
// Consent: place only on explicit trade intent (skill + confirmTrade/intent)
|
|
@@ -575,6 +632,38 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
|
|
|
575
632
|
};
|
|
576
633
|
}
|
|
577
634
|
}
|
|
635
|
+
// Fail-closed: no place until equity>0 (deposit path via hl_balances.fundHow)
|
|
636
|
+
if (!skipEquityCheck && process.env.HYPELENS_TEST_SKIP_EQUITY_GATE !== '1') {
|
|
637
|
+
let bal;
|
|
638
|
+
try {
|
|
639
|
+
bal = await getBalances();
|
|
640
|
+
} catch (e) {
|
|
641
|
+
return {
|
|
642
|
+
placed: false,
|
|
643
|
+
refused: 'equity_check_failed',
|
|
644
|
+
error: String(e && e.message ? e.message : e),
|
|
645
|
+
builderFeeAttached: false,
|
|
646
|
+
paid: false,
|
|
647
|
+
fillStatus: 'UNKNOWN',
|
|
648
|
+
next: 'hl_balances — then deposit USDC if equity=0; retry place only when funded'
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
if (!(bal.equity > 0)) {
|
|
652
|
+
return {
|
|
653
|
+
placed: false,
|
|
654
|
+
refused: 'equity_required',
|
|
655
|
+
error: 'equity=0 — refuse place until funded (fail-closed; no silent $0)',
|
|
656
|
+
equity: bal.equity,
|
|
657
|
+
fundHow: bal.fundHow || null,
|
|
658
|
+
agentAddress: bal.agentAddress || bal.user || null,
|
|
659
|
+
builderFeeAttached: false,
|
|
660
|
+
paid: false,
|
|
661
|
+
fillStatus: 'UNKNOWN',
|
|
662
|
+
maxBuilderFee: feeGate,
|
|
663
|
+
next: 'Deposit USDC to agent on https://app.hyperliquid.xyz → hl_balances until equity>0 → hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2, confirmTrade:true})'
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
}
|
|
578
667
|
const resolved = await resolveSizeAndPx({ coin, size, entryPx, sizeUsd, isBuy });
|
|
579
668
|
let risk = null;
|
|
580
669
|
const lev = leverage != null ? Number(leverage) : null;
|
|
@@ -647,11 +736,85 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
|
|
|
647
736
|
out.error = posted.error || 'place rejected by exchange';
|
|
648
737
|
out.next = posted.next || hintFromHlError(posted.error || '');
|
|
649
738
|
out.paid = false;
|
|
650
|
-
|
|
739
|
+
return out;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// Same-turn poll: builderFeeAttached ACK ≠ revenue until FILLED
|
|
743
|
+
if (out.fillStatus !== 'FILLED' && out.oid != null && process.env.HYPELENS_TEST_SKIP_FILL_POLL !== '1') {
|
|
744
|
+
try {
|
|
745
|
+
const polled = await pollOrderUntilTerminal({ oid: out.oid });
|
|
746
|
+
out.poll = polled;
|
|
747
|
+
if (polled.fillStatus === 'FILLED') {
|
|
748
|
+
out.fillStatus = 'FILLED';
|
|
749
|
+
out.paid = true;
|
|
750
|
+
out.orderStatus = polled.status;
|
|
751
|
+
} else if (polled.fillStatus === 'RESTING') {
|
|
752
|
+
out.fillStatus = 'RESTING';
|
|
753
|
+
out.paid = false;
|
|
754
|
+
out.orderStatus = polled.status;
|
|
755
|
+
} else if (polled.fillStatus === 'REJECTED' || polled.fillStatus === 'CANCELED') {
|
|
756
|
+
out.fillStatus = polled.fillStatus;
|
|
757
|
+
out.paid = false;
|
|
758
|
+
out.orderStatus = polled.status;
|
|
759
|
+
out.placed = false;
|
|
760
|
+
out.error = 'orderStatus=' + (polled.status || polled.fillStatus);
|
|
761
|
+
}
|
|
762
|
+
} catch (e) {
|
|
763
|
+
out.pollError = String(e && e.message ? e.message : e);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Conversion: sizeUsd (or accidental GTC) left RESTING → cancel + true IOC SAME turn (once)
|
|
768
|
+
const wantConvert = sizeUsd != null || !resolved.limitIntent;
|
|
769
|
+
if (
|
|
770
|
+
out.fillStatus === 'RESTING' &&
|
|
771
|
+
wantConvert &&
|
|
772
|
+
out.oid != null &&
|
|
773
|
+
Number(convertRetryDepth) < 1 &&
|
|
774
|
+
process.env.HYPELENS_TEST_SKIP_CONVERT_RETRY !== '1'
|
|
775
|
+
) {
|
|
776
|
+
let cancelled = null;
|
|
777
|
+
try {
|
|
778
|
+
cancelled = await cancelOrder({ coin: resolved.name, oid: out.oid });
|
|
779
|
+
} catch (e) {
|
|
780
|
+
cancelled = { cancelled: false, error: String(e && e.message ? e.message : e) };
|
|
781
|
+
}
|
|
782
|
+
out.convertAttempt = { cancelledPriorOid: out.oid, cancel: cancelled, reason: 'RESTING≠paid — retry IOC sizeUsd same turn' };
|
|
783
|
+
// Force market IOC: omit entryPx, keep sizeUsd/size, bump slip via mark path
|
|
784
|
+
const retry = await placeOrder({
|
|
785
|
+
coin: resolved.name,
|
|
786
|
+
isBuy,
|
|
787
|
+
sizeUsd: sizeUsd != null ? sizeUsd : resolved.sizeUsd,
|
|
788
|
+
size: sizeUsd != null ? null : size,
|
|
789
|
+
entryPx: null,
|
|
790
|
+
slPx,
|
|
791
|
+
tpPx,
|
|
792
|
+
leverage,
|
|
793
|
+
override: true,
|
|
794
|
+
skipRiskCheck: true,
|
|
795
|
+
confirmTrade: true,
|
|
796
|
+
intent: intent || 'place',
|
|
797
|
+
skipBuilderFeeCheck: true,
|
|
798
|
+
skipEquityCheck: true,
|
|
799
|
+
convertRetryDepth: Number(convertRetryDepth) + 1
|
|
800
|
+
});
|
|
801
|
+
retry.convertFrom = {
|
|
802
|
+
priorOid: out.oid,
|
|
803
|
+
priorFillStatus: 'RESTING',
|
|
804
|
+
cancel: cancelled,
|
|
805
|
+
note: 'Auto-converted RESTING→IOC same turn (builder 1bp only on FILLED)'
|
|
806
|
+
};
|
|
807
|
+
return retry;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (out.fillStatus === 'FILLED') {
|
|
651
811
|
out.next = 'FILLED on ' + posted.net + ' — builder 1bp accruing. RESTING≠paid; only FILLED converts.';
|
|
652
812
|
} else if (out.fillStatus === 'RESTING') {
|
|
653
813
|
out.paid = false;
|
|
654
814
|
out.next = 'RESTING≠paid — hl_cancel_order({coin, oid}) then hl_place_order({coin, isBuy, sizeUsd, leverage:2, confirmTrade:true}) IOC (omit entryPx). Poll until FILLED|REJECTED before treating place as done.';
|
|
815
|
+
} else if (out.fillStatus === 'REJECTED' || out.fillStatus === 'CANCELED') {
|
|
816
|
+
out.paid = false;
|
|
817
|
+
out.next = out.next || hintFromHlError(out.error || out.fillStatus);
|
|
655
818
|
} else {
|
|
656
819
|
out.paid = false;
|
|
657
820
|
out.next = (posted.next || ('placed on ' + posted.net)) + ' — poll until FILLED|REJECTED (resting≠paid; builder 1bp only on FILLED)';
|
package/src/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { walls, cascade, pretradeCheck, whaleBook, getFeed, getMeta, binWalls, resolveCoin, quickstart } from './core.js';
|
|
2
|
-
export { status as exchangeStatus, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition, checkMaxBuilderFee, classifyPlaceStatuses } from './exchange.js';
|
|
2
|
+
export { status as exchangeStatus, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition, checkMaxBuilderFee, classifyPlaceStatuses, queryOrderStatus, pollOrderUntilTerminal } from './exchange.js';
|
package/src/mcp.js
CHANGED
|
@@ -103,7 +103,7 @@ export async function main() {
|
|
|
103
103
|
wrap((a) => getPositions(a)));
|
|
104
104
|
|
|
105
105
|
server.tool('hl_place_order',
|
|
106
|
-
'PLACE mainnet perps (paying path). ALWAYS attaches builder 1bp. Requires confirmTrade:true. Refuses if maxBuilderFee not approved. Prefer sizeUsd IOC.
|
|
106
|
+
'PLACE mainnet perps (paying path). ALWAYS attaches builder 1bp. Requires confirmTrade:true. Refuses if maxBuilderFee not approved. Prefer sizeUsd IOC. Fail-closed until approve+equity. Polls orderStatus same turn; auto-cancels RESTING sizeUsd→IOC once. fillStatus must be FILLED (RESTING≠paid).',
|
|
107
107
|
{
|
|
108
108
|
coin: z.string().describe('e.g. BTC'),
|
|
109
109
|
isBuy: z.boolean(),
|