Skip to content

Background Jobs

All goroutines below start inside cmd/api-server/main.go unless noted.

Sportmonks scheduler

Started by: go scheduler.Run(ctx) — only when SPORTMONKS_API_KEY is set.
Lives in: internal/sportmonks/

The scheduler polls Sportmonks every 2 seconds during live matches. Between matches it polls less frequently (using Sportmonks' own fixture-state transitions).

What it does per tick: - Fetches live fixture state and player events for every configured league. - Updates instrument live_base_price in Redis based on player performance (scoring bumps via sportmonks.SetEventBumps). - Writes match events to Redis events:{sportmonksId} list. - Publishes price:{instrumentId} for each changed price. - On FT (full time): calls ProcessCompletedFixture which: 1. Fires marginReplayerAdapter.ReplayUsersAtFT (ADR-0006 pre-FT washouts). 2. Calls autoSellerAdapter.ExecuteSystemSell for every legacy open position. 3. Calls cfdCloserAdapter.CloseAtFTSnapshot for every open CFD position. 4. Sets HSET instrument:{id} frozen 1 during liquidation, clears after.

What breaks if not running: No live price updates during matches. FT auto-close of positions does not fire (users keep positions open past match end).

Squad syncer: sportmonks.NewSquadSyncer runs inside the scheduler. It syncs player rosters from Sportmonks into Postgres squad_players, triggering instrumentSvc.HydrateRedis to keep instrument state fresh.

Weekly spinner

Started by: go runWeeklySpinner(ctx, spinnerSvc).
Lives in: internal/spinner/

Runs once at boot (to catch any missed Monday trigger after a restart) then every Monday at 00:05 UTC.

Calls spinnerSvc.SpinFor(ctx, time.Now()). Uses ON CONFLICT DO NOTHING in Postgres so multiple api-server replicas spinning simultaneously produce exactly one winner row.

What breaks if not running: No weekly tiebreaker winner selected. Leaderboard still functions; the spinner result is a display feature.

Margin reconnect listener

Started by: go runMarginReconnectListener(ctx, redisClient.RDB(), marginSvc).
Lives in: cmd/api-server/main.go

Subscribes to the Redis user:reconnect channel. ws-server publishes a message each time a user transitions from 0 to 1 active connections.

For each received userId, spawns a goroutine calling marginSvc.ReplayLastSeen(ctx, uid). The replay reads the price_ticks buffer since last_seen:{userId} and closes any positions that breached their margin or SL/TP at the breach price — not the current price.

What breaks if not running: Offline users' breached positions accumulate until the 5-min backstop sweeper catches them. No data loss, but breach-price settlement is delayed.

Margin backstop sweeper

Started by: go runMarginBackstopSweeper(ctx, pool, redisClient.RDB(), marginSvc).
Interval: Every 5 minutes.
Redis lease: lease:margin-backstop (TTL 30s, acquire via SETNX).

Queries SELECT DISTINCT user_id FROM cfd_positions WHERE closed_at IS NULL. For each user, calls marginSvc.ReplayLastSeen. Logs total users swept and washouts fired.

Only one api-server replica runs the sweep at a time; others skip the tick if they cannot acquire the lease.

What breaks if not running: Stranded offline users whose WebSocket never reconnects accumulate undetected washouts. The reconnect listener handles online users; this sweeper handles the permanently-offline case.

StopWatch (SL/TP enforcement)

Started by: go positionsStopWatch.Run(ctx).
Lives in: internal/positions/stopwatch.go
Redis lease: lease:stop-watcher.

Subscribes to price:* pub/sub. On each tick: 1. Reads all open positions from Postgres (cached in memory, refreshed every 5s). 2. For any position where the new price breaches stop_loss or take_profit, calls position_close.lua. 3. Also calls marginTickAdapter.EvaluateAndApplyForTick(ctx, userID) for the margin wash-out check.

Single-instance across replicas via the Redis lease.

What breaks if not running: SL/TP orders are not enforced in real time. The 5-min backstop sweeper provides a fallback, but with up to 5 minutes of drift.

Bot engine

Started by: go botEngine.Start(ctx) — only when BOT_ENABLED=1.
Lives in: internal/bot/

Simulates synthetic traders (bot personas) buying and selling instruments. Each persona has a wallet funded from BOT_WALLET. Bots provide liquidity when no real users are active.

The scheduler starts the bot engine when a match goes live. When SPORTMONKS_API_KEY is not set (dev mode without live matches), the engine starts immediately at boot.

What breaks if not running: No synthetic liquidity. Price does not move without real user trades.

Embedded flusher

Started by: go flushSvc.Run(ctx) — only when FLUSHER_EMBEDDED=1.
Lives in: internal/flusher/

Runs the flusher loop and both outbox drainers inside the api-server process. Used in single-binary local dev. In production, the standalone cmd/flusher binary is deployed instead.

See Services — flusher for the drain details.

Google JWKS refresh

Started by: go googleVerifier.StartRefresh(ctx).
Interval: Every 6 hours.

Refetches https://www.googleapis.com/oauth2/v3/certs into the in-memory keys map (RW mutex protected). A failure is logged but does not crash the process; the previous keys continue to work until a Google key rotation causes a verification failure, at which point the next scheduled refresh corrects it.

What breaks if not running: After a Google key rotation (rare; Google rotates keys roughly weekly), new Google logins fail until the next refresh.