Overview
The Mouth backend runs a cron job (bet-cron.ts) that executes every 60 seconds. It performs four automated tasks:
- Expire pending bets — withdraw USDC back to challengers
- Activate partial fills — activate Open PvP bets past their deadline with partial fills
- Sync active bets — align DB status with on-chain state
- Claim finalized bets — distribute winnings to winners
RESOLVER_PRIVATE_KEY), keeping the cron independent from Privy and user wallets.
1. Process Expired Bets
Triggers on: bets withstatus = pending and acceptance_deadline < now
When a challenger creates a bet and no opponent accepts before the deadline, this task:
- Calls
withdraw()on the bet contract via the resolver account - The contract returns the challenger’s USDC deposit
- Updates the bet status to
expiredin the database
The resolver is authorized to call
withdraw() on expired bets. The smart contract checks: require(msg.sender == challenger || isResolver) and require(block.timestamp >= acceptanceDeadline).2. Activate Partial Fills
Triggers on: bets withstatus = filling and acceptance_deadline < now
When an Open PvP bet has received partial fills but the acceptance deadline has passed, this task activates the bet with whatever amount has been filled:
- Calls
activatePartial()on the bet contract via the resolver account - The contract calculates the matched challenger amount proportionally
- Returns the unmatched challenger deposit to the challenger
- Activates the bet (fee was already collected per-deposit, no additional fee here)
- Updates the bet status to
activein the database
activatePartial() is permissionless — anyone can call it. The cron uses the resolver account for convenience. The fee for each opponent deposit was already collected at deposit time.3. Sync Active Bets
Triggers on: bets withstatus = active and expiration_date < now
This task reads the on-chain status of expired active bets and updates the database accordingly. It handles cases where the on-chain state changed (e.g., via manual resolution) but the DB wasn’t updated yet.
This task is read-only — it only reads from the chain and writes to the DB. It does not submit any on-chain transactions.
4. Claim Finalized Bets
Triggers on: bets withstatus = finalized
Once a bet is finalized (either immediately for non-disputeable bets, or after the 48h dispute window for disputeable bets), this task automatically distributes the winnings:
- Calls
resolverClaim()on the bet contract via the resolver account - The contract distributes USDC to all winners in a single transaction
- Updates the bet status to
claimedin the database
Payout Distribution
resolverClaim() handles all outcome types in a single call:
The 2% fee is already collected at deposit time (proportionally as each opponent deposits). The claimable balance is the post-fee amount.
Lifecycle Summary
This diagram shows how the three cron tasks fit into the overall bet lifecycle:Configuration
Source
The cron implementation lives inbackend/src/services/bet-cron.ts. It is started automatically when the backend server boots via startBetCron() in index.ts.