perp-cli 0.3.4 → 0.3.6

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.
@@ -1,5 +1,6 @@
1
1
  import { PacificaAdapter } from "../exchanges/pacifica.js";
2
2
  import { printJson, jsonOk } from "../utils.js";
3
+ import { setEnvVar } from "./init.js";
3
4
  import chalk from "chalk";
4
5
  export function registerManageCommands(program, getAdapter, isJson, getPacificaAdapter) {
5
6
  const manage = program.command("manage").description("Account management");
@@ -288,6 +289,9 @@ export function registerManageCommands(program, getAdapter, isJson, getPacificaA
288
289
  console.log(chalk.gray(" Generating key pair + registering on-chain...\n"));
289
290
  }
290
291
  const { privateKey, publicKey } = await adapter.setupApiKey(keyIndex);
292
+ // Auto-save to .env
293
+ setEnvVar("LIGHTER_API_KEY", privateKey);
294
+ setEnvVar("LIGHTER_ACCOUNT_INDEX", String(adapter.accountIndex));
291
295
  if (isJson()) {
292
296
  return printJson(jsonOk({
293
297
  privateKey,
@@ -295,15 +299,13 @@ export function registerManageCommands(program, getAdapter, isJson, getPacificaA
295
299
  address: adapter.address,
296
300
  accountIndex: adapter.accountIndex,
297
301
  apiKeyIndex: keyIndex,
302
+ savedToEnv: true,
298
303
  }));
299
304
  }
300
- console.log(chalk.green(" API Key Registered!\n"));
305
+ console.log(chalk.green(" API Key Registered & saved to ~/.perp/.env\n"));
301
306
  console.log(` ${chalk.bold("Private Key:")} ${privateKey}`);
302
307
  console.log(` ${chalk.bold("Public Key:")} ${publicKey}`);
303
- console.log();
304
- console.log(chalk.yellow(" Add to your .env file:"));
305
- console.log(chalk.white(` LIGHTER_API_KEY=${privateKey}`));
306
- console.log(chalk.white(` LIGHTER_ACCOUNT_INDEX=${adapter.accountIndex}`));
308
+ console.log(` ${chalk.bold("Account:")} ${adapter.accountIndex}`);
307
309
  console.log();
308
310
  });
309
311
  }
@@ -382,13 +382,41 @@ export function registerWalletCommands(program, isJson) {
382
382
  settings.defaultExchange = resolved;
383
383
  saveSettings(settings);
384
384
  }
385
+ // Auto-setup Lighter API key if setting lighter PK
386
+ let lighterApiSetup = {};
387
+ if (resolved === "lighter") {
388
+ try {
389
+ const { LighterAdapter } = await import("../exchanges/lighter.js");
390
+ const adapter = new LighterAdapter(normalized);
391
+ await adapter.init();
392
+ const { privateKey: apiKey } = await adapter.setupApiKey();
393
+ setEnvVar("LIGHTER_API_KEY", apiKey);
394
+ setEnvVar("LIGHTER_ACCOUNT_INDEX", String(adapter.accountIndex));
395
+ lighterApiSetup = { apiKey, accountIndex: adapter.accountIndex };
396
+ }
397
+ catch (e) {
398
+ lighterApiSetup = { error: e instanceof Error ? e.message : String(e) };
399
+ }
400
+ }
385
401
  if (isJson())
386
- return printJson(jsonOk({ exchange: resolved, address, envFile: ENV_FILE, default: !!opts.default }));
402
+ return printJson(jsonOk({
403
+ exchange: resolved, address, envFile: ENV_FILE, default: !!opts.default,
404
+ ...(resolved === "lighter" && { lighterApiKey: lighterApiSetup }),
405
+ }));
387
406
  console.log(chalk.green(`\n ${resolved} configured.`));
388
407
  console.log(` Address: ${chalk.green(address)}`);
389
408
  console.log(` Saved to: ${chalk.gray("~/.perp/.env")}`);
390
409
  if (opts.default)
391
410
  console.log(` Default: ${chalk.cyan(resolved)}`);
411
+ if (resolved === "lighter") {
412
+ if (lighterApiSetup.apiKey) {
413
+ console.log(chalk.green(` API Key: auto-registered (index: ${lighterApiSetup.accountIndex})`));
414
+ }
415
+ else {
416
+ console.log(chalk.yellow(` API Key: setup failed — ${lighterApiSetup.error}`));
417
+ console.log(chalk.gray(` You can retry: perp -e lighter manage setup-api-key`));
418
+ }
419
+ }
392
420
  console.log();
