Building a Discord betting alerts Python bot that monitors sports betting opportunities in real-time is one of the most practical projects for serious bettors. Unlike generic betting trackers, a Discord bot gives you instant notifications on your phone, desktop, and anywhere your Discord is active.
I've built dozens of betting automation tools, and Discord bots consistently deliver the best user experience for alert systems. The Discord API is rock-solid, the notification system works everywhere, and your betting crew can all subscribe to the same channel.
This tutorial walks through building a production-ready Discord bot that monitors the MoneyLine API for high-value betting opportunities and sends formatted alerts to your Discord server. You'll learn to integrate Discord.py, handle API rate limiting, format betting data for readability, and deploy a bot that actually helps you win.
Setting Up Your Discord Bot Environment
First, create a new Discord application and bot through the Discord Developer Portal. You'll need the bot token and channel ID where alerts will be sent.
import discord
import aiohttp
import asyncio
import json
from datetime import datetime
from typing import Dict, List, Optional
class BettingAlertsBot:
def __init__(self, discord_token: str, moneyline_api_key: str, channel_id: int):
self.discord_token = discord_token
self.api_key = moneyline_api_key
self.channel_id = channel_id
self.api_base = "https://mlapi.bet"
# Discord client setup
intents = discord.Intents.default()
intents.message_content = True
self.client = discord.Client(intents=intents)
# Alert thresholds
self.ev_threshold = 15.0 # Minimum 15% EV
self.arb_threshold = 2.0 # Minimum 2% arbitrage
# Track sent alerts to avoid spam
self.sent_alerts = set()
async def start(self):
"""Start the Discord bot and monitoring loop"""
await self.client.login(self.discord_token)
# Start monitoring in background
asyncio.create_task(self.monitor_opportunities())
await self.client.connect()
The bot initialization sets up Discord connection handling and defines alert thresholds. The sent_alerts set prevents duplicate notifications for the same opportunity.
Fetching High-Value Opportunities from MoneyLine API
The core monitoring logic queries the MoneyLine API's /v1/edge endpoint for profitable betting opportunities. This endpoint returns calculated expected value and arbitrage percentages across multiple sportsbooks.
async def fetch_edge_opportunities(self) -> List[Dict]:
"""Fetch high-EV opportunities from MoneyLine API"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json"
}
params = {
"sport": "all",
"min_ev": self.ev_threshold,
"min_arb": self.arb_threshold,
"limit": 50
}
async with aiohttp.ClientSession() as session:
try:
async with session.get(
f"{self.api_base}/v1/edge",
headers=headers,
params=params
) as response:
if response.status == 200:
data = await response.json()
return data.get("opportunities", [])
else:
print(f"API Error: {response.status}")
return []
except Exception as e:
print(f"Request failed: {e}")
return []
async def monitor_opportunities(self):
"""Main monitoring loop"""
await self.client.wait_until_ready()
channel = self.client.get_channel(self.channel_id)
if not channel:
print(f"Channel {self.channel_id} not found")
return
while not self.client.is_closed():
try:
opportunities = await self.fetch_edge_opportunities()
for opp in opportunities:
alert_key = self.generate_alert_key(opp)
if alert_key not in self.sent_alerts:
await self.send_alert(channel, opp)
self.sent_alerts.add(alert_key)
# Clean old alerts (keep last 1000)
if len(self.sent_alerts) > 1000:
self.sent_alerts = set(list(self.sent_alerts)[-500:])
# Check every 30 seconds
await asyncio.sleep(30)
except Exception as e:
print(f"Monitoring error: {e}")
await asyncio.sleep(60) # Back off on errors
The monitoring loop runs continuously, checking for new opportunities every 30 seconds. The generate_alert_key() method creates unique identifiers to prevent duplicate alerts for the same bet.
Formatting Discord Alert Messages
Discord messages need clear, actionable formatting that works well on mobile devices. The alert formatting includes emojis for quick visual scanning and structured data that's easy to read at a glance.
def generate_alert_key(self, opportunity: Dict) -> str:
"""Generate unique key for opportunity to prevent duplicate alerts"""
return f"{opportunity.get('event_id')}_{opportunity.get('market')}_{opportunity.get('selection')}_{opportunity.get('sportsbook')}"
def format_alert_message(self, opp: Dict) -> str:
"""Format opportunity as Discord message"""
ev_pct = opp.get("ev_percentage", 0)
arb_pct = opp.get("arbitrage_percentage", 0)
odds = opp.get("odds")
fair_odds = opp.get("fair_odds")
# Choose primary alert type
if ev_pct >= self.ev_threshold:
alert_type = f"๐ฅ **{ev_pct:.1f}% EV**"
elif arb_pct >= self.arb_threshold:
alert_type = f"โก **{arb_pct:.1f}% ARB**"
else:
alert_type = "๐ **VALUE BET**"
# Format odds display
odds_display = self.format_odds(odds)
fair_display = self.format_odds(fair_odds) if fair_odds else "N/A"
# Build message
message = f"{alert_type}\n\n"
message += f"**{opp.get('event_name', 'Unknown Event')}**\n"
message += f"๐ฏ {opp.get('market', 'Unknown Market')}: **{opp.get('selection', 'Unknown Selection')}**\n"
message += f"๐ฑ {opp.get('sportsbook', 'Unknown Book')}\n"
message += f"๐ฐ {odds_display} (Fair: {fair_display})\n"
if opp.get("start_time"):
start_time = datetime.fromisoformat(opp["start_time"].replace("Z", "+00:00"))
message += f"๐ {start_time.strftime('%m/%d %I:%M %p ET')}\n"
# Add confidence indicators
confidence = opp.get("confidence", "Medium")
confidence_emoji = {"High": "๐ข", "Medium": "๐ก", "Low": "๐ "}.get(confidence, "โช")
message += f"{confidence_emoji} Confidence: {confidence}"
return message
def format_odds(self, odds: float) -> str:
"""Format odds as American style"""
if not odds or odds <= 1:
return "N/A"
if odds >= 2:
return f"+{int((odds - 1) * 100)}"
else:
return f"-{int(100 / (odds - 1))}"
async def send_alert(self, channel, opportunity: Dict):
"""Send formatted alert to Discord channel"""
message = self.format_alert_message(opportunity)
# Create embed for better formatting
embed = discord.Embed(
title="๐จ Betting Alert",
description=message,
color=0xFF6B35, # Orange color
timestamp=datetime.utcnow()
)
# Add footer with API attribution
embed.set_footer(text="Powered by MoneyLine API",
icon_url="https://www.moneylineapp.com/favicon.ico")
try:
await channel.send(embed=embed)
print(f"Alert sent: {opportunity.get('event_name')} - {opportunity.get('ev_percentage')}% EV")
except Exception as e:
print(f"Failed to send alert: {e}")
The message formatting uses Discord's embed system for professional-looking alerts. Each message includes the opportunity type, event details, sportsbook, odds comparison, and timing information.
Advanced Filtering and Alert Logic
Beyond basic EV and arbitrage thresholds, production betting bots need sophisticated filtering to avoid low-quality alerts that waste your time.
def should_alert(self, opportunity: Dict) -> bool:
"""Advanced filtering logic for alert quality"""
ev_pct = opportunity.get("ev_percentage", 0)
arb_pct = opportunity.get("arbitrage_percentage", 0)
confidence = opportunity.get("confidence", "Low")
sportsbook = opportunity.get("sportsbook", "").lower()
market = opportunity.get("market", "").lower()
# Skip low-confidence opportunities below higher EV thresholds
if confidence == "Low" and ev_pct < 25:
return False
# Skip certain sportsbooks with poor limits or slow grading
blacklisted_books = ["sketchy-book", "slow-payout-book"]
if any(book in sportsbook for book in blacklisted_books):
return False
# Focus on major markets for better liquidity
priority_markets = ["moneyline", "spread", "total", "player_props"]
if not any(market_type in market for market_type in priority_markets):
return False
# Require minimum edge for different market types
market_thresholds = {
"player_props": 20.0, # Higher threshold for prop bets
"live": 30.0, # Higher threshold for live betting
"futures": 40.0 # Much higher threshold for futures
}
for market_type, min_ev in market_thresholds.items():
if market_type in market and ev_pct < min_ev:
return False
return True
async def fetch_and_filter_opportunities(self) -> List[Dict]:
"""Fetch opportunities with advanced filtering"""
raw_opportunities = await self.fetch_edge_opportunities()
# Apply quality filters
filtered_opps = [
opp for opp in raw_opportunities
if self.should_alert(opp)
]
# Sort by EV percentage descending
filtered_opps.sort(key=lambda x: x.get("ev_percentage", 0), reverse=True)
return filtered_opps[:10] # Limit to top 10 opportunities
The filtering system prevents alert fatigue by focusing on high-quality opportunities. You can customize thresholds based on your risk tolerance and preferred markets.
Running and Deploying Your Bot
To deploy your Discord betting alerts bot, you'll need API credentials and a hosting environment that supports continuous operation.
import os
from dotenv import load_dotenv
async def main():
"""Main function to start the betting alerts bot"""
load_dotenv()
# Required environment variables
discord_token = os.getenv("DISCORD_BOT_TOKEN")
moneyline_api_key = os.getenv("MONEYLINE_API_KEY")
channel_id = int(os.getenv("DISCORD_CHANNEL_ID"))
if not all([discord_token, moneyline_api_key, channel_id]):
print("Missing required environment variables")
return
# Initialize and start bot
bot = BettingAlertsBot(
discord_token=discord_token,
moneyline_api_key=moneyline_api_key,
channel_id=channel_id
)
print("Starting betting alerts bot...")
await bot.start()
if __name__ == "__main__":
asyncio.run(main())
Create a .env file with your credentials:
DISCORD_BOT_TOKEN=your_discord_bot_token_here
MONEYLINE_API_KEY=your_moneyline_api_key_here
DISCORD_CHANNEL_ID=123456789012345678
For production deployment, consider using services like Railway, Heroku, or a VPS. The bot needs to run continuously to monitor opportunities in real-time.
Monitoring Performance and Rate Limits
Production betting bots must handle API rate limits gracefully and track performance metrics to ensure reliable operation.
import time
from collections import deque
class RateLimiter:
def __init__(self, max_requests: int, window_seconds: int):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = deque()
async def wait_if_needed(self):
"""Wait if rate limit would be exceeded"""
now = time.time()
# Remove old requests outside the window
while self.requests and self.requests[0] <= now - self.window_seconds:
self.requests.popleft()
# Wait if at rate limit
if len(self.requests) >= self.max_requests:
sleep_time = self.window_seconds - (now - self.requests[0])
if sleep_time > 0:
await asyncio.sleep(sleep_time)
return await self.wait_if_needed()
self.requests.append(now)
# Add to BettingAlertsBot class
def __init__(self, ...):
# ... existing init code ...
self.rate_limiter = RateLimiter(max_requests=100, window_seconds=60)
self.metrics = {
"alerts_sent": 0,
"api_calls": 0,
"errors": 0,
"start_time": time.time()
}
The rate limiter prevents API quota exhaustion, while metrics tracking helps optimize bot performance over time. Check your MoneyLine API dashboard for usage patterns and adjust thresholds accordingly.
For more advanced API integration patterns, see our guide on steam move detection or compare the MoneyLine API against other providers in our API comparison series.
FAQ
How often should the bot check for new opportunities?
Check every 30-60 seconds for optimal balance between timeliness and API usage. More frequent polling (every 10-15 seconds) works for high-volume bettors but consumes API quota faster. Live betting opportunities change rapidly, so consider shorter intervals during peak betting hours.
What EV threshold should I set for Discord alerts?
Start with 15-20% EV for positive expected value bets. Lower thresholds (5-10%) generate too many alerts and cause notification fatigue. Higher thresholds (25%+) catch only the most valuable opportunities but may miss profitable bets. Adjust based on your betting volume and risk tolerance.
How do I prevent duplicate alerts for the same bet?
Use the generate_alert_key() method to create unique identifiers combining event ID, market, selection, and sportsbook. Store sent alert keys in a set and check before sending new alerts. Clear old keys periodically to prevent memory issues during long-running sessions.
Can I customize alerts for specific sports or markets?
Yes, modify the fetch_edge_opportunities() parameters to filter by sport, league, or market type. Add sport-specific filtering in should_alert() method. For example, set higher EV thresholds for player props (20%+) versus main markets (15%+) since prop bets typically have wider margins.
What happens if the Discord bot goes offline?
The bot will stop sending alerts until restarted. Use process managers like PM2 or systemd for automatic restarts. Consider adding health checks that ping a monitoring service. Store critical alerts in a database or log file so you can review missed opportunities when the bot comes back online.