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:
- A user triggers the prediction command
- EventHandlers processes the request and validates server access
- EventHandler checks if the request is existing in the database. If in database, retrieve and present to user, if not in database, continue
- PredictionHandler batches fights into groups of three to be processed by the LLM
- LLMHelper prepares fighter data and formats prompts
- The chosen AI model (GPT-4o or Claude-3.5) analyzes the match ups
- 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:
- Checks for missing or outdated stats
- Validates numerical ranges
- Cross-references fight records
- 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:
- Fighter stat refresh
- Odds data collection
- Prediction generation
- 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
- 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,
}
}
- Weight Cut Analysis
const weightHistory = {
recentWeightClasses: weightClassHistory,
weightClassChanges: changes,
daysBetweenFights: averageTurnaround,
averageDaysBetweenFights: recoveryTime
}
- 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()
}
- 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.
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:
- Creates payment order with server details
- Generates checkout URL for user
- Monitors payment status
- Verifies transaction completion
- 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:
- Creates temporary payment wallet
- Monitors for incoming transaction
- Validates payment amount
- Forwards funds to merchant wallet
- 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:
- Creates payment session
- Handles Apple Pay integration
- Processes card verification
- Manages webhook events
- 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}`);
});
}
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