Skip to main content

Auction Engine - Detailed Design Document

Architecture Version: 3.0 | Last Updated: January 2026 Pattern References: Real-Time Bidding Systems, WebSocket Architecture, Event Sourcing


1. Executive Overview

The Auction Engine brings the thrill of IPL auctions to fantasy sports. Instead of simply picking players from a list, users participate in live auctions where they bid against each other for players. This creates excitement, strategy, and a completely different fantasy experience.

Imagine sitting in a virtual auction room, watching as Virat Kohli comes up for bidding, and you have to decide - do you spend big now, or save your budget for later? That's the experience the Auction Engine delivers.

Key Capabilities:

  • Real-time bidding with sub-100ms latency
  • Multiple auction formats (Live, Silent, Draft, Salary Cap)
  • AI-powered agents that can bid on behalf of users
  • Concurrent auctions supporting thousands of simultaneous rooms
  • Fair play guarantees through cryptographic bid ordering

2. Core Architecture

2.1 High-Level System Design

What this diagram shows: The Auction Engine uses a WebSocket-first architecture for real-time bidding. All clients connect through the WebSocket Gateway, which uses Redis Pub/Sub for horizontal scaling. The Bid Processor validates and sequences bids, while the Timer Service manages the "Going once... Going twice... SOLD!" countdown. All events are persisted to Kafka for durability and replay.


3. Core Concepts

What this diagram shows: The Auction Engine supports multiple auction formats. Live Auction mimics the IPL mega auction with real-time bidding. Silent Auction lets users submit sealed bids. Draft Pick is turn-based selection. Salary Cap Draft combines picking with budget constraints. Each format has its own bidding mechanics and rules.


Auction States & Lifecycle

Every auction goes through a carefully managed lifecycle:

What this diagram shows: This is the heartbeat of a live auction. A player is nominated, bidding opens, and bids flow in. When bidding slows, we enter the "Going once... Going twice... SOLD!" sequence. Any new bid resets back to active bidding. If no one bids at base price, the player goes unsold.


Data Models

Auction Entities

What this diagram shows: The data model tracks everything about an auction. AUCTION defines the auction configuration. AUCTION_PARTICIPANT tracks each user's remaining budget and squad. AUCTION_PLAYER tracks each cricket player's status in the auction. BID records every bid placed, enabling full audit trails.


Live Auction Flow

Here's what happens during a live auction, from player nomination to sale:

What this diagram shows: This is the real-time dance of a live auction. The auctioneer bot nominates players, the engine validates bids, and WebSocket broadcasts keep everyone synchronized. Notice how P3's last-second bid reset the countdown - just like a real auction!


8. Bidding Rules & Validation

8.1 Bid Validation Pipeline

8.2 Validation Rules Implementation

interface BidValidationRule {
name: string;
validate(context: BidContext): Promise<ValidationResult>;
}

class BidValidator {
private rules: BidValidationRule[] = [
new AuctionActiveRule(),
new PlayerUpForBidRule(),
new ParticipantValidRule(),
new SufficientPurseRule(),
new BidAmountRule(),
new IncrementRule(),
new SquadLimitRule(),
new PositionLimitRule(),
];

async validate(bid: Bid, context: AuctionContext): Promise<ValidationResult> {
const bidContext: BidContext = {
bid,
auction: context.auction,
participant: context.participant,
currentPlayer: context.currentPlayer,
currentHighBid: context.currentHighBid,
};

for (const rule of this.rules) {
const result = await rule.validate(bidContext);
if (!result.valid) {
return {
valid: false,
error: result.error,
rule: rule.name,
};
}
}

return { valid: true };
}
}

// Example: Sufficient Purse Rule
class SufficientPurseRule implements BidValidationRule {
name = 'SufficientPurse';

async validate(context: BidContext): Promise<ValidationResult> {
const { bid, participant, auction } = context;

// Calculate minimum required purse after this bid
const remainingSlots = auction.squadSize - participant.playersBought - 1;
const minReserve = remainingSlots * auction.minBidAmount;
const availablePurse = participant.remainingPurse - minReserve;

if (bid.amount > availablePurse) {
return {
valid: false,
error: `Insufficient purse. Max bid: ${availablePurse}`,
};
}

return { valid: true };
}
}

9. Real-Time Communication

9.1 WebSocket Architecture

9.2 Message Types

// WebSocket Message Protocol
type AuctionMessage =
| PlayerNominatedMessage
| BidPlacedMessage
| BidRejectedMessage
| CountdownMessage
| PlayerSoldMessage
| PlayerUnsoldMessage
| AuctionStateMessage
| ParticipantUpdateMessage;

interface PlayerNominatedMessage {
type: 'PLAYER_NOMINATED';
payload: {
playerId: string;
playerName: string;
basePrice: number;
position: string;
stats: PlayerStats;
nominationOrder: number;
};
}

