@hypelens/hypelens-agent-rail 0.1.21 → 0.1.23

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
@@ -1,37 +1,51 @@
1
1
  # @hypelens/hypelens-agent-rail
2
- **Place, cancel, close Hyperliquid perps.** Agent wallet once · auto-round · **1bp on fills — no sub.**
3
- ## Install → approve → place
4
- ```bash
5
- # MCP stdio (Claude / Cursor / any MCP host)
6
- npx @hypelens/hypelens-agent-rail@0.1.21
7
2
 
8
- # Claude Code one-liner
9
- claude mcp add hypelens -- npx @hypelens/hypelens-agent-rail@0.1.21
3
+ Outside-agent Hyperliquid **place** rail. Builder fees → `0x9548B8E9554a1968843B3C380431b10996247c88` at **1bp** (`f=10`) on fills. **Never** trade/farm that wallet.
10
4
 
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
15
- ```
16
- 1. `hl_quickstart` (optional) → `hl_new_agent_wallet` → `HYPELENS_AGENT_PK`
17
- 2. `hl_approve_payloads(agentAddress)` — **MASTER** signs approveAgent + ApproveBuilderFee **0.01%** once, then POST both
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
5
+ ## Fee capture (OpenBroker pattern)
6
+
7
+ 1. Every `hl_place_order` / close **always** attaches `builder: { b: 0x9548…, f: 10 }`.
8
+ 2. **Setup / `hl_approve_payloads`:**
9
+ - If `HYPERLIQUID_PRIVATE_KEY` or `HYPELENS_MASTER_PK` is set → **auto sign+POST** `ApproveBuilderFee` (and `approveAgent` when agent ≠ master).
10
+ - If only agent key (`HYPELENS_AGENT_PK`) → return clear unsigned approve payloads + `maxBuilderFee` status; **refuse place-with-builder** until `maxBuilderFee >= 10` (no silent $0).
11
+ 3. **Consent:** `hl_place_order` requires `confirmTrade: true` or `intent: "trade"|"place"|"execute"`.
12
+
13
+ ## Post-install (REQUIRED — downloads ≠ fees)
20
14
 
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).
15
+ ```bash
16
+ cd skills/hyperliquid-place/scripts && npm install && node setup.mjs
17
+ # or: npx -y @hypelens/hypelens-agent-rail@0.1.23 hypelens-setup
18
+ ```
22
19
 
23
- Default **mainnet**. Practice: `HYPELENS_NET=testnet`. Fee: **1bp on fills** — no sub. Missing `HYPELENS_AGENT_PK` errors point at the same path.
20
+ `setup.mjs` / `hypelens-setup`:
21
+ 1. Registers MCP in OpenClaw/Claude/Cursor config when possible
22
+ 2. Auto `ApproveBuilderFee` when `HYPERLIQUID_PRIVATE_KEY` / `HYPELENS_MASTER_PK` present
23
+ 3. Prints `maxBuilderFee` — place stays refused until approved (no silent $0)
24
+ 4. Next: fund agent if equity=0 → place on explicit trade intent
24
25
 
25
- ## Optional after install: sizing edge
26
+ ## Quick path
26
27
 
27
- `hl_walls` / `hl_whale_book` / `hl_cascade` / `hl_pretrade_check` — live liq heat to improve size/entry. Missing feed is **advisory** (does not block place).
28
+ ```bash
29
+ npx -y @hypelens/hypelens-agent-rail@0.1.23
30
+ # or sticky ClawHub:
31
+ clawhub install hyperliquid-place
32
+ cd skills/hyperliquid-place/scripts && npm install && node start-mcp.mjs
33
+ ```
28
34
 
29
- **Sizing rule:** warn if `sizeUsd` > 20% of account equity; size from balance; confirm mark before place.
35
+ Env:
30
36
 
31
- ## Tools
37
+ | Var | Role |
38
+ | --- | --- |
39
+ | `HYPELENS_AGENT_PK` | Agent key for places |
40
+ | `HYPERLIQUID_PRIVATE_KEY` / `HYPELENS_MASTER_PK` | Master — auto ApproveBuilderFee |
41
+ | `HYPERLIQUID_ACCOUNT_ADDRESS` | Master address when using API wallet |
42
+ | `HYPELENS_NET` | `mainnet` (default) or `testnet` |
32
43
 
33
- Place path: `hl_quickstart`, `hl_exchange_status`, `hl_new_agent_wallet`, `hl_approve_payloads`, `hl_place_order`, `hl_cancel_order`, `hl_close_position`, `hl_balances`, `hl_positions`.
44
+ ## Funnel
34
45
 
35
- Optional edge: `hl_walls`, `hl_cascade`, `hl_pretrade_check`, `hl_whale_book`.
46
+ 1. `hl_quickstart` / `hl_new_agent_wallet`
47
+ 2. `hl_approve_payloads(agentAddress)` — auto if master key; else MASTER signs ApproveBuilderFee **0.01%**
48
+ 3. `hl_balances` (+ fundHow if equity=0)
49
+ 4. On explicit trade intent: `hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2, confirmTrade:true})`
36
50
 
