> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mouth.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Cron Jobs

> Automated background tasks that keep on-chain and off-chain state in sync.

## Overview

The Mouth backend runs a cron job (`bet-cron.ts`) that executes **every 60 seconds**. It performs four automated tasks:

1. **Expire pending bets** — withdraw USDC back to challengers
2. **Activate partial fills** — activate Open PvP bets past their deadline with partial fills
3. **Sync active bets** — align DB status with on-chain state
4. **Claim finalized bets** — distribute winnings to winners

All on-chain operations use the **resolver account** (`RESOLVER_PRIVATE_KEY`), keeping the cron independent from Privy and user wallets.

```mermaid theme={null}
graph LR
    Cron[Cron Loop - 60s] --> Expire[processExpiredBets]
    Cron --> Partial[activatePartialBets]
    Cron --> Sync[syncActiveBets]
    Cron --> Claim[claimFinalizedBets]

    Expire -->|withdraw| Chain[Base L2]
    Partial -->|activatePartial| Chain
    Claim -->|resolverClaim| Chain
    Sync -->|read status| Chain

    Expire -->|status → expired| DB[(PostgreSQL)]
    Partial -->|status → active| DB
    Sync -->|sync status| DB
    Claim -->|status → claimed| DB
```

## 1. Process Expired Bets

**Triggers on**: bets with `status = pending` and `acceptance_deadline < now`

When a challenger creates a bet and no opponent accepts before the deadline, this task:

1. Calls `withdraw()` on the bet contract via the resolver account
2. The contract returns the challenger's USDC deposit
3. Updates the bet status to `expired` in the database

```mermaid theme={null}
sequenceDiagram
    participant Cron
    participant DB as PostgreSQL
    participant Contract as MouthBet

    Cron->>DB: Find pending bets past deadline
    loop Each expired bet
        Cron->>Contract: withdraw() via resolver
        Contract-->>Contract: Transfer USDC back to challenger
        Cron->>DB: SET status = 'expired'
    end
```

<Note>
  The resolver is authorized to call `withdraw()` on expired bets. The smart contract checks: `require(msg.sender == challenger || isResolver)` and `require(block.timestamp >= acceptanceDeadline)`.
</Note>

## 2. Activate Partial Fills

**Triggers on**: bets with `status = 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:

1. Calls `activatePartial()` on the bet contract via the resolver account
2. The contract calculates the matched challenger amount proportionally
3. Returns the unmatched challenger deposit to the challenger
4. Activates the bet (fee was already collected per-deposit, no additional fee here)
5. Updates the bet status to `active` in the database

```mermaid theme={null}
sequenceDiagram
    participant Cron
    participant DB as PostgreSQL
    participant Contract as MouthBet

    Cron->>DB: Find filling bets past deadline
    loop Each partially filled bet
        Cron->>Contract: activatePartial() via resolver
        Contract-->>Contract: Return excess to challenger, activate
        Cron->>DB: SET status = 'active'
    end
```

<Note>
  `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.
</Note>

## 3. Sync Active Bets

**Triggers on**: bets with `status = 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.

| On-chain status | DB update            | Fields set                               |
| --------------- | -------------------- | ---------------------------------------- |
| `Resolved`      | `status → resolved`  | `outcome`, `resolved_at`                 |
| `Finalized`     | `status → finalized` | `outcome`, `resolved_at`, `finalized_at` |
| `Claimed`       | `status → claimed`   | `outcome`, `claimed_at`                  |
| `Active`        | No change            | Waiting for resolution                   |

<Note>
  This task is read-only — it only reads from the chain and writes to the DB. It does not submit any on-chain transactions.
</Note>

## 4. Claim Finalized Bets

**Triggers on**: bets with `status = 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:

1. Calls `resolverClaim()` on the bet contract via the resolver account
2. The contract distributes USDC to all winners in a single transaction
3. Updates the bet status to `claimed` in the database

```mermaid theme={null}
sequenceDiagram
    participant Cron
    participant DB as PostgreSQL
    participant Contract as MouthBet

    Cron->>DB: Find finalized bets
    loop Each finalized bet
        Cron->>Contract: resolverClaim() via resolver
        Contract-->>Contract: Distribute USDC to winner(s)
        Cron->>DB: SET status = 'claimed', claimed_at = now
    end
```

### Payout Distribution

`resolverClaim()` handles all outcome types in a single call:

| Outcome                  | Distribution                            |
| ------------------------ | --------------------------------------- |
| Challenger wins          | 100% of claimable balance to challenger |
| Opponent wins (PvP)      | 100% of claimable balance to opponent   |
| Opponent wins (Open PvP) | Proportional to each opponent's deposit |
| Draw                     | Proportional refund to all parties      |

<Note>
  The 2% fee is already collected at deposit time (proportionally as each opponent deposits). The claimable balance is the post-fee amount.
</Note>

## Lifecycle Summary

This diagram shows how the three cron tasks fit into the overall bet lifecycle:

```
Create bet → [pending]
                │
                ├── Opponent accepts (PvP 1v1) → [active]
                │                                    │
                ├── Partial fills (Open PvP) → [filling]
                │                                    │
                │                          🤖 activatePartialBets() → [active]
                │                                    │
                │               ┌────────────────────┘
                │               ▼
                │             [active]
                │               │
                │               ├── Resolver resolves (disputeable) → [resolved]
                │               │       │
                │               │       └── 48h passes + finalize → [finalized]
                │               │                                       │
                │               ├── Resolver resolves (non-disputeable) → [finalized]
                │               │                                            │
                │               │               ┌────────────────────────────┘
                │               │               ▼
                │               │     🤖 claimFinalizedBets() → [claimed] ✅
                │               │
                │               └── Mutual cancel → [cancelled]
                │
                └── Deadline passes, no opponent
                        │
                        ▼
              🤖 processExpiredBets() → [expired] (USDC returned)
```

## Configuration

| Setting        | Value         | Description                                        |
| -------------- | ------------- | -------------------------------------------------- |
| Interval       | 60 seconds    | How often all four tasks run                       |
| Signing        | Resolver EOA  | Uses `RESOLVER_PRIVATE_KEY` for all on-chain calls |
| Gas            | Resolver pays | ETH on Base (typically \< \$0.01 per tx)           |
| Error handling | Per-bet       | One failed bet does not block others               |

## Source

The cron implementation lives in [`backend/src/services/bet-cron.ts`](https://github.com/mouthbet/mouth/blob/main/backend/src/services/bet-cron.ts). It is started automatically when the backend server boots via `startBetCron()` in `index.ts`.