interface BidPlacedMessage {
type: 'BID_PLACED';
payload: {
bidId: string;
participantId: string;
participantName: string;
amount: number;
timestamp: number;
isLeading: boolean;
};
}

interface CountdownMessage {
type: 'COUNTDOWN';
payload: {
stage: 'GOING_ONCE' | 'GOING_TWICE' | 'SOLD';
currentBid: number;
leadingParticipant: string;
secondsRemaining: number;
};
}

9.3 Connection Management

class AuctionConnectionManager {
private connections: Map<string, WebSocket> = new Map();
private rooms: Map<string, Set<string>> = new Map();
private redis: Redis;

async joinRoom(connectionId: string, auctionId: string): Promise<void> {
// Add to local room
if (!this.rooms.has(auctionId)) {
this.rooms.set(auctionId, new Set());
}
this.rooms.get(auctionId)!.add(connectionId);

// Subscribe to Redis channel for this auction
await this.redis.subscribe(`auction:${auctionId}`, (message) => {
this.broadcastToRoom(auctionId, message);
});

// Send current state to new participant
const state = await this.getAuctionState(auctionId);
this.sendToConnection(connectionId, {
type: 'AUCTION_STATE',
payload: state,
});
}

async broadcastBid(auctionId: string, bid: BidPlacedMessage): Promise<void> {
// Publish to Redis for cross-server broadcast
await this.redis.publish(`auction:${auctionId}`, JSON.stringify(bid));
}

private broadcastToRoom(auctionId: string, message: string): void {
const room = this.rooms.get(auctionId);
if (!room) return;

for (const connectionId of room) {
this.sendToConnection(connectionId, JSON.parse(message));
}
}
}

10. Timer & Countdown Service

10.1 Countdown State Machine

10.2 Timer Implementation

class AuctionTimer {
private timers: Map<string, NodeJS.Timeout> = new Map();
private states: Map<string, CountdownState> = new Map();

private readonly config = {
ACTIVE_TIMEOUT: 5000, // 5 seconds to GOING_ONCE
GOING_ONCE_TIMEOUT: 3000, // 3 seconds to GOING_TWICE
GOING_TWICE_TIMEOUT: 2000, // 2 seconds to SOLD
};

startCountdown(auctionId: string, playerId: string): void {
this.states.set(playerId, 'ACTIVE');
this.scheduleTransition(auctionId, playerId, 'ACTIVE');
}

onBidReceived(auctionId: string, playerId: string): void {
// Cancel existing timer
this.cancelTimer(playerId);

// Reset to ACTIVE state
this.states.set(playerId, 'ACTIVE');
this.scheduleTransition(auctionId, playerId, 'ACTIVE');
}

private scheduleTransition(
auctionId: string,
playerId: string,
currentState: CountdownState
): void {
const timeout = this.getTimeout(currentState);
const nextState = this.getNextState(currentState);

const timer = setTimeout(async () => {
if (nextState === 'SOLD') {
await this.handleSold(auctionId, playerId);
} else {
this.states.set(playerId, nextState);
await this.broadcastCountdown(auctionId, playerId, nextState);
this.scheduleTransition(auctionId, playerId, nextState);
}
}, timeout);

this.timers.set(playerId, timer);
}

private getTimeout(state: CountdownState): number {
switch (state) {
case 'ACTIVE': return this.config.ACTIVE_TIMEOUT;
case 'GOING_ONCE': return this.config.GOING_ONCE_TIMEOUT;
case 'GOING_TWICE': return this.config.GOING_TWICE_TIMEOUT;
default: return 0;
}
}
}

11. AI Agent Integration

11.1 Agent Architecture

11.2 Agent Strategy Implementation

interface AuctionAgent {
id: string;
strategy: AgentStrategy;
evaluate(player: AuctionPlayer, context: AgentContext): BidDecision;
}

interface AgentStrategy {
name: string;
riskTolerance: number; // 0-1
starPlayerPremium: number; // Multiplier for top players
positionPriorities: Record<Position, number>;
budgetAllocation: BudgetAllocation;
}

