Fight Genie: An AI and Node JS Powered Discord Bot for Predicting UFC Fights

Fight Genie: An AI and Node JS Powered Discord Bot for Predicting UFC Fights

From Discord Fight Nights to Fight Genie

During my paternity leave, I turned our Discord UFC fight nights into something unexpected. After seeing ChatGPT achieve 60%+ accuracy predicting fights using manual data input, I knew we could automate it. Using JavaScript and Discord.js, I built a bot combining Claude and GPT to analyze UFC statistics and predict outcomes. When it hit a 70% win rate in early events, my friends pushed me to turn it into a service – and Fight Genie was born.

Today, it’s a full system that autonomously (mostly) handles data updates, secure payment processing, fight analysis, and social media management across multiple Discord servers. As a cloud engineer by day, building a full-stack application has been quite the learning experience. Let me walk you through the technical side of how it works. Note: this blog is by no means a complete picture of exactly how Fight Genie is built but rather a mid-level technical overview in no particular order.

Just in case you don’t read to the end, here’s the link to add Fight Genie to your Discord.

Building Fight Genie: Under the Hood

Fight Genie’s service combines multiple APIs, data processing systems, and automation tools to deliver its predictions and analysis. Let’s break down how these components work together (in no particular order).

The Foundation: Discord.js and Node

The bot runs entirely on Node.js, using Discord.js library for server interactions. Discord.js handles user interaction/events and provides clean API abstractions that power features like interactive buttons, drop-down menus, and embedded messages, Discord direct messages, and more. The library’s built-in rate limiting and caching helps manage server resources.

Data Flow Architecture

The system follows a modular design pattern:

  • Event Handlers manage Discord interactions and route commands
  • Service Classes handle core logic and external API calls
  • Model Classes abstract database operations
  • Utility Classes provide shared functionality

Here’s how a typical prediction request flows through the system:

  1. A user triggers the prediction command
  2. EventHandlers processes the request and validates server access
  3. EventHandler checks if the request is existing in the database. If in database, retrieve and present to user, if not in database, continue
  4. PredictionHandler batches fights into groups of three to be processed by the LLM
  5. LLMHelper prepares fighter data and formats prompts
  6. The chosen AI model (GPT-4o or Claude-3.5) analyzes the match ups
  7. Results are stored and displayed back to Discord

The above request flow is typical for most of the buttons / features that users interact with during its use.

External Service Integration

Fight Genie connects with several APIs:

  • OpenAI’s API (GPT-4o/latest) for primary fight analysis
  • Anthropic’s API (Sonnet 3.5) for comparative predictions
  • The Odds API for live fight odds from FanDuel and Draft Kings, currently using FanDuel odds only
  • PayPal, Stripe, and Solana (crypto) for payment processing
  • Twitter/X API for automated content sharing

Database Architecture

SQL drives the data layer with key tables:

  • cache (stores misc temp data)
  • fighters (stats and career data)
  • events (fight cards and results)
  • predictions (model outputs and accuracy)
  • server_subscriptions (access control)
  • payment_logs (transaction records)
  • stored_predictions (historical prediction outcome storage)
  • promo_codes (storing active and inactive event promo codes)
  • solana_payment_service (used for Solana payment verification)
  • stripe_payment_service (used for Apple Pay / Stripe payment verification)
  • paypal_payment_service (used for PayPal payment verification)

Each table uses indices for quick look-ups, making for fast fighter stat retrievals and prediction tracking.

Performance Optimization

The bot employs specific strategies to stay responsive:

  • Batch processing for fight predictions and analysis
  • Caching for frequently accessed prediction and analysis data
  • Asynchronous operations for API calls
  • Rate limiting for external services
  • Transaction management for data integrity

This architecture lets Fight Genie scale from handling a few servers to managing many prediction requests while maintaining system performance.

UFC Fight Data Collection: Web Scraping and Processing Systems

The UFCStats.com Pipeline

Our scraping system targets UFCStats.com’s structured HTML layout. The site’s consistent formatting makes it ideal for data collection. The FighterStats utility class handles the scraping logic, using Axios for requests and Cheerio for HTML parsing.

Here’s what we collect for each fighter:

  • Striking statistics (strikes per minute, accuracy, defense)
  • Grappling metrics (takedowns, submission attempts)
  • Physical attributes (height, reach, stance)
  • Career records and fight history

Continuous Data Updates

