DeFi Bots Series — Part 2: Orchestrator, LP-Copy Warm-Up & Safe Monitor (Dry-Run)

DeFi Bots Series — Part 2: Orchestrator, LP-Copy Warm-Up & Safe Monitor (Dry-Run)

9/28/20253 min • defi
SolanaMeteoraDLMMBotsSchedulerOrchestratorSupabaseTelegramTypeScript

TL;DR — Added a modular bot orchestrator, a primed LP-copy scanner that only prints new pools, and a monitor strategy that runs dry-run (no txs) while sending Telegram updates. All actions use direct imports of our Meteora executors (close/rebalance), not gRPC.

What landed since Part 1

  • Tx-scanner source for LP-copy (TxScannerPositionsSource)
  • LpCopyStrategy: warm-up seeding, de-dupe, per (follower, leader) state
  • Orchestrator + Scheduler: multiple strategies per tick
  • MonitorStrategy (dry-run): TP/SL, out-of-range ➜ rebalance, Telegram notify
  • Config-first (configs/monitor.json), user overrides, cooldowns
  • Supabase reads: positions table as source of truth
  • Direct-import actions wired (no gRPC path): closePositionAndRecord, rebalanceSinglePosition
  • 🛠️ Minor TS fix in monitor override picker (see snippet below)

Micro-architecture

Orchestrator
├─ LpCopyStrategy (scanner-backed, warm-up, print new pools)
└─ MonitorStrategy (policy -> {HOLD|TP|SL|REBALANCE}, dry-run notify)

└─ imports -> executeTx.{closePositionAndRecord, rebalanceSinglePosition}
+ Supabase (positions/flow) + Telegram

Configs

configs/monitor.json (example):

{
  "enabled": true,
  "dryRun": true,
  "defaultTakeProfitPct": 12,
  "defaultStopLossPct": 6,
  "defaultPlacement": 1,
  "cooldownMin": 20,
  "onlyUserIds": [],
  "overrides": [
    { "userId": 12345, "takeProfitPct": 10, "stopLossPct": 5, "placement": 0, "cooldownMin": 15 }
  ]
}

configs/lp_copy.json:

{
  "intervalSec": 12,
  "followers": [
    {
      "followerUserId": 12345,
      "slippageBps": 800,
      "leaders": [
        { "wallet": "7KHx2Uc5qsqz652eXbu8Qtabi5KLxWJLgxFzcaBzP32i", "allowPools": "*", "scaleBps": 10000 }
      ]
    }
  ]
}

Orchestrator wiring

const orch = new Orchestrator();
const lp   = new LpCopyStrategy(loadLpCopyCfg());
await (lp as any).prime?.();             // warm-up, avoid first-tick spam
orch.register(lp);
 
const mon  = new MonitorStrategy(loadMonitorCfg()); // dryRun=true by default
orch.register(mon);
 
new Scheduler(() => orch.tick(), (lpCfg.intervalSec ?? 12) * 1000).start();

LP-copy warm-up behavior

  • First boot: seed “seen pools” for each (follower, leader), no print.

  • Ticks: only 🆕 new pools print once:

    [lp-copy] warm-up follower=12345 leader=... pools=4
    🆕 [lp-copy] NEW POOL leader=... follower=12345 pool=... pair=?/? pos=... tx=...
    [lp-copy] no new pools for leader=... follower=12345

Monitor strategy (dry-run)

  • Reads positions from Supabase.

  • Computes PnL + range status (via Meteora + Birdeye).

  • Decision:

    • >= TP → TAKE_PROFIT
    • <= -SL → STOP_LOSS
    • Out of range → REBALANCE
    • Else → HOLD
  • With "dryRun": true, it only notifies on Telegram (no tx).

Telegram message shape:

🤖 *Monitor* (dry-run)
pool: `9kXy…Lg2F`  pos: `Dt8o…ZcEw`
uPnL $12.34 | total 5.67% ($34.56)
range: [1070..1086] active=1088 🔴 out
action: *REBALANCE*
Would REBALANCE with placement=SYMMETRIC

TS nit fix

Map config keys → override fields (to satisfy the type system):

// inside MonitorStrategy
private pick<K extends keyof MonitorCfg>(key: K, userId: number): MonitorCfg[K] {
  const o = this.cfg.overrides?.find(v => v.userId === Number(userId));
  const fromOverride =
    key === "defaultTakeProfitPct" ? (o?.takeProfitPct as any) :
    key === "defaultStopLossPct"  ? (o?.stopLossPct  as any) :
    key === "defaultPlacement"    ? (o?.placement   as any) :
    key === "cooldownMin"         ? (o?.cooldownMin as any) :
    undefined;
  return (fromOverride ?? (this.cfg as any)[key]) as MonitorCfg[K];
}

How I’m testing it (now)

  1. Infra up (no scheduler): Telegram receiver + Supabase + creds.
  2. Open 1 position manually (gRPC); verify in user_lp_positions.
  3. Run bot with dryRun=true; inspect logs + Telegram notifications.
  4. Force paths: tweak thresholds (tiny TP/SL) or ranges to trigger each action (still dry-run).
  5. Then flip "dryRun": false for one test user to validate txs + DB writes.

Notes & guardrails

  • All state (monitor cooldowns, lp-copy seen pools) is in-memory; for prod, persist “seen” and “last action” in Supabase to survive restarts.
  • Scanner uses Anchor discriminators + inner createAccount; avoids bin-array traps at scan time.
  • LP-copy execution path (funding swaps, ATAs, APR/TVL filters) is intentionally deferred.

Up next (Part 3)

We’ll revive a simplified Telegram bot surface, connect Supabase thoroughly, verify Privy wallet + SOL balance, and run the Monitor with actions on for a controlled position. Then layer in APR/TVL filters and Jupiter pre-flight for LP-copy execution.

Stay Updated

Get notified when I publish new articles about Web3 development, hackathon experiences, and cryptography insights.

You might also like

DeFi Bots Series — Part 3: Telegram Bot Lite, Portfolio RPC, and a Lean Path to the Scheduler

I stripped our Telegram surface down to a fast, durable “Lite” mode: no Kafka, no AI agent in the middle—just clean wallet UX, on-chain balances via RPC, token prices from Jupiter, PnL wired to Supabase, and buttons that actually do something. This sets the table for the trading scheduler.

DeFi Bots Series — Part 5: Live Rebalance on Meteora DLMM (RPC Profiles, Clean PnL & One-Sided Liquidity)

I rewired RPC handling with role-based profiles, unified LP strategy controls, fixed PnL accounting, and executed a live one-sided rebalance on a PUMP/USDC DLMM pool over gRPC—end to end with Supabase ledgering.

DeFi Bots Series — Part 4: Prepping the Monitor — Decimals, Prices, Symbols & Clean Balances

Before we let the monitor act on positions, we hardened the boring bits: one source of truth for decimals, fast prices, safe BigInt math, and clean balance reads. The goal is simple: trustworthy PnL so alerts and actions are correct.