Appearance
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 cronAnything 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
| Job | Schedule | File | Purpose |
|---|---|---|---|
cronBatchDeposit | * * * * * — every minute | services/dcaCron.js | CSIP execution + auto-claims |
autopilotCron | * * * * * — every minute | services/autopilotCron.js | Autopilot trigger evaluation |
attendanceCron | 0 1 * * * — 01:00 daily | services/attendanceCron.js | Streaks and attendance roll-up |
proposalStatusJob | */10 * * * * — every 10 min | services/daoCron.js | DAO proposal lifecycle |
initBridgeStatusCron | */60 * * * * * — every 60 s | services/bridgeStatusCron.js | Bridge status reconciliation |
initPriceAlertsCron | */2 * * * * * — every 2 s | services/priceAlertsCron.js | Price alert firing |
initPredictionCron | */5, */30, * * * * * * | services/predictionCron.js | Prediction market settlement — three schedules |
discordBoosterCron | — | services/discordBoosterCron.js | Discord 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:
| Job | File | Consequence of being off |
|---|---|---|
cronJobService | services/cron.js (*/10 * * * *) | General maintenance not running |
cronClaimRewards | services/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
| Variable | Effect |
|---|---|
CRON=false | All crons |
DISABLE_BRIDGE_STATUS_CRON | Just bridge status |
AUTOPILOT_ADDRESS_* empty | Autopilot 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. NotificationsBoth 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.jsThe 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| Grep | Finds |
|---|---|
cron-ables | The startup banner confirming crons started |
cron job is disabled | CRON=false is set |
Route via | Execution-engine route rejections (Route via odos failed: …) |
Not swap calldata | Calldata-guard hits |
delivers no in-tx output | Probe rejections (solver providers) |
error creating record | DCA 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:
| Rule | Why |
|---|---|
Wrap per-item work in try/catch | One bad row must not kill the batch. Both money-moving crons learned this the hard way |
| Record the error on the row | lastError / lastAttemptAt is what makes a stuck item diagnosable without log archaeology |
| Do not advance a cursor on failure | dcaCron and the Autopilot dca trigger both leave lastFillAt untouched so a missed cycle retries |
| Sync from chain, do not trust the DB | The self-heal step exists because another executor, or a user's own transaction, changes state behind you |
| Be honest about frequency | Every job shares one event loop with the API |
Respect CRON | Register inside the existing gate |
Non-cron background work
| Mechanism | Purpose |
|---|---|
services/marketplaceEventListener.js | Watches marketplace contract events and reconciles listings/offers |
| Socket.io namespaces | Realtime chat, prices, prediction updates, poker tables |
services/depositProcessor.js | Deposit verification |
scripts/cron-deposit-verification.js | A script, not a scheduled job |
Known gaps
| Gap | Impact |
|---|---|
| No cron health monitoring | A silently dead cron is invisible until a user complains |
| No executor gas alerting | See the danger note above |
cronClaimRewards disabled | Automated reward claiming not running |
| Crons in the API process | Blocks horizontal scaling; high-frequency jobs share the request event loop |
| No per-job metrics | No success rate, no duration, no last-run timestamp |
topN = 5 is a silent cap | If all five best routes fail, the position skips with no distinction between "no liquidity" and "five flaky endpoints" |