When typing $upcoming from any channel in a server where the Fight Genie is added and purchased.

The scraping system runs through several key processes:

async updateFighterStats(fighterName) {
const stats = await this.scrapeFighterStats(fighterName);
if (!stats) return null;
await database.storeFighterStats(stats);
return stats;
}

We maintain data freshness through:

  • Pre-event fighter stat updates
  • Post-fight result processing
  • Regular validation checks
  • Error tracking and retry mechanisms

Data Validation Process

The DataValidator class ensures data quality:

  1. Checks for missing or outdated stats
  2. Validates numerical ranges
  3. Cross-references fight records
  4. Flags inconsistencies for review

Event Data Management

The system tracks:

  • Upcoming fight cards
  • Fighter match ups
  • Event details (date, location)
  • Fight results and methods

Each event update triggers:

  1. Fighter stat refresh
  2. Odds data collection
  3. Prediction generation
  4. Performance tracking

Error Handling

We implement error recovery for the following (more planned in the future):

  • Network timeout handling
  • Rate limit management
  • Data validation fallbacks
  • Automatic retry logic

Caching Strategy

The StatsManager implements a tiered caching system:

  • Short-term memory cache for active events
  • Database caching for fighter stats
  • Prediction result caching
  • Odds data temporary storage

This data collection system provides the foundation for Fight Genie’s analysis engine, ensuring reliable and current UFC statistics for the AI models to process.

Large Language Model (LLM) Integration and Fight Analysis

Let me break down the analysis pipeline that (I think) sets Fight Genie apart from simple prompting.

Unlike just asking “who wins, here’s some stats?”, we dig deep into the stats and build a complete picture.

Here’s what we feed into each fight analysis, based on the generateEnhancedPredictionsWithAI function:

const enrichedFights = await Promise.all(
    fightData.map(async (fight) => {
        // Full career statistics and fight history
        const fighter1Data = await getFighterCompleteData(fight.fighter1);
        const fighter2Data = await getFighterCompleteData(fight.fighter2);

        // Style matchup analysis
        const matchupAnalysis = await PredictionModel.analyzeFightMatchup(
            fight.fighter1, 
            fight.fighter2
        );

        // Deep metrics collection
        const [
            // Strike efficiency metrics
            fighter1Effectiveness,
            fighter2Effectiveness,

            // Last 3-5 fights performance
            fighter1RecentForm,
            fighter2RecentForm,

            // Weight cut history and recovery patterns
            fighter1WeightHistory,
            fighter2WeightHistory,

            // Head-to-head style analysis
            historicalMatchup,
            styleMatchup,

            // Physical advantages/disadvantages
            physicalComparison,
        ] = await Promise.all([
            calculateFighterEffectiveness(fighter1Data.basics, fight.fighter1),
            calculateFighterEffectiveness(fighter2Data.basics, fight.fighter2),
            getDetailedRecentForm(fight.fighter1),
            getDetailedRecentForm(fight.fighter2),
            getWeightCutHistory(fight.fighter1),
            getWeightCutHistory(fight.fighter2),
            getDetailedStyleMatchup(fight.fighter1, fight.fighter2),
            comparePhysicalAttributes(fighter1Data.basics, fighter2Data.basics)
        ]);

        return {
            ...fight,
            fighter1Stats: {
                basics: fighter1Data.basics,
                history: fighter1Data.history,
                effectiveness: fighter1Effectiveness,
                recentForm: fighter1RecentForm,
                stylistics: matchupAnalysis,
                weightCutHistory: fighter1WeightHistory,
            },
            fighter2Stats: {
                basics: fighter2Data.basics,
                history: fighter2Data.history,
                effectiveness: fighter2Effectiveness,
                recentForm: fighter2RecentForm,
                stylistics: matchupAnalysis,
                weightCutHistory: fighter2WeightHistory,
            },
            matchupAnalysis: {
                historical: historicalMatchup,
                stylistic: styleMatchup,
                physical: physicalComparison,
            },
        };
    })
);

What Sets Us Apart

  1. Fighter Effectiveness Metrics
const effectiveness = {
    striking: {
        accuracy: strAcc,
        volume: slpm,
        defense: strDef,
        differential: slpm - sapm,
        significantStrikesLanded: strikes,
        headStrikeAccuracy: headAcc,
        bodyStrikeAccuracy: bodyAcc,
        legStrikeAccuracy: legAcc,
    },
    grappling: {
        takedownAccuracy: tdAcc,
        takedownDefense: tdDef,
        takedownsPerFight: tdAvg,
        submissionsPerFight: subAvg,
        submissionAttempts: attempts,
        reversalsPerFight: reversals,
        averagePositionTime: controlTime,
    }
}
  1. Weight Cut Analysis
