Skip to content

Usage

How to run, configure, and operate the bot day-to-day.

Last updated: 2026-08-31


npm scripts

Command Purpose When to use
npm install Install dependencies Once after clone, or when package.json changes
npm run encrypt-key Encrypt an existing raw private key into ./keystore.json Once during setup
npm run encrypt-key -- --generate Create a new wallet and write it straight to ./keystore.json — the key never exists in plaintext, only the address is printed Setting up a dedicated bot wallet
npm run launch-token Deploy a fixed-supply ERC-20 and seed a V2 pool with it, then print the POSITIONS_JSON entry Once, only when you need to create the position the bot will watch. Honors DRY_RUN
npm run build:token Recompile contracts/LaunchToken.sol into abis/LaunchToken.json Only after editing the contract; pass -- --check to verify without writing
npm run dryrun Run with DRY_RUN=true — no on-chain tx, full simulation Every time before going live; ≥24h continuous before flipping production
npm start Production run; honors NODE_ENV and DRY_RUN from .env When validation is complete
npm run dev node --watch — restarts on file changes Local code iteration only
npm test Run vitest in watch mode While developing tests
npm run test:run Run vitest once and exit CI, or to verify before commit
npm run lint ESLint on src/ Before commit
npm run audit npm audit excluding dev deps Periodically; before any release
npm run audit:evm-only Audit excluding optional Solana deps EVM-only deployments
npm run security:scan Mechanical security/privacy invariant checks Before commit; also runs on every PR
npm run verify lint + security:scan + full test suite + audit The pre-push gate — same checks CI runs

Docker

docker compose build
docker compose up -d
docker compose logs -f

The compose file mounts .env and keystore.json from the host. Health is just process liveness — there is no HTTP endpoint. To stop:

docker compose down

⚠️ Single process per signer. Do not run docker compose up -d and npm start against the same wallet — the reentrancy guard is in-memory and you will hit nonce collisions / double-spends.

The bot enforces this on a single host: it claims data/bot.lock at boot and refuses to start if another live process already holds it, naming that pid. npm run launch-token refuses for the same reason. If the other process genuinely signs with a different wallet, set ALLOW_CONCURRENT_BOT=true (the bot's equivalent of the launcher's --force-concurrent); a forced start deliberately leaves the existing lock in place rather than claiming it. The lock does not span containers or machines — docker-compose.yml mounts no ./data volume, so a containerised run cannot see a host lock.


Environment variables

Grouped by purpose. All variables are validated by zod in 📁 src/config.js; the bot refuses to start on schema violation.

Global

Var Default Description
LOG_LEVEL info trace/debug/info/warn/error
DRY_RUN true If true, executors log intent without broadcasting
NODE_ENV production If production, raw EVM_PRIVATE_KEY is refused
ALLOW_CONCURRENT_BOT false Start even though another live process holds data/bot.lock. Only safe if that process signs with a different wallet — see the warning under Docker

Key management

See Getting Started → Set up your signer. Priority: Ledger > keystore > raw key.

Var Description
USE_LEDGER Enable Ledger signer
LEDGER_DERIVATION_PATH BIP44 path; 44'/60'/0'/0/0 = first account
KEYSTORE_PATH Path to encrypted JSON keystore
KEYSTORE_PASSWORD Optional; if blank, prompts at startup
EVM_PRIVATE_KEY Raw key (dev only — blocked in production)
SOLANA_PRIVATE_KEY Base58 secret key

RPC

Var Description
ETH_RPC_HTTP / ETH_RPC_WS Ethereum mainnet
BASE_RPC_HTTP / BASE_RPC_WS Base mainnet
SOLANA_RPC_HTTP / SOLANA_RPC_WS Solana mainnet-beta

WebSocket URLs are required for any chain you watch — the bot subscribes to events, it does not poll.

Triggers

Var Default Description
TRIGGER_PRICE_DROP_PCT 15 Remove if price drops ≥N% from entry
TRIGGER_PRICE_RISE_PCT 50 Remove if price rises ≥N% (take profit)
TRIGGER_MAX_AGE_SEC 0 Auto-remove after N seconds (0 = disabled)
TRIGGER_VOLUME_SPIKE_MULT 10 Fire if recent vol > N × rolling avg
TRIGGER_VOLUME_WINDOW_BLOCKS 50 Window size for volume average
TRIGGER_CUSTOM_EVENT_ADDRESS — Contract to watch for custom log
TRIGGER_CUSTOM_EVENT_TOPIC — Topic0 of the event
EMERGENCY_WALLET — Dead-man's switch — see Safety

Execution safety

