@raintree-technology/perps 0.1.1 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.2] - 2026-02-26
11
+
12
+ ### Added
13
+ - Added `perps setup decibel-key` for guided Decibel API key onboarding with:
14
+ - Geomi key-capture guidance in interactive mode
15
+ - live token validation against Decibel REST
16
+ - optional account-overview validation
17
+ - encrypted local vault persistence by default
18
+ - optional env-file export for CI/shell workflows
19
+ - Added encrypted vault helpers for single-secret lifecycle management:
20
+ - `saveSecretToCredentialVault`
21
+ - `getLatestCredentialVaultSecret`
22
+ - Added release-ready Decibel onboarding docs covering mainnet startup, automation boundaries, and CI-safe non-interactive flows.
23
+
24
+ ### Changed
25
+ - Updated Decibel default endpoints and package addresses for current Aptos mainnet/testnet deployment values.
26
+ - `loadConfig` now falls back to encrypted-vault Decibel bearer token when `DECIBEL_API_BEARER_TOKEN` is not set in environment.
27
+ - `setup wizard` now consumes vault-stored Decibel bearer token fallback during auth bootstrap.
28
+ - `asset book` now passes resolved exchange credentials and transport URLs into adapter connect flow for cleaner Decibel startup behavior.
29
+
30
+ ### Fixed
31
+ - Updated Decibel on-chain function routing to `dex_accounts_entry::*` entrypoints for order placement/cancel/config calls.
32
+ - Hardened Decibel WebSocket shutdown path to avoid close-time crashes when sockets are still in `CONNECTING` state.
33
+ - Clarified Decibel auth/trading error messages to point users directly to Geomi API key source and required env/vault values.
34
+
35
+ ### Tests
36
+ - Expanded Decibel coverage for:
37
+ - vault secret persistence/read-back
38
+ - `setup decibel-key` interactive and non-interactive flows
39
+ - config vault-fallback credential resolution
40
+ - Decibel websocket feed and simple orderbook command behavior.
41
+
10
42
  ## [0.1.1] - 2026-02-25
11
43
 
12
44
  ### Fixed
@@ -39,6 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
39
71
  - Risk management: limits, drawdown, position sizing
40
72
  - Execution journal and safety checks
41
73
 
42
- [Unreleased]: https://github.com/raintree-technology/perps/compare/v0.1.1...HEAD
74
+ [Unreleased]: https://github.com/raintree-technology/perps/compare/v0.1.2...HEAD
75
+ [0.1.2]: https://github.com/raintree-technology/perps/releases/tag/v0.1.2
43
76
  [0.1.1]: https://github.com/raintree-technology/perps/releases/tag/v0.1.1
44
77
  [0.1.0]: https://github.com/raintree-technology/perps/releases/tag/v0.1.0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # perps
2
2
 
