Skip to content

Extending the bot

Three common extension paths: add a new DEX, add a new chain, add a new trigger type. Follow the checklists exactly — each touch-point exists to enforce an invariant.

Last updated: 2026-05-13


1. Adding a new DEX

flowchart LR
    A[1. ABIs + Addresses<br/>abis/index.js] --> B[2. Executor<br/>src/executors/&lt;dex&gt;.js]
    B --> C[3. Dispatch case<br/>src/executors/index.js]
    C --> D[4. Monitor branch<br/>src/monitors/priceMonitor.js]
    D --> E[5. dex enum entry<br/>src/config.js PositionSchema]
    E --> F[6. Docs example<br/>docs/usage.md POSITIONS_JSON]
    F --> G[7. Tests<br/>tests/executor&lt;Dex&gt;.test.js]

Step-by-step

  1. ABIs. Add a minimal human-readable ABI JSON to 📁 abis/. Re-export from 📁 abis/index.js. Add deployed addresses to the ADDRESSES map keyed by chain → dex → role:

    ADDRESSES.ethereum['my-dex'] = {
      router: '0x...',
      factory: '0x...',
    };
    

  2. Executor. Create 📁 src/executors/myDex.js exporting:

    export async function executeMyDexRemoval({ position, signer }) {
      _assertNotBlacklisted([position.token0, position.token1]);   // INVARIANT
      if (await gasAboveCap(signer.provider)) return abort();      // INVARIANT
      const amountMin = applySlippage(expected, config.MAX_SLIPPAGE_BPS);
      if (config.DRY_RUN) return logIntent({  });                 // INVARIANT
      const tx = await router.removeLiquidity(/* … */);
      const receipt = await tx.wait();
      return decodeReceived(receipt);
    }
    

  3. All bigint math. No Number mixing.
  4. On revert, throw — the dispatcher's pRetry handles up to 2 retries.

  5. Dispatch. In 📁 src/executors/index.js add a case:

    case 'my-dex': return executeMyDexRemoval(ctx);
    
    The wrapper around dispatch already provides pRetry(2) and txLimiter.acquire('remove') — do not add a second rate-limit call inside executeMyDexRemoval.

  6. Monitor branch. In 📁 src/monitors/priceMonitor.js add _watchMyDex(position) that subscribes to the pool's swap event (or equivalent), decodes price, emits priceUpdate. Mirror the pattern in _watchUniswapV2 / _watchUniswapV3.

  7. Config enum. Add the literal string to dex in PositionSchema (📁 src/config.js). Without this, positions referencing the new DEX are rejected at startup.

  8. Docs. Add an example entry to the POSITIONS_JSON section of docs/usage.md. Update docs/architecture.md module map and the Executor specifics section if the new DEX has a notably different flow.

  9. Tests. Add tests/executorMyDex.test.js covering: blacklist check, dry-run stub, slippage application, gas cap abort, log-decode of received amounts.


2. Adding a new EVM chain

flowchart LR
    A[1. CHAINS map<br/>src/chains/evm.js] --> B[2. zod schema<br/>src/config.js]
    B --> C[3. ADDRESSES<br/>abis/index.js]
    C --> D[4. Explorer URL<br/>src/alerts/index.js]
    D --> E[5. chain enum<br/>PositionSchema]
    E --> F[6. Docs]

Step-by-step

  1. Chain map. In 📁 src/chains/evm.js, add to CHAINS:

    arbitrum: {
      chainId: 42161,
      rpcHttpEnv: 'ARBITRUM_RPC_HTTP',
      rpcWsEnv: 'ARBITRUM_RPC_WS',
    },
    

  2. Env schema. Add ARBITRUM_RPC_HTTP and ARBITRUM_RPC_WS to the zod schema in 📁 src/config.js (both optional strings — makeWsProvider only loads chains referenced by configured positions).

  3. Addresses. Add ADDRESSES.arbitrum = { 'uniswap-v3': { positionManager: '0x…', /* … */ } } etc. for every DEX you'll use on the new chain.

  4. Explorer URL. Add a case in 📁 src/alerts/index.js::explorerUrl so Telegram/Discord alerts link correctly. Example: case 'arbitrum': return \https://arbiscan.io/tx/\${hash}`;`

  5. Position schema. Add the chain string to the chain enum in PositionSchema. Without this, positions on the new chain are rejected.

  6. Docs. Update Usage → Environment variables with the new RPC envs.

💡 The WebSocket reconnect logic in makeWsProvider is chain-agnostic. You do not need to write reconnect code per chain.


3. Adding a new trigger type

Less common, but the pattern:

  1. Predicate. Add the evaluation in 📁 src/triggers/triggerEngine.js::evaluate. Mirror existing predicate style — return a boolean. Read inputs from the position state or from a monitor-supplied event payload.

  2. Config. If the trigger needs new env vars, add them to the zod schema (📁 src/config.js). Always provide a default that disables the trigger (e.g., 0 for thresholds, empty string for addresses).

  3. Monitor wiring (if applicable). If the new trigger depends on a new event source, add a subscription in priceMonitor.js or eventMonitor.js that emits the relevant event into the engine.

  4. Logger redaction. If the new env var contains a secret-ish value, add it to the redaction list.

  5. Tests. Add cases to tests/triggerEngine.test.js covering: predicate true → fires, predicate false → silent, firing guard still applies, completed terminal still applies.

  6. Docs. Add a row to the trigger table in docs/architecture.md and the env table in docs/usage.md.


4. House style

  • ESM, no TypeScript. Imports use .js suffix even for sibling files. Types as JSDoc comments where useful.
  • All BigInt math for token amounts. Do not introduce Number arithmetic in executors.
  • Structured logging. logger.info({ tx, block, pair }, 'submitted') — object first, message second.
  • No process.env reads outside src/config.js. The zod schema is the only legal entry point.
  • Comments only where the why is non-obvious. No "this calls foo" comments; the code already says that. Comments for invariants, workarounds, and surprising constraints only.
  • No backwards-compat shims for internal API changes. If you change a function's signature, update every caller in the same commit.

5. PR / commit etiquette

  • One concern per commit. Adding a DEX, adding a chain, and adding a trigger are three commits.
  • Tests in the same commit as the code they cover. Reviewers should never see "tests TODO" as a separate commit.
  • Update docs in the same commit. If you touched architecture, update docs/architecture.md and bump the "Last updated" header. If you added env vars, update .env.example AND docs/usage.md.
  • CHANGELOG entry. Add a one-line bullet to docs/CHANGELOG.md under the appropriate dated section.

6. Anti-patterns (do not do these)

  • ❌ Reading process.env.FOO outside src/config.js.
  • ❌ Calling txLimiter.acquire inside an executor (the dispatcher already does it — you'll double-acquire and deadlock).
  • ❌ Skipping the gas cap check "because the position is small" — the cap is a system invariant, not a per-position policy.
  • ❌ Logging a Wallet or Keypair object.
  • ❌ Adding Number arithmetic to token amounts.
  • ❌ Catching errors in executors silently — the engine relies on throws to call markFailed.
  • ❌ Adding a setInterval price poll. Everything is event-driven by design.
  • ❌ Adding hypothetical-future flexibility. Three similar lines beats a premature abstraction; we'll refactor when there's a third real call site.