37
- MIT. [HypeLens](https://github.com/polyparlay/hypelens).
51
+ Skipping ApproveBuilderFee = **$0 fees**. Place is hard-refused until on-chain `maxBuilderFee` covers 1bp.
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * OpenBroker-style post-install one-shot:
4
+ * 1) Wire MCP into host config (OpenClaw / Claude / Cursor) when possible
5
+ * 2) Ensure agent key (generate if missing)
6
+ * 3) Auto ApproveBuilderFee when HYPERLIQUID_PRIVATE_KEY / HYPELENS_MASTER_PK present
7
+ * 4) Print maxBuilderFee + fund + place next (never silent $0)
8
+ * NEVER trades/farms 0x9548…
9
+ */
10
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
11
+ import { homedir } from 'node:os';
12
+ import { join, dirname } from 'node:path';
13
+ import { fileURLToPath, pathToFileURL } from 'node:url';
14
+ import { createRequire } from 'node:module';
15
+
16
+ const FORBIDDEN = '0x9548B8E9554a1968843B3C380431b10996247c88';
17
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
18
+
19
+ function loadRail() {
20
+ process.env.HYPELENS_QUIET = '1';
21
+ return import(pathToFileURL(join(ROOT, 'src', 'index.js')).href);
22
+ }
23
+
24
+ function expand(p) {
25
+ return p.replace(/^~(?=\/|$)/, homedir());
26
+ }
27
+
28
+ /** Candidate MCP host config files (best-effort; never overwrite unrelated keys). */
29
+ function hostConfigPaths() {
30
+ const h = homedir();
31
+ return [
32
+ { kind: 'openclaw', path: join(h, '.openclaw', 'openclaw.json') },
33
+ { kind: 'openclaw', path: join(h, '.openclaw', 'config.json') },
34
+ { kind: 'openclaw', path: join(h, '.openclaw', 'mcp.json') },
35
+ { kind: 'claude', path: join(h, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json') },
36
+ { kind: 'claude', path: join(h, '.config', 'Claude', 'claude_desktop_config.json') },
37
+ { kind: 'cursor', path: join(h, '.cursor', 'mcp.json') },
38
+ ];
39
+ }
40
+
41
+ function mcpServerEntry(scriptsDir) {
42
+ const start = scriptsDir ? join(scriptsDir, 'start-mcp.mjs') : null;
43
+ if (start && existsSync(start)) {
44
+ return {
45
+ command: 'node',
46
+ args: [start],
47
+ env: {
48
+ HYPELENS_NET: process.env.HYPELENS_NET || 'mainnet',
49
+ ...(process.env.HYPELENS_AGENT_PK ? { HYPELENS_AGENT_PK: process.env.HYPELENS_AGENT_PK } : {}),
50
+ ...(process.env.HYPERLIQUID_PRIVATE_KEY ? { HYPERLIQUID_PRIVATE_KEY: process.env.HYPERLIQUID_PRIVATE_KEY } : {}),
51
+ ...(process.env.HYPELENS_MASTER_PK ? { HYPELENS_MASTER_PK: process.env.HYPELENS_MASTER_PK } : {}),
52
+ ...(process.env.HYPERLIQUID_ACCOUNT_ADDRESS ? { HYPERLIQUID_ACCOUNT_ADDRESS: process.env.HYPERLIQUID_ACCOUNT_ADDRESS } : {}),
53
+ },
54
+ };
55
+ }
56
+ return {
57
+ command: 'npx',
58
+ args: ['-y', '@hypelens/hypelens-agent-rail@0.1.23'],
59
+ env: {
60
+ HYPELENS_NET: process.env.HYPELENS_NET || 'mainnet',
61
+ ...(process.env.HYPELENS_AGENT_PK ? { HYPELENS_AGENT_PK: process.env.HYPELENS_AGENT_PK } : {}),
62
+ ...(process.env.HYPERLIQUID_PRIVATE_KEY ? { HYPERLIQUID_PRIVATE_KEY: process.env.HYPERLIQUID_PRIVATE_KEY } : {}),
63
+ ...(process.env.HYPELENS_MASTER_PK ? { HYPELENS_MASTER_PK: process.env.HYPELENS_MASTER_PK } : {}),
64
+ },
65
+ };
66
+ }
67
+
68
+ function wireMcp({ scriptsDir, name = 'hyperliquid-place' } = {}) {
69
+ const entry = mcpServerEntry(scriptsDir);
70
+ const wired = [];
71
+ const suggested = { mcpServers: { [name]: entry } };
72
+ for (const host of hostConfigPaths()) {
73
+ try {
74
+ const dir = dirname(host.path);
75
+ if (!existsSync(dir) && host.kind === 'openclaw') {
76
+ mkdirSync(dir, { recursive: true });
77
+ }
78
+ if (!existsSync(dirname(host.path))) continue;
79
+ let cfg = {};
80
+ if (existsSync(host.path)) {
81
+ cfg = JSON.parse(readFileSync(host.path, 'utf8'));
82
+ } else if (host.kind !== 'openclaw') {
83
+ // only auto-create openclaw configs; for others print snippet
84
+ continue;
85
+ }
86
+ if (!cfg.mcpServers || typeof cfg.mcpServers !== 'object') cfg.mcpServers = {};
87
+ cfg.mcpServers[name] = entry;
88
+ writeFileSync(host.path, JSON.stringify(cfg, null, 2) + '\n', { mode: 0o600 });
89
+ try { chmodSync(host.path, 0o600); } catch (_) {}
90
+ wired.push({ kind: host.kind, path: host.path });
91
+ } catch (e) {
92
+ wired.push({ kind: host.kind, path: host.path, error: String(e && e.message ? e.message : e) });
93
+ }
94
+ }
95
+ return { wired, suggested, entry };
96
+ }
97
+
98
+ async function runApprove() {
99
+ const rail = await loadRail();
100
+ const { newAgentWallet, approvePayloads, checkMaxBuilderFee, exchangeStatus } = rail;
101
+ const status = exchangeStatus();
102
+ let agentPk = process.env.HYPELENS_AGENT_PK || '';
103
+ let agentAddress = null;
104
+ let generated = null;
105
+ if (!agentPk && process.env.HYPERLIQUID_PRIVATE_KEY) {
106
+ // Fresh-wallet OpenBroker: sole key places + can auto-approve
107
+ agentPk = process.env.HYPERLIQUID_PRIVATE_KEY;
108
+ process.env.HYPELENS_AGENT_PK = agentPk;
109
+ }
110
+ if (!agentPk) {
111
+ generated = newAgentWallet();
112
+ agentPk = generated.privateKey;
113
+ agentAddress = generated.address;
114
+ process.env.HYPELENS_AGENT_PK = agentPk;
115
+ } else {
116
+ const { loadShipped } = await import(pathToFileURL(join(ROOT, 'src', 'load.js')).href);
117
+ const { signer } = loadShipped();
118
+ agentAddress = signer.addressFromPrivateKey(agentPk);
119
+ }
120
+ if (agentAddress && agentAddress.toLowerCase() === FORBIDDEN.toLowerCase()) {
121
+ throw new Error('Refusing setup for builder sink wallet ' + FORBIDDEN);
122
+ }
123
+ const approve = await approvePayloads(agentAddress);
124
+ let fee = null;
125
+ try { fee = await checkMaxBuilderFee({ user: agentAddress }); } catch (e) {
126
+ fee = { error: String(e && e.message ? e.message : e), approved: false };
127
+ }
128
+ return { status, generated, agentAddress, approve, maxBuilderFee: fee };
129
+ }
130
+
131
+ function parseArgs(argv) {
132
+ const out = { scriptsDir: null, writeEnv: null, json: false };
133
+ for (let i = 2; i < argv.length; i++) {
134
+ const a = argv[i];
135
+ if (a === '--json') out.json = true;
136
+ else if (a === '--scripts-dir' && argv[i + 1]) out.scriptsDir = expand(argv[++i]);
137
+ else if (a === '--write-env' && argv[i + 1]) out.writeEnv = expand(argv[++i]);
138
+ }
139
+ // default scripts dir = cwd if start-mcp.mjs present
140
+ if (!out.scriptsDir && existsSync(join(process.cwd(), 'start-mcp.mjs'))) out.scriptsDir = process.cwd();
141
+ return out;
142
+ }
143
+
144
+ async function main() {
145
+ const args = parseArgs(process.argv);
146
+ const mcp = wireMcp({ scriptsDir: args.scriptsDir });
147
+ const feePath = await runApprove();
148
+ if (args.writeEnv && feePath.generated) {
149
+ const lines = [
150
+ `# hypelens setup ${new Date().toISOString()}`,
151
+ `HYPELENS_NET=${process.env.HYPELENS_NET || 'mainnet'}`,
152
+ `HYPELENS_AGENT_PK=${feePath.generated.privateKey}`,
153
+ process.env.HYPERLIQUID_PRIVATE_KEY ? `HYPERLIQUID_PRIVATE_KEY=${process.env.HYPERLIQUID_PRIVATE_KEY}` : '# export HYPERLIQUID_PRIVATE_KEY=<master> for auto ApproveBuilderFee',
154
+ '',
155
+ ];
156
+ writeFileSync(args.writeEnv, lines.join('\n'), { mode: 0o600 });
157
+ try { chmodSync(args.writeEnv, 0o600); } catch (_) {}
158
+ }
159
+ const approved = Boolean(feePath.maxBuilderFee && feePath.maxBuilderFee.approved) || Boolean(feePath.approve && feePath.approve.autoApproved);
160
+ const report = {
161
+ ok: true,
162
+ package: '@hypelens/hypelens-agent-rail',
163
+ builder: FORBIDDEN,
164
+ builderFee: '1bp (f=10) — never trade builder wallet',
165
+ mcpWired: mcp.wired.filter((w) => !w.error),
166
+ mcpWireErrors: mcp.wired.filter((w) => w.error),
167
+ mcpSuggested: mcp.suggested,
168
+ agentAddress: feePath.agentAddress,
169
+ generatedAgent: Boolean(feePath.generated),
170
+ generatedPrivateKey: feePath.generated ? feePath.generated.privateKey : undefined,
171
+ approveMode: feePath.approve && feePath.approve.mode,
172
+ autoApproved: Boolean(feePath.approve && feePath.approve.autoApproved),
173
+ maxBuilderFee: feePath.maxBuilderFee,
174
+ placeAllowed: approved,
175
+ next: !approved
176
+ ? 'Export HYPERLIQUID_PRIVATE_KEY=<master> and re-run setup (auto ApproveBuilderFee), OR MASTER-sign approve payloads. Place is REFUSED until maxBuilderFee>=10 — no silent $0.'
177
+ : 'Restart MCP host. hl_balances — if equity=0 deposit USDC to agentAddress. On explicit trade intent: hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2, confirmTrade:true})',
178
+ funnel: ['mcp_wired', 'approve_builder_fee', 'fund_if_needed', 'place_on_intent'],
179
+ };
180
+ if (args.json) {
181
+ console.log(JSON.stringify(report, null, 2));
182
+ } else {
183
+ console.log('HypeLens setup (OpenBroker)');
184
+ console.log('- MCP wired:', report.mcpWired.length ? report.mcpWired.map((w) => w.path).join(', ') : '(none — paste mcpSuggested into host config)');
185
+ if (report.mcpWireErrors.length) console.log('- MCP wire errors:', report.mcpWireErrors);
186
+ console.log('- Agent:', report.agentAddress);
187
+ if (report.generatedAgent) {
188
+ console.log('- Generated HYPELENS_AGENT_PK (store securely):', report.generatedPrivateKey);
189
+ }
190
+ console.log('- Approve mode:', report.approveMode, '| autoApproved:', report.autoApproved);
191
+ console.log('- maxBuilderFee:', JSON.stringify(report.maxBuilderFee));
192
+ console.log('- placeAllowed:', report.placeAllowed);
193
+ console.log('- NEXT:', report.next);
194
+ if (!report.mcpWired.length) {
195
+ console.log('\nPaste into host mcpServers:\n' + JSON.stringify(report.mcpSuggested, null, 2));
196
+ }
197
+ }
198
+ process.exit(approved ? 0 : 2);
199
+ }
200
+
201
+ main().catch((e) => {
202
+ console.error('setup failed:', e && e.message ? e.message : e);
203
+ process.exit(1);
204
+ });
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@hypelens/hypelens-agent-rail",
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",
3
+ "version": "0.1.23",
4
+ "description": "Mainnet place rail. Post-install: hypelens-setup wires MCP + OpenBroker auto ApproveBuilderFee when master key set; refuse place until maxBuilderFee>=10; always builder f=10. npx @hypelens/hypelens-agent-rail@0.1.23",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {
8
8
  "hypelens-rail-mcp": "bin/hypelens-rail-mcp.js",
9
- "hypelens-agent-rail": "bin/hypelens-rail-mcp.js"
9
+ "hypelens-agent-rail": "bin/hypelens-rail-mcp.js",
10
+ "hypelens-setup": "bin/hypelens-setup.js"
10
11
  },
11
12
  "main": "src/index.js",
12
13
  "files": [
@@ -19,7 +20,8 @@
19
20
  "scripts": {
20
21
  "test": "node --test test/exchange.test.mjs test/risk.test.mjs",
21
22
  "prepack": "node -e \"console.log(\\\"prepack: vendor already shipped\\\")\"",
22
- "mcp": "node bin/hypelens-rail-mcp.js"
23
+ "mcp": "node bin/hypelens-rail-mcp.js",
24
+ "setup": "node bin/hypelens-setup.js"
23
25
  },
24
26
  "engines": {
25
27
  "node": ">=20"
@@ -1,61 +1,24 @@
1
1
  ---
2
- name: Hyperliquid Trading Place
2
+ name: hypelens-agent-rail
3
3
  description: >-
4
- Trade Hyperliquid perpetual futures: place, cancel, close orders; check
5
- balances and positions with P&L. Use when the user asks about Hyperliquid
6
- trading, portfolio status, crypto positions, or wants to execute trades.
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
- perps hl_place_order.
10
- topics: [hyperliquid, trading, place, order, cancel]
11
- metadata:
12
- openclaw:
13
- emoji: "📈"
14
- requires:
15
- bins: [npx]
16
- envVars:
17
- - name: HYPELENS_NET
18
- required: false
19
- description: "mainnet (default fee path) or testnet escape"
20
- - name: HYPELENS_AGENT_PK
21
- required: false
22
- description: agent wallet pk for place/cancel/close
4
+ Hyperliquid place rail with OpenBroker ApproveBuilderFee auto when master key
5
+ set; refuse place until maxBuilderFee>=10; always 1bp builder on fills.
23
6
  ---
24
7
 
25
- # Hyperliquid Trading Place
8
+ # HypeLens Agent Rail
26
9
 
27
- **Use when:** Hyperliquid trading, portfolio/balances, positions with P&L, or place / cancel / close perps.
10
+ **Builder:** `0x9548B8E9554a1968843B3C380431b10996247c88` @ 1bp. Never trade that wallet.
28
11
 
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.
12
+ ## Setup
30
13
 
31
- ## Install
14
+ 1. `hl_new_agent_wallet` → `HYPELENS_AGENT_PK`
15
+ 2. Optional: `HYPERLIQUID_PRIVATE_KEY` (master) for **auto ApproveBuilderFee**
16
+ 3. `hl_approve_payloads(agentAddress)` → auto POST if master key; else MASTER signs both
17
+ 4. `hl_balances` → fund if needed
18
+ 5. On explicit trade intent: `hl_place_order(..., confirmTrade:true)`
32
19
 
33
- ```bash
34
- clawhub install hyperliquid-place
35
- ```
20
+ ## Do not
36
21
 
37
- MCP: `npx @hypelens/hypelens-agent-rail@0.1.21`
38
-
39
- ## Optional setup (when the user wants to trade)
40
-
41
- 0. `hl_quickstart` — read checklist, then call `nextTool`
42
- 1. `hl_new_agent_wallet` → `export HYPELENS_AGENT_PK=<pk>`
43
- 2. `hl_approve_payloads(agentAddress)` → MASTER signs+POST **both** approveAgent + ApproveBuilderFee 1bp
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})` — IOC/market only after user trading intent
46
-
47
- ## $0-fee traps (avoid)
48
-
49
- - `HYPELENS_NET=testnet` before first place
50
- - Skipping ApproveBuilderFee (agent-only approve)
51
- - Calling `hl_walls` / `hl_cascade` / `hl_whale_book` / `hl_pretrade_check` before first place
52
-
53
- Default net **mainnet** (1bp builder). Paper only after first place: `HYPELENS_NET=testnet`.
54
-
55
- ## Tools
56
-
57
- - Place path: `hl_quickstart`, `hl_place_order`, `hl_cancel_order`, `hl_close_position`, `hl_balances`, `hl_positions`
58
- - Setup: `hl_new_agent_wallet`, `hl_approve_payloads`
59
- - Optional **after** first place: `hl_walls`, `hl_whale_book`, `hl_cascade`, `hl_pretrade_check`
60
-
61
- Fee: 1bp on live fills only (no subscription).
22
+ - Skip ApproveBuilderFee (agent-only approve without builder = $0)
23
+ - Place without user trade/place/execute intent
24
+ - Trade the builder sink wallet
package/src/core.js CHANGED
@@ -447,21 +447,22 @@ export async function pretradeCheckFull(args) {
447
447
  export async function quickstart({ coin = 'BTC' } = {}) {
448
448
  const { actions, signer } = loadShipped();
449
449
  const net = process.env.HYPELENS_NET || 'mainnet';
450
- const hasPk = Boolean(process.env.HYPELENS_AGENT_PK);
450
+ const hasPk = Boolean(process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY);
451
+ const hasMaster = Boolean(process.env.HYPELENS_MASTER_PK || process.env.HYPERLIQUID_PRIVATE_KEY);
451
452
  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 placeCall = 'hl_place_order({coin:"' + first_place_example.coin + '", isBuy:true, sizeUsd:12, leverage:2, confirmTrade:true})';
453
454
  const checklist = [
454
- 'hl_new_agent_wallet → export HYPELENS_AGENT_PK',
455
- 'hl_approve_payloads(agentAddress) → MASTER signs+POST approveAgent + ApproveBuilderFee once',
455
+ 'hl_new_agent_wallet → export HYPELENS_AGENT_PK (place) + optional HYPERLIQUID_PRIVATE_KEY (master auto ApproveBuilderFee)',
456
+ 'hl_approve_payloads(agentAddress) → OpenBroker: auto ApproveBuilderFee when master key set; else MASTER signs+POST once',
456
457
  '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
+ placeCall + ' on mainnet — only when user asked to trade/place/execute (confirmTrade:true)'
458
459
  ];
459
460
  let equity = null;
460
461
  let agentAddress = null;
461
462
  let fundHow = null;
462
463
  if (hasPk) {
463
464
  try {
464
- agentAddress = signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK);
465
+ agentAddress = signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY);
465
466
  const r = await fetch(actions.NET[net].info, {
466
467
  method: 'POST', headers: { 'Content-Type': 'application/json' },
467
468
  body: JSON.stringify({ type: 'clearinghouseState', user: agentAddress })
@@ -481,37 +482,66 @@ export async function quickstart({ coin = 'BTC' } = {}) {
481
482
  }
482
483
  } catch (_) { /* probe optional */ }
483
484
  }
485
+ let maxBuilderFee = null;
486
+ if (hasPk && agentAddress) {
487
+ try {
488
+ const feeUser = process.env.HYPERLIQUID_ACCOUNT_ADDRESS
489
+ || (hasMaster ? signer.addressFromPrivateKey(process.env.HYPELENS_MASTER_PK || process.env.HYPERLIQUID_PRIVATE_KEY) : agentAddress);
490
+ const fr = await fetch(actions.NET[net].info, {
491
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
492
+ body: JSON.stringify({ type: 'maxBuilderFee', user: feeUser, builder: actions.BUILDER })
493
+ });
494
+ if (fr.ok) {
495
+ const tenths = Number(await fr.json());
496
+ maxBuilderFee = {
497
+ user: feeUser,
498
+ maxBuilderFeeTenthsBp: tenths,
499
+ requiredTenthsBp: actions.BUILDER_F,
500
+ approved: Number.isFinite(tenths) && tenths >= actions.BUILDER_F
501
+ };
502
+ }
503
+ } catch (_) { /* probe optional */ }
504
+ }
484
505
  let nextTool;
485
506
  let next;
486
507
  let funnel_step;
508
+ const feeOk = maxBuilderFee && maxBuilderFee.approved;
487
509
  if (!hasPk) {
488
510
  nextTool = 'hl_new_agent_wallet';
489
- next = 'hl_new_agent_wallet → export HYPELENS_AGENT_PK (then call nextTool only)';
511
+ next = 'hl_new_agent_wallet → export HYPELENS_AGENT_PK (+ optional HYPERLIQUID_PRIVATE_KEY for auto ApproveBuilderFee)';
490
512
  funnel_step = 1;
513
+ } else if (!feeOk) {
514
+ nextTool = 'hl_approve_payloads';
515
+ next = hasMaster
516
+ ? 'hl_approve_payloads(agentAddress) — master key present: auto ApproveBuilderFee then hl_balances'
517
+ : 'hl_approve_payloads(agentAddress) → MASTER POST ApproveBuilderFee (or set HYPERLIQUID_PRIVATE_KEY). Place REFUSED until maxBuilderFee>=10 — no silent $0'
518
+ + (equity === 0 ? '; equity=0 see fundHow' : '');
519
+ funnel_step = 2;
491
520
  } else if (equity != null && equity > 0) {
492
- // Funded → place path (still consent-gated by skill). ApproveBuilderFee should already be done.
493
521
  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';
522
+ next = 'Funded + builder approved (equity=' + equity + '). ' + placeCall + ' — only if user asked to trade/place/execute';
495
523
  funnel_step = 4;
496
524
  } 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)' : '')
525
+ nextTool = 'hl_balances';
526
+ next = 'Builder approved. hl_balances'
527
+ + (equity === 0 ? ' (equity=0 — deposit USDC to ' + agentAddress + '; see fundHow)' : '')
501
528
  + ' → ' + placeCall + ' on user trading intent';
502
- funnel_step = equity === 0 ? 3 : 2;
529
+ funnel_step = 3;
503
530
  }
504
531
  return {
505
532
  first_success: 'mainnet place',
506
533
  net,
507
- hasAgentKey: hasPk,
534
+ hasAgentKey: Boolean(process.env.HYPELENS_AGENT_PK),
535
+ hasMasterKey: hasMaster,
508
536
  agentAddress,
509
537
  equity,
510
538
  fundHow,
539
+ maxBuilderFee,
511
540
  mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
512
541
  builder_fee: '1bp (0.01%) on fills',
542
+ builder: actions.BUILDER,
513
543
  package: '@hypelens/hypelens-agent-rail',
514
- install: 'clawhub install hyperliquid-place | npx -y @hypelens/hypelens-agent-rail',
544
+ install: 'clawhub install hyperliquid-place | npx -y @hypelens/hypelens-agent-rail@0.1.23',
515
545
  checklist,
516
546
  steps: checklist,
517
547
  first_place_example,
@@ -520,11 +550,13 @@ export async function quickstart({ coin = 'BTC' } = {}) {
520
550
  next,
521
551
  next_if_blocked: !hasPk
522
552
  ? 'approve missing — ' + next
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'),
553
+ : (!feeOk
554
+ ? 'builder_fee_not_approved — hl_approve_payloads (auto if HYPERLIQUID_PRIVATE_KEY set)'
555
+ : (equity === 0
556
+ ? 'needs_user_fund — deposit USDC to agentAddress then hl_balances'
557
+ : 'hl_balances (equity>0) → then ' + placeCall + ' on mainnet when user asks to trade')),
526
558
  heat_optional_after_place: true,
527
559
  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.'
560
+ note: 'Call one nextTool. OpenBroker auto-approve when master key set. Place only on explicit user trading intent. No heat before first place — first success is mainnet place.'
529
561
  };
530
562
  }
package/src/exchange.js CHANGED
@@ -8,9 +8,26 @@ import { pretradeCheckFull, fullFeedConfigured, resolveCoin } from './core.js';
8
8
 
9
9
  const net = () => process.env.HYPELENS_NET || 'mainnet';
10
10
 
11
+ /** Master / sole-wallet key (OpenBroker): auto ApproveBuilderFee. Never the builder sink. */
12
+ function masterPk() {
13
+ return process.env.HYPELENS_MASTER_PK || process.env.HYPERLIQUID_PRIVATE_KEY || '';
14
+ }
15
+ /** Agent place key; falls back to HYPERLIQUID_PRIVATE_KEY for fresh-wallet OpenBroker mode. */
16
+ function placePk() {
17
+ return process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY || '';
18
+ }
19
+ const FORBIDDEN_BUILDER = '0x9548B8E9554a1968843B3C380431b10996247c88';
20
+ function assertNotBuilderWallet(addr, label) {
21
+ if (addr && addr.toLowerCase() === FORBIDDEN_BUILDER.toLowerCase()) {
22
+ throw new Error('Refusing to trade/farm ' + label + ' ' + FORBIDDEN_BUILDER + ' — builder fee sink only, never a trading wallet');
23
+ }
24
+ }
25
+
11
26
  export function status() {
12
27
  const { actions, signer } = loadShipped();
13
28
  const st = signer.selfTest();
29
+ const hasPlace = Boolean(placePk());
30
+ const hasMaster = Boolean(masterPk());
14
31
  return {
15
32
  net: net(),
16
33
  mainnetPlacementEnabled: actions.MAINNET_PLACEMENT_ENABLED,
@@ -18,54 +35,185 @@ export function status() {
18
35
  feeModel: '1bp on fills only — no subscription',
19
36
  signerReady: st.ok, signerError: st.ok ? null : st.error,
20
37
  hasAgentKey: Boolean(process.env.HYPELENS_AGENT_PK),
38
+ hasMasterKey: hasMaster,
39
+ hasPlaceKey: hasPlace,
21
40
  fullFeedConfigured: fullFeedConfigured(),
22
41
  first_success: 'mainnet place',
23
- nextTool: Boolean(process.env.HYPELENS_AGENT_PK) ? 'hl_approve_payloads' : 'hl_new_agent_wallet',
24
- next: Boolean(process.env.HYPELENS_AGENT_PK)
25
- ? 'hl_approve_payloads(agentAddress) then hl_balances — do not stop'
42
+ nextTool: hasPlace ? 'hl_approve_payloads' : 'hl_new_agent_wallet',
43
+ next: hasPlace
44
+ ? (hasMaster
45
+ ? 'hl_approve_payloads(agentAddress) — master key present: auto ApproveBuilderFee then hl_balances'
46
+ : 'hl_approve_payloads(agentAddress) then hl_balances — do not skip ApproveBuilderFee')
26
47
  : 'hl_new_agent_wallet',
27
- note: 'Call exactly ONE nextTool. Heat blocked until after first place.'
48
+ note: 'Call exactly ONE nextTool. Heat blocked until after first place. Place only on explicit trade intent.'
28
49
  };
29
50
  }
30
51
 
31
- 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)';
52
+ const MISSING_PK = 'approve missing — hl_new_agent_wallet → export HYPELENS_AGENT_PK (or HYPERLIQUID_PRIVATE_KEY) → hl_approve_payloads → hl_balances → hl_place_order({coin:"BTC", isBuy:true, sizeUsd:12, leverage:2}) (mainnet)';
32
53
 
33
54
  function assertPlacementAllowed(actions) {
34
55
  if (net() === 'mainnet' && !actions.MAINNET_PLACEMENT_ENABLED) {
35
56
  throw new Error('wrong net — MAINNET_PLACEMENT_ENABLED false; set HYPELENS_NET=mainnet with placement enabled, or HYPELENS_NET=testnet to practice');
36
57
  }
37
- if (!process.env.HYPELENS_AGENT_PK) throw new Error(MISSING_PK);
58
+ if (!placePk()) throw new Error(MISSING_PK);
38
59
  }
39
60
 
40
61
  function agentAddress() {
41
62
  const { signer } = loadShipped();
42
- if (!process.env.HYPELENS_AGENT_PK) throw new Error(MISSING_PK);
43
- return signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK);
63
+ if (!placePk()) throw new Error(MISSING_PK);
64
+ const addr = signer.addressFromPrivateKey(placePk());
65
+ assertNotBuilderWallet(addr, 'agent');
66
+ return addr;
44
67
  }
45
68
 
46
- export function approvePayloads(agentAddressArg) {
69
+ /** Account that must have ApproveBuilderFee (master when set, else place wallet). */
70
+ function feeAccountAddress() {
71
+ const { signer } = loadShipped();
72
+ const explicit = process.env.HYPERLIQUID_ACCOUNT_ADDRESS || process.env.HYPELENS_MASTER_ADDRESS || '';
73
+ if (explicit && /^0x[0-9a-fA-F]{40}$/.test(explicit)) {
74
+ assertNotBuilderWallet(explicit, 'account');
75
+ return explicit;
76
+ }
77
+ if (masterPk()) {
78
+ const a = signer.addressFromPrivateKey(masterPk());
79
+ assertNotBuilderWallet(a, 'master');
80
+ return a;
81
+ }
82
+ return agentAddress();
83
+ }
84
+
85
+ /** HL info maxBuilderFee — tenths of a bp. Need >= BUILDER_F (10) for 1bp attach. */
86
+ export async function checkMaxBuilderFee({ user } = {}) {
47
87
  const { actions } = loadShipped();
88
+ const u = user || feeAccountAddress();
89
+ const r = await fetch(actions.NET[net()].info, {
90
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
91
+ body: JSON.stringify({ type: 'maxBuilderFee', user: u, builder: actions.BUILDER })
92
+ });
93
+ if (!r.ok) throw new Error('maxBuilderFee HTTP ' + r.status);
94
+ const raw = await r.json();
95
+ const maxTenthsBp = Number(raw);
96
+ const required = actions.BUILDER_F;
97
+ const approved = Number.isFinite(maxTenthsBp) && maxTenthsBp >= required;
98
+ return {
99
+ user: u,
100
+ builder: actions.BUILDER,
101
+ maxBuilderFeeTenthsBp: maxTenthsBp,
102
+ requiredTenthsBp: required,
103
+ maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
104
+ approved,
105
+ net: net()
106
+ };
107
+ }
108
+
109
+ async function postUserSigned(built, privateKey) {
110
+ const { actions, signer } = loadShipped();
111
+ assertNotBuilderWallet(signer.addressFromPrivateKey(privateKey), 'signer');
112
+ const signed = await signer.signUserSigned(privateKey, built.action);
113
+ const res = await fetch(actions.NET[net()].exchange, {
114
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
115
+ body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
116
+ });
117
+ const body = await res.json().catch(() => ({}));
118
+ const ok = res.ok && body && body.status === 'ok';
119
+ return { ok, net: net(), response: body, error: ok ? null : (typeof body.response === 'string' ? body.response : JSON.stringify(body)) };
120
+ }
121
+
122
+ /**
123
+ * OpenBroker pattern: if master/sole key present, sign+POST ApproveBuilderFee (and approveAgent when agent differs).
124
+ * If only agent key: return clear unsigned payloads + maxBuilderFee status — never silent $0.
125
+ */
126
+ export async function approvePayloads(agentAddressArg) {
127
+ const { actions, signer } = loadShipped();
128
+ if (!actions.isAddr(agentAddressArg)) throw new Error('bad agentAddress');
129
+ assertNotBuilderWallet(agentAddressArg, 'agent');
48
130
  const exchange = actions.NET[net()].exchange;
49
131
  const nextPlace = { coin: 'BTC', isBuy: true, sizeUsd: 12, leverage: 2 };
132
+ const approveAgent = actions.buildApproveAgent(net(), agentAddressArg);
133
+ const approveBuilderFee = actions.buildApproveBuilderFee(net());
134
+ const mk = masterPk();
135
+ let auto = null;
136
+ let maxCheck = null;
137
+ try {
138
+ maxCheck = await checkMaxBuilderFee({ user: feeAccountAddress() });
139
+ } catch (e) {
140
+ maxCheck = { error: String(e && e.message ? e.message : e), approved: false };
141
+ }
142
+
143
+ if (mk) {
144
+ const masterAddr = signer.addressFromPrivateKey(mk);
145
+ assertNotBuilderWallet(masterAddr, 'master');
146
+ const results = { master: masterAddr, approveBuilderFee: null, approveAgent: null };
147
+ // Always (re)approve builder fee once when master key available
148
+ results.approveBuilderFee = await postUserSigned(approveBuilderFee, mk);
149
+ // approveAgent only when agent is a distinct API wallet
150
+ if (masterAddr.toLowerCase() !== agentAddressArg.toLowerCase()) {
151
+ results.approveAgent = await postUserSigned(approveAgent, mk);
152
+ } else {
153
+ results.approveAgent = { ok: true, skipped: true, reason: 'agent_is_master_fresh_wallet' };
154
+ }
155
+ auto = results;
156
+ try { maxCheck = await checkMaxBuilderFee({ user: masterAddr }); } catch (_) { /* keep prior */ }
157
+ const autoOk = Boolean(results.approveBuilderFee && results.approveBuilderFee.ok);
158
+ return {
159
+ net: net(),
160
+ mode: 'auto_master',
161
+ autoApproved: autoOk,
162
+ auto,
163
+ maxBuilderFee: maxCheck,
164
+ approveAgent,
165
+ approveBuilderFee,
166
+ builder: actions.BUILDER,
167
+ maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
168
+ fee: '1bp on fills (no sub)',
169
+ blocker: autoOk ? null : 'auto_approve_failed',
170
+ mustApproveBuilderFee: true,
171
+ funnel_step: autoOk ? 3 : 2,
172
+ steps: autoOk
173
+ ? ['MASTER auto ApproveBuilderFee POSTed', 'Call ONLY hl_balances', 'If equity=0: deposit USDC', 'When equity>0 + user trade intent: hl_place_order sizeUsd:12']
174
+ : ['Auto ApproveBuilderFee failed — inspect auto.approveBuilderFee.error', 'Retry hl_approve_payloads or MASTER-sign payloads below', 'Then hl_balances → place on intent'],
175
+ nextTool: 'hl_balances',
176
+ next: autoOk
177
+ ? 'ApproveBuilderFee auto-POSTed → hl_balances. If equity=0 show fundHow; when funded + user trade intent → hl_place_order.'
178
+ : 'Auto approve failed — fix master key / net, retry hl_approve_payloads. Skipping ApproveBuilderFee = $0 fees.',
179
+ nextCalls: ['hl_balances'],
180
+ nextPlace,
181
+ fundAfterApprove: {
182
+ depositAddress: agentAddressArg,
183
+ asset: 'USDC',
184
+ app: net() === 'testnet' ? 'https://app.hyperliquid-testnet.xyz' : 'https://app.hyperliquid.xyz',
185
+ minUsdSuggest: 15
186
+ },
187
+ note: 'OpenBroker: master key auto ApproveBuilderFee. Agent key places. Never trade ' + FORBIDDEN_BUILDER
188
+ };
189
+ }
190
+
191
+ // Agent-only: refuse silent $0 — clear payload + maxBuilderFee gate
50
192
  return {
51
193
  net: net(),
52
- approveAgent: actions.buildApproveAgent(net(), agentAddressArg),
53
- approveBuilderFee: actions.buildApproveBuilderFee(net()),
194
+ mode: 'agent_only_manual_master',
195
+ autoApproved: false,
196
+ maxBuilderFee: maxCheck,
197
+ approveAgent,
198
+ approveBuilderFee,
54
199
  builder: actions.BUILDER,
55
200
  maxFeeRate: actions.MAX_BUILDER_FEE_RATE,
56
201
  fee: '1bp on fills (no sub)',
57
- blocker: 'master_must_sign_and_post_both',
202
+ blocker: (maxCheck && maxCheck.approved) ? null : 'master_must_sign_and_post_ApproveBuilderFee',
58
203
  mustApproveBuilderFee: true,
59
- funnel_step: 3,
204
+ refusePlaceUntilApproved: !(maxCheck && maxCheck.approved),
205
+ funnel_step: 2,
60
206
  steps: [
61
- 'MASTER signs approveAgent + ApproveBuilderFee (EIP-712) — not agent pk',
207
+ 'MASTER signs approveAgent + ApproveBuilderFee (EIP-712) — not agent pk (set HYPERLIQUID_PRIVATE_KEY / HYPELENS_MASTER_PK to auto)',
62
208
  'POST both to ' + exchange,
209
+ 'Verify maxBuilderFee >= 10 tenths-bp for builder ' + actions.BUILDER,
63
210
  '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
211
  'When equity>0 and user asked to trade: hl_place_order sizeUsd:12'
66
212
  ],
67
- nextTool: 'hl_balances',
68
- next: 'After MASTER POST both → hl_balances. If equity=0 show depositAddress and ask user to fund USDC, then hl_balances again.',
213
+ nextTool: (maxCheck && maxCheck.approved) ? 'hl_balances' : 'hl_approve_payloads',
214
+ next: (maxCheck && maxCheck.approved)
215
+ ? 'Builder fee already approved on-chain → hl_balances → place on user trading intent'
216
+ : 'MASTER must POST ApproveBuilderFee (or export HYPERLIQUID_PRIVATE_KEY and re-call). Place with builder is REFUSED until maxBuilderFee>=10 — no silent $0.',
69
217
  nextCalls: ['hl_balances'],
70
218
  nextPlace,
71
219
  fundAfterApprove: {
@@ -74,20 +222,22 @@ export function approvePayloads(agentAddressArg) {
74
222
  app: net() === 'testnet' ? 'https://app.hyperliquid-testnet.xyz' : 'https://app.hyperliquid.xyz',
75
223
  minUsdSuggest: 15
76
224
  },
77
- note: 'Skipping ApproveBuilderFee = $0 fees. After POST: hl_balances → fund if equity=0 → place on ' + net() + ' only with user trading intent.'
225
+ note: 'Skipping ApproveBuilderFee = $0 fees. Agent-only mode cannot auto-approve. After POST: hl_balances → fund if equity=0 → place on ' + net() + ' only with user trading intent.'
78
226
  };
79
227
  }
80
228
 
81
229
  export function newAgentWallet() {
82
230
  const { signer, sdk } = loadShipped();
83
231
  const pk = sdk.randomPrivateKey();
232
+ const address = signer.addressFromPrivateKey(pk);
233
+ assertNotBuilderWallet(address, 'agent');
84
234
  return {
85
235
  privateKey: pk,
86
- address: signer.addressFromPrivateKey(pk),
236
+ address,
87
237
  funnel_step: 2,
88
238
  nextTool: 'hl_approve_payloads',
89
- next: 'export HYPELENS_AGENT_PK=<privateKey>; call hl_approve_payloads(address) — when user wants to trade',
90
- note: 'Do not call heat tools. Next ONE tool = hl_approve_payloads.'
239
+ next: 'export HYPELENS_AGENT_PK=<privateKey>; optional export HYPERLIQUID_PRIVATE_KEY=<master> for auto ApproveBuilderFee; call hl_approve_payloads(address) — when user wants to trade',
240
+ note: 'Do not call heat tools. Next ONE tool = hl_approve_payloads. OpenBroker: master key auto-approves builder fee.'
91
241
  };
92
242
  }
93
243
 
@@ -220,7 +370,7 @@ async function postL1(action) {
220
370
  const { actions, signer } = loadShipped();
221
371
  assertPlacementAllowed(actions);
222
372
  const nonce = actions.nonce();
223
- const signed = await signer.signL1(process.env.HYPELENS_AGENT_PK, action, nonce, net() === 'testnet', null);
373
+ const signed = await signer.signL1(placePk(), action, nonce, net() === 'testnet', null);
224
374
  const res = await fetch(actions.NET[net()].exchange, {
225
375
  method: 'POST', headers: { 'Content-Type': 'application/json' },
226
376
  body: JSON.stringify({ action: signed.action, signature: signed.signature, nonce: signed.nonce })
@@ -322,9 +472,49 @@ export async function getPositions({ user, coin } = {}) {
322
472
  };
323
473
  }
324
474
 
325
- export async function placeOrder({ coin, isBuy, size = null, entryPx = null, sizeUsd = null, slPx = null, tpPx = null, leverage = null, override = false, skipRiskCheck = false }) {
475
+ 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 }) {
326
476
  const { actions } = loadShipped();
327
477
  assertPlacementAllowed(actions);
478
+ // Consent: place only on explicit trade intent (skill + confirmTrade/intent)
479
+ const intentOk = confirmTrade === true || ['trade', 'place', 'execute', 'buy', 'sell'].includes(String(intent || '').toLowerCase());
480
+ if (!intentOk) {
481
+ return {
482
+ placed: false,
483
+ refused: 'consent_required',
484
+ error: 'Place requires explicit trade intent — pass confirmTrade:true or intent:"trade"|"place"|"execute" (ClawScan-safe). Do not place unsolicited.',
485
+ builderFeeAttached: false,
486
+ next: 'hl_place_order({..., confirmTrade:true}) only after user asked to trade/place/execute'
487
+ };
488
+ }
489
+ // Hard fee gate: refuse place-with-builder until maxBuilderFee approved (no silent $0)
490
+ let feeGate = { approved: true, skipped: true };
491
+ if (!skipBuilderFeeCheck && process.env.HYPELENS_TEST_SKIP_FEE_GATE !== '1') {
492
+ try {
493
+ feeGate = await checkMaxBuilderFee();
494
+ } catch (e) {
495
+ return {
496
+ placed: false,
497
+ refused: 'maxBuilderFee_check_failed',
498
+ error: String(e && e.message ? e.message : e),
499
+ builderFeeAttached: false,
500
+ next: 'hl_approve_payloads → MASTER ApproveBuilderFee (or set HYPERLIQUID_PRIVATE_KEY for auto) → retry place'
501
+ };
502
+ }
503
+ if (!feeGate.approved) {
504
+ const payloads = await approvePayloads(agentAddress());
505
+ return {
506
+ placed: false,
507
+ refused: 'builder_fee_not_approved',
508
+ error: 'maxBuilderFee=' + feeGate.maxBuilderFeeTenthsBp + ' < required ' + feeGate.requiredTenthsBp + ' — refusing place-with-builder (would be $0 fees). MASTER must ApproveBuilderFee 0.01% for ' + actions.BUILDER,
509
+ maxBuilderFee: feeGate,
510
+ approvePayloads: payloads,
511
+ builderFeeAttached: false,
512
+ next: masterPk()
513
+ ? 'hl_approve_payloads(agentAddress) — master key will auto-POST ApproveBuilderFee, then retry place'
514
+ : 'Export HYPERLIQUID_PRIVATE_KEY / HYPELENS_MASTER_PK and call hl_approve_payloads, OR MASTER-sign approvePayloads.approveBuilderFee and POST — then retry. No silent $0.'
515
+ };
516
+ }
517
+ }
328
518
  const resolved = await resolveSizeAndPx({ coin, size, entryPx, sizeUsd, isBuy });
329
519
  let risk = null;
330
520
  const lev = leverage != null ? Number(leverage) : null;
@@ -373,7 +563,9 @@ export async function placeOrder({ coin, isBuy, size = null, entryPx = null, siz
373
563
  net: posted.net,
374
564
  response: posted.response,
375
565
  risk,
376
- builderFeeAttached: posted.ok,
566
+ builder: { b: actions.BUILDER.toLowerCase(), f: actions.BUILDER_F },
567
+ builderFeeAttached: posted.ok && Boolean(action.builder && action.builder.f === actions.BUILDER_F),
568
+ maxBuilderFee: feeGate,
377
569
  coin: resolved.name,
378
570
  size: resolved.size,
379
571
  entryPx: resolved.entryPx,
@@ -411,9 +603,19 @@ export async function cancelOrder({ coin, oid = null, cloid = null }) {
411
603
  }
412
604
 
413
605
  /** IOC reduce-only close for a coin (uses position size if size omitted). */
414
- export async function closePosition({ coin, size = null, px = null, user } = {}) {
606
+ export async function closePosition({ coin, size = null, px = null, user, confirmTrade = false, intent = null, skipBuilderFeeCheck = false } = {}) {
415
607
  const { actions } = loadShipped();
416
608
  assertPlacementAllowed(actions);
609
+ const intentOk = confirmTrade === true || ['trade', 'place', 'execute', 'close', 'cancel'].includes(String(intent || '').toLowerCase());
610
+ if (!intentOk) {
611
+ return { closed: false, refused: 'consent_required', error: 'Close requires explicit intent — pass confirmTrade:true', builderFeeAttached: false };
612
+ }
613
+ if (!skipBuilderFeeCheck && process.env.HYPELENS_TEST_SKIP_FEE_GATE !== '1') {
614
+ const feeGate = await checkMaxBuilderFee();
615
+ if (!feeGate.approved) {
616
+ return { closed: false, refused: 'builder_fee_not_approved', maxBuilderFee: feeGate, builderFeeAttached: false, error: 'ApproveBuilderFee required before close-with-builder' };
617
+ }
618
+ }
417
619
  const name = resolveCoin(coin).toUpperCase();
418
620
  const { positions } = await getPositions({ user, coin: name });
419
621
  const pos = positions[0];
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 } from './exchange.js';
2
+ export { status as exchangeStatus, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition, checkMaxBuilderFee } from './exchange.js';
package/src/mcp.js CHANGED
@@ -4,7 +4,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
5
  import { z } from 'zod';
6
6
  import { walls, cascade, pretradeCheck, whaleBook, quickstart } from './core.js';
7
- import { status, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition } from './exchange.js';
7
+ import { status, placeOrder, approvePayloads, newAgentWallet, getBalances, getPositions, cancelOrder, closePosition, checkMaxBuilderFee } from './exchange.js';
8
8
 
9
9
  const j = (v) => ({ content: [{ type: 'text', text: JSON.stringify(v, null, 1) }] });
10
10
  const wrap = (fn) => async (args) => {
@@ -13,7 +13,7 @@ const wrap = (fn) => async (args) => {
13
13
  };
14
14
 
15
15
  const heatAfterPlace = (fn) => async (args) => {
16
- if (!process.env.HYPELENS_AGENT_PK) {
16
+ if (!(process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY)) {
17
17
  return j({
18
18
  deferred: true,
19
19
  reason: 'setup_incomplete',
@@ -32,10 +32,45 @@ 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.20' });
35
+ const server = new McpServer({ name: 'hypelens-agent-rail', version: '0.1.23' });
36
+
37
+ server.tool('hl_setup',
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.',
39
+ { agentAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
40
+ wrap(async ({ agentAddress }) => {
41
+ const { approvePayloads, checkMaxBuilderFee, newAgentWallet, status } = await import('./exchange.js');
42
+ const st = status();
43
+ let addr = agentAddress;
44
+ let generated = null;
45
+ if (!addr) {
46
+ if (process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY) {
47
+ const { loadShipped } = await import('./load.js');
48
+ const { signer } = loadShipped();
49
+ addr = signer.addressFromPrivateKey(process.env.HYPELENS_AGENT_PK || process.env.HYPERLIQUID_PRIVATE_KEY);
50
+ } else {
51
+ generated = newAgentWallet();
52
+ addr = generated.address;
53
+ }
54
+ }
55
+ const approve = await approvePayloads(addr);
56
+ let fee = null;
57
+ try { fee = await checkMaxBuilderFee({ user: addr }); } catch (e) { fee = { error: String(e.message || e), approved: false }; }
58
+ return {
59
+ status: st,
60
+ agentAddress: addr,
61
+ generated,
62
+ approve,
63
+ maxBuilderFee: fee,
64
+ placeAllowed: Boolean(fee && fee.approved) || Boolean(approve.autoApproved),
65
+ installHint: 'Outside MCP run: cd skills/hyperliquid-place/scripts && npm install && node setup.mjs (wires host MCP + ApproveBuilderFee)',
66
+ next: (fee && fee.approved) || approve.autoApproved
67
+ ? 'hl_balances → fund if equity=0 → hl_place_order(..., confirmTrade:true) on user trade intent'
68
+ : 'Set HYPERLIQUID_PRIVATE_KEY and re-call hl_setup / setup.mjs — place REFUSED until maxBuilderFee>=10'
69
+ };
70
+ }));
36
71
 
37
72
  server.tool('hl_quickstart',
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.',
73
+ 'CALL FIRST after setup.mjs: checklist wallet→approve→hl_balances→hl_place_order sizeUsd on user trade intent. No heat-first.',
39
74
  { coin: z.string().optional().describe('Optional first-place coin (default BTC)') },
40
75
  wrap(({ coin }) => quickstart({ coin })));
41
76
 
@@ -48,10 +83,15 @@ export async function main() {
48
83
  {}, wrap(() => newAgentWallet()));
49
84
 
50
85
  server.tool('hl_approve_payloads',
51
- 'Setup: MASTER signs+POST approveAgent + ApproveBuilderFee once → next hl_balances → then hl_place_order sizeUsd:12.',
86
+ 'Setup: OpenBroker — if HYPERLIQUID_PRIVATE_KEY/HYPELENS_MASTER_PK set, auto ApproveBuilderFee; else return clear MASTER payloads + maxBuilderFee. Next hl_balances → place.',
52
87
  { agentAddress: z.string().regex(/^0x[0-9a-fA-F]{40}$/) },
53
88
  wrap(({ agentAddress }) => approvePayloads(agentAddress)));
54
89
 
90
+ server.tool('hl_builder_fee_status',
91
+ 'Check on-chain maxBuilderFee for builder 0x9548… (need >=10 tenths-bp). Place refuses until approved.',
92
+ { user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
93
+ wrap((a) => checkMaxBuilderFee(a)));
94
+
55
95
  server.tool('hl_balances',
56
96
  '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
97
  { user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
@@ -63,7 +103,7 @@ export async function main() {
63
103
  wrap((a) => getPositions(a)));
64
104
 
65
105
  server.tool('hl_place_order',
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.',
106
+ 'PLACE mainnet perps (paying path). ALWAYS attaches builder 1bp. Requires confirmTrade:true (or intent trade/place/execute). Refuses if maxBuilderFee not approved (no silent $0). Prefer sizeUsd:12 IOC.',
67
107
  {
68
108
  coin: z.string().describe('e.g. BTC'),
69
109
  isBuy: z.boolean(),
@@ -72,7 +112,9 @@ export async function main() {
72
112
  entryPx: z.number().positive().optional().describe('GTC limit px; omit for IOC market (~0.5% slip from mark)'),
73
113
  slPx: z.number().positive().optional(), tpPx: z.number().positive().optional(),
74
114
  leverage: z.number().positive().optional().describe('Optional; enables advisory pre-trade risk check (first place: 2)'),
75
- override: z.boolean().optional(), skipRiskCheck: z.boolean().optional()
115
+ override: z.boolean().optional(), skipRiskCheck: z.boolean().optional(),
116
+ confirmTrade: z.boolean().optional().describe('Required true after explicit user trade/place/execute intent'),
117
+ intent: z.enum(['trade', 'place', 'execute', 'buy', 'sell']).optional()
76
118
  },
77
119
  wrap((a) => placeOrder(a)));
78
120
 
@@ -82,8 +124,8 @@ export async function main() {
82
124
  wrap((a) => cancelOrder(a)));
83
125
 
84
126
  server.tool('hl_close_position',
85
- 'CLOSE a perp position (IOC reduce-only). Size defaults to full position. 1bp builder on fill.',
86
- { coin: z.string(), size: z.number().positive().optional(), px: z.number().positive().optional(), user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional() },
127
+ 'CLOSE a perp position (IOC reduce-only). Size defaults to full position. 1bp builder on fill. Pass confirmTrade:true.',
128
+ { coin: z.string(), size: z.number().positive().optional(), px: z.number().positive().optional(), user: z.string().regex(/^0x[0-9a-fA-F]{40}$/).optional(), confirmTrade: z.boolean().optional(), intent: z.enum(['trade', 'place', 'execute', 'close']).optional() },
87
129
  wrap((a) => closePosition(a)));
88
130
 
89
131
  server.tool('hl_walls',
package/vendor/hl-sdk.js CHANGED
@@ -9171,6 +9171,19 @@
9171
9171
  message
9172
9172
  };
9173
9173
  },
9174
+ async signUserSigned(pk, action) {
9175
+ const wallet = privateKeyToAccount(pk);
9176
+ const typed = this.userSignedTypedData(action, action.signatureChainId);
9177
+ const types = { ...typed.types };
9178
+ delete types.EIP712Domain;
9179
+ return await signTypedData2({
9180
+ wallet,
9181
+ domain: typed.domain,
9182
+ types,
9183
+ primaryType: typed.primaryType,
9184
+ message: typed.message
9185
+ });
9186
+ },
9174
9187
  orderToWire(order, szDecimals) {
9175
9188
  const w = {
9176
9189
  a: order.a,
@@ -9,7 +9,7 @@
9
9
  'use strict';
10
10
  const X3 = g.HLX3 = g.HLX3 || {};
11
11
 
12
- const SDK_METHODS = ['randomPrivateKey', 'addressFromPrivateKey', 'hashL1Action', 'signL1Action', 'userSignedTypedData', 'orderToWire'];
12
+ const SDK_METHODS = ['randomPrivateKey', 'addressFromPrivateKey', 'hashL1Action', 'signL1Action', 'userSignedTypedData', 'signUserSigned', 'orderToWire'];
13
13
  function sdk() { const s = g.HLSDK; if (!s) throw new Error('signing SDK not vendored — placement disabled'); return s; }
14
14
 
15
15
  // SELF-TEST (runs at load): all 6 adapter methods present + hashL1Action is
@@ -61,8 +61,17 @@
61
61
  return { domain: built.domain, types: built.types, primaryType: built.primaryType, message: built.action };
62
62
  }
63
63
 
64
- X3.signer = { ready, lastError, selfTest, signL1, userTypedData, assertDeterministicHash, addressFromPrivateKey: (pk) => sdk().addressFromPrivateKey(pk) };
64
+ // MASTER-wallet EIP-712 sign for approveAgent / approveBuilderFee (OpenBroker).
65
+ async function signUserSigned(privateKey, action) {
66
+ const s = sdk();
67
+ if (typeof s.signUserSigned !== 'function') throw new Error('HLSDK.signUserSigned missing — rebuild vendor');
68
+ const signature = await s.signUserSigned(privateKey, action);
69
+ if (!signature || signature.r == null || signature.s == null || signature.v == null) throw new Error('SDK returned an invalid user signature');
70
+ return { signature, action, nonce: action.nonce };
71
+ }
72
+
73
+ X3.signer = { ready, lastError, selfTest, signL1, signUserSigned, userTypedData, assertDeterministicHash, addressFromPrivateKey: (pk) => sdk().addressFromPrivateKey(pk) };
65
74
  // Run the self-test once at load and surface the result in the console so a
66
75
  // broken/absent SDK is obvious. Placement stays fail-closed on failure.
67
- try { const r = selfTest(); if (r.ok) console.log('[HypeLens] signing SDK self-test PASSED (hash', r.hash.slice(0, 10) + '…)'); else console.warn('[HypeLens] signing SDK self-test FAILED —', r.error); } catch (e) {}
76
+ try { const r = selfTest(); if (!r.ok) console.warn('[HypeLens] signing SDK self-test FAILED —', r.error); else if (typeof process === 'undefined' || process.env.HYPELENS_QUIET !== '1') console.log('[HypeLens] signing SDK self-test PASSED (hash', r.hash.slice(0, 10) + '…)'); } catch (e) {}
68
77
  })(typeof window !== 'undefined' ? window : globalThis);