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/<dex>.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<Dex>.test.js]
Step-by-step¶
-
ABIs. Add a minimal human-readable ABI JSON to
📁 abis/. Re-export from📁 abis/index.js. Add deployed addresses to theADDRESSESmap keyed by chain → dex → role: -
Executor. Create
📁 src/executors/myDex.jsexporting: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); } - All bigint math. No
Numbermixing. -
On revert, throw — the dispatcher's
pRetryhandles up to 2 retries. -
Dispatch. In
The wrapper around📁 src/executors/index.jsadd a case:dispatchalready providespRetry(2)andtxLimiter.acquire('remove')— do not add a second rate-limit call insideexecuteMyDexRemoval. -
Monitor branch. In
📁 src/monitors/priceMonitor.jsadd_watchMyDex(position)that subscribes to the pool's swap event (or equivalent), decodes price, emitspriceUpdate. Mirror the pattern in_watchUniswapV2/_watchUniswapV3. -
Config enum. Add the literal string to
dexinPositionSchema(📁 src/config.js). Without this, positions referencing the new DEX are rejected at startup. -
Docs. Add an example entry to the
POSITIONS_JSONsection ofdocs/usage.md. Updatedocs/architecture.mdmodule map and the Executor specifics section if the new DEX has a notably different flow. -
Tests. Add
tests/executorMyDex.test.jscovering: 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¶
-
Chain map. In
📁 src/chains/evm.js, add toCHAINS: -
Env schema. Add
ARBITRUM_RPC_HTTPandARBITRUM_RPC_WSto the zod schema in📁 src/config.js(both optional strings —makeWsProvideronly loads chains referenced by configured positions). -
Addresses. Add
ADDRESSES.arbitrum = { 'uniswap-v3': { positionManager: '0x…', /* … */ } }etc. for every DEX you'll use on the new chain. -
Explorer URL. Add a case in
📁 src/alerts/index.js::explorerUrlso Telegram/Discord alerts link correctly. Example:case 'arbitrum': return \https://arbiscan.io/tx/\${hash}`;` -
Position schema. Add the chain string to the
chainenum inPositionSchema. Without this, positions on the new chain are rejected. -
Docs. Update Usage → Environment variables with the new RPC envs.
💡 The WebSocket reconnect logic in
makeWsProvideris chain-agnostic. You do not need to write reconnect code per chain.
3. Adding a new trigger type¶
Less common, but the pattern:
-
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. -
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.,0for thresholds, empty string for addresses). -
Monitor wiring (if applicable). If the new trigger depends on a new event source, add a subscription in
priceMonitor.jsoreventMonitor.jsthat emits the relevant event into the engine. -
Logger redaction. If the new env var contains a secret-ish value, add it to the redaction list.
-
Tests. Add cases to
tests/triggerEngine.test.jscovering: predicate true → fires, predicate false → silent,firingguard still applies,completedterminal still applies. -
Docs. Add a row to the trigger table in
docs/architecture.mdand the env table indocs/usage.md.
4. House style¶
- ESM, no TypeScript. Imports use
.jssuffix even for sibling files. Types as JSDoc comments where useful. - All BigInt math for token amounts. Do not introduce
Numberarithmetic in executors. - Structured logging.
logger.info({ tx, block, pair }, 'submitted')— object first, message second. - No
process.envreads outsidesrc/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.mdand bump the "Last updated" header. If you added env vars, update.env.exampleANDdocs/usage.md. - CHANGELOG entry. Add a one-line bullet to
docs/CHANGELOG.mdunder the appropriate dated section.
6. Anti-patterns (do not do these)¶
- ❌ Reading
process.env.FOOoutsidesrc/config.js. - ❌ Calling
txLimiter.acquireinside 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
WalletorKeypairobject. - ❌ Adding
Numberarithmetic to token amounts. - ❌ Catching errors in executors silently — the engine relies on throws to call
markFailed. - ❌ Adding a
setIntervalprice 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.