const weightHistory = {
    recentWeightClasses: weightClassHistory,
    weightClassChanges: changes,
    daysBetweenFights: averageTurnaround,
    averageDaysBetweenFights: recoveryTime
}
  1. Style Matchup Intelligence
const stylistic = {
    stylistic_comparison: {
        fighter1_tendencies: {
            finishing_preference: methodAnalysis,
            position_preference: positionAnalysis,
        },
        fighter2_tendencies: {
            finishing_preference: methodAnalysis,
            position_preference: positionAnalysis,
        }
    },
    style_clash_rating: calculateStyleClashRating()
}
  1. Performance Trend Analysis
const recentForm = {
    fights: recentFights,
    trend: analyzeFightTrend(),
    consistency: analyzeFightConsistency(),
    finishRate: calculateFinishRate(),
    winStreak: calculateWinStreak()
}

This is vastly different from a simple prompt like “Who wins between Fighter A and Fighter B?” because:

  • We analyze dozens of performance metrics per fighter
  • Track historical patterns and style matchups
  • Consider physical attributes and recent performance
  • Evaluate weight cut impacts
  • Analyze common opponent performances
  • Calculate statistical edges against betting lines
  • Track finish rates and method preferences
  • Consider cardio and pace metrics
  • Evaluate defensive vulnerabilities
  • Analyze strike placement tendencies

The LLM receives all this structured data along with enhanced contextual prompting to generate predictions that consider far more factors than a human could manually analyze. This is why Fight Genie can identify subtle advantages and value opportunities that might not be obvious from surface-level analysis.

Processing in Batches

We handle fights in groups of three to optimize API usage and response times. Each batch includes:

  • Fighter career statistics
  • Recent performance metrics
  • Style matchup analysis
  • Physical comparison data
  • Betting odds integration

Prompt Engineering

Our prompts focus on specific aspects:

