@hypelens/hypelens-agent-rail 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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.24 hypelens-setup
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.24
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
@@ -55,7 +55,7 @@ function mcpServerEntry(scriptsDir) {
55
55
  }
56
56
  return {
57
57
  command: 'npx',
58
- args: ['-y', '@hypelens/hypelens-agent-rail@0.1.24'],
58
+ args: ['-y', '@hypelens/hypelens-agent-rail@0.1.25'],
59
59
  env: {
60
60
  HYPELENS_NET: process.env.HYPELENS_NET || 'mainnet',
61
61
  ...(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.24",
4
- "description": "Mainnet place rail. Post-install setup.mjs wires MCP + ApproveBuilderFee; refuse place until maxBuilderFee>=10; hl_place_order confirmTrade \u2192 poll until FILLED (RESTING\u2260paid); builder 1bp. npx @hypelens/hypelens-agent-rail@0.1.24",
3
+ "version": "0.1.26",
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.26",
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. Prefer ClawHub: clawhub install hyperliquid-place then
5
- cd skills/hyperliquid-place/scripts && npm install && node setup.mjs (REQUIRED —
6
- wires MCP + ApproveBuilderFee; start-mcp refused until setup). Then fund →
7
- hl_place_order(..., confirmTrade:true) → poll until fillStatus=FILLED (RESTING≠paid).
8
- Builder 0x9548… @1bp. Alt: npx @hypelens/hypelens-agent-rail@0.1.24
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
- # HypeLens Agent Rail
9
+ # hypelens-agent-rail
12
10
 
13
- **Builder:** `0x9548B8E9554a1968843B3C380431b10996247c88` @ 1bp. Never trade that wallet.
11
+ Paying path only: **FILLED** places with builder `0x9548…` @ 1bp.
14
12
 
15
- ## Preferred install (conversion)
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
- ```bash
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
- } else if (out.fillStatus === 'FILLED') {
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
@@ -32,7 +32,7 @@ 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.24' });
35
+ const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.25' });
36
36
 
37
37
  server.tool('hl_setup',
38
38
  'POST-INSTALL one-shot status: has keys? maxBuilderFee? auto ApproveBuilderFee if master key set. Place refused until approved. Prefer scripts/setup.mjs outside MCP to also wire host MCP config.',
@@ -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. Response fillStatus must be FILLED to convert (RESTING≠paid) — poll/retry until FILLED|REJECTED.',
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(),