Inside the Keeper Fleet: 12 Autonomous Jobs That Run THRYX 24/7
12 min read
A token launchpad with 178 users, 1,650 tokens, and 11,400 trades needs to do a lot of housekeeping that users never see. Fees need to be swept from hooks, distributed to stakers, converted to ETH for gas sponsorship, and used to maintain the NEW/WETH pool's price peg. All of this runs as Node.js cron jobs on the THRYX server, signing transactions with the deployer key and routing them through a private mempool. If the server goes down, trading still works (the Diamond contract is permissionless), but fee collection, staker yield, and pool maintenance pause until it recovers.
The keeper topology
| Keeper | Cadence | What it does | Touches on-chain? |
|---|---|---|---|
| pool-mm-keeper | 90s (+/-30% jitter) | Arbs NEW/WETH pool drift back to peg + collects LP fees every 5 cycles | Yes (treasuryBuyNew/sellNewForEthFor + collectLpFeesBatch) |
| fee-router-keeper | 5 min | Wraps Diamond surplus ETH to WETH, notifies FeeRouter, distributes to 5 buckets | Yes (wrapDiamondEthToWeth + notifyIncomingWeth + distributePending) |
| hook-fee-collector | 30 min | Sweeps protocolClaimable WETH+THRYX from V5 and V6.4 anti-sniper hooks | Yes (sweepHookFees + forwardProtocolFees) |
| paymaster-auto-rescue | 2 min | Emergency Diamond ETH refill via NEW sell + hook sweep if below 0.001 ETH | Yes (sellNewForEthFor + sweepHookFees + reconcilePaymasterEthBalance) |
| solvency-monitor | 60s | Checks Diamond WETH vs staker debt; triggers 4-step recovery cascade, then emails admin | Yes (recovery steps: reconcile, distribute, sweep, sell) |
| paymaster-keeper | 5 min | Main NEW-to-ETH conversion for paymaster balance (binary-search sell sizing) | Yes (sell + unwrap) |
| buyback-burn | 30 min | Buys NEW with pendingBurnWeth, sends to 0xdead | Yes (swap + burn) |
| hook-fee-collector (V6.4) | 30 min | Sweeps sandwich-tax fees from V6.4 hook on NEW/WETH pool | Yes (forwardProtocolFees) |
| eth-rate-keeper | 5 min | Caches ETH/USD price from Chainlink + DexScreener fallback | Yes (Chainlink read) |
| force-sell-watcher | 60s | Monitors autotrader positions for stop-loss triggers | No (DB reads only) |
| streak-payouts | 24h | Credits THRYX streak rewards for consecutive-day traders | No (DB writes only) |
| daily-digest | 24h | Sends trading summary emails to active users | No (email only) |
The dependency chain
Keepers form an implicit dependency chain through the Diamond's shared state. Hook-fee-collector sweeps fees into the Diamond, increasing its WETH balance. Fee-router-keeper reads that WETH balance and distributes it to 5 buckets. The staker share from fee-router-keeper bumps accRewardPerShareWei, which the solvency-monitor reads to compute staker debt. The pool-mm-keeper also routes 50% of LP-fee collections through the FeeRouter inline (not waiting for the fee-router-keeper's next cycle).
The dependency is unidirectional: hook-fee-collector never reads staker state, and solvency-monitor never writes fee-router state. Each keeper can run, fail, or skip independently. The system converges to the correct state over time as long as the keepers eventually run. There is no keeper that blocks another keeper from executing.
Deduplication and leader election
THRYX runs on a single Render instance, so there is no multi-server leader election problem. However, keepers still need deduplication because Node.js timers can drift, and a server restart can cause two intervals to fire in quick succession. Each keeper calls claimJobTick(name, minIntervalMs) at the start of every cycle. This function does an atomic INSERT ... ON CONFLICT UPDATE on a Turso DB row, succeeding only if the last claim was more than minIntervalMs ago. If the claim fails, the keeper skips silently. This prevents double-execution even during server restarts.
The pool-mm-keeper in detail
The pool-mm-keeper is the most complex keeper, doing three things per cycle:
- LP fee collection: every 5th cycle (7.5 minutes), calls collectLpFeesBatch() on 4 Diamond-owned LP NFTs. Routes 50% of collected ETH immediately through FeeRouter so stakers earn from LP fees without waiting for the fee-router-keeper.
- Drift detection: reads the pool's current tick via V4 PoolManager.extsload. Computes drift = tick - PEG_TICK (peg = 234,800). If |drift| > BAND_TICKS (1,500), an arb is needed.
- Drift arb: if tick is above peg (NEW over-priced in the pool), calls treasuryBuyNew() with Diamond's own WETH to push tick down. If tick is below peg (NEW under-priced), calls sellNewForEthFor() to push tick up. Swap sizes are capped at 0.001 ETH or 10M NEW per cycle, with 75-100% random jitter to prevent bot pattern-matching.
The timing jitter deserves special mention. Instead of a fixed setInterval(90000), the keeper uses self-rescheduling setTimeout with a random delay in [63s, 117s] (base 90s * [0.7, 1.3]). Sandwich bots that fingerprint "a swap from 0x7a3E... hits the pool exactly every 90 seconds" now face a 54-second uncertainty window, making pre-positioning unprofitable.
The solvency-monitor recovery cascade
The solvency-monitor is the protocol's safety net. Every 60 seconds, it reads Diamond ETH, WETH, totalStakedNew, accRewardPerShareWei, and paymasterEthBalance. It computes staker debt (totalStaked * accRPS / 1e18) and surplus (Diamond WETH - staker debt + insurance fund). If surplus goes negative for 2 consecutive ticks, the monitor launches a 4-step automated recovery before emailing the admin:
- Reconcile: calls reconcilePaymasterEthBalance() to sync stored paymaster balance with actual Diamond ETH.
- Distribute: calls distributePending() to process any queued FeeRouter pending amounts (in case a prior distribution was blocked by the staker-debt guard).
- Sweep: calls sweepHookFees() to pull accumulated WETH from anti-sniper hooks.
- Emergency sell: calls sellNewForEthFor() with a small NEW amount to inject ETH directly.
One step fires per tick (60s spacing between attempts), giving on-chain state time to settle. If all four steps fail to restore solvency, the monitor sends an email to the admin with the current deficit amount, triggering manual investigation. The email has a 6-hour cooldown and only re-fires if the deficit has grown by at least 10% since the last alert.
Staker-debt guards (shared invariant)
Three keepers share the same invariant check: pool-mm-keeper, fee-router-keeper, and paymaster-auto-rescue. Before executing any operation that spends Diamond WETH or increases staker debt, they read getStakingSummary() and compute totalStaked * accRPS / 1e18. If Diamond WETH is at or below that amount, the operation is skipped. This prevents a cascade where one keeper's action makes staker claims insolvent, which was the root cause of the 2026-05-24 incident.
Kill switches and environment overrides
Every keeper has an environment variable kill switch:
| Keeper | Kill switch env var | Default |
|---|---|---|
| pool-mm-keeper | POOL_MM_LIVE | true |
| fee-router-keeper | FEE_ROUTER_LIVE | true |
| paymaster-auto-rescue | PAYMASTER_AUTO_RESCUE_ENABLED | true |
| pool-mm-keeper arb leg | POOL_MM_ARB_ENABLED | auto (enabled if private RPC set) |
Setting any of these to "false" in the Render environment variables immediately disables the keeper on the next tick. No code deploy required. The admin API at /api/admin/manage/jobs lists all registered keepers with their last tick timestamp, success/failure counts, and skip reasons.
Observability
Every keeper registers itself via registerJob(name, { intervalMs, description }). Each tick records one of three outcomes: recordSuccess(details), recordSkip(reason), or recordFailure(error). The admin endpoint GET /api/admin/manage/jobs returns the full keeper topology with per-keeper state: last tick time, consecutive failures, total ticks, and the most recent report object. This is the primary debugging surface when something goes wrong with protocol automation.
Frequently asked questions
Frequently asked
- What happens if the THRYX server goes down?
- Trading continues because the Diamond contract is permissionless. Users can still buy, sell, launch, and claim through the relay. What pauses: fee distribution to stakers, pool arb maintenance, hook fee sweeps, and paymaster refills. The paymaster has enough runway (0.001+ ETH) for hundreds of transactions. When the server comes back, all keepers resume and process backlogs automatically.
- How much gas do the keepers consume?
- All keepers combined use approximately 0.001-0.005 ETH/day in gas on Base. The pool-mm-keeper is the largest consumer (90s cadence, ~300K gas per arb). At Base gas prices, this is $2-10/day, fully covered by the protocol fee revenue the keepers process.
- Can external parties run the keepers?
- Most keeper operations are owner-only (treasuryBuyNew, wrapDiamondEthToWeth, sellNewForEthFor). The two exceptions are distributePending() and forwardProtocolFees(), which are permissionless. In theory, anyone could run a fee-distribution bot. In practice, the server keepers handle it, and permissionless callers would just be duplicating work.
- How do I check if a specific keeper is running?
- GET /api/admin/manage/jobs returns the full keeper state. Look for the keeper name in the response. lastTickAt shows when it last ran. consecutiveFailures > 3 means something is wrong. The admin panel at thryx.fun also surfaces keeper health on the Overview card.
- Why not run keepers on-chain (like Chainlink Keepers)?
- Cost. Chainlink Keepers charge per upkeep execution on top of gas. On Base, the gas costs are already negligible. Running keepers as Node.js jobs on a $7/month Render instance is orders of magnitude cheaper than paying for an on-chain automation service. If Render goes down, the protocol is safe (just paused), so the reliability trade-off is acceptable at current scale.