const prompt = `Analyze the following fight data:
${JSON.stringify(formattedData, null, 2)}

For each fight, evaluate:

1. Style Matchup Dynamics:
- How each fighter's style matches up against their opponent
- Historical performance against similar fighting styles
- Key technical advantages/disadvantages
- Range management and distance control

2. Statistical Edge Analysis:
- Significant strike differentials and accuracy
- Defensive metrics and vulnerability patterns
- Grappling efficiency and control time
- Phase transition success rates

3. Form and Momentum:
- Recent performance trends
- Quality of competition faced
- Improvements or declines in key areas
- Recovery and durability factors

4. Fight-Specific Factors:
- Weight class dynamics and size advantages
- Cardio and pace considerations
- Tournament/rankings implications
- Location and venue impact

Response Format

The AI returns structured JSON predictions:

{
    "fights": [{
        "fighter1": "Name",
        "fighter2": "Name",
        "predictedWinner": "Name",
        "confidence": 75,
        "method": "KO/TKO/Submission/Decision",
        "reasoning": "Detailed analysis",
        "probabilityBreakdown": {
            "ko_tko": 45,
            "submission": 15,
            "decision": 40
        }
    }]
}

Model Selection

The system uses GPT-4o and Claude-3.5 depending which model is selected, defaulting to GPT-4o.

if (model === "gpt") {
    return await generatePredictionsWithGPT(enhancedPrompt);
} else {
    return await generatePredictionsWithClaude(enhancedPrompt);
}

Performance Tracking

When running $stats Fight Genie will check the predictions for the most recent event against the actual event results at UFCStats.com and write them to its database. Fight Genie then calculates the results and displays them in a nice to read channel message.

The ModelStatsCommand tracks each model’s performance:

  • Win rate percentage
  • lock rate percentage
  • Method prediction accuracy
  • Confidence correlation

This analytical system enables Fight Genie’s predictions.

Our Detailed DM Breakdowns

Fight Genie’s detailed analysis system sends full fight breakdowns via DMs, offering deeper insights than possible in server channels. This system lets users dive into the reasoning behind predictions.

Button to send detailed card analysis to Discord DMs
Partial screenshot of our UFC 310 Prelims DM breakdown

Code Implementation

Here’s how the detailed analysis flows:

static async sendDetailedAnalysis(interaction, predictions, event, model) {
    try {
        const modelName = model === "gpt" ? "GPT-4o" : "Claude-3.5";
        const modelEmoji = model === "gpt" ? "🧠" : "🤖";

        // Get predictions for both cards
        const mainCardPredictions = await this.getStoredPrediction(event.event_id, "main", model);
        const prelimPredictions = await this.getStoredPrediction(event.event_id, "prelims", model);

        // Process each fight with rich analysis
        const sections = this.splitAnalysis(fight);
        for (const section of sections) {
            if (fieldCount >= MAX_FIELDS) {
                mainCardEmbeds.push(currentEmbed);
                currentEmbed = createNewEmbed(mainCardEmbeds.length + 1, 'main');
                fieldCount = 0;
            }
            currentEmbed.addFields(section);
            fieldCount++;
        }

Analysis Structure

Each DM contains detailed sections:

static splitAnalysis(fight) {
    return [
        {
            name: '🥊 Fight',
            value: [
                `**${fight.fighter1} vs ${fight.fighter2}**`,
                `${this.getConfidenceEmoji(fight.confidence)} Prediction: ${fight.predictedWinner}`,
                `${this.getMethodEmoji(fight.method)} Method: ${fight.method}`
            ].join('\n')
        },
        {
            name: '📊 Probability',
            value: [
                `KO/TKO: ${fight.probabilityBreakdown?.ko_tko}%`,
                `Submission: ${fight.probabilityBreakdown?.submission}%`,
                `Decision: ${fight.probabilityBreakdown?.decision}%`
            ].join('\n')
        }
    ];
}

Key Analysis Components

The detailed breakdown includes:

  • Style matchup evaluations
  • Statistical advantages
  • Recent performance impacts
  • Physical comparisons
  • Betting value identification

Value for Users

This system provides:

  • In-depth reasoning behind predictions
  • Confidence level explanations
  • Method probability breakdowns
  • Key statistical edges
  • Betting line analysis

Format Management

The system handles Discord’s DM/embed limitations:

const MAX_FIELDS = 25;
if (fieldCount >= MAX_FIELDS) {
    // Split into new embed when limit reached
    mainCardEmbeds.push(currentEmbed);
    currentEmbed = createNewEmbed(mainCardEmbeds.length + 1, 'main');
    fieldCount = 0;
}

Error Handling

The system includes error management to handle when someone doesn’t have DMs enabled and general errors:

try {
    await interaction.user.send({ embeds: [embed] });
} catch (dmError) {
    if (dmError.code === 50007) {
        await interaction.editReply({
            content: "❌ Unable to send detailed analysis. Please enable DMs.",
            ephemeral: true
        });
    }
}

This detailed analysis system turns raw data and AI generated insights into actionable fight intelligence, giving one more understanding of each prediction’s reasoning.

Fight Genie’s Payment Processing Systems

PayPal Integration

PayPal serves as our primary USD payment processor, handling standard credit/debit transactions:

static async createPaymentOrder(userId, serverId, amount, paymentType) {
    const orderData = {
        intent: "CAPTURE",
        purchase_units: [{
            reference_id: `${userId}-${serverId}`,
            amount: {
                currency_code: "USD",
                value: amount.toFixed(2)
            },
            description: paymentType === 'SERVER_LIFETIME' ?
                'Fight Genie Lifetime Server Access' :
                'Fight Genie Event Access'
        }],
        application_context: {
            brand_name: 'Fight Genie',
            user_action: 'PAY_NOW',
            return_url: 'https://discord.com/channels/@me'
        }
    };

The PayPal flow:

  1. Creates payment order with server details
  2. Generates checkout URL for user
  3. Monitors payment status
  4. Verifies transaction completion
  5. Activates server access upon confirmation

Solana Cryptocurrency Processing

Solana payments offer a 10% discount and use a temporary wallet system:

static async generateSolanaPaymentAddress(serverId, paymentType) {
    const intermediateWallet = Keypair.generate();
    const paymentId = `SOL-${Date.now()}-${Math.random().toString(36).substring(7)}`;

    // Store payment request
    await database.query(`
        INSERT INTO solana_payments (
            payment_id,
            payment_address,
            status,
            expected_amount
        ) VALUES (?, ?, 'PENDING', ?)
    `, [paymentId, intermediateWallet.toString(), amount]);

The Solana verification process:

  1. Creates temporary payment wallet
  2. Monitors for incoming transaction
  3. Validates payment amount
  4. Forwards funds to merchant wallet
  5. Records transaction details

Stripe for Apple Pay Integration

Stripe handles Apple Pay/Card transactions:

static async createPaymentSession(serverId, paymentType, userId) {
    const session = await stripe.checkout.sessions.create({
        payment_method_types: ['card'],
        line_items: [{
            price_data: {
                currency: 'usd',
                product_data: {
                    name: paymentDetails.name,
                    description: paymentDetails.description
                },
                unit_amount: Math.round(paymentDetails.price * 100)
            },
            quantity: 1
        }],
        mode: 'payment',
        metadata: {
            server_id: serverId,
            payment_type: paymentType
        }
    });

The Stripe flow includes:

  1. Creates payment session
  2. Handles Apple Pay integration
  3. Processes card verification
  4. Manages webhook events
  5. Confirms payment completion

Shared Payment Infrastructure

All payment methods share core functionality:

static async activateServerAccess(serverId, paymentId, paymentType) {
    if (paymentType === 'SERVER_LIFETIME') {
        await this.activateServerLifetimeSubscription(serverId, paymentId);
    } else {
        // Set expiration to 1:30 AM EST post-event
        const eventDate = new Date(event.Date);
        const expirationDate = new Date(eventDate);
        expirationDate.setDate(eventDate.getDate() + 1);
        expirationDate.setHours(1, 30, 0, 0);

        await this.activateServerEventAccess(serverId, paymentId);
    }
}

Common features across processors:

  • Transaction logging
  • Access activation
  • Error handling
  • Payment verification
  • User notifications
  • Database updates

Each payment system provides specific advantages while maintaining consistent server access management and transaction recording.

Our Social Media Brain

The TweetAutomation class handles Fight Genie’s social media presence with scheduled, data-driven content:

class TweetAutomation {
    static async scheduleTweets() {
        // Weekly State Report - Mondays
        schedule.scheduleJob('weekly-state', '26 18 * * 1', async () => {
            const weeklyThread = await this.generateWeeklyState();
        });

        // Value Picks - Tuesdays
        schedule.scheduleJob('value-picks', '53 11 * * 2', async () => {
            const valuePicks = await this.getValuePicks();
        });

        // UFC History & Event Promo - Wednesdays
        schedule.scheduleJob('ufc-history-promo', '40 19 * * 3', async () => {
            const historyPrompt = await this.generateHistoricalContent();
        });

Console Monitoring System

The system provides real-time tweet scheduling visibility in the console of the server running the bot:

async showUpcomingTweets() {
    console.log('\n=== 📅 FIGHT GENIE TWEET SCHEDULE ===\n');
    Object.entries(scheduledJobs).forEach(([name, job]) => {
        const nextRun = job.nextInvocation();
        console.log(`\n${name}:`);
        console.log(`Next Run: ${nextRun?.toLocaleString()}`);
        console.log(`Pattern: ${job.cronPattern}`);
    });
}
Console Monitoring System

Content Generation Types

The system generates various tweet types to be sent out via TwitterAPIv2.

Below is an example of our UFC History Promo tweet that we send out weekly. Other scheduled and reoccurring tweets are sent out using this same method.

    // UFC History & Event Promo - Tuesdays at 7:40 PM
    schedule.scheduleJob('ufc-history-promo', '40 19 * * 3', async () => {
        try {
            if (!(await this.checkTweetSent('ufc_history_promo'))) {
                console.log('Executing UFC history and event promo');
                const event = await this.getUpcomingEvent();

                if (!event) {
                    console.log('No upcoming event found');
                    return;
                }

                const historyPrompt = {
                    role: "system",
                    content: `You are an expert UFC historian. Create an interesting historical UFC fact that:
                    1. Is verifiably true and significant
                    2. Would be fascinating to both casual and hardcore MMA fans
                    3. Has some thematic or narrative connection to ${event.Event}
                    4. Involves either:
                       - A similar matchup/situation
                       - The same weight class
                       - The same venue
                       - A similar stakes/title situation
                       - Or a relevant record/milestone
                    5. Would make fans more excited for the upcoming event
                    
                    Focus on dramatic moments, significant records, or compelling narratives.
                    Include specific details like dates and numbers where relevant.
                    Consider current storylines and fan interests.
                    Look for unique angles that highlight the significance of the upcoming event.`
                };

                const historyCompletion = await this.openai.chat.completions.create({
                    model: "chatgpt-4o-latest",
                    messages: [historyPrompt],
                    temperature: 0.9
                });

                const tweet = await this.generateTweet({
                    historical: {
                        fact: historyCompletion.choices[0].message.content,
                        generated: true
                    },
                    event: event
                }, 'ufc_history');

                if (!this.testMode) {
                    await this.twitter.v2.tweet(tweet);
                }
                await this.logTweet('ufc_history_promo', event.event_id, tweet);
                console.log('Posted UFC history and event promo');
            }
        } catch (error) {
            console.error('Error posting UFC history promo:', error);
        }
    });

Tweet Categories and Timing

  • Model Performance Updates (Sundays, 2 PM)
  • Weekly State Reports (Mondays, 6:26 PM)
  • Value Picks (Tuesdays, 10:23 PM)
  • Historical Facts (Wednesdays, 7:40 PM)
  • Event Promos (Event day specific)

Promo Code Distribution

async scheduleEventPromoTweets(event) {
    const promoCodes = await database.query(`
        SELECT code 
        FROM promo_codes 
        WHERE current_uses = 0 
        LIMIT 3
    `);

    for (let i = 0; i < promoCodes.length; i++) {
        const hour = Math.floor(Math.random() * (endHour - startHour)) + startHour;
        const minute = Math.floor(Math.random() * 60);
        const tweetTime = new Date(eventDate);
        tweetTime.setHours(hour, minute);

Performance Tracking

Each tweet category tracks:

  • Engagement metrics
  • Click-through rates
  • Promo code usage
  • Model performance stats
  • Historical accuracy

Test Mode System

If an admin in our home discord server types $testposts all posts that are scheduled will be sent to a file to review before they are sent out via the Twitter.v2 API.

if (this.testMode) {
    await this.logTweet('MODEL STATS THREAD', tweets.join('\n\n'));
} else {
    let lastTweetId;
    for (const tweet of tweets) {
        const response = lastTweetId ?
            await this.twitter.v2.reply(tweet, lastTweetId) :
            await this.twitter.v2.tweet(tweet);
        lastTweetId = response.data.id;
    }
}

The system ensures consistent social media presence while maintaining some engagement through varied content types and strategic timing. The console monitoring provides clear visibility into scheduled content, while the test mode allows for content verification before posting. We plan to add different types of content and AI generated replies in the future.

Promo Code System

The system manages event-specific access codes through a structured database approach:

class PromoCommand {
    static async validatePromoCode(code, serverId) {
        const promoData = await database.query(`
            SELECT p.*, e.Event, e.Date
            FROM promo_codes p
            JOIN events e ON p.event_id = e.event_id
            WHERE p.code = ?
            AND p.current_uses < p.max_uses
            AND p.is_active = TRUE
            AND e.Date = (
                SELECT MIN(Date)
                FROM events
                WHERE Date > date('now')
            )
        `, [code]);

Code Generation and Management

Creating new codes for upcoming events:

static async handleCreateNextEventCodes(message, count = 10) {
    const nextEvent = await database.query(`
        SELECT event_id 
        FROM events 
        WHERE Date > date('now') 
        ORDER BY Date ASC 
        LIMIT 1
    `);

    const codes = [];
    for (let i = 0; i < count; i++) {
        const code = 'FG' + Math.random().toString(36).substring(2, 8).toUpperCase();
        await database.query(`
            INSERT INTO promo_codes (code, event_id, max_uses)
            VALUES (?, ?, 1)
        `, [code, nextEvent[0].event_id]);
        codes.push(code);
    }

Redemption Process

When a user redeems a code:

static async redeemPromoCode(code, serverId, event) {
    await database.query('BEGIN TRANSACTION');

    // Increment usage count
    await database.query(`
        UPDATE promo_codes
        SET current_uses = current_uses + 1
        WHERE code = ?
    `, [code]);

    // Set expiration to 1:30 AM EST post-event
    const eventDate = new Date(event.Date);
    const expirationDate = new Date(eventDate);
    expirationDate.setDate(eventDate.getDate() + 1);
    expirationDate.setHours(1, 30, 0, 0);

    // Create subscription
    await database.query(`
        INSERT INTO server_subscriptions (
            server_id,
            subscription_type,
            status,
            event_id,
            expiration_date,
            promo_code
        ) VALUES (?, 'EVENT', 'ACTIVE', ?, ?, ?)
    `, [serverId, event.event_id, expirationDate.toISOString(), code]);

Validation Checks

The system verifies several conditions:

if (!promoData?.[0]) {
    return { valid: false, reason: '❌ Invalid or expired promo code.' };
}

const existingAccess = await database.query(`
    SELECT * FROM server_subscriptions
    WHERE server_id = ?
    AND event_id = ?
    AND status = 'ACTIVE'