3
- Universal CLI for perpetual DEXes — by [Raintree Technology](https://raintree.technology).
3
+ Universal CLI for perpetual DEXes.
4
4
 
5
5
  [![CI](https://github.com/raintree-technology/perps/actions/workflows/ci.yml/badge.svg)](https://github.com/raintree-technology/perps/actions/workflows/ci.yml)
6
6
  [![npm](https://img.shields.io/npm/v/@raintree-technology/perps)](https://www.npmjs.com/package/@raintree-technology/perps)
@@ -12,6 +12,22 @@ npm install -g @raintree-technology/perps
12
12
  perps --help
13
13
  ```
14
14
 
15
+ ## Install Paths
16
+
17
+ Canonical install path:
18
+
19
+ ```bash
20
+ npm install -g @raintree-technology/perps
21
+ ```
22
+
23
+ GitHub install path (for source snapshots and pre-release testing):
24
+
25
+ ```bash
26
+ npm install github:raintree-technology/perps
27
+ ```
28
+
29
+ The package includes lifecycle build hooks so git installs produce runnable CLI artifacts (`dist/`) automatically.
30
+
15
31
  ## Supported Exchanges
16
32
 
17
33
  | Exchange | Chain | Status | Onboarding |
@@ -57,7 +73,7 @@ perps data ccxt binanceusdm fetchTicker --args '["BTC/USDT:USDT"]' --json
57
73
  Credentials are handled with care:
58
74
 
59
75
  - **Testnet by default** — mainnet requires explicit `--mainnet` flag or `*_NETWORK=mainnet` env var
60
- - **Encrypted at rest** — `perps setup wizard` stores keys in an AES-256-GCM encrypted local vault (`~/.perp/`)
76
+ - **Encrypted at rest** — setup flows (including `perps setup wizard` and `perps setup decibel-key`) store secrets in an AES-256-GCM local vault (`~/.perp/`)
61
77
  - **Owner-only permissions** — all credential files are `chmod 600`, directories `chmod 700`
62
78
  - **Env vars supported** — standard `HYPERLIQUID_PRIVATE_KEY`, `AEVO_SIGNING_KEY`, etc. for CI/automation
63
79
  - **No telemetry, no phoning home** — your keys and trades stay on your machine
@@ -87,6 +103,10 @@ All five adapters implement the same `PerpDEXAdapter` interface — swap `Hyperl
87
103
  # Recommended: interactive wizard
88
104
  perps setup wizard
89
105
 
106
+ # Decibel key helper: guide + validate + store in encrypted vault (env export optional)
107
+ perps setup decibel-key
108
+ perps setup decibel-key --env-file .env.local
109
+
90
110
  # Or set env vars directly
91
111
  export HYPERLIQUID_PRIVATE_KEY="0x..."
92
112
  ```
@@ -98,7 +118,7 @@ export HYPERLIQUID_PRIVATE_KEY="0x..."
98
118
  |----------|-------|---------|
99
119
  | Hyperliquid | `HYPERLIQUID_WALLET_ADDRESS` | `HYPERLIQUID_PRIVATE_KEY` |
100
120
  | Aevo | `AEVO_API_KEY` + `AEVO_API_SECRET` | + `AEVO_SIGNING_KEY` |
101
- | Decibel | `DECIBEL_API_WALLET_ADDRESS` + `DECIBEL_API_BEARER_TOKEN` | + `DECIBEL_API_WALLET_PRIVATE_KEY` |
121
+ | Decibel | Market data: `DECIBEL_API_BEARER_TOKEN`; account reads: + `DECIBEL_API_WALLET_ADDRESS` | + `DECIBEL_API_WALLET_PRIVATE_KEY` |
102
122
  | Orderly | `ORDERLY_ACCOUNT_ID` + `ORDERLY_KEY` + `ORDERLY_SECRET` | + `ORDERLY_TRADING_SECRET` |
103
123
  | Paradex | `PARADEX_ACCOUNT_ADDRESS` + `PARADEX_PRIVATE_KEY` | same |
104
124
 
@@ -125,12 +145,25 @@ perps signal --help # Trade signals
125
145
  perps replay --help # Execution replay
126
146
  perps data --help # Raw exchange data (ccxt, pmxt)
127
147
  perps setup --help # Onboarding wizard
148
+ perps setup decibel-key --help # Decibel bearer key bootstrap + vault storage
128
149
  perps config --help # Settings (show, get, set)
129
150
  perps doctor # Health check
130
151
  ```
131
152
 
132
153
  Every command supports `--json` for machine-readable output and `--help` for usage details.
133
154
 
155
+ Agent-friendly mode:
156
+
157
+ ```bash
158
+ # Non-interactive execution
159
+ perps setup wizard --yes --json
160
+
161
+ # Machine-readable responses with schema version
162
+ perps markets ls --json
163
+ ```
164
+
165
+ JSON payloads are schema-versioned, and failures return deterministic exit codes.
166
+
134
167
  ## Exit Codes
135
168
 
136
169
  | Code | Meaning |
@@ -164,7 +197,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. Please read our [Code of
164
197
 
165
198
  ## Author
166
199
 
167
- [Raintree Technology](https://raintree.technology) ([@raintree_tech](https://x.com/raintree_tech)) — created by [Zachary Roth](https://zacharyr0th.com) ([@zacharyr0th](https://x.com/zacharyr0th))
200
+ [Raintree Technology](https://raintree.technology)
168
201
 
169
202
  ## License
170
203
 
@@ -27,7 +27,7 @@ export class DecibelOrderManager {
27
27
  const chainSize = formatSize(sizeUnits, market);
28
28
  const isBuy = side === "buy";
29
29
  const payload = {
30
- function: `${this.packageAddress}::dex_accounts::place_order_to_subaccount`,
30
+ function: `${this.packageAddress}::dex_accounts_entry::place_order_to_subaccount`,
31
31
  typeArguments: [],
32
32
  functionArguments: [
33
33
  this.subaccountAddress,
@@ -68,7 +68,7 @@ export class DecibelOrderManager {
68
68
  }
69
69
  async cancelOrder(orderId, marketAddr) {
70
70
  const payload = {
71
- function: `${this.packageAddress}::dex_accounts::cancel_order_to_subaccount`,
71
+ function: `${this.packageAddress}::dex_accounts_entry::cancel_order_to_subaccount`,
72
72
  typeArguments: [],
73
73
  functionArguments: [this.subaccountAddress, orderId, marketAddr],
74
74
  };
@@ -85,7 +85,7 @@ export class DecibelOrderManager {
85
85
  }
86
86
  async cancelAllOrders(marketAddr) {
87
87
  const payload = {
88
- function: `${this.packageAddress}::dex_accounts::cancel_bulk_order_to_subaccount`,
88
+ function: `${this.packageAddress}::dex_accounts_entry::cancel_bulk_order_to_subaccount`,
89
89
  typeArguments: [],
90
90
  functionArguments: [this.subaccountAddress, marketAddr],
91
91
  };
@@ -103,7 +103,7 @@ export class DecibelOrderManager {
103
103
  async configureUserSettingsForMarket(marketAddr, isCross, userLeverageBps) {
104
104
  const leverageBps = Math.max(1, Math.round(userLeverageBps));
105
105
  const payload = {
106
- function: `${this.packageAddress}::dex_accounts::configure_user_settings_for_market`,
106
+ function: `${this.packageAddress}::dex_accounts_entry::configure_user_settings_for_market`,
107
107
  typeArguments: [],
108
108
  functionArguments: [this.subaccountAddress, marketAddr, isCross, leverageBps],
109
109
  };
@@ -112,7 +112,7 @@ export class DecibelRestClient {
112
112
  };
113
113
  if (requiresAuth) {
114
114
  if (!this.bearerToken) {
115
- throw new Error(`Decibel endpoint '${path}' requires DECIBEL_API_BEARER_TOKEN`);
115
+ throw new Error(`Decibel endpoint '${path}' requires DECIBEL_API_BEARER_TOKEN (Client API key secret from app.decibel.trade -> Geomi -> API Keys).`);
116
116
  }
117
117
  headers.Authorization = `Bearer ${this.bearerToken}`;
118
118
  }
@@ -41,9 +41,21 @@ export class DecibelWsFeed extends EventEmitter {
41
41
  this.shouldReconnect = false;
42
42
  this.clearReconnectTimer();
43
43
  this.stopPing();
44
- this.ws?.removeAllListeners();
45
- this.ws?.close();
44
+ const ws = this.ws;
46
45
  this.ws = null;
46
+ if (!ws) {
47
+ return;
48
+ }
49
+ // Closing a CONNECTING socket can emit an error from ws; keep a no-op handler
50
+ // so shutdown does not crash the process.
51
+ if (ws.readyState === WebSocket.CONNECTING) {
52
+ ws.once("error", () => { });
53
+ ws.terminate();
54
+ return;
55
+ }
56
+ if (ws.readyState === WebSocket.OPEN) {
57
+ ws.close();
58
+ }
47
59
  }
48
60
  isConnected() {
49
61
  return this.ws?.readyState === WebSocket.OPEN;
@@ -14,13 +14,13 @@ const DECIBEL_DEFAULTS = {
14
14
  fullnodeUrl: "https://api.testnet.aptoslabs.com/v1",
15
15
  restUrl: "https://api.testnet.aptoslabs.com/decibel",
16
16
  wsUrl: "wss://api.testnet.aptoslabs.com/decibel/ws",
17
- packageAddress: "0x95254fcb0816b9f3ec71aa4de5f5f7f8f3efeef9239f0d705a4cd3fe2f452de3",
17
+ packageAddress: "0x952535c3049e52f195f26798c2f1340d7dd5100edbe0f464e520a974d16fbe9f",
18
18
  },
19
19
  mainnet: {
20
20
  fullnodeUrl: "https://api.mainnet.aptoslabs.com/v1",
21
21
  restUrl: "https://api.mainnet.aptoslabs.com/decibel",
22
22
  wsUrl: "wss://api.mainnet.aptoslabs.com/decibel/ws",
23
- packageAddress: "0xb8a5788314451ce4d2fbbad32e1bad88d4184b73943b7fe5166eab93cf1a5a95",
23
+ packageAddress: "0x50ead22afd6ffd9769e3b3d6e0e64a2a350d68e8b102c4e72e33d0b8cfdfdb06",
24
24
  },
25
25
  };
26
26
  export class DecibelAdapter {
@@ -809,15 +809,15 @@ export class DecibelAdapter {
809
809
  }
810
810
  ensureAccountReadAuth() {
811
811
  if (!this.accountAddress) {
812
- throw new Error("Decibel account reads require account address (set DECIBEL_API_WALLET_ADDRESS or subaccount).");
812
+ throw new Error("Decibel account reads require account address (set DECIBEL_API_WALLET_ADDRESS or DECIBEL_SUBACCOUNT_ADDRESS).");
813
813
  }
814
814
  if (!this.config?.credentials?.apiBearerToken) {
815
- throw new Error("Decibel account reads require DECIBEL_API_BEARER_TOKEN.");
815
+ throw new Error("Decibel account reads require DECIBEL_API_BEARER_TOKEN (Client API key secret from app.decibel.trade -> Geomi -> API Keys).");
816
816
  }
817
817
  }
818
818
  ensureOrderManager() {
819
819
  if (!this.orderManager) {
820
- throw new Error("Decibel trading requires private key + package settings + subaccount (DECIBEL_API_WALLET_PRIVATE_KEY, DECIBEL_API_WALLET_ADDRESS, DECIBEL_API_BEARER_TOKEN).");
820
+ throw new Error("Decibel trading requires DECIBEL_API_WALLET_PRIVATE_KEY, DECIBEL_API_WALLET_ADDRESS, DECIBEL_API_BEARER_TOKEN, and package/network settings.");
821
821
  }
822
822
  }
823
823
  requireMarket(symbol) {
@@ -107,7 +107,6 @@ export function printSessionBanner(args) {
107
107
  for (const line of getPerpsAsciiMark()) {
108
108
  console.log(highlighter.brand(line));
109
109
  }
110
- console.log(highlighter.dim("by Raintree Technology"));
111
110
  console.log("");
112
111
  console.log(`${highlighter.dim("mode")} ${networkTag} ${highlighter.dim("exchange")} ${exchangeTag}`);
113
112
  console.log(`${highlighter.dim("command")} ${commandTag} ${highlighter.dim("version")} ${highlighter.dim(`v${args.version}`)}`);
@@ -8,6 +8,8 @@ import { createContext } from "./context.js";
8
8
  import { highlighter, printCommandCompletion, printSessionBanner } from "./experience.js";
9
9
  import { commandPathFromCommand, resolveTestnetMode } from "./network-defaults.js";
10
10
  import { outputError } from "./output.js";
11
+ import { inferExitCode } from "../lib/exit-codes.js";
12
+ import { withJsonContract } from "../lib/contracts.js";
11
13
  const require = createRequire(import.meta.url);
12
14
  const pkg = require("../../package.json");
13
15
  const commandMeta = new WeakMap();
@@ -30,6 +32,7 @@ export function createProgram() {
30
32
  .description("Universal CLI for perpetual DEXes")
31
33
  .showSuggestionAfterError(true)
32
34
  .showHelpAfterError()
35
+ .exitOverride()
33
36
  .version(pkg.version)
34
37
  .option("--json", "Output in JSON format", false)
35
38
  .option("--human", "Enable human-first output UX (banner, timing, rich formatting)", false)
@@ -62,17 +65,37 @@ Examples:
62
65
  setExchange(opts.exchange);
63
66
  }
64
67
  catch (err) {
65
- outputError(err instanceof Error ? err.message : String(err));
66
- console.error(highlighter.dim(`Available exchanges: ${getAvailableExchanges().join(", ")}`));
67
- process.exit(1);
68
+ const message = err instanceof Error ? err.message : String(err);
69
+ const exitCode = inferExitCode(err);
70
+ if (opts.json) {
71
+ console.error(JSON.stringify(withJsonContract("perps.error", {
72
+ status: "error",
73
+ error: { message, exitCode },
74
+ }), null, 2));
75
+ }
76
+ else {
77
+ outputError(message);
78
+ console.error(highlighter.dim(`Available exchanges: ${getAvailableExchanges().join(", ")}`));
79
+ }
80
+ process.exit(exitCode);
68
81
  }
69
82
  let config;
70
83
  try {
71
84
  config = loadConfig(isTestnet, opts.exchange);
72
85
  }
73
86
  catch (err) {
74
- outputError(err instanceof Error ? err.message : String(err));
75
- process.exit(1);
87
+ const message = err instanceof Error ? err.message : String(err);
88
+ const exitCode = inferExitCode(err);
89
+ if (opts.json) {
90
+ console.error(JSON.stringify(withJsonContract("perps.error", {
91
+ status: "error",
92
+ error: { message, exitCode },
93
+ }), null, 2));
94
+ }
95
+ else {
96
+ outputError(message);
97
+ }
98
+ process.exit(exitCode);
76
99
  }
77
100
  const context = createContext(config);
78
101
  // Store metadata on the root command
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Simple order book command using the adapter interface
3
3
  */
4
- import { getContext, getOutputOptions } from "../../cli/program.js";
4
+ import { getContext, getOutputOptions, getSelectedExchange } from "../../cli/program.js";
5
5
  import { output, outputError } from "../../cli/output.js";
6
6
  import { getExchangeAdapter } from "../../lib/exchange.js";
7
+ import { getExchangeCredentials } from "../../lib/config.js";
7
8
  export function registerBookSimpleCommand(asset) {
8
9
  asset
9
10
  .command("book <symbol>")
@@ -12,12 +13,19 @@ export function registerBookSimpleCommand(asset) {
12
13
  .action(async function (symbol) {
13
14
  const ctx = getContext(this);
14
15
  const outputOpts = getOutputOptions(this);
16
+ const exchangeId = getSelectedExchange(this);
15
17
  const opts = this.opts();
16
18
  const depth = parseInt(opts.depth, 10);
17
19
  const adapter = getExchangeAdapter();
18
20
  let connected = false;
19
21
  try {
20
- await adapter.connect({ testnet: ctx.config.testnet });
22
+ const credentials = getExchangeCredentials(ctx.config, exchangeId);
23
+ await adapter.connect({
24
+ testnet: ctx.config.testnet,
25
+ rpcUrl: credentials.fullnodeUrl,
26
+ wsUrl: credentials.wsUrl,
27
+ credentials,
28
+ });
21
29
  connected = true;
22
30
  // Normalize symbol (accept BTC, BTC-PERP, btc, etc.)
23
31
  let market = symbol.toUpperCase();
@@ -6,6 +6,7 @@
6
6
  import { output, outputError } from "../../cli/output.js";
7
7
  import { getContext, getOutputOptions, getVerbose } from "../../cli/program.js";
8
8
  import { getAvailableExchanges, getExchangeAdapter, getExchangeAdapterById, } from "../../lib/exchange.js";
9
+ import { getExchangeCredentials } from "../../lib/config.js";
9
10
  import { pLimit } from "../../lib/rate-limit.js";
10
11
  export function registerMarketsLsSimpleCommand(markets) {
11
12
  markets
@@ -38,7 +39,8 @@ async function handleSingleExchange(ctx, outputOpts, opts, limit = 0) {
38
39
  const adapter = getExchangeAdapter();
39
40
  let connected = false;
40
41
  try {
41
- await adapter.connect({ testnet: ctx.config.testnet });
42
+ const credentials = getExchangeCredentials(ctx.config, adapter.info.id);
43
+ await adapter.connect({ testnet: ctx.config.testnet, credentials });
42
44
  connected = true;
43
45
  const markets = await adapter.getMarkets();
44
46
  let filtered = markets;
@@ -88,7 +90,8 @@ async function fetchAllExchangeMarkets(ctx) {
88
90
  const connectedAdapters = [];
89
91
  const tasks = exchangeIds.map((id) => async () => {
90
92
  const adapter = getExchangeAdapterById(id);
91
- await adapter.connect({ testnet: ctx.config.testnet });
93
+ const credentials = getExchangeCredentials(ctx.config, id);
94
+ await adapter.connect({ testnet: ctx.config.testnet, credentials });
92
95
  connectedAdapters.push(adapter);
93
96
  const markets = await adapter.getMarkets();
94
97
  return { id, name: adapter.info.name, markets };
@@ -1,14 +1,81 @@
1
- import { existsSync, writeFileSync } from "node:fs";
1
+ import { execFileSync } from "node:child_process";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
3
  import { resolve } from "node:path";
3
4
  import { getOutputOptions } from "../../cli/program.js";
4
5
  import { output, outputError, outputSuccess } from "../../cli/output.js";
5
6
  import { withJsonContract } from "../../lib/contracts.js";
6
7
  import { createAccount, getAccountByAliasForExchange, } from "../../lib/db/index.js";
7
8
  import { privateKeyToAccount } from "viem/accounts";
8
- import { getCredentialVaultSummary, saveOnboardingProfileToVault, } from "../../lib/credential-vault.js";
9
+ import { getCredentialVaultSummary, getLatestCredentialVaultSecret, saveSecretToCredentialVault, saveOnboardingProfileToVault, } from "../../lib/credential-vault.js";
9
10
  import { ALL_SETUP_CHAINS, ALL_SETUP_EXCHANGES, buildOnboardingEnvFile, parseSetupChains, parseSetupExchanges, parseSetupMode, resolveExchangesFromChains, runOnboardingStateMachine, } from "../../lib/onboarding.js";
10
- import { confirm, multiSelect, select } from "../../lib/prompts.js";
11
+ import { confirm, multiSelect, pressEnterOrEsc, prompt, select } from "../../lib/prompts.js";
11
12
  import { hardenPrivateFile, PRIVATE_FILE_MODE } from "../../lib/fs-security.js";
13
+ import { FetchError, fetchWithTimeout } from "../../lib/fetch.js";
14
+ const DECIBEL_REST_DEFAULTS = {
15
+ mainnet: "https://api.mainnet.aptoslabs.com/decibel",
16
+ testnet: "https://api.testnet.aptoslabs.com/decibel",
17
+ };
18
+ function normalizeText(value) {
19
+ if (!value)
20
+ return undefined;
21
+ const trimmed = value.trim();
22
+ return trimmed.length > 0 ? trimmed : undefined;
23
+ }
24
+ function normalizeNetwork(value) {
25
+ const normalized = normalizeText(value)?.toLowerCase();
26
+ if (normalized === "testnet")
27
+ return "testnet";
28
+ return "mainnet";
29
+ }
30
+ function resolveDecibelNetwork(opts, command) {
31
+ if (opts.network) {
32
+ return normalizeNetwork(opts.network);
33
+ }
34
+ const globalOpts = command.optsWithGlobals();
35
+ if (globalOpts.mainnet)
36
+ return "mainnet";
37
+ if (globalOpts.testnet)
38
+ return "testnet";
39
+ return normalizeNetwork(process.env.DECIBEL_NETWORK);
40
+ }
41
+ function resolveDecibelRestUrl(opts, command) {
42
+ const explicit = normalizeText(opts.restUrl) ?? normalizeText(process.env.DECIBEL_REST_URL);
43
+ if (explicit) {
44
+ return explicit.replace(/\/+$/, "");
45
+ }
46
+ return DECIBEL_REST_DEFAULTS[resolveDecibelNetwork(opts, command)];
47
+ }
48
+ function openExternalUrl(url) {
49
+ const platform = process.platform;
50
+ if (platform === "darwin") {
51
+ execFileSync("open", [url], { stdio: "ignore" });
52
+ return;
53
+ }
54
+ if (platform === "win32") {
55
+ execFileSync("cmd", ["/c", "start", "", url], { stdio: "ignore" });
56
+ return;
57
+ }
58
+ execFileSync("xdg-open", [url], { stdio: "ignore" });
59
+ }
60
+ function upsertEnvVar(content, key, value) {
61
+ const lines = content.split(/\r?\n/);
62
+ const matcher = new RegExp(`^\\s*#?\\s*${key}=`);
63
+ let replaced = false;
64
+ const next = lines.map((line) => {
65
+ if (matcher.test(line)) {
66
+ replaced = true;
67
+ return `${key}=${value}`;
68
+ }
69
+ return line;
70
+ });
71
+ if (!replaced) {
72
+ if (next.length > 0 && next[next.length - 1].trim() !== "") {
73
+ next.push("");
74
+ }
75
+ next.push(`${key}=${value}`);
76
+ }
77
+ return `${next.join("\n").replace(/\n+$/, "")}\n`;
78
+ }
12
79
  const MODE_DESCRIPTIONS = {
13
80
  "mainnet-data": "No credentials. Use real mainnet data immediately.",
14
81
  "testnet-execution": "Force selected exchanges to testnet for all commands.",
@@ -37,8 +104,11 @@ function normalizeAuthOptions(opts) {
37
104
  const orderlyChainId = opts.orderlyChainId ??
38
105
  process.env.ORDERLY_CHAIN_ID ??
39
106
  process.env.ORDERLY_TESTNET_FAUCET_CHAIN_ID;
107
+ const vaultDecibelBearer = getLatestCredentialVaultSecret("DECIBEL_API_BEARER_TOKEN");
40
108
  return {
41
- decibelBearerToken: opts.decibelBearerToken ?? process.env.DECIBEL_API_BEARER_TOKEN,
109
+ decibelBearerToken: opts.decibelBearerToken ??
110
+ process.env.DECIBEL_API_BEARER_TOKEN ??
111
+ vaultDecibelBearer,
42
112
  aevoApiKey: opts.aevoApiKey ?? process.env.AEVO_API_KEY,
43
113
  aevoApiSecret: opts.aevoApiSecret ?? process.env.AEVO_API_SECRET,
44
114
  aevoAccountPrivateKey: (opts.aevoAccountPrivateKey ??
@@ -302,6 +372,224 @@ function printWizardSummary(args) {
302
372
  }
303
373
  export function registerSetupCommands(program) {
304
374
  const setup = program.command("setup").description("Onboarding and environment setup");
375
+ setup
376
+ .command("decibel-key")
377
+ .description("Bootstrap a Decibel Client API key flow, validate it, and store it in the encrypted vault")
378
+ .option("--token <token>", "Decibel Client API key secret (overrides DECIBEL_API_BEARER_TOKEN)")
379
+ .option("--wallet-address <address>", "Account address to check with account_overview")
380
+ .option("--subaccount-address <address>", "Subaccount address to check with account_overview")
381
+ .option("--network <network>", "Decibel network (mainnet or testnet)")
382
+ .option("--rest-url <url>", "Decibel REST URL override")
383
+ .option("--env-file <path>", "Write/update DECIBEL_API_BEARER_TOKEN in this env file")
384
+ .option("--no-vault", "Skip encrypted vault persistence for DECIBEL_API_BEARER_TOKEN")
385
+ .option("--no-open-browser", "Do not open Geomi/API keys pages automatically")
386
+ .option("--require-account", "Fail if account_overview check fails")
387
+ .option("--yes", "Skip interactive confirmations")
388
+ .action(async function () {
389
+ const outputOpts = getOutputOptions(this);
390
+ const opts = this.opts();
391
+ const hasInteractiveTty = process.stdin.isTTY && process.stdout.isTTY;
392
+ const assumeDefaults = Boolean(opts.yes) || !hasInteractiveTty;
393
+ const network = resolveDecibelNetwork(opts, this);
394
+ const restUrl = resolveDecibelRestUrl(opts, this);
395
+ const persistToVault = opts.vault ?? true;
396
+ try {
397
+ const vaultToken = normalizeText(getLatestCredentialVaultSecret("DECIBEL_API_BEARER_TOKEN"));
398
+ let tokenSource = "interactive";
399
+ let token = normalizeText(opts.token);
400
+ if (token) {
401
+ tokenSource = "cli";
402
+ }
403
+ else {
404
+ token = normalizeText(process.env.DECIBEL_API_BEARER_TOKEN);
405
+ if (token) {
406
+ tokenSource = "env";
407
+ }
408
+ else if (vaultToken) {
409
+ token = vaultToken;
410
+ tokenSource = "vault";
411
+ }
412
+ }
413
+ const usedInteractiveCapture = !token;
414
+ if (!token) {
415
+ if (!hasInteractiveTty) {
416
+ throw new Error("No Decibel API key found. Pass --token, set DECIBEL_API_BEARER_TOKEN, or run once interactively to store it in the encrypted vault.");
417
+ }
418
+ console.log("Decibel key generation is currently handled in Geomi (web UI).");
419
+ if (opts.openBrowser ?? true) {
420
+ let shouldOpen = true;
421
+ if (!assumeDefaults) {
422
+ shouldOpen = await pressEnterOrEsc("Press Enter to open Decibel + Geomi API keys pages (or Esc to skip)");
423
+ }
424
+ if (shouldOpen) {
425
+ try {
426
+ openExternalUrl("https://app.decibel.trade");
427
+ openExternalUrl("https://app.decibel.trade/geomi");
428
+ }
429
+ catch {
430
+ // Fall through with manual links.
431
+ }
432
+ }
433
+ }
434
+ console.log("");
435
+ console.log("Create/copy your Key Secret from Geomi -> API Keys, then paste it below.");
436
+ token = normalizeText(await prompt("Paste DECIBEL_API_BEARER_TOKEN (Key Secret):"));
437
+ if (!token) {
438
+ throw new Error("No Decibel API key provided.");
439
+ }
440
+ tokenSource = "interactive";
441
+ }
442
+ const headers = {
443
+ Authorization: `Bearer ${token}`,
444
+ Origin: "https://app.decibel.trade",
445
+ };
446
+ const marketsUrl = new URL("api/v1/markets", `${restUrl.replace(/\/+$/, "")}/`).toString();
447
+ const markets = await fetchWithTimeout(marketsUrl, {
448
+ headers,
449
+ retries: 0,
450
+ });
451
+ const marketCount = Array.isArray(markets) ? markets.length : 0;
452
+ if (marketCount === 0) {
453
+ throw new Error("Decibel token validated but returned no markets.");
454
+ }
455
+ const accountAddress = normalizeText(opts.subaccountAddress) ??
456
+ normalizeText(opts.walletAddress) ??
457
+ normalizeText(process.env.DECIBEL_SUBACCOUNT_ADDRESS) ??
458
+ normalizeText(process.env.DECIBEL_API_WALLET_ADDRESS);
459
+ let accountCheck;
460
+ if (accountAddress) {
461
+ const accountUrl = new URL("api/v1/account_overview", `${restUrl.replace(/\/+$/, "")}/`);
462
+ accountUrl.searchParams.set("account", accountAddress);
463
+ try {
464
+ await fetchWithTimeout(accountUrl.toString(), {
465
+ headers,
466
+ retries: 0,
467
+ });
468
+ accountCheck = {
469
+ status: "ok",
470
+ detail: `account_overview succeeded for ${accountAddress}`,
471
+ };
472
+ }
473
+ catch (err) {
474
+ const detail = err instanceof Error ? err.message : String(err);
475
+ accountCheck = {
476
+ status: "failed",
477
+ detail: `account_overview failed for ${accountAddress}: ${detail}`,
478
+ };
479
+ if (opts.requireAccount) {
480
+ throw new Error(accountCheck.detail);
481
+ }
482
+ }
483
+ }
484
+ else {
485
+ accountCheck = {
486
+ status: "skipped",
487
+ detail: "No DECIBEL_API_WALLET_ADDRESS/DECIBEL_SUBACCOUNT_ADDRESS provided.",
488
+ };
489
+ }
490
+ let vaultRef;
491
+ let vaultStatus = "disabled";
492
+ let vaultPath;
493
+ if (persistToVault) {
494
+ vaultPath = getCredentialVaultSummary().path;
495
+ if (vaultToken === token) {
496
+ vaultStatus = "unchanged";
497
+ }
498
+ else {
499
+ vaultRef = saveSecretToCredentialVault("DECIBEL_API_BEARER_TOKEN", token, {
500
+ mode: "setup.decibel-key",
501
+ exchange: "decibel",
502
+ });
503
+ vaultStatus = "stored";
504
+ vaultPath = vaultRef.path;
505
+ }
506
+ }
507
+ let envFilePath;
508
+ if (opts.envFile) {
509
+ envFilePath = resolve(opts.envFile.trim());
510
+ const existing = existsSync(envFilePath) ? readFileSync(envFilePath, "utf8") : "";
511
+ const updated = upsertEnvVar(existing, "DECIBEL_API_BEARER_TOKEN", token);
512
+ writeFileSync(envFilePath, updated, { mode: PRIVATE_FILE_MODE });
513
+ hardenPrivateFile(envFilePath);
514
+ }
515
+ const nextSteps = envFilePath
516
+ ? [
517
+ `source ${envFilePath}`,
518
+ "perps markets ls -e decibel",
519
+ "perps asset book BTC-PERP -e decibel",
520
+ ]
521
+ : persistToVault
522
+ ? [
523
+ "perps markets ls -e decibel",
524
+ "perps asset book BTC-PERP -e decibel",
525
+ ]
526
+ : [
527
+ "Export DECIBEL_API_BEARER_TOKEN in your shell (or re-run without --no-vault).",
528
+ "perps markets ls -e decibel",
529
+ "perps asset book BTC-PERP -e decibel",
530
+ ];
531
+ const payload = withJsonContract("setup.decibel_key.result", {
532
+ network,
533
+ restUrl,
534
+ tokenSource,
535
+ tokenCapturedInteractively: usedInteractiveCapture,
536
+ tokenValidated: true,
537
+ marketCount,
538
+ accountCheck,
539
+ vault: {
540
+ enabled: persistToVault,
541
+ status: vaultStatus,
542
+ id: vaultRef?.id,
543
+ path: vaultPath,
544
+ },
545
+ envFile: envFilePath,
546
+ envVar: "DECIBEL_API_BEARER_TOKEN",
547
+ nextSteps,
548
+ });
549
+ if (outputOpts.json) {
550
+ output(payload, outputOpts);
551
+ return;
552
+ }
553
+ outputSuccess("Decibel API key validated successfully.");
554
+ console.log("");
555
+ console.log(`Network: ${network}`);
556
+ console.log(`REST URL: ${restUrl}`);
557
+ console.log(`Markets checked: ${marketCount}`);
558
+ console.log(`Token source: ${tokenSource}`);
559
+ if (accountCheck) {
560
+ console.log(`Account check: [${accountCheck.status}] ${accountCheck.detail}`);
561
+ }
562
+ if (persistToVault && vaultPath) {
563
+ if (vaultStatus === "stored" && vaultRef) {
564
+ console.log(`Vault: stored ${vaultRef.id} (${vaultPath})`);
565
+ }
566
+ else {
567
+ console.log(`Vault: unchanged (${vaultPath})`);
568
+ }
569
+ }
570
+ else {
571
+ console.log("Vault: skipped (--no-vault)");
572
+ }
573
+ if (envFilePath) {
574
+ console.log(`Env file: ${envFilePath}`);
575
+ }
576
+ console.log("");
577
+ console.log("Next steps:");
578
+ for (const [index, step] of nextSteps.entries()) {
579
+ console.log(`${index + 1}) ${step}`);
580
+ }
581
+ console.log("");
582
+ }
583
+ catch (err) {
584
+ const message = err instanceof FetchError
585
+ ? `Decibel key bootstrap failed: ${err.message}`
586
+ : err instanceof Error
587
+ ? err.message
588
+ : String(err);
589
+ outputError(message);
590
+ process.exit(1);
591
+ }
592
+ });
305
593
  setup
306
594
  .command("wizard")
307
595
  .description("Interactive setup for mainnet data defaults and testnet onboarding")
package/dist/index.js CHANGED
@@ -1,4 +1,38 @@
1
1
  #!/usr/bin/env node
2
2
  import { createProgram } from "./cli/program.js";
3
+ import { withJsonContract } from "./lib/contracts.js";
4
+ import { inferExitCode } from "./lib/exit-codes.js";
5
+ const isJsonMode = process.argv.includes("--json");
3
6
  const program = createProgram();
4
- program.parse();
7
+ if (isJsonMode) {
8
+ program.configureOutput({
9
+ writeOut: () => undefined,
10
+ writeErr: () => undefined,
11
+ });
12
+ }
13
+ try {
14
+ await program.parseAsync(process.argv);
15
+ }
16
+ catch (err) {
17
+ const message = err instanceof Error ? err.message : String(err);
18
+ const commanderExitCode = typeof err === "object" && err !== null && "exitCode" in err
19
+ ? Number(err.exitCode)
20
+ : Number.NaN;
21
+ const commanderCode = typeof err === "object" && err !== null && "code" in err
22
+ ? String(err.code ?? "")
23
+ : "";
24
+ const exitCode = Number.isFinite(commanderExitCode) && commanderExitCode === 0
25
+ ? 0
26
+ : commanderCode.startsWith("commander.")
27
+ ? inferExitCode(err)
28
+ : Number.isFinite(commanderExitCode)
29
+ ? commanderExitCode
30
+ : inferExitCode(err);
31
+ if (isJsonMode && exitCode !== 0) {
32
+ console.error(JSON.stringify(withJsonContract("perps.error", {
33
+ status: "error",
34
+ error: { message, exitCode },
35
+ }), null, 2));
36
+ }
37
+ process.exit(exitCode);
38
+ }
@@ -1,5 +1,6 @@
1
1
  import { privateKeyToAccount } from "viem/accounts";
2
2
  import { getDefaultAccountForExchange } from "./db/index.js";
3
+ import { getLatestCredentialVaultSecret } from "./credential-vault.js";
3
4
  import { parseEnv } from "./schema.js";
4
5
  import { recordTelemetryMetric } from "./telemetry.js";
5
6
  const DECIBEL_NETWORK_DEFAULTS = {
@@ -7,13 +8,13 @@ const DECIBEL_NETWORK_DEFAULTS = {
7
8
  fullnodeUrl: "https://api.testnet.aptoslabs.com/v1",
8
9
  restUrl: "https://api.testnet.aptoslabs.com/decibel",
9
10
  wsUrl: "wss://api.testnet.aptoslabs.com/decibel/ws",
10
- packageAddress: "0x95254fcb0816b9f3ec71aa4de5f5f7f8f3efeef9239f0d705a4cd3fe2f452de3",
11
+ packageAddress: "0x952535c3049e52f195f26798c2f1340d7dd5100edbe0f464e520a974d16fbe9f",
11
12
  },
12
13
  mainnet: {
13
14
  fullnodeUrl: "https://api.mainnet.aptoslabs.com/v1",
14
15
  restUrl: "https://api.mainnet.aptoslabs.com/decibel",
15
16
  wsUrl: "wss://api.mainnet.aptoslabs.com/decibel/ws",
16
- packageAddress: "0xb8a5788314451ce4d2fbbad32e1bad88d4184b73943b7fe5166eab93cf1a5a95",
17
+ packageAddress: "0x50ead22afd6ffd9769e3b3d6e0e64a2a350d68e8b102c4e72e33d0b8cfdfdb06",
17
18
  },
18
19
  };
19
20
  const ORDERLY_DEFAULTS = {
@@ -84,10 +85,14 @@ function resolveHyperliquidSettings(testnet, env) {
84
85
  function resolveDecibelSettings(testnet, env) {
85
86
  const network = resolveExchangeNetwork(testnet, env.DECIBEL_NETWORK);
86
87
  const defaults = DECIBEL_NETWORK_DEFAULTS[network];
88
+ const envBearerToken = env.DECIBEL_API_BEARER_TOKEN;
89
+ const vaultBearerToken = envBearerToken
90
+ ? undefined
91
+ : getLatestCredentialVaultSecret("DECIBEL_API_BEARER_TOKEN");
87
92
  const credentials = {
88
93
  apiWalletPrivateKey: env.DECIBEL_API_WALLET_PRIVATE_KEY,
89
94
  apiWalletAddress: env.DECIBEL_API_WALLET_ADDRESS,
90
- apiBearerToken: env.DECIBEL_API_BEARER_TOKEN,
95
+ apiBearerToken: envBearerToken ?? vaultBearerToken,
91
96
  subaccountAddress: env.DECIBEL_SUBACCOUNT_ADDRESS,
92
97
  };
93
98
  const hasAnyCredentials = !!credentials.apiWalletPrivateKey ||
@@ -291,7 +296,7 @@ export function getExchangeCredentials(config, exchangeId, requirement = {}) {
291
296
  if (requirement.requireReadAccess &&
292
297
  (!credentials.apiBearerToken || !credentials.accountAddress)) {
293
298
  recordAuth("read", "failed");
294
- throw new Error("Decibel authenticated reads require DECIBEL_API_WALLET_ADDRESS + DECIBEL_API_BEARER_TOKEN.");
299
+ throw new Error("Decibel authenticated reads require DECIBEL_API_WALLET_ADDRESS + DECIBEL_API_BEARER_TOKEN (Client API key secret from app.decibel.trade -> Geomi -> API Keys).");
295
300
  }
296
301
  if (requirement.requireReadAccess) {
297
302
  recordAuth("read", "success");
@@ -302,7 +307,7 @@ export function getExchangeCredentials(config, exchangeId, requirement = {}) {
302
307
  !credentials.apiBearerToken ||
303
308
  !credentials.packageAddress)) {
304
309
  recordAuth("trade", "failed");
305
- throw new Error("Decibel trading requires DECIBEL_API_WALLET_PRIVATE_KEY, DECIBEL_API_WALLET_ADDRESS, DECIBEL_API_BEARER_TOKEN, and package/network settings.");
310
+ throw new Error("Decibel trading requires DECIBEL_API_WALLET_PRIVATE_KEY, DECIBEL_API_WALLET_ADDRESS, and DECIBEL_API_BEARER_TOKEN (Client API key secret from app.decibel.trade -> Geomi -> API Keys), plus package/network settings.");
306
311
  }
307
312
  if (requirement.requireTrading) {
308
313
  recordAuth("trade", "success");
@@ -20,3 +20,11 @@ export declare function getCredentialVaultSummary(): {
20
20
  entries: number;
21
21
  updatedAt?: string;
22
22
  };
23
+ export declare function getLatestCredentialVaultSecret(secretKey: string): string | undefined;
24
+ export declare function saveSecretToCredentialVault(secretKey: string, secretValue: string, options?: {
25
+ mode?: string;
26
+ exchange?: string;
27
+ }): {
28
+ id: string;
29
+ path: string;
30
+ };
@@ -1,7 +1,7 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { CREDENTIAL_VAULT_PATH } from "./paths.js";
4
- import { encryptSecret } from "./secrets.js";
4
+ import { decryptSecret, encryptSecret } from "./secrets.js";
5
5
  import { ensurePrivateDir, hardenPrivateFile, PRIVATE_FILE_MODE } from "./fs-security.js";
6
6
  const VAULT_SCHEMA_VERSION = 1;
7
7
  function createVault() {
@@ -78,6 +78,25 @@ function buildId(profile) {
78
78
  const suffix = Math.random().toString(36).slice(2, 8);
79
79
  return `onb-${day}-${suffix}`;
80
80
  }
81
+ function buildSecretId(now) {
82
+ const day = now.toISOString().slice(0, 10).replace(/-/g, "");
83
+ const suffix = Math.random().toString(36).slice(2, 8);
84
+ return `sec-${day}-${suffix}`;
85
+ }
86
+ function normalizeSecretKey(secretKey) {
87
+ const normalized = secretKey.trim().toUpperCase();
88
+ if (!/^[A-Z0-9_]+$/.test(normalized)) {
89
+ throw new Error(`Invalid vault secret key '${secretKey}'`);
90
+ }
91
+ return normalized;
92
+ }
93
+ function normalizeSecretValue(secretValue) {
94
+ const normalized = secretValue.trim();
95
+ if (!normalized) {
96
+ throw new Error("Vault secret value cannot be empty");
97
+ }
98
+ return normalized;
99
+ }
81
100
  export function saveOnboardingProfileToVault(profile) {
82
101
  const vault = readVault();
83
102
  const id = buildId(profile);
@@ -107,3 +126,44 @@ export function getCredentialVaultSummary() {
107
126
  updatedAt: vault.updatedAt,
108
127
  };
109
128
  }
129
+ export function getLatestCredentialVaultSecret(secretKey) {
130
+ const normalizedKey = normalizeSecretKey(secretKey);
131
+ const vault = readVault();
132
+ for (const entry of vault.entries) {
133
+ const encryptedValue = entry?.encrypted?.[normalizedKey];
134
+ if (typeof encryptedValue !== "string" || encryptedValue.trim().length === 0) {
135
+ continue;
136
+ }
137
+ try {
138
+ const plaintext = decryptSecret(encryptedValue).trim();
139
+ if (plaintext.length > 0) {
140
+ return plaintext;
141
+ }
142
+ }
143
+ catch {
144
+ // Skip malformed entries and continue searching older entries.
145
+ }
146
+ }
147
+ return undefined;
148
+ }
149
+ export function saveSecretToCredentialVault(secretKey, secretValue, options = {}) {
150
+ const key = normalizeSecretKey(secretKey);
151
+ const value = normalizeSecretValue(secretValue);
152
+ const now = new Date();
153
+ const vault = readVault();
154
+ const id = buildSecretId(now);
155
+ const entry = {
156
+ id,
157
+ createdAt: now.toISOString(),
158
+ mode: options.mode ?? "manual_secret",
159
+ exchanges: options.exchange ? [options.exchange] : [],
160
+ wallets: {},
161
+ encrypted: {
162
+ [key]: encryptSecret(value),
163
+ },
164
+ };
165
+ vault.entries.unshift(entry);
166
+ vault.updatedAt = now.toISOString();
167
+ writeVault(vault);
168
+ return { id, path: CREDENTIAL_VAULT_PATH };
169
+ }
@@ -25,7 +25,11 @@ export function inferExitCode(err) {
25
25
  const message = err instanceof Error ? err.message.toLowerCase() : String(err).toLowerCase();
26
26
  if (message.includes("must be") ||
27
27
  message.includes("invalid") ||
28
- message.includes("use --yes with --json")) {
28
+ message.includes("use --yes with --json") ||
29
+ message.includes("unknown option") ||
30
+ message.includes("too many arguments") ||
31
+ message.includes("missing required argument") ||
32
+ message.includes("option argument missing")) {
29
33
  return EXIT_CODES.VALIDATION_ERROR;
30
34
  }
31
35
  if (message.includes("not configured") ||
@@ -612,7 +612,7 @@ export async function bootstrapOnboardingAuth(profile, options = {}) {
612
612
  exchange: "decibel",
613
613
  service: "decibel auth",
614
614
  status: "ok",
615
- detail: "using DECIBEL_API_BEARER_TOKEN",
615
+ detail: "using DECIBEL_API_BEARER_TOKEN (Client API key secret)",
616
616
  });
617
617
  }
618
618
  else {
@@ -620,7 +620,7 @@ export async function bootstrapOnboardingAuth(profile, options = {}) {
620
620
  exchange: "decibel",
621
621
  service: "decibel auth",
622
622
  status: "manual",
623
- detail: "still requires DECIBEL_API_BEARER_TOKEN",
623
+ detail: "still requires DECIBEL_API_BEARER_TOKEN (Client API key secret from app.decibel.trade -> Geomi -> API Keys)",
624
624
  });
625
625
  }
626
626
  }
@@ -837,7 +837,7 @@ const ONBOARDING_PROVIDERS = {
837
837
  manualAuthFollowUp: {
838
838
  exchange: "decibel",
839
839
  env: ["DECIBEL_API_BEARER_TOKEN"],
840
- detail: "Set a Decibel bearer token for authenticated reads/trading after wallet generation.",
840
+ detail: "Create a key in app.decibel.trade -> Geomi -> API Keys, copy the Key Secret, and set DECIBEL_API_BEARER_TOKEN.",
841
841
  },
842
842
  fundingTask: ({ profile, options }) => requestAptosFaucet({
843
843
  aptosAddress: profile.wallets.aptosAddress,
@@ -60,80 +60,80 @@ export declare const envSchema: z.ZodEffects<z.ZodObject<{
60
60
  EXECUTION_TAKE_PROFIT_PCT: number;
61
61
  EXECUTION_SPREAD_OFFSET_PCT: number;
62
62
  OPERATOR_HEARTBEAT_INTERVAL_MS: number;
63
- DECIBEL_REST_URL?: string | undefined;
64
- DECIBEL_WS_URL?: string | undefined;
65
- DECIBEL_FULLNODE_URL?: string | undefined;
66
- AEVO_REST_URL?: string | undefined;
67
63
  ORDERLY_REST_URL?: string | undefined;
68
- PARADEX_REST_URL?: string | undefined;
69
- HYPERLIQUID_PRIVATE_KEY?: string | undefined;
70
- HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
71
- HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
72
- DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
73
- DECIBEL_API_WALLET_ADDRESS?: string | undefined;
64
+ AEVO_REST_URL?: string | undefined;
65
+ PARADEX_ACCOUNT_ADDRESS?: string | undefined;
66
+ PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
74
67
  DECIBEL_API_BEARER_TOKEN?: string | undefined;
75
- DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
76
- DECIBEL_PACKAGE_ADDRESS?: string | undefined;
77
- DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
78
68
  AEVO_API_KEY?: string | undefined;
79
69
  AEVO_API_SECRET?: string | undefined;
80
- AEVO_SIGNING_KEY?: string | undefined;
81
70
  AEVO_ACCOUNT_PRIVATE_KEY?: string | undefined;
82
- AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
83
- ORDERLY_BROKER_ID?: string | undefined;
84
- ORDERLY_CHAIN_ID?: string | undefined;
85
71
  ORDERLY_ACCOUNT_ID?: string | undefined;
86
72
  ORDERLY_KEY?: string | undefined;
87
73
  ORDERLY_SECRET?: string | undefined;
88
- ORDERLY_TRADING_KEY?: string | undefined;
89
- ORDERLY_TRADING_SECRET?: string | undefined;
90
- ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
74
+ ORDERLY_BROKER_ID?: string | undefined;
91
75
  PARADEX_API_BEARER_TOKEN?: string | undefined;
92
- PARADEX_ACCOUNT_ADDRESS?: string | undefined;
76
+ HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
77
+ DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
78
+ AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
79
+ ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
80
+ PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
81
+ PARADEX_REST_URL?: string | undefined;
82
+ HYPERLIQUID_PRIVATE_KEY?: string | undefined;
83
+ DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
84
+ AEVO_SIGNING_KEY?: string | undefined;
85
+ ORDERLY_TRADING_SECRET?: string | undefined;
93
86
  PARADEX_PRIVATE_KEY?: string | undefined;
94
- PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
87
+ DECIBEL_REST_URL?: string | undefined;
88
+ DECIBEL_WS_URL?: string | undefined;
89
+ DECIBEL_FULLNODE_URL?: string | undefined;
90
+ HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
91
+ DECIBEL_API_WALLET_ADDRESS?: string | undefined;
92
+ DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
93
+ DECIBEL_PACKAGE_ADDRESS?: string | undefined;
94
+ ORDERLY_CHAIN_ID?: string | undefined;
95
+ ORDERLY_TRADING_KEY?: string | undefined;
95
96
  PARADEX_CHAIN_ID?: string | undefined;
96
- PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
97
97
  PERPS_ALLOW_UNTRUSTED_ENDPOINTS?: "0" | "1" | "true" | "false" | undefined;
98
98
  PERPS_BLOCKED_EXCHANGES?: string | undefined;
99
99
  PERPS_BLOCKED_MARKETS?: string | undefined;
100
100
  PERPS_BLOCKED_MARKET_PATTERNS?: string | undefined;
101
101
  PERPS_REPUTATION_BLOCKLIST_FILE?: string | undefined;
102
102
  }, {
103
- DECIBEL_REST_URL?: string | undefined;
104
- DECIBEL_WS_URL?: string | undefined;
105
- DECIBEL_FULLNODE_URL?: string | undefined;
106
- AEVO_REST_URL?: string | undefined;
107
103
  ORDERLY_REST_URL?: string | undefined;
108
- PARADEX_REST_URL?: string | undefined;
109
- HYPERLIQUID_PRIVATE_KEY?: string | undefined;
110
- HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
111
- HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
112
- DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
113
- DECIBEL_API_WALLET_ADDRESS?: string | undefined;
104
+ AEVO_REST_URL?: string | undefined;
105
+ PARADEX_ACCOUNT_ADDRESS?: string | undefined;
106
+ PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
114
107
  DECIBEL_API_BEARER_TOKEN?: string | undefined;
115
- DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
116
- DECIBEL_PACKAGE_ADDRESS?: string | undefined;
117
- DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
118
108
  AEVO_API_KEY?: string | undefined;
119
109
  AEVO_API_SECRET?: string | undefined;
120
- AEVO_SIGNING_KEY?: string | undefined;
121
110
  AEVO_ACCOUNT_PRIVATE_KEY?: string | undefined;
122
- AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
123
- ORDERLY_BROKER_ID?: string | undefined;
124
- ORDERLY_CHAIN_ID?: string | undefined;
125
111
  ORDERLY_ACCOUNT_ID?: string | undefined;
126
112
  ORDERLY_KEY?: string | undefined;
127
113
  ORDERLY_SECRET?: string | undefined;
128
- ORDERLY_TRADING_KEY?: string | undefined;
129
- ORDERLY_TRADING_SECRET?: string | undefined;
130
- ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
114
+ ORDERLY_BROKER_ID?: string | undefined;
131
115
  PARADEX_API_BEARER_TOKEN?: string | undefined;
132
- PARADEX_ACCOUNT_ADDRESS?: string | undefined;
116
+ HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
117
+ DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
118
+ AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
119
+ ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
120
+ PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
121
+ PARADEX_REST_URL?: string | undefined;
122
+ HYPERLIQUID_PRIVATE_KEY?: string | undefined;
123
+ DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
124
+ AEVO_SIGNING_KEY?: string | undefined;
125
+ ORDERLY_TRADING_SECRET?: string | undefined;
133
126
  PARADEX_PRIVATE_KEY?: string | undefined;
134
- PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
127
+ DECIBEL_REST_URL?: string | undefined;
128
+ DECIBEL_WS_URL?: string | undefined;
129
+ DECIBEL_FULLNODE_URL?: string | undefined;
130
+ HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
131
+ DECIBEL_API_WALLET_ADDRESS?: string | undefined;
132
+ DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
133
+ DECIBEL_PACKAGE_ADDRESS?: string | undefined;
134
+ ORDERLY_CHAIN_ID?: string | undefined;
135
+ ORDERLY_TRADING_KEY?: string | undefined;
135
136
  PARADEX_CHAIN_ID?: string | undefined;
136
- PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
137
137
  RISK_MAX_POSITION_SIZE_USD?: number | undefined;
138
138
  RISK_MAX_TOTAL_EXPOSURE_USD?: number | undefined;
139
139
  RISK_MAX_LEVERAGE?: number | undefined;
@@ -160,80 +160,80 @@ export declare const envSchema: z.ZodEffects<z.ZodObject<{
160
160
  EXECUTION_TAKE_PROFIT_PCT: number;
161
161
  EXECUTION_SPREAD_OFFSET_PCT: number;
162
162
  OPERATOR_HEARTBEAT_INTERVAL_MS: number;
163
- DECIBEL_REST_URL?: string | undefined;
164
- DECIBEL_WS_URL?: string | undefined;
165
- DECIBEL_FULLNODE_URL?: string | undefined;
166
- AEVO_REST_URL?: string | undefined;
167
163
  ORDERLY_REST_URL?: string | undefined;
168
- PARADEX_REST_URL?: string | undefined;
169
- HYPERLIQUID_PRIVATE_KEY?: string | undefined;
170
- HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
171
- HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
172
- DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
173
- DECIBEL_API_WALLET_ADDRESS?: string | undefined;
164
+ AEVO_REST_URL?: string | undefined;
165
+ PARADEX_ACCOUNT_ADDRESS?: string | undefined;
166
+ PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
174
167
  DECIBEL_API_BEARER_TOKEN?: string | undefined;
175
- DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
176
- DECIBEL_PACKAGE_ADDRESS?: string | undefined;
177
- DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
178
168
  AEVO_API_KEY?: string | undefined;
179
169
  AEVO_API_SECRET?: string | undefined;
180
- AEVO_SIGNING_KEY?: string | undefined;
181
170
  AEVO_ACCOUNT_PRIVATE_KEY?: string | undefined;
182
- AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
183
- ORDERLY_BROKER_ID?: string | undefined;
184
- ORDERLY_CHAIN_ID?: string | undefined;
185
171
  ORDERLY_ACCOUNT_ID?: string | undefined;
186
172
  ORDERLY_KEY?: string | undefined;
187
173
  ORDERLY_SECRET?: string | undefined;
188
- ORDERLY_TRADING_KEY?: string | undefined;
189
- ORDERLY_TRADING_SECRET?: string | undefined;
190
- ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
174
+ ORDERLY_BROKER_ID?: string | undefined;
191
175
  PARADEX_API_BEARER_TOKEN?: string | undefined;
192
- PARADEX_ACCOUNT_ADDRESS?: string | undefined;
176
+ HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
177
+ DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
178
+ AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
179
+ ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
180
+ PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
181
+ PARADEX_REST_URL?: string | undefined;
182
+ HYPERLIQUID_PRIVATE_KEY?: string | undefined;
183
+ DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
184
+ AEVO_SIGNING_KEY?: string | undefined;
185
+ ORDERLY_TRADING_SECRET?: string | undefined;
193
186
  PARADEX_PRIVATE_KEY?: string | undefined;
194
- PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
187
+ DECIBEL_REST_URL?: string | undefined;
188
+ DECIBEL_WS_URL?: string | undefined;
189
+ DECIBEL_FULLNODE_URL?: string | undefined;
190
+ HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
191
+ DECIBEL_API_WALLET_ADDRESS?: string | undefined;
192
+ DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
193
+ DECIBEL_PACKAGE_ADDRESS?: string | undefined;
194
+ ORDERLY_CHAIN_ID?: string | undefined;
195
+ ORDERLY_TRADING_KEY?: string | undefined;
195
196
  PARADEX_CHAIN_ID?: string | undefined;
196
- PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
197
197
  PERPS_ALLOW_UNTRUSTED_ENDPOINTS?: "0" | "1" | "true" | "false" | undefined;
198
198
  PERPS_BLOCKED_EXCHANGES?: string | undefined;
199
199
  PERPS_BLOCKED_MARKETS?: string | undefined;
200
200
  PERPS_BLOCKED_MARKET_PATTERNS?: string | undefined;
201
201
  PERPS_REPUTATION_BLOCKLIST_FILE?: string | undefined;
202
202
  }, {
203
- DECIBEL_REST_URL?: string | undefined;
204
- DECIBEL_WS_URL?: string | undefined;
205
- DECIBEL_FULLNODE_URL?: string | undefined;
206
- AEVO_REST_URL?: string | undefined;
207
203
  ORDERLY_REST_URL?: string | undefined;
208
- PARADEX_REST_URL?: string | undefined;
209
- HYPERLIQUID_PRIVATE_KEY?: string | undefined;
210
- HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
211
- HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
212
- DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
213
- DECIBEL_API_WALLET_ADDRESS?: string | undefined;
204
+ AEVO_REST_URL?: string | undefined;
205
+ PARADEX_ACCOUNT_ADDRESS?: string | undefined;
206
+ PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
214
207
  DECIBEL_API_BEARER_TOKEN?: string | undefined;
215
- DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
216
- DECIBEL_PACKAGE_ADDRESS?: string | undefined;
217
- DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
218
208
  AEVO_API_KEY?: string | undefined;
219
209
  AEVO_API_SECRET?: string | undefined;
220
- AEVO_SIGNING_KEY?: string | undefined;
221
210
  AEVO_ACCOUNT_PRIVATE_KEY?: string | undefined;
222
- AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
223
- ORDERLY_BROKER_ID?: string | undefined;
224
- ORDERLY_CHAIN_ID?: string | undefined;
225
211
  ORDERLY_ACCOUNT_ID?: string | undefined;
226
212
  ORDERLY_KEY?: string | undefined;
227
213
  ORDERLY_SECRET?: string | undefined;
228
- ORDERLY_TRADING_KEY?: string | undefined;
229
- ORDERLY_TRADING_SECRET?: string | undefined;
230
- ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
214
+ ORDERLY_BROKER_ID?: string | undefined;
231
215
  PARADEX_API_BEARER_TOKEN?: string | undefined;
232
- PARADEX_ACCOUNT_ADDRESS?: string | undefined;
216
+ HYPERLIQUID_NETWORK?: "testnet" | "mainnet" | undefined;
217
+ DECIBEL_NETWORK?: "testnet" | "mainnet" | undefined;
218
+ AEVO_NETWORK?: "testnet" | "mainnet" | undefined;
219
+ ORDERLY_NETWORK?: "testnet" | "mainnet" | undefined;
220
+ PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
221
+ PARADEX_REST_URL?: string | undefined;
222
+ HYPERLIQUID_PRIVATE_KEY?: string | undefined;
223
+ DECIBEL_API_WALLET_PRIVATE_KEY?: string | undefined;
224
+ AEVO_SIGNING_KEY?: string | undefined;
225
+ ORDERLY_TRADING_SECRET?: string | undefined;
233
226
  PARADEX_PRIVATE_KEY?: string | undefined;
234
- PARADEX_ETHEREUM_ACCOUNT?: string | undefined;
227
+ DECIBEL_REST_URL?: string | undefined;
228
+ DECIBEL_WS_URL?: string | undefined;
229
+ DECIBEL_FULLNODE_URL?: string | undefined;
230
+ HYPERLIQUID_WALLET_ADDRESS?: string | undefined;
231
+ DECIBEL_API_WALLET_ADDRESS?: string | undefined;
232
+ DECIBEL_SUBACCOUNT_ADDRESS?: string | undefined;
233
+ DECIBEL_PACKAGE_ADDRESS?: string | undefined;
234
+ ORDERLY_CHAIN_ID?: string | undefined;
235
+ ORDERLY_TRADING_KEY?: string | undefined;
235
236
  PARADEX_CHAIN_ID?: string | undefined;
236
- PARADEX_NETWORK?: "testnet" | "mainnet" | undefined;
237
237
  RISK_MAX_POSITION_SIZE_USD?: number | undefined;
238
238
  RISK_MAX_TOTAL_EXPOSURE_USD?: number | undefined;
239
239
  RISK_MAX_LEVERAGE?: number | undefined;
@@ -34,9 +34,23 @@ const secureWsUrl = z
34
34
  message: "must use wss:// (ws:// allowed only for localhost)",
35
35
  });
36
36
  const EXCHANGE_URL_HOST_ALLOWLIST = {
37
- DECIBEL_REST_URL: ["api.testnet.aptoslabs.com", "api.mainnet.aptoslabs.com"],
38
- DECIBEL_WS_URL: ["api.testnet.aptoslabs.com", "api.mainnet.aptoslabs.com"],
39
- DECIBEL_FULLNODE_URL: ["api.testnet.aptoslabs.com", "api.mainnet.aptoslabs.com"],
37
+ DECIBEL_REST_URL: [
38
+ "api.decibel.trade",
39
+ "api.testnet.aptoslabs.com",
40
+ "api.netna.aptoslabs.com",
41
+ "api.mainnet.aptoslabs.com",
42
+ ],
43
+ DECIBEL_WS_URL: [
44
+ "api.decibel.trade",
45
+ "api.testnet.aptoslabs.com",
46
+ "api.netna.aptoslabs.com",
47
+ "api.mainnet.aptoslabs.com",
48
+ ],
49
+ DECIBEL_FULLNODE_URL: [
50
+ "api.testnet.aptoslabs.com",
51
+ "api.netna.aptoslabs.com",
52
+ "api.mainnet.aptoslabs.com",
53
+ ],
40
54
  AEVO_REST_URL: ["api-testnet.aevo.xyz", "api.aevo.xyz"],
41
55
  ORDERLY_REST_URL: ["testnet-api.orderly.org", "api-evm.orderly.org"],
42
56
  PARADEX_REST_URL: ["api.testnet.paradex.trade", "api.prod.paradex.trade"],
@@ -118,13 +132,6 @@ export const envSchema = z
118
132
  message: "required when DECIBEL_API_WALLET_PRIVATE_KEY is set",
119
133
  });
120
134
  }
121
- if (env.DECIBEL_API_BEARER_TOKEN && !env.DECIBEL_API_WALLET_ADDRESS) {
122
- ctx.addIssue({
123
- code: z.ZodIssueCode.custom,
124
- path: ["DECIBEL_API_WALLET_ADDRESS"],
125
- message: "required when DECIBEL_API_BEARER_TOKEN is set",
126
- });
127
- }
128
135
  const aevoCredentialValues = [env.AEVO_API_KEY, env.AEVO_API_SECRET];
129
136
  const aevoCredentialCount = aevoCredentialValues.filter(Boolean).length;
130
137
  if (aevoCredentialCount === 1) {
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raintree-technology/perps",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Universal CLI for perpetual DEXes",
5
5
  "author": "Raintree Technology (https://raintree.technology)",
6
6
  "contributors": [
@@ -63,6 +63,8 @@
63
63
  "sideEffects": false,
64
64
  "scripts": {
65
65
  "build": "tsc",
66
+ "prepare": "npm run build",
67
+ "prepack": "npm run build",
66
68
  "dev": "tsx src/index.ts",
67
69
  "typecheck": "tsc --noEmit",
68
70
  "smoke": "node scripts/smoke.mjs",
@@ -82,7 +84,7 @@
82
84
  "lint:fix": "biome lint --write src scripts",
83
85
  "publint": "publint",
84
86
  "attw": "attw --pack . --profile esm-only",
85
- "pack:check": "npm pack --dry-run",
87
+ "pack:check": "npm run build && npm pack --dry-run && node scripts/verify-pack.mjs",
86
88
  "prepublishOnly": "npm run build && npm run publint && npm run attw"
87
89
  },
88
90
  "dependencies": {
@@ -105,6 +107,7 @@
105
107
  "zod": "^3.24.0"
106
108
  },
107
109
  "devDependencies": {
110
+ "next": "^16.1.6",
108
111
  "@arethetypeswrong/cli": "^0.18.2",
109
112
  "@biomejs/biome": "^1.9.4",
110
113
  "@types/better-sqlite3": "^7.6.13",