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
| Metric | Target | Measurement |
|---|---|---|
| Bid Latency (p50) | < 50ms | Time from bid to broadcast |
| Bid Latency (p99) | < 200ms | Worst case latency |
| Concurrent Auctions | 1,000+ | Simultaneous live auctions |
| Participants per Auction | 100+ | Users in single auction |
| Bids per Second | 10,000+ | System-wide throughput |
| WebSocket Connections | 100,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
| Scenario | Detection | Recovery |
|---|---|---|
| WebSocket disconnect | Heartbeat timeout | Auto-reconnect with state sync |
| Bid processing failure | Exception caught | Retry with exponential backoff |
| Redis unavailable | Health check | Failover to replica |
| Auction state corruption | Checksum mismatch | Replay 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
| Phase | Feature | Description |
|---|---|---|
| Q1 2026 | Voice Bidding | Voice commands for bids |
| Q2 2026 | AR Auction Room | Immersive AR experience |
| Q3 2026 | Cross-Platform Sync | Seamless mobile/web transition |
| Q4 2026 | AI Auctioneer | AI-powered auction host |
21. Appendix
21.1 Glossary
| Term | Definition |
|---|---|
| Base Price | Minimum starting bid for a player |
| Purse | Budget available to a participant |
| RTM | Right to Match - ability to match winning bid |
| Unsold | Player who received no bids at base price |
21.2 References
Document Version: 3.0.0 | Last Updated: January 2026