diff --git a/KfChatDotNetBot/Commands/Kasino/LegitCheckCommand.cs b/KfChatDotNetBot/Commands/Kasino/LegitCheckCommand.cs
new file mode 100644
index 0000000..390b90e
--- /dev/null
+++ b/KfChatDotNetBot/Commands/Kasino/LegitCheckCommand.cs
@@ -0,0 +1,155 @@
+using System.ComponentModel;
+using System.Reflection;
+using System.Text.RegularExpressions;
+using KfChatDotNetBot.Extensions;
+using KfChatDotNetBot.Models;
+using KfChatDotNetBot.Models.DbModels;
+using KfChatDotNetBot.Services;
+using KfChatDotNetWsClient.Models.Events;
+using Microsoft.EntityFrameworkCore;
+
+namespace KfChatDotNetBot.Commands.Kasino;
+
+///
+/// Command to check a user's kasino "legitimacy" by calculating their Return-to-Player (RTP) statistics.
+/// RTP represents the percentage of wagered money returned to the player over time.
+/// An RTP of 100% means break-even, above 100% means profit, below means loss.
+///
+[KasinoCommand]
+public class LegitCheckCommand : ICommand
+{
+ public List Patterns =>
+ [
+ new Regex(@"^legitcheck (?\d+)$", RegexOptions.IgnoreCase),
+ ];
+
+ public string? HelpText => "Check a user's kasino RTP statistics";
+ public UserRight RequiredRight => UserRight.Loser;
+ public TimeSpan Timeout => TimeSpan.FromSeconds(10);
+
+ public RateLimitOptionsModel? RateLimitOptions => new()
+ {
+ MaxInvocations = 3,
+ Window = TimeSpan.FromSeconds(30)
+ };
+
+ // Minimum wagers required for a game to be considered for "luckiest game"
+ // This prevents small sample sizes from skewing results (e.g., 1 win on 1 bet = 200% RTP)
+ private const int MinWagersForLuckiestGame = 10;
+
+ public async Task RunCommand(ChatBot botInstance, MessageModel message, UserDbModel user, GroupCollection arguments,
+ CancellationToken ctx)
+ {
+ await using var db = new ApplicationDbContext();
+
+ var targetUserId = int.Parse(arguments["user_id"].Value);
+ var targetUser = await db.Users.FirstOrDefaultAsync(u => u.KfId == targetUserId, ctx);
+
+ if (targetUser == null)
+ {
+ await botInstance.SendChatMessageAsync($"{user.FormatUsername()}, the user ID you gave doesn't exist.",
+ true);
+ return;
+ }
+
+ // A user can have multiple gambler entities (e.g., if they abandoned an account or got reset).
+ // We want to aggregate stats across ALL their gambler entities for a complete picture.
+ var gamblerIds = await db.Gamblers
+ .Where(g => g.User.Id == targetUser.Id)
+ .Select(g => g.Id)
+ .ToListAsync(ctx);
+
+ if (gamblerIds.Count == 0)
+ {
+ await botInstance.SendChatMessageAsync(
+ $"{user.FormatUsername()}, {targetUser.KfUsername} has never played at the kasino.", true);
+ return;
+ }
+
+ // Only include completed wagers - incomplete ones are pending bets (e.g., event outcomes)
+ var wagers = await db.Wagers
+ .Where(w => gamblerIds.Contains(w.Gambler.Id) && w.IsComplete)
+ .ToListAsync(ctx);
+
+ if (wagers.Count == 0)
+ {
+ await botInstance.SendChatMessageAsync(
+ $"{user.FormatUsername()}, {targetUser.KfUsername} has no completed kasino wagers on record.", true);
+ return;
+ }
+
+ // RTP Calculation:
+ // - WagerAmount: The amount the user bet
+ // - WagerEffect: The net change to balance (negative for loss, positive for profit)
+ // - TotalReturned = WagerAmount + WagerEffect (what they got back)
+ // Example: Bet 100, lost -> WagerEffect = -100, Returned = 100 + (-100) = 0
+ // Example: Bet 100, won 150 total -> WagerEffect = +50 (profit), Returned = 100 + 50 = 150
+ // - RTP% = (TotalReturned / TotalWagered) * 100
+ var totalWagered = wagers.Sum(w => w.WagerAmount);
+ var totalReturned = wagers.Sum(w => w.WagerAmount + w.WagerEffect);
+ var overallRtp = totalWagered > 0 ? (totalReturned / totalWagered) * 100 : 0;
+
+ // Group wagers by game type to find per-game statistics
+ var gameStats = wagers
+ .GroupBy(w => w.Game)
+ .Select(g => new GameStatistic
+ {
+ Game = g.Key,
+ WagerCount = g.Count(),
+ TotalWagered = g.Sum(w => w.WagerAmount),
+ TotalReturned = g.Sum(w => w.WagerAmount + w.WagerEffect)
+ })
+ .ToList();
+
+ // Calculate RTP for each game (done separately to avoid division in the LINQ projection)
+ foreach (var stat in gameStats)
+ {
+ stat.Rtp = stat.TotalWagered > 0 ? (stat.TotalReturned / stat.TotalWagered) * 100 : 0;
+ }
+
+ // Find the game with highest RTP, but only if they have enough wagers to be statistically meaningful
+ var luckiestGame = gameStats
+ .Where(g => g.WagerCount >= MinWagersForLuckiestGame)
+ .MaxBy(g => g.Rtp);
+
+ // Build response
+ var response =
+ $"{user.FormatUsername()}, {targetUser.KfUsername} RTP: {overallRtp:F2}% | " +
+ $"Wagered: {await totalWagered.FormatKasinoCurrencyAsync()} | " +
+ $"Returned: {await totalReturned.FormatKasinoCurrencyAsync()} | " +
+ $"Wagers: {wagers.Count:N0}";
+
+ if (luckiestGame != null)
+ {
+ var gameName = GetGameDisplayName(luckiestGame.Game);
+ response +=
+ $" | Luckiest: {gameName} ({luckiestGame.Rtp:F2}% RTP, {luckiestGame.WagerCount:N0} wagers, {await luckiestGame.TotalWagered.FormatKasinoCurrencyAsync()} wagered)";
+ }
+
+ await botInstance.SendChatMessageAsync(response, true);
+ }
+
+ ///
+ /// Gets the display name for a WagerGame enum value.
+ /// Uses the [Description] attribute if present (e.g., LambChop -> "Lambchop"),
+ /// otherwise falls back to the enum name itself.
+ ///
+ private static string GetGameDisplayName(WagerGame game)
+ {
+ var memberInfo = typeof(WagerGame).GetMember(game.ToString()).FirstOrDefault();
+ var descriptionAttribute = memberInfo?.GetCustomAttribute();
+ return descriptionAttribute?.Description ?? game.ToString();
+ }
+
+ ///
+ /// Helper class to hold per-game statistics during calculation.
+ ///
+ private class GameStatistic
+ {
+ public WagerGame Game { get; init; }
+ public int WagerCount { get; init; }
+ public decimal TotalWagered { get; init; }
+ public decimal TotalReturned { get; init; }
+ public decimal Rtp { get; set; }
+ }
+}