`, [serverId, promoData[0].event_id]);

if (existingAccess?.length > 0) {
    return { valid: false, reason: '❌ Server already has access.' };
}

Admin Management

Admins can check promo code status:

static async handleCheckPromos(message) {
    const codes = await database.query(`
        SELECT 
            p.code,
            p.max_uses,
            p.current_uses,
            p.is_active,
            e.Event as event_name,
            e.Date as event_date,
            (SELECT COUNT(*) 
             FROM server_subscriptions 
             WHERE promo_code = p.code) as total_redeemed
        FROM promo_codes p
        JOIN events e ON p.event_id = e.event_id
        ORDER BY p.code_id ASC
    `);

Social Media Distribution

The system integrates with tweet automation for code distribution:

async schedulePromoTweets(event) {
    const promoCodes = await database.query(`
        SELECT code 
        FROM promo_codes 
        WHERE current_uses = 0 
        LIMIT 3
    `);

    if (promoCodes?.length) {
        const promoTweet = `🎯 Fight Genie ${event.Event} - FREE ACCESS CODE

${promoCodes[i].code}

To redeem:
1. Add our bot to your server
2. Type $promo "${promoCodes[i].code}"
3. Follow @FightGenie`;

User Experience

Upon successful redemption, users receive confirmation:

const embed = new EmbedBuilder()
    .setColor('#00ff00')
    .setTitle('✅ Promo Code Redeemed!')
    .setDescription([
        `Successfully activated event access for ${guildName}!`,
        '',
        '🎯 Valid only for:',
        `Event: ${validation.event.Event}`,
        `Date: ${new Date(validation.event.Date).toLocaleDateString()}`,
        `Access expires: ${new Date(result.expirationDate).toLocaleString()}`
    ].join('\n'));

This promo code system provides controlled access distribution while maintaining proper database integrity and user experience through the redemption process.

Closing Thoughts

Building Fight Genie has taken me beyond my usual comfort zone of Azure cloud infrastructure and automation. As a cloud engineer, I typically focus on deploying and managing solutions – building this full stack application has been an amazing learning experience and a breath of fresh air for me.

The project pulled together various technologies I’d never explored in depth before (but always wanted to): Discord.js, LLM APIs, web scraping, payment processing, and social media automation. Each component brought its own challenges and learning opportunities. The satisfaction of seeing users interact with something I built from scratch is quite different from my usual work optimizing cloud resources and automating deployments.

The prediction battle between GPT-4o and Claude-3.5 adds an interesting element to the project. Based on what I’ve seen of Claude’s analytical capabilities during development, I suspect it might pull ahead in prediction accuracy over the coming events. But that’s what makes this project exciting – we’re tracking real performance metrics between these AI models in a practical application.

In the future, I’ll be writing another technical piece focusing specifically on the infrastructure side – how we’re handling deployment, scaling, and monitoring. That’s more in my wheelhouse as a cloud engineer, but it’s been invaluable seeing both sides of the application lifecycle through this project.

Looking ahead, I’m excited to expand Fight Genie’s capabilities and potentially bring other developers (friends) on board. We’ve built a solid foundation, but there’s so much room for new features and improvements. Whether Claude proves to be the better fight predictor or not, the project has already taught me more than I expected about full-stack development, AI integration, and building user-focused services.

For now, it’s time to let the models battle it out and see which one predicts better!

If you want to add Fight Genie to your discord you can head over to https://fightgenie.ai to get started.

Thanks for reading this far!

-Rudy


Relevant Links:

https://github.com/RCFromCLE/fightgenie-ufc-bot
https://discord.js.org/docs/packages/discord.js/main
https://solana.com/docs/clients/javascript
https://developer.paypal.com/sdk/js/reference/
https://docs.stripe.com/js
https://the-odds-api.com/liveapi/guides/v4/#overview
https://www.npmjs.com/package/node-schedule
https://developer.x.com/en/docs/x-for-websites/javascript-api/overview

Leave a Reply