Skip to content

Architecture

System design, module responsibilities, data flow, and the invariants you must not break.

Last updated: 2026-05-13


1. High-level data flow

flowchart LR
    subgraph Onchain[On-chain]
        UniV2[Uniswap V2 Pairs]
        UniV3[Uniswap V3 Pools + NFTs]
        Ray[Raydium AMM/CPMM/CLMM]
        EmgWallet[(Emergency Wallet)]
    end

    subgraph Bot[TradingBot Process]
        direction TB
        Monitors[Monitors<br/>priceMonitor + eventMonitor]
        TE[Trigger Engine]
        Exec[Executor Dispatch]
        Alerter[Alerter]

        Monitors -- normalized events --> TE
        TE -- 'fire(position)' --> Exec
        Exec -- removeLiquidity tx --> SignerLayer
        Exec -- success/failure --> TE
        TE -- 'alert(...)' --> Alerter
        Exec -- 'alert(...)' --> Alerter
    end

    subgraph SignerLayer[Signer Stack]
        Ledger[Ledger Device]
        Keystore[Encrypted Keystore]
        RawKey[Raw Key dev only]
    end

    UniV2 -. swap events .-> Monitors
    UniV3 -. swap events .-> Monitors
    Ray -. account updates .-> Monitors
    EmgWallet -. tx inclusion .-> Monitors

    SignerLayer -- signed tx --> Broadcast[(RPC Provider)]
    Broadcast --> UniV2
    Broadcast --> UniV3
    Broadcast --> Ray

    Alerter --> Telegram[(Telegram)]
    Alerter --> Discord[(Discord)]
    Telegram -- '/remove, /status' --> TE

Principles: - Push, not poll. Every input is a WebSocket subscription. No setInterval price loops. - Single-writer concurrency. One Node process per signer. The reentrancy guard is in-memory; running two instances against the same wallet causes nonce collisions and double-spends. - Pre-flight everything. Gas cap check, slippage calc, blacklist check, NFT-ownership check, and (for V2) a honeypot staticCall simulation all run before the broadcast.


2. Module map

flowchart TD
    Index[src/index.js<br/>entry & glue] --> Cfg[src/config.js<br/>zod env loader]
    Index --> Log[src/logger.js<br/>pino + redaction]
    Index --> Chains[src/chains/]
    Index --> Monitors[src/monitors/]
    Index --> TE[src/triggers/triggerEngine.js]
    Index --> ExecIdx[src/executors/index.js<br/>dispatch + retry]
    Index --> Alerts[src/alerts/]
    Index --> Sec[src/security/]

    Chains --> Evm[chains/evm.js<br/>makeWsProvider + chain map]
    Chains --> Sol[chains/solana.js<br/>connection]

    Monitors --> PriceMon[priceMonitor.js<br/>V2/V3/Ray price feeds]
    Monitors --> EvtMon[eventMonitor.js<br/>custom logs + emergency wallet]

    ExecIdx --> ExecV2[executors/uniswapV2.js]
    ExecIdx --> ExecV3[executors/uniswapV3.js]
    ExecIdx --> ExecRay[executors/raydium.js]
    ExecIdx --> Appr[executors/approvalManager.js]

    Sec --> Keystore[security/keystore.js<br/>signer priority chain]
    Sec --> RateLim[security/rateLimit.js<br/>token bucket]

    Alerts --> Tg[alerts/telegram.js]
    Alerts --> Dc[alerts/discord.js]
    Alerts --> AlertIdx[alerts/index.js<br/>fan-out + explorer URLs]

    Utils[src/utils/] --> Gas[utils/gas.js<br/>EIP-1559 + cap]
    Utils --> Slip[utils/slippage.js<br/>bigint applySlippage]
    Utils --> Pnl[utils/pnl.js<br/>USD value calc]

    ExecV2 --> Gas
    ExecV3 --> Gas
    ExecV2 --> Slip
    ExecV3 --> Slip
    ExecRay --> Slip
    ExecV2 --> Appr
    ExecIdx --> RateLim
