Skip to content

Cron jobs

Every scheduled job runs in-process inside bz-backend, started in index.js behind a single master switch.

The master switch

bash
CRON=false     # disables EVERY cron

Anything other than the literal string false enables them.

Set CRON=false in every local .env

Local development points at production Atlas. With crons enabled, your laptop becomes a second CSIP and Autopilot executor using the production PRIVATE_KEY_EXECUTOR — submitting duplicate transactions in competition with the real cron.

This is also why the backend runs single-instance

ecosystem.config.cjs uses PM2 fork mode with instances: 1. cluster mode would start N copies of every cron, so dcaCron would submit the same batch N times. Horizontal scaling requires moving the crons out of the API process first.

Active jobs

JobScheduleFilePurpose
cronBatchDeposit* * * * * — every minuteservices/dcaCron.jsCSIP execution + auto-claims
autopilotCron* * * * * — every minuteservices/autopilotCron.jsAutopilot trigger evaluation
attendanceCron0 1 * * * — 01:00 dailyservices/attendanceCron.jsStreaks and attendance roll-up
proposalStatusJob*/10 * * * * — every 10 minservices/daoCron.jsDAO proposal lifecycle
initBridgeStatusCron*/60 * * * * * — every 60 sservices/bridgeStatusCron.jsBridge status reconciliation
initPriceAlertsCron*/2 * * * * * — every 2 sservices/priceAlertsCron.jsPrice alert firing
initPredictionCron*/5, */30, * * * * * *services/predictionCron.jsPrediction market settlement — three schedules
discordBoosterCronservices/discordBoosterCron.jsDiscord booster bonuses

priceAlertsCron runs every two seconds

And predictionCron has a * * * * * * (every second) schedule. Those are the two highest-frequency jobs in the process, sharing the event loop with every API request. If the backend feels slow under load, check what those are doing before looking elsewhere.

Disabled jobs

Commented out in index.js:

JobFileConsequence of being off
cronJobServiceservices/cron.js (*/10 * * * *)General maintenance not running
cronClaimRewardsservices/claim-cron.js (* * * * *)Automated reward claiming is not running

cronClaimRewards being off is a live product gap

services/autoClaim.js, models/autoClaimModel.js and autoClaimLogModel.js all exist, so the automated-claim feature is built. It simply is not scheduled. Users must claim manually. Worth confirming whether that is intentional before someone "fixes" it and starts paying out gas.

Individual switches

VariableEffect
CRON=falseAll crons
DISABLE_BRIDGE_STATUS_CRONJust bridge status
AUTOPILOT_ADDRESS_* emptyAutopilot cron skips that chain — this is how Base is inert

The two that move money

dcaCron — CSIP execution

tick (every minute)
  1. Read DcaInvestment where status: 'success'
  2. ON-CHAIN SELF-HEAL SYNC — mirror processed / status / depositTime / interval
     from chain into Mongo (so another executor's work is not lost)
  3. Filter to due positions; group per chain
  4. getBestExecutableRoute (top 5 candidates, calldata guard, estimateGas probe)
     with dstWalletAddress = the DCA vault
  5. batchExecuteDeposit([...]) with gasOverrides
  6. Record fills to DcaFill (unique txHash + positionId)
  7. processAutoClaims()

Per-position try/catch writes lastError and lastAttemptAt onto the document — cleared on success, rendered as an amber "retrying" state in the UI.

autopilotCron — Autopilot execution

tick (every minute)
  1. autopilotSync — mirror on-chain AgentAccount state into Mongo
  2. autopilotPrices — CoinMarketCap batch → Redis rolling series
  3. Build ctx: prices (address-keyed), tokenMeta, maxInWindow
  4. Per active agent:
       evaluateAgent → at most ONE trade intent
       refuse if the DB policy hash ≠ the on-chain policyHash
       RugCheck safety gate (trusted majors skipped; unscored BLOCKS)
       getBestExecutableRoute with dstWalletAddress = the Autopilot vault
  5. batchExecuteTrades([...]) with gasOverrides
  6. Notifications

Both share services/executionEngine.js. Detail: execution engine.

Gas dependency

Both money-moving crons stop silently when the executor runs dry

PRIVATE_KEY_EXECUTOR (0x5935745431D1385ae1a9CE2644fb5f5d1f140459) must hold native gas on each automation chain. When it empties, scheduled buys stop retrying and nothing alerts.

Arbitrum consumption is roughly 0.00002 ETH per batch after the gas-price fix.

bash
node scripts/checkDeployerBalance.js

The gas-price fix is worth understanding

gasOverrides uses the network gasPrice × 2 rather than letting ethers auto-fill EIP-1559 fees. Ethers' default 1.5-gwei priority tip is roughly 77× Arbitrum's real gas price, and the executor must reserve gasLimit × maxFeePerGas — so that tip made the wallet appear permanently out of gas.

Monitoring a cron

There is no dashboard. Read the logs:

bash
./scripts/deploy.gcp.sh logs bz-backend 500
GrepFinds
cron-ablesThe startup banner confirming crons started
cron job is disabledCRON=false is set
Route viaExecution-engine route rejections (Route via odos failed: …)
Not swap calldataCalldata-guard hits
delivers no in-tx outputProbe rejections (solver providers)
error creating recordDCA creation failures

Per-position and per-agent errors also land in the database: DcaInvestment.lastError, and autopilotEventModel for Autopilot.

Adding a cron

js
// index.js, inside the CRON != 'false' block
import myCron from './services/myCron.js'

myCron.start()          // or initMyCron()

Rules worth following:

RuleWhy
Wrap per-item work in try/catchOne bad row must not kill the batch. Both money-moving crons learned this the hard way
Record the error on the rowlastError / lastAttemptAt is what makes a stuck item diagnosable without log archaeology
Do not advance a cursor on failuredcaCron and the Autopilot dca trigger both leave lastFillAt untouched so a missed cycle retries
Sync from chain, do not trust the DBThe self-heal step exists because another executor, or a user's own transaction, changes state behind you
Be honest about frequencyEvery job shares one event loop with the API
Respect CRONRegister inside the existing gate

Non-cron background work

MechanismPurpose
services/marketplaceEventListener.jsWatches marketplace contract events and reconciles listings/offers
Socket.io namespacesRealtime chat, prices, prediction updates, poker tables
services/depositProcessor.jsDeposit verification
scripts/cron-deposit-verification.jsA script, not a scheduled job

Known gaps

GapImpact
No cron health monitoringA silently dead cron is invisible until a user complains
No executor gas alertingSee the danger note above
cronClaimRewards disabledAutomated reward claiming not running
Crons in the API processBlocks horizontal scaling; high-frequency jobs share the request event loop
No per-job metricsNo success rate, no duration, no last-run timestamp
topN = 5 is a silent capIf all five best routes fail, the position skips with no distinction between "no liquidity" and "five flaky endpoints"

Internal + developer documentation. Usage figures are point-in-time snapshots — re-verify before publishing them.