class BalancedAgent implements AuctionAgent {
strategy: AgentStrategy = {
name: 'Balanced',
riskTolerance: 0.5,
starPlayerPremium: 1.3,
positionPriorities: {
BATSMAN: 0.3,
BOWLER: 0.3,
ALL_ROUNDER: 0.25,
WICKET_KEEPER: 0.15,
},
budgetAllocation: {
starPlayers: 0.4, // 40% for top 3 players
corePlayers: 0.4, // 40% for core squad
fillers: 0.2, // 20% for remaining slots
},
};

evaluate(player: AuctionPlayer, context: AgentContext): BidDecision {
// Calculate player value based on stats and form
const baseValue = this.calculatePlayerValue(player);

// Adjust for position need
const positionNeed = this.getPositionNeed(player.position, context);
const adjustedValue = baseValue * positionNeed;

// Apply strategy modifiers
const maxBid = this.calculateMaxBid(adjustedValue, context);

// Decide whether to bid
const currentBid = context.currentHighBid || player.basePrice;

if (currentBid < maxBid) {
return {
shouldBid: true,
amount: this.calculateBidAmount(currentBid, maxBid, context),
confidence: this.calculateConfidence(player, context),
};
}

return { shouldBid: false };
}

private calculateBidAmount(
currentBid: number,
maxBid: number,
context: AgentContext
): number {
// Bid increment based on strategy
const increment = this.getBidIncrement(currentBid);
const proposedBid = currentBid + increment;

// Add randomness to avoid predictable patterns
const variance = (Math.random() - 0.5) * 0.1 * increment;

return Math.min(proposedBid + variance, maxBid);
}
}

12. Concurrency & Fairness

12.1 Bid Ordering with Timestamps

12.2 Distributed Lock for Bid Processing

class BidSequencer {
private redis: Redis;

async processBid(auctionId: string, playerId: string, bid: Bid): Promise<BidResult> {
const lockKey = `auction:${auctionId}:player:${playerId}:lock`;

// Acquire distributed lock
const lock = await this.redis.set(lockKey, bid.id, 'NX', 'PX', 100);

if (!lock) {
// Another bid is being processed, queue this one
return this.queueBid(auctionId, playerId, bid);
}

try {
// Get current state
const currentBid = await this.getCurrentHighBid(auctionId, playerId);

// Validate bid
if (bid.amount <= currentBid) {
return { success: false, reason: 'Bid too low' };
}

// Process bid atomically
await this.redis.multi()
.set(`auction:${auctionId}:player:${playerId}:high_bid`, bid.amount)
.set(`auction:${auctionId}:player:${playerId}:high_bidder`, bid.participantId)
.lpush(`auction:${auctionId}:player:${playerId}:bids`, JSON.stringify(bid))
.exec();

return { success: true, bid };
} finally {
// Release lock
await this.redis.del(lockKey);
}
}
}

13. Event Sourcing for Auctions

13.1 Event Store Design

13.2 Event Definitions

// Auction Events
type AuctionEvent =
| AuctionCreatedEvent
| ParticipantJoinedEvent
| PlayerNominatedEvent
| BidPlacedEvent
| BidRejectedEvent
| PlayerSoldEvent
| PlayerUnsoldEvent
| AuctionCompletedEvent;

interface BidPlacedEvent {
type: 'BID_PLACED';
aggregateId: string; // auctionId
sequence: number;
timestamp: Date;
payload: {
bidId: string;
playerId: string;
participantId: string;
amount: number;
previousHighBid: number | null;
previousHighBidder: string | null;
};
}

interface PlayerSoldEvent {
type: 'PLAYER_SOLD';
aggregateId: string;
sequence: number;
timestamp: Date;
payload: {
playerId: string;
playerName: string;
soldTo: string;
soldToName: string;
finalPrice: number;
totalBids: number;
biddingDuration: number;
};
}

// Event Store Implementation
class AuctionEventStore {
async append(auctionId: string, events: AuctionEvent[]): Promise<void> {
const topic = `auction.events.${auctionId}`;

for (const event of events) {
await this.kafka.produce(topic, {
key: auctionId,
value: JSON.stringify(event),
headers: {
eventType: event.type,
timestamp: event.timestamp.toISOString(),
},
});
}
}

async replay(auctionId: string): Promise<AuctionState> {
const events = await this.kafka.consume(`auction.events.${auctionId}`);

let state = AuctionState.initial();
for (const event of events) {
state = state.apply(event);
}

return state;
}
}

14. Scalability & Performance

14.1 Horizontal Scaling Architecture

14.2 Performance Targets

MetricTargetMeasurement
Bid Latency (p50)< 50msTime from bid to broadcast
Bid Latency (p99)< 200msWorst case latency
Concurrent Auctions1,000+Simultaneous live auctions
Participants per Auction100+Users in single auction
Bids per Second10,000+System-wide throughput
WebSocket Connections100,000+Concurrent connections

14.3 Optimization Strategies

// Connection pooling for Redis
const redisPool = new GenericPool({
create: () => new Redis(config),
destroy: (client) => client.quit(),
min: 10,
max: 100,
});

// Batch bid processing
class BatchBidProcessor {
private queue: Bid[] = [];
private processing = false;

async addBid(bid: Bid): Promise<void> {
this.queue.push(bid);

if (!this.processing) {
this.processing = true;
await this.processBatch();
}
}

private async processBatch(): Promise<void> {
while (this.queue.length > 0) {
// Process up to 100 bids at once
const batch = this.queue.splice(0, 100);

// Group by auction for efficient processing
const byAuction = this.groupByAuction(batch);

// Process each auction's bids in parallel
await Promise.all(
Object.entries(byAuction).map(([auctionId, bids]) =>
this.processAuctionBids(auctionId, bids)
)
);
}

this.processing = false;
}
}

