perp-cli 0.7.7 → 0.7.9
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 +13 -5
- package/dist/commands/agent.js +2 -2
- package/dist/commands/bot.js +20 -1
- package/dist/commands/bridge.js +2 -2
- package/dist/commands/funds.js +56 -11
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +41 -6
- package/dist/commands/manage.js +5 -1
- package/dist/commands/settings.js +3 -3
- package/dist/commands/wallet.js +40 -3
- package/dist/config.js +2 -2
- package/dist/exchanges/hyperliquid.js +2 -2
- package/dist/exchanges/lighter.js +7 -7
- package/dist/exchanges/pacifica.js +7 -7
- package/dist/index.js +13 -12
- package/dist/mcp-server.js +1 -1
- package/dist/pacifica/constants.d.ts +1 -1
- package/dist/pacifica/constants.js +4 -1
- package/package.json +2 -2
- package/skills/perp-cli/SKILL.md +10 -9
- package/skills/perp-cli/references/agent-operations.md +13 -11
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/perp-cli)
|
|
5
5
|
[](https://github.com/hypurrquant/perp-cli/blob/main/LICENSE)
|
|
6
6
|
|
|
7
|
-
Multi-DEX perpetual futures CLI — **Pacifica** (Solana), **Hyperliquid** (HyperEVM), **Lighter** (Ethereum).
|
|
7
|
+
Multi-DEX perpetual futures CLI — **Pacifica** (Solana), **Hyperliquid** (HyperEVM), **Lighter** (Ethereum), **Aster** (BNB Chain).
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
npm install -g perp-cli # global install
|
|
@@ -16,7 +16,7 @@ npx -y perp-cli --json portfolio
|
|
|
16
16
|
|
|
17
17
|
## Features
|
|
18
18
|
|
|
19
|
-
- **
|
|
19
|
+
- **4 Exchanges** — trade, bridge, arbitrage across Pacifica, Hyperliquid, Lighter, Aster
|
|
20
20
|
- **Funding Rate Arb** — perp-perp + spot-perp scan & one-command dual-leg execution
|
|
21
21
|
- **Portfolio** — single call returns balances, positions, risk level across all exchanges
|
|
22
22
|
- **Funds** — deposit, withdraw, CCTP bridge, internal transfer in one group
|
|
@@ -28,9 +28,13 @@ npx -y perp-cli --json portfolio
|
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
30
|
# Set exchange keys
|
|
31
|
-
perp wallet set hl <EVM_KEY> # Hyperliquid
|
|
32
31
|
perp wallet set pac <SOLANA_KEY> # Pacifica
|
|
32
|
+
perp wallet set hl <EVM_KEY> # Hyperliquid
|
|
33
33
|
perp wallet set lt <EVM_KEY> # Lighter (API key auto-generated)
|
|
34
|
+
perp wallet set aster <API_KEY> # Aster (BNB Chain)
|
|
35
|
+
|
|
36
|
+
# Or interactive setup wizard
|
|
37
|
+
perp setup
|
|
34
38
|
|
|
35
39
|
# Verify
|
|
36
40
|
perp wallet show
|
|
@@ -64,6 +68,8 @@ Same EVM key works for both Hyperliquid and Lighter.
|
|
|
64
68
|
| `jobs` | Background job management (tmux) |
|
|
65
69
|
| `alerts` | Telegram funding rate alerts with background daemon |
|
|
66
70
|
| `agent` | Schema introspection, capabilities, health check |
|
|
71
|
+
| `setup` | Interactive setup wizard (alias: `init`) |
|
|
72
|
+
| `status` | Unified dashboard: balances, positions, arb opps |
|
|
67
73
|
|
|
68
74
|
## Core Commands
|
|
69
75
|
|
|
@@ -159,7 +165,7 @@ perp alerts stop # stop background daemon
|
|
|
159
165
|
|
|
160
166
|
Setup flow: BotFather token → bot validation → send `/start` to bot → auto-detect chat ID → test message.
|
|
161
167
|
|
|
162
|
-
Exchange flag: `-e hyperliquid` / `-e pacifica` / `-e lighter` (aliases: `hl`, `pac`, `lt`).
|
|
168
|
+
Exchange flag: `-e hyperliquid` / `-e pacifica` / `-e lighter` / `-e aster` (aliases: `hl`, `pac`, `lt`).
|
|
163
169
|
|
|
164
170
|
Global flags: `--json`, `--fields <f>`, `--ndjson`, `--dry-run`, `--dex <name>` (HIP-3), `-w, --wallet <name>`.
|
|
165
171
|
|
|
@@ -186,8 +192,10 @@ Optional: add keys for trading and account data:
|
|
|
186
192
|
```json
|
|
187
193
|
{
|
|
188
194
|
"env": {
|
|
195
|
+
"PACIFICA_PRIVATE_KEY": "your-solana-key",
|
|
189
196
|
"HYPERLIQUID_PRIVATE_KEY": "your-evm-key",
|
|
190
|
-
"
|
|
197
|
+
"LIGHTER_PRIVATE_KEY": "your-evm-key",
|
|
198
|
+
"ASTER_API_KEY": "your-api-key"
|
|
191
199
|
}
|
|
192
200
|
}
|
|
193
201
|
```
|
package/dist/commands/agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { printJson, jsonOk } from "../utils.js";
|
|
2
|
+
import { printJson, jsonOk, jsonError } from "../utils.js";
|
|
3
3
|
import { ERROR_CODES } from "../errors.js";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
const _require = createRequire(import.meta.url);
|
|
@@ -274,7 +274,7 @@ export function registerAgentCommands(program, getAdapter, isJson) {
|
|
|
274
274
|
}
|
|
275
275
|
catch (err) {
|
|
276
276
|
const msg = err instanceof Error ? err.message : String(err);
|
|
277
|
-
console.error(JSON.stringify(
|
|
277
|
+
console.error(JSON.stringify(jsonError("EXEC_FAILED", msg)));
|
|
278
278
|
process.exit(1);
|
|
279
279
|
}
|
|
280
280
|
});
|
package/dist/commands/bot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import { printJson, jsonOk } from "../utils.js";
|
|
2
|
+
import { printJson, jsonOk, jsonError } from "../utils.js";
|
|
3
3
|
import { loadBotConfig, parseStrategy, quickGridConfig, quickDCAConfig, runBot, PRESETS, getPreset, getPresetsByStrategy } from "../bot/index.js";
|
|
4
4
|
import { updateJobState } from "../jobs.js";
|
|
5
5
|
import { runTWAP, runFundingArb, runGrid, runDCA, runTrailingStop } from "../strategies/index.js";
|
|
@@ -444,6 +444,10 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
|
|
|
444
444
|
"grid-mm", "liquidation-mm", "regime-mm",
|
|
445
445
|
];
|
|
446
446
|
if (sym === "ALL" && symbolRequiredStrategies.includes(strategyName)) {
|
|
447
|
+
if (isJson()) {
|
|
448
|
+
console.error(JSON.stringify(jsonError("INVALID_PARAMS", `The '${strategyName}' strategy requires a <symbol> argument`)));
|
|
449
|
+
return process.exit(1);
|
|
450
|
+
}
|
|
447
451
|
console.error(chalk.red(`\n Error: The '${strategyName}' strategy requires a <symbol> argument.`));
|
|
448
452
|
console.error(chalk.gray(` Usage: perp bot run ${strategyName} <symbol>\n`));
|
|
449
453
|
return process.exit(1);
|
|
@@ -499,6 +503,10 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
|
|
|
499
503
|
const { getStrategy, listStrategies: listStrats } = await import("../bot/strategy-registry.js");
|
|
500
504
|
if (!getStrategy(strategyName)) {
|
|
501
505
|
const available = listStrats();
|
|
506
|
+
if (isJson()) {
|
|
507
|
+
console.error(JSON.stringify(jsonError("INVALID_PARAMS", `Unknown strategy: "${strategyName}"`, { details: { available } })));
|
|
508
|
+
return process.exit(1);
|
|
509
|
+
}
|
|
502
510
|
console.error(chalk.red(`\n Unknown strategy: "${strategyName}"`));
|
|
503
511
|
console.error(chalk.gray(`\n Available strategies:`));
|
|
504
512
|
for (const s of available)
|
|
@@ -532,6 +540,17 @@ export function registerBotCommands(program, getAdapter, getAdapterFor, isJson)
|
|
|
532
540
|
.command("example")
|
|
533
541
|
.description("Print example YAML bot configs")
|
|
534
542
|
.action(() => {
|
|
543
|
+
if (isJson()) {
|
|
544
|
+
printJson(jsonOk({
|
|
545
|
+
examples: [
|
|
546
|
+
{ name: "eth-grid-bot", type: "grid", file: "~/.perp/bots/eth-grid.yaml", config: { exchange: "hyperliquid", symbol: "ETH", strategy: { type: "grid", grids: 15, size: 0.1, side: "neutral", range_mode: "auto", range_pct: 3, rebalance: true, leverage: 5 }, entry_conditions: [{ type: "volatility_below", value: 5 }], exit_conditions: [{ type: "volatility_above", value: 10 }, { type: "time_after", value: 86400 }], risk: { max_drawdown: 50, max_daily_loss: 30, pause_after_loss_sec: 300 } } },
|
|
547
|
+
{ name: "eth-dca-dip", type: "dca", file: "~/.perp/bots/eth-dca.yaml", config: { exchange: "hyperliquid", symbol: "ETH", strategy: { type: "dca", amount: 0.01, interval_sec: 3600, total_orders: 24 }, entry_conditions: [{ type: "price_below", value: 2500 }], exit_conditions: [{ type: "price_above", value: 2800 }], risk: { max_drawdown: 100, max_daily_loss: 50 } } },
|
|
548
|
+
{ name: "funding-arb", type: "funding-arb", file: "~/.perp/bots/funding-arb.yaml", config: { exchange: "hyperliquid", symbol: "ETH", strategy: { type: "funding-arb", min_spread: 20, close_spread: 5, size_usd: 100, max_positions: 3, exchanges: ["pacifica", "hyperliquid"] }, entry_conditions: [{ type: "always", value: 0 }], risk: { max_drawdown: 200, max_daily_loss: 50 } } },
|
|
549
|
+
],
|
|
550
|
+
usage: ["perp bot start ~/.perp/bots/eth-grid.yaml", "perp bot start ~/.perp/bots/eth-grid.yaml --background"],
|
|
551
|
+
}));
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
535
554
|
console.log(chalk.cyan.bold("\n Example Bot Configs\n"));
|
|
536
555
|
console.log(chalk.white.bold(" 1. Grid Bot (auto range)"));
|
|
537
556
|
console.log(chalk.gray(` Save to ~/.perp/bots/eth-grid.yaml:\n`));
|
package/dist/commands/bridge.js
CHANGED
|
@@ -201,7 +201,7 @@ export function registerBridgeCommands(program, isJson) {
|
|
|
201
201
|
if (!gasCheck.ok) {
|
|
202
202
|
const msg = gasCheck.errors.map(e => ` ✗ ${e}`).join("\n");
|
|
203
203
|
if (isJson())
|
|
204
|
-
return printJson({ ok: false, error: "Insufficient gas", details: gasCheck.errors });
|
|
204
|
+
return printJson({ ok: false, error: { code: "INSUFFICIENT_BALANCE", message: "Insufficient gas for transaction", details: gasCheck.errors }, meta: { timestamp: new Date().toISOString() } });
|
|
205
205
|
console.log(chalk.red("\n Insufficient gas for bridge:\n"));
|
|
206
206
|
console.log(chalk.red(msg));
|
|
207
207
|
if (needsDstGas) {
|
|
@@ -343,7 +343,7 @@ export function registerBridgeCommands(program, isJson) {
|
|
|
343
343
|
if (!gasCheck.ok) {
|
|
344
344
|
const msg = gasCheck.errors.map(e => ` ✗ ${e}`).join("\n");
|
|
345
345
|
if (isJson())
|
|
346
|
-
return printJson({ ok: false, error: "Insufficient gas", details: gasCheck.errors });
|
|
346
|
+
return printJson({ ok: false, error: { code: "INSUFFICIENT_BALANCE", message: "Insufficient gas for transaction", details: gasCheck.errors }, meta: { timestamp: new Date().toISOString() } });
|
|
347
347
|
console.log(chalk.red("\n Insufficient gas for bridge:\n"));
|
|
348
348
|
console.log(chalk.red(msg));
|
|
349
349
|
if (needsDstGas) {
|
package/dist/commands/funds.js
CHANGED
|
@@ -117,8 +117,13 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
|
|
|
117
117
|
const connection = new Connection(rpcUrl, "confirmed");
|
|
118
118
|
const solBalance = await connection.getBalance(pacDirect.keypair.publicKey);
|
|
119
119
|
if (solBalance < 5_000_000) {
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
if (isJson()) {
|
|
121
|
+
printJson(jsonError("INSUFFICIENT_BALANCE", "Insufficient SOL for gas. Need ~0.005 SOL."));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.error(chalk.red(" Insufficient SOL for gas. Need ~0.005 SOL."));
|
|
125
|
+
console.error(chalk.gray(" Tip: Use relayer for gasless deposits (start relayer server)\n"));
|
|
126
|
+
}
|
|
122
127
|
process.exit(1);
|
|
123
128
|
}
|
|
124
129
|
const { PublicKey } = await import("@solana/web3.js");
|
|
@@ -128,7 +133,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
|
|
|
128
133
|
? tokenAccounts.value[0].account.data.parsed.info.tokenAmount.uiAmount ?? 0
|
|
129
134
|
: 0;
|
|
130
135
|
if (usdcBalance < amountNum) {
|
|
131
|
-
|
|
136
|
+
if (isJson()) {
|
|
137
|
+
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC on Solana. Have: $${formatUsd(usdcBalance)}, need: $${formatUsd(amountNum)}`));
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
console.error(chalk.red(` Insufficient USDC on Solana. Have: $${formatUsd(usdcBalance)}, need: $${formatUsd(amountNum)}`));
|
|
141
|
+
}
|
|
132
142
|
process.exit(1);
|
|
133
143
|
}
|
|
134
144
|
const ix = await buildDepositInstruction(pacDirect.keypair.publicKey, amountNum, network);
|
|
@@ -181,7 +191,7 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
|
|
|
181
191
|
pk = await loadPrivateKey("hyperliquid");
|
|
182
192
|
}
|
|
183
193
|
catch (err) {
|
|
184
|
-
throw new Error(`Private key not configured for Hyperliquid. Run: perp
|
|
194
|
+
throw new Error(`Private key not configured for Hyperliquid. Run: perp setup`);
|
|
185
195
|
}
|
|
186
196
|
const wallet = new ethers.Wallet(pk);
|
|
187
197
|
try {
|
|
@@ -231,7 +241,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
|
|
|
231
241
|
const USDC_ARB = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
|
|
232
242
|
const ethBal = await provider.getBalance(wallet.address);
|
|
233
243
|
if (ethBal < ethers.parseEther("0.0001")) {
|
|
234
|
-
|
|
244
|
+
if (isJson()) {
|
|
245
|
+
printJson(jsonError("INSUFFICIENT_BALANCE", "Insufficient ETH on Arbitrum for gas."));
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
console.error(chalk.red(" Insufficient ETH on Arbitrum for gas."));
|
|
249
|
+
}
|
|
235
250
|
process.exit(1);
|
|
236
251
|
}
|
|
237
252
|
const usdc = new ethers.Contract(USDC_ARB, [
|
|
@@ -241,7 +256,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
|
|
|
241
256
|
const amountRaw = ethers.parseUnits(amount, 6);
|
|
242
257
|
const usdcBal = await usdc.balanceOf(wallet.address);
|
|
243
258
|
if (usdcBal < amountRaw) {
|
|
244
|
-
|
|
259
|
+
if (isJson()) {
|
|
260
|
+
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
console.error(chalk.red(` Insufficient USDC. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
264
|
+
}
|
|
245
265
|
process.exit(1);
|
|
246
266
|
}
|
|
247
267
|
try {
|
|
@@ -306,12 +326,22 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
|
|
|
306
326
|
const amountRaw = ethers.parseUnits(amount, 6);
|
|
307
327
|
const ethBal = await provider.getBalance(wallet.address);
|
|
308
328
|
if (ethBal < ethers.parseEther("0.003")) {
|
|
309
|
-
|
|
329
|
+
if (isJson()) {
|
|
330
|
+
printJson(jsonError("INSUFFICIENT_BALANCE", "Insufficient ETH on Ethereum for gas. Need ~0.003 ETH ($3+)."));
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
console.error(chalk.red(" Insufficient ETH on Ethereum for gas. Need ~0.003 ETH ($3+)."));
|
|
334
|
+
}
|
|
310
335
|
process.exit(1);
|
|
311
336
|
}
|
|
312
337
|
const usdcBal = await usdc.balanceOf(wallet.address);
|
|
313
338
|
if (usdcBal < amountRaw) {
|
|
314
|
-
|
|
339
|
+
if (isJson()) {
|
|
340
|
+
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC on Ethereum. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
console.error(chalk.red(` Insufficient USDC on Ethereum. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
344
|
+
}
|
|
315
345
|
process.exit(1);
|
|
316
346
|
}
|
|
317
347
|
const allowance = await usdc.allowance(wallet.address, LIGHTER_CONTRACT);
|
|
@@ -399,7 +429,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
|
|
|
399
429
|
console.log(` Intent address: ${intentAddress}`);
|
|
400
430
|
const gasBal = await provider.getBalance(wallet.address);
|
|
401
431
|
if (gasBal < ethers.parseEther("0.00005")) {
|
|
402
|
-
|
|
432
|
+
if (isJson()) {
|
|
433
|
+
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient gas token on ${chainInfo.name}. Need native token for gas (~$0.01).`));
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
console.error(chalk.red(` Insufficient gas token on ${chainInfo.name}. Need native token for gas (~$0.01).`));
|
|
437
|
+
}
|
|
403
438
|
process.exit(1);
|
|
404
439
|
}
|
|
405
440
|
const usdc = new ethers.Contract(chainInfo.usdc, [
|
|
@@ -409,7 +444,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
|
|
|
409
444
|
const amountRaw = ethers.parseUnits(amount, 6);
|
|
410
445
|
const usdcBal = await usdc.balanceOf(wallet.address);
|
|
411
446
|
if (usdcBal < amountRaw) {
|
|
412
|
-
|
|
447
|
+
if (isJson()) {
|
|
448
|
+
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
console.error(chalk.red(` Insufficient USDC on ${chainInfo.name}. Have: $${formatUsd(Number(ethers.formatUnits(usdcBal, 6)))}`));
|
|
452
|
+
}
|
|
413
453
|
process.exit(1);
|
|
414
454
|
}
|
|
415
455
|
if (!isJson())
|
|
@@ -534,7 +574,12 @@ export function registerFundsCommands(program, getAdapter, isJson, getNetwork) {
|
|
|
534
574
|
const balance = await adapter.getBalance();
|
|
535
575
|
const available = Number(balance.available);
|
|
536
576
|
if (amountNum > available) {
|
|
537
|
-
|
|
577
|
+
if (isJson()) {
|
|
578
|
+
printJson(jsonError("INSUFFICIENT_BALANCE", `Insufficient withdrawable balance. Available: $${formatUsd(available)}`));
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
console.error(chalk.red(` Insufficient withdrawable balance. Available: $${formatUsd(available)}`));
|
|
582
|
+
}
|
|
538
583
|
process.exit(1);
|
|
539
584
|
}
|
|
540
585
|
try {
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ declare function loadEnvFile(): Record<string, string>;
|
|
|
5
5
|
declare function setEnvVar(key: string, value: string): void;
|
|
6
6
|
export declare const EXCHANGE_ENV_MAP: Record<string, {
|
|
7
7
|
envKey: string;
|
|
8
|
+
envKeySecret?: string;
|
|
8
9
|
chain: "solana" | "evm" | "apikey";
|
|
9
10
|
label: string;
|
|
10
11
|
}>;
|
package/dist/commands/init.js
CHANGED
|
@@ -31,7 +31,7 @@ function writeEnvFile(env) {
|
|
|
31
31
|
ensurePerpDir();
|
|
32
32
|
const lines = [
|
|
33
33
|
"# perp-cli configuration",
|
|
34
|
-
"# Generated by 'perp
|
|
34
|
+
"# Generated by 'perp setup' — edit freely",
|
|
35
35
|
"",
|
|
36
36
|
];
|
|
37
37
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -70,7 +70,7 @@ export const EXCHANGE_ENV_MAP = {
|
|
|
70
70
|
pacifica: { envKey: "PACIFICA_PRIVATE_KEY", chain: "solana", label: "Pacifica (Solana)" },
|
|
71
71
|
hyperliquid: { envKey: "HL_PRIVATE_KEY", chain: "evm", label: "Hyperliquid (EVM)" },
|
|
72
72
|
lighter: { envKey: "LIGHTER_PRIVATE_KEY", chain: "evm", label: "Lighter (EVM)" },
|
|
73
|
-
aster: { envKey: "ASTER_API_KEY", chain: "apikey", label: "Aster (BNB Chain)" },
|
|
73
|
+
aster: { envKey: "ASTER_API_KEY", envKeySecret: "ASTER_API_SECRET", chain: "apikey", label: "Aster (BNB Chain)" },
|
|
74
74
|
};
|
|
75
75
|
// ── Validate keys ─────────────────────────────────────────────
|
|
76
76
|
export async function validateKey(chain, key) {
|
|
@@ -109,9 +109,10 @@ export async function validateKey(chain, key) {
|
|
|
109
109
|
}
|
|
110
110
|
// ── Commands ──────────────────────────────────────────────────
|
|
111
111
|
export function registerInitCommand(program) {
|
|
112
|
-
// ── perp
|
|
112
|
+
// ── perp setup (interactive) ──
|
|
113
113
|
program
|
|
114
|
-
.command("
|
|
114
|
+
.command("setup")
|
|
115
|
+
.alias("init")
|
|
115
116
|
.description("Interactive setup wizard — configure keys & default exchange")
|
|
116
117
|
.action(async () => {
|
|
117
118
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -145,8 +146,9 @@ export function registerInitCommand(program) {
|
|
|
145
146
|
console.log(` 1) ${chalk.cyan("Pacifica")} ${chalk.gray("— Solana perps (USDC on Solana)")}`);
|
|
146
147
|
console.log(` 2) ${chalk.cyan("Hyperliquid")} ${chalk.gray("— EVM perps (USDC on Arbitrum)")}`);
|
|
147
148
|
console.log(` 3) ${chalk.cyan("Lighter")} ${chalk.gray("— EVM perps (USDC on Ethereum/Arb)")}`);
|
|
149
|
+
console.log(` 4) ${chalk.cyan("Aster")} ${chalk.gray("— BNB Chain perps (API key + secret)")}`);
|
|
148
150
|
console.log();
|
|
149
|
-
const exchangeChoice = await ask(rl, " Which exchange(s)? (1,2,3 or 'all'): ");
|
|
151
|
+
const exchangeChoice = await ask(rl, " Which exchange(s)? (1,2,3,4 or 'all'): ");
|
|
150
152
|
const selected = parseExchangeChoice(exchangeChoice);
|
|
151
153
|
if (selected.length === 0) {
|
|
152
154
|
console.log(chalk.red("\n No exchanges selected.\n"));
|
|
@@ -157,6 +159,7 @@ export function registerInitCommand(program) {
|
|
|
157
159
|
// EVM exchanges can share the same key
|
|
158
160
|
const evmExchanges = selected.filter((e) => EXCHANGE_ENV_MAP[e].chain === "evm");
|
|
159
161
|
const solExchanges = selected.filter((e) => EXCHANGE_ENV_MAP[e].chain === "solana");
|
|
162
|
+
const apikeyExchanges = selected.filter((e) => EXCHANGE_ENV_MAP[e].chain === "apikey");
|
|
160
163
|
const newEnv = { ...env };
|
|
161
164
|
if (evmExchanges.length > 0) {
|
|
162
165
|
console.log(chalk.cyan.bold("\n EVM Private Key") + chalk.gray(` (for ${evmExchanges.join(", ")})`));
|
|
@@ -212,6 +215,34 @@ export function registerInitCommand(program) {
|
|
|
212
215
|
}
|
|
213
216
|
}
|
|
214
217
|
}
|
|
218
|
+
if (apikeyExchanges.length > 0) {
|
|
219
|
+
for (const ex of apikeyExchanges) {
|
|
220
|
+
const info = EXCHANGE_ENV_MAP[ex];
|
|
221
|
+
console.log(chalk.cyan.bold(`\n ${info.label} API Credentials`));
|
|
222
|
+
const apiKey = await ask(rl, ` ${info.label} API key: `);
|
|
223
|
+
if (!apiKey) {
|
|
224
|
+
console.log(chalk.gray(" Skipped."));
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const { valid, address } = await validateKey("apikey", apiKey);
|
|
228
|
+
if (!valid) {
|
|
229
|
+
console.log(chalk.red(" Invalid API key (must be at least 16 chars), skipped."));
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
newEnv[info.envKey] = apiKey;
|
|
233
|
+
console.log(` ${chalk.green("OK")} ${chalk.gray(address)}`);
|
|
234
|
+
if (info.envKeySecret) {
|
|
235
|
+
const secret = await ask(rl, ` ${info.label} API secret: `);
|
|
236
|
+
if (secret) {
|
|
237
|
+
newEnv[info.envKeySecret] = secret;
|
|
238
|
+
console.log(` ${chalk.green("OK")} secret saved`);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
console.log(chalk.yellow(" No secret provided — some operations may fail."));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
215
246
|
// Default exchange
|
|
216
247
|
const settings = loadSettings();
|
|
217
248
|
if (selected.length === 1) {
|
|
@@ -255,7 +286,9 @@ export function registerInitCommand(program) {
|
|
|
255
286
|
}
|
|
256
287
|
function parseExchangeChoice(input) {
|
|
257
288
|
const lower = input.toLowerCase().trim();
|
|
258
|
-
if (lower === "all" || lower === "1,2,3")
|
|
289
|
+
if (lower === "all" || lower === "1,2,3,4")
|
|
290
|
+
return ["pacifica", "hyperliquid", "lighter", "aster"];
|
|
291
|
+
if (lower === "1,2,3")
|
|
259
292
|
return ["pacifica", "hyperliquid", "lighter"];
|
|
260
293
|
const exchanges = [];
|
|
261
294
|
const parts = lower.split(/[,\s]+/);
|
|
@@ -266,6 +299,8 @@ function parseExchangeChoice(input) {
|
|
|
266
299
|
exchanges.push("hyperliquid");
|
|
267
300
|
else if (p === "3" || p.startsWith("lig") || p === "lt")
|
|
268
301
|
exchanges.push("lighter");
|
|
302
|
+
else if (p === "4" || p.startsWith("ast"))
|
|
303
|
+
exchanges.push("aster");
|
|
269
304
|
}
|
|
270
305
|
return [...new Set(exchanges)];
|
|
271
306
|
}
|
package/dist/commands/manage.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { printJson, jsonOk } from "../utils.js";
|
|
1
|
+
import { printJson, jsonOk, jsonError } from "../utils.js";
|
|
2
2
|
import { setEnvVar } from "./init.js";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { hasPacificaSdk, hasLighterAccount } from "../exchanges/capabilities.js";
|
|
@@ -19,6 +19,10 @@ export function registerManageCommands(program, getAdapter, isJson, getPacificaA
|
|
|
19
19
|
.action(async (symbol, mode) => {
|
|
20
20
|
const m = mode.toLowerCase();
|
|
21
21
|
if (m !== "cross" && m !== "isolated") {
|
|
22
|
+
if (isJson()) {
|
|
23
|
+
console.error(JSON.stringify(jsonError("INVALID_PARAMS", "Mode must be cross or isolated")));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
22
26
|
console.error(chalk.red("Mode must be cross or isolated"));
|
|
23
27
|
process.exit(1);
|
|
24
28
|
}
|
|
@@ -115,7 +115,7 @@ export function registerSettingsCommands(program, isJson, getAdapterForExchange)
|
|
|
115
115
|
.action(async (opts) => {
|
|
116
116
|
await withJsonErrors(isJson(), async () => {
|
|
117
117
|
if (!getAdapterForExchange) {
|
|
118
|
-
console.error(chalk.red(" Fee sync requires exchange adapters. Set up keys first with `perp
|
|
118
|
+
console.error(chalk.red(" Fee sync requires exchange adapters. Set up keys first with `perp setup`."));
|
|
119
119
|
return;
|
|
120
120
|
}
|
|
121
121
|
const s = loadSettings();
|
|
@@ -284,7 +284,7 @@ export function registerSettingsCommands(program, isJson, getAdapterForExchange)
|
|
|
284
284
|
console.log(chalk.cyan.bold("\n perp-cli Configuration\n"));
|
|
285
285
|
console.log(` File: ${chalk.gray(ENV_FILE)}\n`);
|
|
286
286
|
if (results.length === 0) {
|
|
287
|
-
console.log(chalk.gray(" No keys configured. Run 'perp
|
|
287
|
+
console.log(chalk.gray(" No keys configured. Run 'perp setup' or 'perp settings env set <exchange> <key>'\n"));
|
|
288
288
|
return;
|
|
289
289
|
}
|
|
290
290
|
for (const { name, address, source } of results) {
|
|
@@ -347,7 +347,7 @@ export function registerSettingsCommands(program, isJson, getAdapterForExchange)
|
|
|
347
347
|
delete env[envKey];
|
|
348
348
|
// Rewrite file
|
|
349
349
|
const { writeFileSync } = await import("fs");
|
|
350
|
-
const lines = ["# perp-cli configuration", "# Generated by 'perp
|
|
350
|
+
const lines = ["# perp-cli configuration", "# Generated by 'perp setup' — edit freely", ""];
|
|
351
351
|
for (const [k, v] of Object.entries(env))
|
|
352
352
|
lines.push(`${k}=${v}`);
|
|
353
353
|
lines.push("");
|
package/dist/commands/wallet.js
CHANGED
|
@@ -464,7 +464,7 @@ export function registerWalletCommands(program, isJson) {
|
|
|
464
464
|
console.log(chalk.cyan.bold("\n Configured Wallets\n"));
|
|
465
465
|
if (results.length === 0) {
|
|
466
466
|
console.log(chalk.gray(" No keys configured."));
|
|
467
|
-
console.log(chalk.gray(` Run ${chalk.cyan("perp
|
|
467
|
+
console.log(chalk.gray(` Run ${chalk.cyan("perp setup")} or ${chalk.cyan("perp wallet set <exchange> <key>")}\n`));
|
|
468
468
|
return;
|
|
469
469
|
}
|
|
470
470
|
for (const { name, address, source } of results) {
|
|
@@ -579,9 +579,46 @@ export function registerWalletCommands(program, isJson) {
|
|
|
579
579
|
const activeEntries = Object.entries(store.active)
|
|
580
580
|
.map(([ex, wn]) => ({ exchange: ex, ...store.wallets[wn] }))
|
|
581
581
|
.filter((e) => e.address);
|
|
582
|
+
// Fallback to .env-based wallets when no named wallets are active
|
|
582
583
|
if (activeEntries.length === 0) {
|
|
583
|
-
|
|
584
|
-
|
|
584
|
+
const stored = loadEnvFile();
|
|
585
|
+
const envEntries = [];
|
|
586
|
+
for (const [exchange, info] of Object.entries(EXCHANGE_ENV_MAP)) {
|
|
587
|
+
if (info.chain === "apikey")
|
|
588
|
+
continue; // API key exchanges don't have on-chain addresses
|
|
589
|
+
const key = stored[info.envKey] || process.env[info.envKey];
|
|
590
|
+
if (key) {
|
|
591
|
+
const { valid, address } = await validateKey(info.chain, key);
|
|
592
|
+
if (valid)
|
|
593
|
+
envEntries.push({ exchange, chain: info.chain, address });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (envEntries.length === 0) {
|
|
597
|
+
if (isJson()) {
|
|
598
|
+
printJson({ ok: false, error: { code: "INVALID_PARAMS", message: "No wallets configured. Run 'perp setup' or 'perp wallet set <exchange> <key>'." }, meta: { timestamp: new Date().toISOString() } });
|
|
599
|
+
}
|
|
600
|
+
else {
|
|
601
|
+
console.error(chalk.gray("\n No wallets configured. Run 'perp setup' or 'perp wallet set <exchange> <key>'.\n"));
|
|
602
|
+
}
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
for (const we of envEntries) {
|
|
606
|
+
if (!isJson())
|
|
607
|
+
console.log(chalk.cyan.bold(`\n ${we.exchange} (${we.address.slice(0, 8)}...)`));
|
|
608
|
+
const balances = we.chain === "solana"
|
|
609
|
+
? await getSolanaBalances(we.address, opts.testnet)
|
|
610
|
+
: await getEvmBalances(we.address, opts.testnet);
|
|
611
|
+
if (isJson()) {
|
|
612
|
+
printJson(jsonOk({ exchange: we.exchange, address: we.address, balances }));
|
|
613
|
+
continue;
|
|
614
|
+
}
|
|
615
|
+
const rows = balances.map((b) => [
|
|
616
|
+
chalk.white.bold(b.token), b.balance, b.usdValue || chalk.gray("-"),
|
|
617
|
+
]);
|
|
618
|
+
console.log(makeTable(["Token", "Balance", "USD Value"], rows));
|
|
619
|
+
}
|
|
620
|
+
console.log();
|
|
621
|
+
return;
|
|
585
622
|
}
|
|
586
623
|
for (const ae of activeEntries) {
|
|
587
624
|
if (!isJson())
|
package/dist/config.js
CHANGED
|
@@ -51,13 +51,13 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
|
|
|
51
51
|
catch {
|
|
52
52
|
// wallet module not available, skip
|
|
53
53
|
}
|
|
54
|
-
//
|
|
54
|
+
// 6. Legacy key file (~/.perp/<exchange>.key)
|
|
55
55
|
const keyFile = resolve(process.env.HOME || "~", ".perp", `${exchange}.key`);
|
|
56
56
|
if (existsSync(keyFile)) {
|
|
57
57
|
return readFileSync(keyFile, "utf-8").trim();
|
|
58
58
|
}
|
|
59
59
|
throw new Error(`No private key configured for ${exchange}.\n\n` +
|
|
60
|
-
` Quick start: ${chalk.cyan("perp
|
|
60
|
+
` Quick start: ${chalk.cyan("perp setup")}\n\n` +
|
|
61
61
|
` Or manually:\n` +
|
|
62
62
|
` perp wallet set ${exchange} <key>\n` +
|
|
63
63
|
` ${envMap[exchange][0]}=<key> (env var)\n` +
|
|
@@ -253,7 +253,7 @@ export class HyperliquidAdapter {
|
|
|
253
253
|
}
|
|
254
254
|
ensureAddress() {
|
|
255
255
|
if (!this._address) {
|
|
256
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
256
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
/** Always fetches live clearinghouseState, writes result to shared cache for dashboard */
|
|
@@ -447,7 +447,7 @@ export class HyperliquidAdapter {
|
|
|
447
447
|
*/
|
|
448
448
|
ensureSigner() {
|
|
449
449
|
if (!this._evmSigner) {
|
|
450
|
-
throw new Error("No private key configured. Run: perp
|
|
450
|
+
throw new Error("No private key configured. Run: perp setup");
|
|
451
451
|
}
|
|
452
452
|
}
|
|
453
453
|
/**
|
|
@@ -317,7 +317,7 @@ export class LighterAdapter {
|
|
|
317
317
|
}
|
|
318
318
|
async getBalance() {
|
|
319
319
|
if (!this._address)
|
|
320
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
320
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
321
321
|
const acct = await this.fetchAccount();
|
|
322
322
|
if (!acct)
|
|
323
323
|
return { equity: "0", available: "0", marginUsed: "0", unrealizedPnl: "0" };
|
|
@@ -334,7 +334,7 @@ export class LighterAdapter {
|
|
|
334
334
|
}
|
|
335
335
|
async getPositions() {
|
|
336
336
|
if (!this._address)
|
|
337
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
337
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
338
338
|
const acct = await this.fetchAccount();
|
|
339
339
|
if (!acct)
|
|
340
340
|
return [];
|
|
@@ -380,7 +380,7 @@ export class LighterAdapter {
|
|
|
380
380
|
}
|
|
381
381
|
async getOpenOrders() {
|
|
382
382
|
if (!this._address)
|
|
383
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
383
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
384
384
|
if (this._accountIndex < 0 || this._readOnly)
|
|
385
385
|
return [];
|
|
386
386
|
try {
|
|
@@ -648,7 +648,7 @@ export class LighterAdapter {
|
|
|
648
648
|
}
|
|
649
649
|
async getOrderHistory(limit = 30) {
|
|
650
650
|
if (!this._address)
|
|
651
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
651
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
652
652
|
const raw = await this._getOrderHistoryRaw();
|
|
653
653
|
const orders = (raw.orders ?? []);
|
|
654
654
|
return orders.slice(0, limit).map((o) => ({
|
|
@@ -664,7 +664,7 @@ export class LighterAdapter {
|
|
|
664
664
|
}
|
|
665
665
|
async getTradeHistory(limit = 30) {
|
|
666
666
|
if (!this._address)
|
|
667
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
667
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
668
668
|
const raw = await this._getTradeHistoryRaw(limit);
|
|
669
669
|
const trades = (raw.trades ?? []);
|
|
670
670
|
return trades.slice(0, limit).map((t) => ({
|
|
@@ -678,7 +678,7 @@ export class LighterAdapter {
|
|
|
678
678
|
}
|
|
679
679
|
async getFundingPayments(limit = 200) {
|
|
680
680
|
if (!this._address)
|
|
681
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
681
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
682
682
|
const raw = await this._getPositionFundingRaw();
|
|
683
683
|
const items = (raw.funding ?? []);
|
|
684
684
|
return items.slice(0, limit).map((f) => ({
|
|
@@ -1027,7 +1027,7 @@ export class LighterAdapter {
|
|
|
1027
1027
|
*/
|
|
1028
1028
|
async setupApiKey(apiKeyIndex = 4) {
|
|
1029
1029
|
if (!this._evmSigner) {
|
|
1030
|
-
throw new Error("No EVM private key configured. Run: perp
|
|
1030
|
+
throw new Error("No EVM private key configured. Run: perp setup");
|
|
1031
1031
|
}
|
|
1032
1032
|
if (this._accountIndex < 0) {
|
|
1033
1033
|
throw new Error("Account index not available. Call init() first or set LIGHTER_ACCOUNT_INDEX.");
|
|
@@ -19,7 +19,7 @@ export class PacificaAdapter {
|
|
|
19
19
|
}
|
|
20
20
|
ensureSigner() {
|
|
21
21
|
if (!this._hasRealKey) {
|
|
22
|
-
throw new Error("No private key configured. Run: perp
|
|
22
|
+
throw new Error("No private key configured. Run: perp setup");
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
/** Inject an external Solana signer. */
|
|
@@ -80,7 +80,7 @@ export class PacificaAdapter {
|
|
|
80
80
|
}
|
|
81
81
|
async getBalance() {
|
|
82
82
|
if (!this._hasRealKey)
|
|
83
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
83
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
84
84
|
const [info, positions, prices] = await Promise.all([
|
|
85
85
|
this.client.getAccount(this.account),
|
|
86
86
|
this._getPositions(),
|
|
@@ -110,7 +110,7 @@ export class PacificaAdapter {
|
|
|
110
110
|
}
|
|
111
111
|
async getPositions() {
|
|
112
112
|
if (!this._hasRealKey)
|
|
113
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
113
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
114
114
|
const [positions, prices] = await Promise.all([
|
|
115
115
|
this._getPositions(),
|
|
116
116
|
this._getPrices(),
|
|
@@ -152,7 +152,7 @@ export class PacificaAdapter {
|
|
|
152
152
|
}
|
|
153
153
|
async getOpenOrders() {
|
|
154
154
|
if (!this._hasRealKey)
|
|
155
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
155
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
156
156
|
const orders = await this.client.getOrders(this.account);
|
|
157
157
|
return orders.map((o) => {
|
|
158
158
|
const raw = o;
|
|
@@ -264,7 +264,7 @@ export class PacificaAdapter {
|
|
|
264
264
|
}
|
|
265
265
|
async getOrderHistory(limit = 30) {
|
|
266
266
|
if (!this._hasRealKey)
|
|
267
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
267
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
268
268
|
const result = await this.client.getOrderHistory(this.account);
|
|
269
269
|
const data = (result.data ?? result.orders ?? []);
|
|
270
270
|
if (!Array.isArray(data))
|
|
@@ -282,7 +282,7 @@ export class PacificaAdapter {
|
|
|
282
282
|
}
|
|
283
283
|
async getTradeHistory(limit = 30) {
|
|
284
284
|
if (!this._hasRealKey)
|
|
285
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
285
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
286
286
|
const raw = await this.client.getTradeHistory(this.account);
|
|
287
287
|
const trades = (raw.data ?? raw);
|
|
288
288
|
if (!Array.isArray(trades))
|
|
@@ -298,7 +298,7 @@ export class PacificaAdapter {
|
|
|
298
298
|
}
|
|
299
299
|
async getFundingPayments(limit = 200) {
|
|
300
300
|
if (!this._hasRealKey)
|
|
301
|
-
throw new Error("No private key configured — account data unavailable. Run: perp
|
|
301
|
+
throw new Error("No private key configured — account data unavailable. Run: perp setup");
|
|
302
302
|
const raw = await this.client.getFundingAccountHistory(this.account);
|
|
303
303
|
const history = (raw.data ?? raw);
|
|
304
304
|
if (!Array.isArray(history))
|
package/dist/index.js
CHANGED
|
@@ -646,12 +646,12 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
|
|
|
646
646
|
if (!status.hasWallets && !hasEnvKey && !settings.defaultExchange) {
|
|
647
647
|
// Fresh install — onboarding
|
|
648
648
|
console.log(chalk.yellow.bold("\n ⚡ Get started:\n"));
|
|
649
|
-
console.log(` ${chalk.cyan("perp
|
|
650
|
-
console.log(chalk.gray(`\n
|
|
651
|
-
console.log(` ${chalk.green("perp market
|
|
652
|
-
console.log(` ${chalk.green("perp
|
|
653
|
-
console.log(` ${chalk.green("perp
|
|
654
|
-
console.log(` ${chalk.green("perp --help")}
|
|
649
|
+
console.log(` ${chalk.cyan("perp setup")} setup wizard`);
|
|
650
|
+
console.log(chalk.gray(`\n Explore without a wallet:`));
|
|
651
|
+
console.log(` ${chalk.green("perp market mid BTC")} quick price check`);
|
|
652
|
+
console.log(` ${chalk.green("perp arb scan")} funding rate arbitrage`);
|
|
653
|
+
console.log(` ${chalk.green("perp market list")} available markets`);
|
|
654
|
+
console.log(` ${chalk.green("perp --help")} all commands\n`);
|
|
655
655
|
}
|
|
656
656
|
else {
|
|
657
657
|
// Configured — show exchange status + balance
|
|
@@ -721,12 +721,13 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
|
|
|
721
721
|
console.log(chalk.gray(" ─".repeat(20)));
|
|
722
722
|
console.log(` ${chalk.white.bold("Total".padEnd(16))} ${chalk.white.bold(`$${totalEquity.toLocaleString("en", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`)}${totalPositions > 0 ? chalk.yellow(` ${totalPositions} pos`) : ""}`);
|
|
723
723
|
}
|
|
724
|
-
console.log(chalk.white.bold("\n
|
|
725
|
-
console.log(` ${chalk.green("perp portfolio")}
|
|
726
|
-
console.log(` ${chalk.green("perp
|
|
727
|
-
console.log(` ${chalk.green("perp arb
|
|
728
|
-
console.log(` ${chalk.green("perp
|
|
729
|
-
console.log(` ${chalk.green("perp
|
|
724
|
+
console.log(chalk.white.bold("\n Quick:"));
|
|
725
|
+
console.log(` ${chalk.green("perp portfolio")} balances + positions + risk`);
|
|
726
|
+
console.log(` ${chalk.green("perp arb scan")} funding arbitrage opportunities`);
|
|
727
|
+
console.log(` ${chalk.green("perp arb status")} open arb positions + PnL`);
|
|
728
|
+
console.log(` ${chalk.green("perp status")} full dashboard`);
|
|
729
|
+
console.log(` ${chalk.green("perp dashboard")} live web monitoring`);
|
|
730
|
+
console.log(` ${chalk.green("perp --help")} all commands\n`);
|
|
730
731
|
}
|
|
731
732
|
}
|
|
732
733
|
catch {
|
package/dist/mcp-server.js
CHANGED
|
@@ -27,7 +27,7 @@ async function getOrCreateAdapter(exchange) {
|
|
|
27
27
|
return adapters.get(key);
|
|
28
28
|
const pk = await tryLoadPrivateKey(key);
|
|
29
29
|
if (!pk)
|
|
30
|
-
throw new Error(`No private key configured for ${exchange}. Run: perp
|
|
30
|
+
throw new Error(`No private key configured for ${exchange}. Run: perp setup`);
|
|
31
31
|
let adapter;
|
|
32
32
|
switch (key) {
|
|
33
33
|
case "pacifica": {
|
|
@@ -12,7 +12,7 @@ export declare const TESTNET_PACIFICA_VAULT = "5SDFdHZGTZbyRYu54CgmRkCGnPHC5pYaN
|
|
|
12
12
|
export declare const TESTNET_USDC_MINT = "USDPqRbLidFGufty2s3oizmDEKdqx7ePTqzDMbf5ZKM";
|
|
13
13
|
export declare const DEFAULT_EXPIRY_WINDOW = 5000;
|
|
14
14
|
export declare const USDC_DECIMALS = 6;
|
|
15
|
-
export declare const BUILDER_CODE
|
|
15
|
+
export declare const BUILDER_CODE = "PERPCLI";
|
|
16
16
|
export type Network = "mainnet" | "testnet";
|
|
17
17
|
export declare function getNetworkConfig(network?: Network): {
|
|
18
18
|
restUrl: string;
|
|
@@ -20,7 +20,10 @@ export const DEFAULT_EXPIRY_WINDOW = 5000; // 5 seconds
|
|
|
20
20
|
// USDC decimals
|
|
21
21
|
export const USDC_DECIMALS = 6;
|
|
22
22
|
// Builder code — 0% fee, used for Pacifica hackathon tracking
|
|
23
|
-
|
|
23
|
+
// The actual 3-way fallback (env → settings.referralCodes.pacifica → "PERPCLI") is
|
|
24
|
+
// resolved in index.ts before constructing PacificaAdapter. This constant is only
|
|
25
|
+
// a last-resort default for direct PacificaClient usage without a builderCode arg.
|
|
26
|
+
export const BUILDER_CODE = "PERPCLI";
|
|
24
27
|
export function getNetworkConfig(network = "mainnet") {
|
|
25
28
|
if (network === "testnet") {
|
|
26
29
|
return {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "perp-cli",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter",
|
|
3
|
+
"version": "0.7.9",
|
|
4
|
+
"description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
|
|
5
5
|
"bin": {
|
|
6
6
|
"perp": "dist/index.js",
|
|
7
7
|
"perp-mcp": "dist/mcp-server.js"
|
package/skills/perp-cli/SKILL.md
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: perp-cli
|
|
3
|
-
description: "Multi-DEX perpetual futures trading CLI for Pacifica (Solana), Hyperliquid (EVM),
|
|
3
|
+
description: "Multi-DEX perpetual futures trading CLI for Pacifica (Solana), Hyperliquid (EVM), Lighter (Ethereum), and Aster (BNB Chain). Use when user asks to: trade perps, check funding rates, scan arbitrage (perp-perp or spot-perp), delta-neutral strategies, bridge USDC, manage positions/orders, deposit/withdraw, spot+perp hedge, HIP-3 DEX trading, or mentions perp-cli, hypurrquant, Pacifica, Hyperliquid, Lighter, Aster, HyperEVM, funding arb, U-token (UBTC/UETH/USOL)."
|
|
4
4
|
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.
|
|
8
|
+
version: "0.7.7"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# perp-cli Agent Guide
|
|
12
12
|
|
|
13
|
-
Multi-DEX perpetual futures CLI + MCP server — Pacifica (Solana), Hyperliquid (HyperEVM), Lighter (Ethereum).
|
|
13
|
+
Multi-DEX perpetual futures CLI + MCP server — Pacifica (Solana), Hyperliquid (HyperEVM), Lighter (Ethereum), Aster (BNB Chain).
|
|
14
14
|
|
|
15
15
|
[](https://glama.ai/mcp/servers/hypurrquant/perp-cli)
|
|
16
16
|
|
|
@@ -34,14 +34,14 @@ Multi-DEX perpetual futures CLI + MCP server — Pacifica (Solana), Hyperliquid
|
|
|
34
34
|
1. **Always use `--json`** on every command.
|
|
35
35
|
2. **Always use `--dry-run`** before any mutating trade (then execute without it after user confirms).
|
|
36
36
|
3. **Always use `--fields`** to reduce output when you only need specific data (saves tokens).
|
|
37
|
-
4. **NEVER use `perp
|
|
37
|
+
4. **NEVER use `perp setup`** (alias: `perp init`) — interactive, will hang.
|
|
38
38
|
5. **NEVER trade without user confirmation.**
|
|
39
39
|
6. **NEVER read ~/.perp/.env or key files.**
|
|
40
40
|
|
|
41
41
|
## Install
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
perp --version 2>/dev/null # check if exists (must be >= 0.
|
|
44
|
+
perp --version 2>/dev/null # check if exists (must be >= 0.7.7)
|
|
45
45
|
npm install -g perp-cli@latest 2>/dev/null || npx -y perp-cli@latest --json --version
|
|
46
46
|
```
|
|
47
47
|
|
|
@@ -49,14 +49,15 @@ Use `perp` if global install works, otherwise `npx -y perp-cli@latest` as prefix
|
|
|
49
49
|
|
|
50
50
|
## Global Flags
|
|
51
51
|
|
|
52
|
-
`--json` (required) | `-e hl/pac/lt` (exchange) | `--dex <name>` (HIP-3) | `-w <wallet>` | `--dry-run` | `--fields <f1,f2>` | `--ndjson`
|
|
52
|
+
`--json` (required) | `-e hl/pac/lt/aster` (exchange) | `--dex <name>` (HIP-3) | `-w <wallet>` | `--dry-run` | `--fields <f1,f2>` | `--ndjson`
|
|
53
53
|
|
|
54
54
|
## Wallet Setup
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
perp --json wallet set hl <EVM_KEY> # Hyperliquid
|
|
58
57
|
perp --json wallet set pac <SOLANA_KEY> # Pacifica
|
|
58
|
+
perp --json wallet set hl <EVM_KEY> # Hyperliquid
|
|
59
59
|
perp --json wallet set lt <EVM_KEY> # Lighter (API key auto-generated)
|
|
60
|
+
perp --json wallet set aster <API_KEY> # Aster (BNB Chain)
|
|
60
61
|
perp --json wallet show # verify
|
|
61
62
|
```
|
|
62
63
|
|
|
@@ -96,7 +97,7 @@ perp --json arb exec BTC spot:lt pac 50 # spot buy(LT) + perp short(PAC)
|
|
|
96
97
|
perp --json arb close ETH # sells spot + buys back perp
|
|
97
98
|
```
|
|
98
99
|
|
|
99
|
-
Spot exchanges: HL, LT. Pacifica
|
|
100
|
+
Spot exchanges: HL, LT. Pacifica and Aster are perp-only.
|
|
100
101
|
|
|
101
102
|
## Bot Engine (19 Strategies)
|
|
102
103
|
|
|
@@ -114,7 +115,7 @@ perp bot quick-dca <symbol> <side> <amount> <interval>
|
|
|
114
115
|
perp bot quick-arb # quick funding arb
|
|
115
116
|
```
|
|
116
117
|
|
|
117
|
-
**Strategies:** simple-mm, engine-mm, avellaneda-mm, regime-mm, grid-mm, liquidation-mm, momentum-breakout, mean-reversion, aggressive-taker,
|
|
118
|
+
**Strategies:** grid, dca, funding-arb, funding-arb-v2, funding-auto, basis-arb, simple-mm, engine-mm, avellaneda-mm, regime-mm, grid-mm, liquidation-mm, momentum-breakout, mean-reversion, aggressive-taker, hedge-agent, rfq-agent, claude-agent, apex
|
|
118
119
|
|
|
119
120
|
**Flags:** `--dry-run` (simulate without executing), `--headless` (no TUI, log only)
|
|
120
121
|
|
|
@@ -6,8 +6,7 @@ Complete reference for non-interactive CLI operations. Every command here is saf
|
|
|
6
6
|
|
|
7
7
|
### NEVER use these (interactive, will hang):
|
|
8
8
|
```
|
|
9
|
-
perp
|
|
10
|
-
perp wallet setup # removed, but may appear in old docs
|
|
9
|
+
perp setup # interactive wizard — asks questions via stdin (alias: perp init)
|
|
11
10
|
```
|
|
12
11
|
|
|
13
12
|
### ALWAYS use these (non-interactive, agent-safe):
|
|
@@ -101,14 +100,16 @@ perp --json wallet set lt 0xKEY # same key, different exchange binding
|
|
|
101
100
|
|
|
102
101
|
| Exchange | Chain | Key Format | Example |
|
|
103
102
|
|----------|-------|-----------|---------|
|
|
103
|
+
| Pacifica | Solana | Base58 string | `5KQwrPbwdL6PhXu...` |
|
|
104
104
|
| Hyperliquid | EVM | Hex with 0x prefix, 66 chars | `0x4c0883a69102937d...` |
|
|
105
105
|
| Lighter | EVM | Hex with 0x prefix, 66 chars | `0x4c0883a69102937d...` |
|
|
106
|
-
|
|
|
106
|
+
| Aster | API Key | Hex string | `3971f62b...` |
|
|
107
107
|
|
|
108
108
|
Aliases for exchange names:
|
|
109
|
-
- `hl` or `hyperliquid`
|
|
110
109
|
- `pac` or `pacifica`
|
|
110
|
+
- `hl` or `hyperliquid`
|
|
111
111
|
- `lt` or `lighter`
|
|
112
|
+
- `aster`
|
|
112
113
|
|
|
113
114
|
## Deposit & Withdraw Flows
|
|
114
115
|
|
|
@@ -256,11 +257,11 @@ Error case:
|
|
|
256
257
|
|
|
257
258
|
Symbols are auto-resolved by the CLI. **Always use bare symbols** (e.g., `BTC`, `SOL`, `ICP`) — the CLI handles exchange-specific naming automatically:
|
|
258
259
|
|
|
259
|
-
| Input |
|
|
260
|
-
|
|
261
|
-
| `ICP` | → `ICP-PERP` | → `ICP` | → `ICP` |
|
|
262
|
-
| `BTC` | → `BTC` | → `BTC` | → `BTC` |
|
|
263
|
-
| `SOL` | → `SOL` | → `SOL` | → `SOL` |
|
|
260
|
+
| Input | Pacifica | Hyperliquid | Lighter | Aster |
|
|
261
|
+
|-------|----------|-------------|---------|-------|
|
|
262
|
+
| `ICP` | → `ICP` | → `ICP-PERP` | → `ICP` | → `ICP` |
|
|
263
|
+
| `BTC` | → `BTC` | → `BTC` | → `BTC` | → `BTC` |
|
|
264
|
+
| `SOL` | → `SOL` | → `SOL` | → `SOL` | → `SOL` |
|
|
264
265
|
|
|
265
266
|
- `arb scan` returns bare symbols — pass them directly to trade/leverage commands on any exchange.
|
|
266
267
|
- Do NOT manually add `-PERP` suffix — the CLI resolves this automatically.
|
|
@@ -269,15 +270,16 @@ Symbols are auto-resolved by the CLI. **Always use bare symbols** (e.g., `BTC`,
|
|
|
269
270
|
|
|
270
271
|
| Exchange | Min Order (notional) | Notes |
|
|
271
272
|
|----------|---------------------|-------|
|
|
272
|
-
| Hyperliquid | **$10** | Rejects orders below $10 notional |
|
|
273
273
|
| Pacifica | ~$1 (varies by symbol) | Lower minimums |
|
|
274
|
+
| Hyperliquid | **$10** | Rejects orders below $10 notional |
|
|
274
275
|
| Lighter | Varies by symbol | Check market info |
|
|
276
|
+
| Aster | Varies by symbol | BNB Chain, API key based |
|
|
275
277
|
|
|
276
278
|
**`trade check` is advisory only** — it returns `valid: true/false` but does NOT block execution. The exchange itself enforces minimums and will reject with an error if the order is too small.
|
|
277
279
|
|
|
278
280
|
## Common Agent Mistakes
|
|
279
281
|
|
|
280
|
-
1. **Using `perp
|
|
282
|
+
1. **Using `perp setup`** (alias: `perp init`) — interactive, will hang forever. Use `wallet set` instead.
|
|
281
283
|
2. **Forgetting `--json`** — output becomes unparseable human text.
|
|
282
284
|
3. **Trading with zero balance** — check `account balance` first, tell user to deposit.
|
|
283
285
|
4. **Retrying a trade without checking** — leads to double positions. Always check `account positions` after a trade, even if it seemed to fail.
|