393
421
  });
394
422
  // ── show (show configured exchanges with public addresses) ──
@@ -62,6 +62,23 @@ export class LighterAdapter {
62
62
  ? this._accountIndexInit
63
63
  : json.accounts[0].account_index;
64
64
  }
65
+ // Auto-generate API key if we have PK but no API key and account exists
66
+ if (!this._apiKey && this._accountIndex >= 0) {
67
+ try {
68
+ const { privateKey: apiKey } = await this.setupApiKey();
69
+ this._apiKey = apiKey;
70
+ // Save to .env for future use
71
+ try {
72
+ const { setEnvVar } = await import("../commands/init.js");
73
+ setEnvVar("LIGHTER_API_KEY", apiKey);
74
+ setEnvVar("LIGHTER_ACCOUNT_INDEX", String(this._accountIndex));
75
+ }
76
+ catch { /* non-critical — env save may fail in some contexts */ }
77
+ }
78
+ catch {
79
+ // Auto-setup failed (no gas, network issue, etc.) — continue in read-only mode
80
+ }
81
+ }
65
82
  // Initialize signer for trading if we have an API key
66
83
  if (this._apiKey) {
67
84
  const { LighterSignerClient } = require("lighter-sdk");
package/dist/index.js CHANGED
@@ -48,7 +48,7 @@ const _defaultExchange = _settings.defaultExchange || "pacifica";
48
48
  program
49
49
  .name("perp")
50
50
  .description("Multi-DEX Perpetual Futures CLI (Pacifica, Hyperliquid, Lighter)")
51
- .version("0.3.4")
51
+ .version("0.3.6")
52
52
  .option("-e, --exchange <exchange>", `Exchange: pacifica, hyperliquid, lighter (default: ${_defaultExchange})`, _defaultExchange)
53
53
  .option("-n, --network <network>", "Network: mainnet or testnet", "mainnet")
54
54
  .option("-k, --private-key <key>", "Private key")
@@ -383,7 +383,7 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
383
383
  process.env.LIGHTER_PRIVATE_KEY);
