VeztaVezta
Architecture

Worker Domains

Domain-specialized worker containers for horizontal scaling and fault isolation

The Vezta backend worker process is partitioned into eight specialized worker domains (traders, market, misc, websocket, rtds, ai, sports, combos), alongside the HTTP-only api process. Each domain owns a subset of BullMQ queues and NestJS providers, gating execution via the backgroundOnly() helper. This enables horizontal scaling — add more worker containers per domain independently — and fault isolation — a SIGKILL in the misc domain won't drop traders order execution.

Domain Overview

DomainRoleKey Queues
tradersOrder execution, automated trading, and smart-money signalsinsider-signals, insider-cluster-refresh, telegram-signals, top-trader, leaderboard-calc, tracker-refresh, prediction-refresh, copy-trade-exec, copy-trade-tp-sl
marketMarket data ingestion and syncmarket-sync, trade-sync, short-duration-sync, holders-sync, catalog-sync, match-sync, price-snapshot, signal-detector
miscNotifications, rewards, alerts, and everything elsenotification-dispatch, push, price-alert-check, tp-sl-monitor, order-monitor, sniper-monitor, spread-scanner, share-card-gen, wallet-profile, wallet-list-refresh, rewards-sync, standings, weather
websocketPersistent exchange WebSocket connectionsNo BullMQ queues — Polymarket CLOB WS, Kalshi WS, Binance WS
rtdsPolymarket RTDS crypto price feedNo BullMQ queues — Chainlink price stream for BTC/ETH/SOL/XRP
aiAgent chat + research pipelineagent-request, agent-resume, agent-summarize, research-request (gated by FF_AGENT_QUEUE_SPLIT)
sportsESPN sports-stats pipelinesports-stats, prematch-reminder
combosMulti-leg combo / parlay tradescombo catalog sync + eligibility / settlement
allLegacy fallback (single-worker mode)All queues (pre-Phase 4 configuration)

How Domain Gating Works

The backgroundOnly() function in src/common/background/ acts as a provider gate. It accepts a module/provider list and an optional domain constraint:

// src/modules/monitor/monitor.module.ts
@Module({
  providers: [
    backgroundOnly([
      SignalIngestionProcessor,
      SignalClassifierProcessor,
    ], 'misc'),  // Only runs when WORKER_DOMAIN=misc or all
  ],
})

The shouldRunInDomain(domain) helper checks the WORKER_DOMAIN environment variable. Providers gated with backgroundOnly() are conditionally registered — they only exist in the DI container when the domain matches.

Deployment Topology

In production, nine application containers run from the same Docker image:

ContainerWORKER_DOMAINBACKGROUND_JOBS_ENABLEDLiveness Port
vezta-production-apiapifalse3001 (HTTP)
vezta-production-worker-traderstraderstrue3002
vezta-production-worker-marketmarkettrue3003
vezta-production-worker-miscmisctrue3004
vezta-production-worker-websocketwebsockettrue3005
vezta-production-worker-aiaitrue3006
vezta-production-worker-rtdsrtdsfalse3007
vezta-production-worker-sportssportstrue3008
vezta-production-worker-comboscombostrue3009

The API container always has BACKGROUND_JOBS_ENABLED=false — it never consumes BullMQ queues. Worker containers use BACKGROUND_JOBS_ENABLED=true to activate queue consumption, then filter by domain.

Liveness Endpoint

Each worker container exposes a minimal HTTP server on its WORKER_HEALTH_PORT (30023009, one per domain) with a /health endpoint. This is used by Docker health checks and the deployment orchestration to determine when a worker is ready.

Entrypoints

The Docker image has two entrypoints wired via separate CMD directives:

EntrypointFileRole
APIdist/src/mainFastify HTTP server, controllers, WebSocket gateway
Workerdist/src/workerBullMQ consumer, liveness HTTP server

The dist/src/worker entrypoint uses createApplicationContext() (not createApplication()) — it bootstraps the full DI container but skips HTTP controller registration, since workers have no REST API surface.

Scaling Behavior

  • traders — Scale with order volume. Each instance consumes from the same Redis-backed queue; BullMQ ensures each job is processed exactly once.
  • market — Typically one instance. Market sync uses repeatable jobs with fixed schedules; multiple instances would duplicate work.
  • misc — One instance for leaderboard, notifications, email. Share card generation is CPU-bound (Satori SVG → sharp PNG); if this becomes a bottleneck, decouple into its own domain.
  • websocket — Scale independently. Each instance maintains persistent WS connections (Polymarket CLOB WS, Kalshi WS, Sports WS, User WS, Binance). Multiple instances provide connection redundancy.
  • rtds — Typically one instance. Streams Chainlink crypto prices from Polymarket RTDS; low resource footprint (384 MB limit).
  • ai — Scale with concurrent agent chats. Runs the agent-request / agent-resume / agent-summarize (+ research-request) queues over a Redis-Streams event bus; active only when FF_AGENT_QUEUE_SPLIT=true, otherwise the chat loop runs in-process on the API container.
  • sports — Typically one instance. ESPN pollers (live scores, standings, prematch/resolve) are BullMQ schedulers, so it must run with BACKGROUND_JOBS_ENABLED=true.
  • combos — Typically one instance. Combo catalog sync plus eligibility and settlement for multi-leg parlay trades.

On this page