Var Default Description
MAX_SLIPPAGE_BPS 100 100 = 1%. Applied to amountMin calculations
MAX_GAS_GWEI 150 Hard cap; tx aborts before broadcast if (base+priority) exceeds. One global value across all chains — 150 is Ethereum-scale and never trips on Base (base fee ~0.005 gwei), so it is not a brake there. Base: 5. See Safety
GAS_PRIORITY_GWEI 2 Priority-fee floor, not a fixed tip — the chain's own suggestion wins when higher. Base: 0.001
TX_DEADLINE_SEC 600 V2 deadline param
TOKEN_BLACKLIST — Comma-separated, lowercase. Enforced per-executor
AUTO_REVOKE_APPROVALS true After removal, allowance → 0

Rate limiting

Var Default Description
MAX_TX_PER_MINUTE 3 Token-bucket cap
MAX_TX_PER_HOUR 20 Same

Token launch (npm run launch-token)

Read only by the launch script — the bot never touches these, and leaving them blank has no effect on npm start. They are validated at launch time by buildLaunchPlan() in 📁 src/launch/tokenLauncher.js, not by the zod schema, precisely so that an unset launch variable can never block a normal boot.

Var Default Description
LAUNCH_CHAIN — ethereum, base or bsc
LAUNCH_DEX — uniswap-v2 or pancakeswap-v2. V3 has no single-call add-liquidity path
LAUNCH_TOKEN_NAME — ERC-20 name
LAUNCH_TOKEN_SYMBOL — ERC-20 symbol; also used as the position label
LAUNCH_TOKEN_DECIMALS 18 Integer 0–18
LAUNCH_TOKEN_SUPPLY — Total supply in whole tokens. A string: coercing a large supply through a JS number loses precision
LAUNCH_LIQUIDITY_TOKEN_AMOUNT — How much of the supply goes into the pool. Must not exceed the supply
LAUNCH_LIQUIDITY_ETH_AMOUNT — Native coin for the other side. With the line above, this sets the opening price
LAUNCH_OWNER signer Must equal the signing wallet, or be blank. The supply is minted to the owner and the signer seeds the pool, so a mismatch would deploy the token and then revert on addLiquidityETH — it is rejected at plan time instead. Use it to assert which wallet you expect; transferOwnership() afterwards to move pause rights
LAUNCH_RENOUNCE_OWNERSHIP false Renounce in the same run, permanently dropping pause/unpause

MAX_SLIPPAGE_BPS, MAX_GAS_GWEI, TX_DEADLINE_SEC, TOKEN_BLACKLIST and the rate limiter all apply to a launch exactly as they do to a removal.

Alerts

Var Description
TELEGRAM_BOT_TOKEN From @BotFather
TELEGRAM_CHAT_ID Numeric only — no leading colon
DISCORD_WEBHOOK_URL Server → channel webhook
ALERT_ON_TRIGGER Send when a trigger fires
ALERT_ON_SUCCESS Send when tx confirms
ALERT_ON_ERROR Send on failure

Positions config

POSITIONS_JSON is a JSON array. Each element describes one LP position to watch.

V2 (Uniswap V2, PancakeSwap V2, SushiSwap, …)

{
  "chain": "ethereum",
  "dex": "uniswap-v2",
  "type": "v2",
  "pair": "WETH/USDC",
  "address": "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc",
  "entryPrice": 1850.0,
  "triggers": { "priceDropPct": 10, "priceRisePct": 40 }
}
  • address = LP pair contract address.
  • entryPrice (optional) = override the auto-detected entry; used by percent triggers.
  • triggers (optional) = per-position overrides of the global trigger env vars.

V3 (Uniswap V3, PancakeSwap V3)

{
  "chain": "ethereum",
  "dex": "uniswap-v3",
  "type": "v3",
  "pair": "WETH/USDC",
  "tokenId": 123456,
  "poolAddress": "0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640",
  "feeTier": 500
}
  • tokenId = your NFT ID from the NonfungiblePositionManager.
  • poolAddress is required — the price feed listens on the pool, not the NFT.

Raydium (Solana)

{
  "chain": "solana",
  "dex": "raydium",
  "type": "amm-v4",
  "pair": "SOL/USDC",
  "address": "<pool-id>",
  "lpMint": "<lp-mint>"
}
  • type is amm-v4, cpmm, or clmm. The executor dispatches accordingly.

Telegram bot commands

If TELEGRAM_BOT_TOKEN is set, the bot listens for the following commands from the configured chat:

Command Effect
/status Reply with current monitored positions, prices, last trigger evaluation
/remove Manually trigger removal for all positions
/remove <label> Trigger removal for a single position whose pair matches <label>

⚠️ /remove is irreversible. There is no confirmation prompt — by design. If you don't want manual remote triggers, leave TELEGRAM_BOT_TOKEN blank.


Logs

Structured JSON via pino. Pipe through pino-pretty for human reading:

npm start | npx pino-pretty

The redaction list in 📁 src/logger.js masks: EVM_PRIVATE_KEY, SOLANA_PRIVATE_KEY, KEYSTORE_PASSWORD, TELEGRAM_BOT_TOKEN, DISCORD_WEBHOOK_URL, signer objects. If you add new sensitive env vars, update the redaction list. See Safety.