384
384
  if (!status.hasWallets && !hasEnvKey && !settings.defaultExchange) {
385
385
  // Fresh install — onboarding
386
- console.log(chalk.cyan.bold("\n Welcome to perp-cli!") + chalk.gray(" v0.3.4\n"));
386
+ console.log(chalk.cyan.bold("\n Welcome to perp-cli!") + chalk.gray(" v0.3.6\n"));
387
387
  console.log(" Multi-DEX perpetual futures CLI for Pacifica, Hyperliquid, and Lighter.\n");
388
388
  console.log(` Get started: ${chalk.cyan("perp init")}`);
389
389
  console.log(chalk.gray(`\n Or explore without a wallet:`));
@@ -396,7 +396,7 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
396
396
  // Configured — show status overview
397
397
  const defaultEx = settings.defaultExchange || "pacifica";
398
398
  const activeEntries = Object.entries(status.active);
399
- console.log(chalk.cyan.bold("\n perp-cli") + chalk.gray(" v0.3.4\n"));
399
+ console.log(chalk.cyan.bold("\n perp-cli") + chalk.gray(" v0.3.6\n"));
400
400
  console.log(` Default exchange: ${chalk.cyan(defaultEx)}`);
401
401
  if (activeEntries.length > 0) {
402
402
  console.log(chalk.white.bold("\n Wallets:"));
@@ -56,7 +56,7 @@ function err(error, meta) {
56
56
  return JSON.stringify({ ok: false, error, meta }, null, 2);
57
57
  }
58
58
  // ── MCP Server ──
59
- const server = new McpServer({ name: "perp-cli", version: "0.3.4" }, { capabilities: { tools: {}, resources: {} } });
59
+ const server = new McpServer({ name: "perp-cli", version: "0.3.6" }, { capabilities: { tools: {}, resources: {} } });
60
60
  // ============================================================
61
61
  // Market Data tools (read-only, no private key needed)
62
62
  // ============================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "perp-cli",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
5
5
  "bin": {
6
6
  "perp": "./dist/index.js",
@@ -5,7 +5,7 @@ allowed-tools: "Bash(perp:*), Bash(npx perp-cli:*), Bash(npx -y perp-cli:*)"
5
5
  license: MIT
6
6
  metadata:
7
7
  author: hypurrquant
8
- version: "0.3.4"
8
+ version: "0.3.6"
9
9
  mcp-server: perp-cli
10
10
  ---
11
11
 
@@ -22,6 +22,7 @@ Multi-DEX perpetual futures CLI — Pacifica (Solana), Hyperliquid (HyperEVM), L
22
22
  5. **Verify wallet before any operation.** Run `perp --json wallet show` first.
23
23
  6. **Use ISOLATED margin for arb.** Set `perp --json manage margin <SYM> isolated` before opening positions. Cross margin can cascade liquidations.
24
24
  7. **Monitor positions continuously.** Run `perp --json risk status` and `perp --json -e <EX> account positions` every 15 minutes while positions are open.
25
+ 8. **NEVER read ~/.perp/.env or any key files directly.** Private keys are managed by the CLI internally. Use `perp --json wallet show` to check wallet status. Never attempt to read, cat, or access key files — this is a security violation.
25
26
 
26
27
  ## Step 1: Install
27
28
 
@@ -81,6 +81,15 @@ perp --json bridge status <orderId> # wait for completion
81
81
  perp --json arb rates
82
82
  ```
83
83
 
84
+ ### Lighter API Key Setup
85
+ Lighter uses a separate API key for trading, but **this is handled automatically**.
86
+ When `LIGHTER_PRIVATE_KEY` is set (env var or `wallet set`), the CLI auto-generates and saves the API key on first use.
87
+
88
+ If auto-setup fails (e.g. no ETH for gas on Lighter chain), retry manually:
89
+ ```bash
90
+ perp --json -e lighter manage setup-api-key
91
+ ```
92
+
84
93
  ### Using the Same EVM Key for Multiple Exchanges
85
94
  One EVM private key works for both Hyperliquid and Lighter:
86
95
  ```bash
@@ -79,6 +79,40 @@ When evaluating an arb opportunity:
79
79
  - Both positions should open near-simultaneously to minimize directional exposure
80
80
  - If capital needs to bridge first, you're exposed during transit
81
81
 
82
+ ### Hold Duration Target
83
+
84
+ **Before entering any arb position, set a target hold duration.** This is critical for calculating whether the position is worth entering.
85
+
86
+ ```
87
+ Expected Profit = hourly_spread × target_hours
88
+ Entry/Exit Cost = open_fees + close_fees + slippage (both legs)
89
+ Net Profit = Expected Profit - Entry/Exit Cost
90
+ ```
91
+
92
+ **Only enter if Net Profit > 0 with a comfortable margin.**
93
+
94
+ Guidelines for hold duration:
95
+ - **Stable spreads (20-50 bps):** target 8-24 hours. These are reliable but low-yield — you need time for the funding to accumulate past your entry/exit costs.
96
+ - **Elevated spreads (50-100 bps):** target 4-8 hours. Higher yield, but likely to compress. Take profit earlier.
97
+ - **Spike spreads (>100 bps):** target 1-4 hours. These revert quickly. Must cover entry/exit costs within a few funding cycles.
98
+
99
+ **Re-evaluate at each funding settlement (every hour):**
100
+ 1. Is the spread still above breakeven for the remaining target hours?
101
+ 2. If spread compressed, should I exit early or extend the hold?
102
+ 3. Has a better opportunity appeared? (Remember: switching has its own cost)
103
+
104
+ **Track your actual hold durations vs targets over time.** This builds intuition for how long spreads persist on each exchange pair.
105
+
106
+ Example entry decision log:
107
+ ```
108
+ Entry: BTC HL↔PAC | Spread: 35 bps | Target hold: 12h
109
+ Expected: 35 bps × 12h = 420 bps gross
110
+ Entry/exit cost: ~80 bps (fees + slippage both legs)
111
+ Net expected: ~340 bps → ENTER
112
+ Hour 6 check: spread compressed to 15 bps → below breakeven for remaining 6h → EXIT
113
+ Actual hold: 6h | Actual net: ~130 bps
114
+ ```
115
+
82
116
  ### Monitoring Active Positions
83
117
  ```bash
84
118
  perp --json portfolio # unified multi-exchange view