15. Monitoring & Observability

15.1 Key Metrics

const auctionMetrics = {
// Bid Processing
bidLatency: new Histogram({
name: 'auction_bid_latency_seconds',
help: 'Bid processing latency',
labelNames: ['auction_type'],
buckets: [0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1],
}),

bidsPerSecond: new Counter({
name: 'auction_bids_total',
help: 'Total bids processed',
labelNames: ['auction_id', 'status'],
}),

// WebSocket
activeConnections: new Gauge({
name: 'auction_websocket_connections',
help: 'Active WebSocket connections',
labelNames: ['server_id'],
}),

messageLatency: new Histogram({
name: 'auction_message_latency_seconds',
help: 'WebSocket message delivery latency',
buckets: [0.005, 0.01, 0.025, 0.05, 0.1],
}),

// Auction State
activeAuctions: new Gauge({
name: 'auction_active_total',
help: 'Number of active auctions',
labelNames: ['type'],
}),

playersPerAuction: new Histogram({
name: 'auction_players_total',
help: 'Players per auction',
buckets: [10, 25, 50, 100, 200],
}),
};

15.2 Distributed Tracing


16. Error Handling & Recovery

16.1 Failure Scenarios

ScenarioDetectionRecovery
WebSocket disconnectHeartbeat timeoutAuto-reconnect with state sync
Bid processing failureException caughtRetry with exponential backoff
Redis unavailableHealth checkFailover to replica
Auction state corruptionChecksum mismatchReplay from event store

16.2 Reconnection Protocol

class AuctionReconnectionHandler {
async handleReconnect(
connectionId: string,
auctionId: string,
lastEventId: string
): Promise<void> {
// Get events since last known state
const missedEvents = await this.eventStore.getEventsSince(
auctionId,
lastEventId
);

// Send missed events to client
for (const event of missedEvents) {
await this.sendToConnection(connectionId, event);
}

// Send current state snapshot
const currentState = await this.getAuctionState(auctionId);
await this.sendToConnection(connectionId, {
type: 'STATE_SYNC',
payload: currentState,
});
}
}

17. Security Considerations

17.1 Anti-Cheating Measures

17.2 Rate Limiting Configuration

const auctionRateLimits = {
// Bids per participant per auction
bidsPerMinute: {
limit: 60,
window: 60000,
keyGenerator: (req) => `${req.user.id}:${req.auctionId}`,
},

// Connections per user
connectionsPerUser: {
limit: 3,
keyGenerator: (req) => req.user.id,
},

// Global bid rate
globalBidsPerSecond: {
limit: 10000,
window: 1000,
},
};

18. Testing Strategy

18.1 Load Testing Scenarios

describe('Auction Load Tests', () => {
it('should handle 100 concurrent bidders', async () => {
const auction = await createTestAuction({ maxParticipants: 100 });

// Simulate 100 concurrent bidders
const bidders = Array.from({ length: 100 }, (_, i) =>
createTestBidder(`bidder-${i}`)
);

// All bidders join simultaneously
await Promise.all(bidders.map(b => b.join(auction.id)));

// Simulate bidding war
const player = await nominatePlayer(auction.id);

// 100 bids in rapid succession
const bidPromises = bidders.map((b, i) =>
b.placeBid(player.id, player.basePrice + i)
);

const results = await Promise.all(bidPromises);

// Verify exactly one winner
const winners = results.filter(r => r.isWinning);
expect(winners).toHaveLength(1);
});

it('should maintain sub-100ms latency under load', async () => {
const latencies: number[] = [];

// Place 1000 bids and measure latency
for (let i = 0; i < 1000; i++) {
const start = performance.now();
await placeBid(testAuction.id, testPlayer.id, basePrice + i);
latencies.push(performance.now() - start);
}

const p99 = percentile(latencies, 99);
expect(p99).toBeLessThan(100);
});
});

19. Integration with Contest Engine

19.1 Auction-to-Contest Flow


20. Future Enhancements

20.1 Roadmap

PhaseFeatureDescription
Q1 2026Voice BiddingVoice commands for bids
Q2 2026AR Auction RoomImmersive AR experience
Q3 2026Cross-Platform SyncSeamless mobile/web transition
Q4 2026AI AuctioneerAI-powered auction host

21. Appendix

21.1 Glossary

TermDefinition
Base PriceMinimum starting bid for a player
PurseBudget available to a participant
RTMRight to Match - ability to match winning bid
UnsoldPlayer who received no bids at base price

21.2 References


Document Version: 3.0.0 | Last Updated: January 2026