Layer Files Responsibility
Entry src/index.js Wire everything; subscribe monitors to engine; subscribe engine to executors
Config src/config.js Single source of truth for env; zod validation; refuses raw key in prod
Chains src/chains/{evm,solana}.js RPC providers; WebSocket reconnect logic
Monitors src/monitors/* Subscribe and emit normalized events
Triggers src/triggers/triggerEngine.js Evaluate trigger predicates; firing reentrancy guard; completed terminal set
Executors src/executors/* Per-DEX removal flow; rate-limit + retry wrapper in index.js
Alerts src/alerts/* Telegram + Discord fan-out; Telegram command listener
Security src/security/* Keystore loader; rate limiter
Utils src/utils/* Gas builder, slippage math, PnL
ABIs abis/* Minimal human-readable ABIs + ADDRESSES map (chain → dex → role)

3. Lifecycle of one removal

sequenceDiagram
    autonumber
    participant Pool as DEX Pool
    participant Mon as priceMonitor
    participant TE as TriggerEngine
    participant Exec as Executor Dispatch
    participant Impl as V2/V3/Ray Impl
    participant Rate as RateLimiter
    participant Signer
    participant RPC
    participant Alert

    Pool->>Mon: Swap event (or account update)
    Mon->>Mon: decode price (sqrtPriceX96 / reserves / AmmRpcData)
    Mon->>TE: priceUpdate(position, price, deltaPct)
    TE->>TE: evaluate triggers
    alt trigger matches AND not already firing AND not completed
        TE->>TE: firing.add(position)
        TE->>Exec: fire(position, signer)
        Exec->>Rate: acquire('remove')
        Rate-->>Exec: ok
        Exec->>Impl: executeXxxRemoval({position, signer})
        Impl->>Impl: blacklist check
        Impl->>Impl: gas cap check
        Impl->>Impl: slippage calc (staticCall for V3)
        Impl->>Signer: sign tx
        Signer->>RPC: broadcast
        RPC-->>Impl: receipt
        Impl->>Alert: success
        Impl-->>Exec: { tx, amounts }
        Exec->>TE: markCompleted(position)
        TE->>TE: completed.add(position) ; firing.delete(position)
    else trigger no match
        TE-->>Mon: noop
    end

Failure path: if Impl throws, pRetry retries up to 2× with exponential backoff. Final failure → Exec calls TE.markFailed(position) which clears firing but does not add to completed — so the next trigger event will retry. This is deliberate: a single failure should not permanently disable a position.


4. Trigger engine state machine

stateDiagram-v2
    [*] --> Idle: position registered
    Idle --> Firing: trigger matches
    Firing --> Idle: markFailed (retryable failure)
    Firing --> Completed: markCompleted (tx confirmed)
    Completed --> [*]
    Idle --> Idle: trigger no match / already completed / already firing

    note right of Firing
        Reentrancy guard:
        all further triggers for this
        position are dropped while
        in this state
    end note

    note right of Completed
        Terminal. Position never
        fires again in this process.
    end note

Trigger types evaluated:

Trigger Source Predicate
priceDropPct priceMonitor (entry - current) / entry * 100 >= threshold
priceRisePct priceMonitor (current - entry) / entry * 100 >= threshold
maxAge internal timer now - entryTimestamp >= TRIGGER_MAX_AGE_SEC
volumeSpike priceMonitor rolling buffer (cap 500 entries per position) recentVol > rollingAvg * TRIGGER_VOLUME_SPIKE_MULT
customEvent eventMonitor log filter topic0 matches TRIGGER_CUSTOM_EVENT_TOPIC
emergency eventMonitor block.prefetchedTransactions[*].from == EMERGENCY_WALLET (confirmed inclusion only — not mempool)
manual Telegram /remove Bypasses all other conditions

5. Executor specifics

Uniswap V2 (and PancakeSwap V2)

flowchart LR
    Start --> BL[blacklist check]
    BL --> StaticSim[router.removeLiquidity.staticCall<br/>honeypot pre-flight]
    StaticSim --> Allow[approvalManager.ensureAllowance LP token]
    Allow --> GasCheck[gasAboveCap?]
    GasCheck -- yes --> Abort
    GasCheck -- no --> Tx[router.removeLiquidity]
    Tx --> Decode[parse Transfer logs<br/>compute received amounts]
    Decode --> Revoke{AUTO_REVOKE_<br/>APPROVALS?}
    Revoke -- yes --> Set0[allowance ← 0]
    Revoke -- no --> Done
    Set0 --> Done

Uniswap V3 (and PancakeSwap V3)

flowchart LR
    Start --> BL[blacklist check]
    BL --> Own[pm.ownerOf tokenId == signer]
    Own --> StaticSim[decreaseLiquidity.staticCall<br/>derive amount0/1Min]
    StaticSim --> GasCheck[gasAboveCap?]
    GasCheck -- yes --> Abort
    GasCheck -- no --> MC[multicall<br/>decreaseLiquidity + collect MAX,MAX + burn]
    MC --> Decode[decode Transfer logs]
    Decode --> Done

Atomic single-tx removal. NFT is burned at the end so the position cannot be re-used.

Raydium

Dispatch by position.type: - amm-v4liquidity.removeLiquidity({ ... slippage: bps/10000 }) - cpmm → CPMM withdraw call - clmm → CLMM decrease + collect

Solana lacks an EIP-1559 gas market; the gas cap check is skipped. Slippage is enforced via the SDK's slippage param. The SDK is lazy-loaded so EVM-only deployments don't pay the Solana dep cost at startup.


6. Cross-cutting invariants

These have caused real bugs in the past. Do not violate them.

# Invariant Where enforced Why
1 src/config.js is the only place that reads process.env src/config.js zod schema Centralized validation; impossible to forget a coercion
2 Every executor entry-point calls txLimiter.acquire('remove') src/executors/index.js (dispatch wrapper) Rate limit can't be bypassed by adding a new executor
3 gasAboveCap() checked before broadcast on every EVM tx per executor Hard cap on gas — refuses to broadcast in fee spikes
4 _assertNotBlacklisted([token0, token1]) per executor per executor Blacklist is per-call, not global registration
5 V3: pm.ownerOf(tokenId) verified before any tx executors/uniswapV3.js Prevents wasted gas if NFT was transferred out
6 V3 position config must include poolAddress monitors/priceMonitor.js Price feed listens on pool, not NFT; missing → warn + skip
7 Logger redaction list updated when adding sensitive env vars src/logger.js Never log raw keys / tokens
8 WS reconnect preserved in makeWsProvider src/chains/evm.js ethers v6 does not reconnect natively
9 Single process per signer operational In-memory reentrancy guard; nonce collisions otherwise
10 Telegram TELEGRAM_CHAT_ID is pure numeric, no leading colon env config HTTP 400 on send; see CHANGELOG 2026-05-13

7. Known limitations

  • Raydium price-delta decoder is implemented but uses AmmRpcData pre-computed prices only; manual PoolInfoLayout decoding is not wired. Sufficient for AMM v4; CLMM tick math is delegated to SDK.
  • MEV exposure on Ethereum mainnet is real. Use a private mempool RPC (Flashbots Protect) for any non-trivial position size. See Safety → MEV.
  • Emergency wallet detection has ~12s latency — it checks confirmed block inclusion, not mempool. This is a deliberate trade-off against false positives.
  • No persistent state. Process restart clears the firing and completed sets. A position already removed on-chain will not re-fire (executors check on-chain state), but a position mid-removal at restart is a hazard — prefer graceful shutdown.

8. File-level pointers

Quick map for navigation:

Concern File:Line ref
Entry & glue 📁 src/index.js
Env schema 📁 src/config.js (zod object)
Trigger evaluation 📁 src/triggers/triggerEngine.js
V2 executor 📁 src/executors/uniswapV2.js
V3 executor 📁 src/executors/uniswapV3.js
Raydium executor + dispatch 📁 src/executors/raydium.js
Rate limiter 📁 src/security/rateLimit.js
Keystore priority chain 📁 src/security/keystore.js
Gas cap + EIP-1559 📁 src/utils/gas.js
Slippage (bigint) 📁 src/utils/slippage.js
WS reconnect 📁 src/chains/evm.js (makeWsProvider)
Addresses + ABIs 📁 abis/index.js