using Predictalytics.Application.Interfaces; using Predictalytics.Domain.Enums; using System.Collections.Concurrent; namespace Predictalytics.Application.Services; /// /// Token-bucket rate limiter with per-platform configuration. /// public class RateLimiterService : IRateLimiter { private readonly ConcurrentDictionary _semaphores = new(); private readonly ConcurrentDictionary _lastRequest = new(); private readonly ConcurrentDictionary _blockedUntil = new(); // Minimum delay between requests per platform (milliseconds) private static readonly Dictionary PlatformDelays = new() { { PlatformType.Polymarket, 200 }, { PlatformType.Limitless, 500 }, { PlatformType.Azuro, 1000 }, { PlatformType.Myriad, 1000 }, { PlatformType.PredictFun, 1000 }, { PlatformType.Kalshi, 500 }, { PlatformType.Stake, 1000 } }; public async Task WaitAsync(PlatformType platform, CancellationToken ct = default) { var sem = _semaphores.GetOrAdd(platform, _ => new SemaphoreSlim(1, 1)); await sem.WaitAsync(ct); try { // 1. Check if we are currently blocked due to a 429 if (_blockedUntil.TryGetValue(platform, out var blockedUntil)) { var waitTime = blockedUntil - DateTime.UtcNow; if (waitTime > TimeSpan.Zero) { await Task.Delay(waitTime, ct); } } // 2. Normal token bucket delay if (_lastRequest.TryGetValue(platform, out var last)) { var delayMs = PlatformDelays.GetValueOrDefault(platform, 1000); var elapsed = (DateTime.UtcNow - last).TotalMilliseconds; if (elapsed < delayMs) await Task.Delay((int)(delayMs - elapsed), ct); } _lastRequest[platform] = DateTime.UtcNow; } finally { sem.Release(); } } public bool CanMakeRequest(PlatformType platform) { if (_blockedUntil.TryGetValue(platform, out var blockedUntil) && blockedUntil > DateTime.UtcNow) return false; if (!_lastRequest.TryGetValue(platform, out var last)) return true; var delayMs = PlatformDelays.GetValueOrDefault(platform, 1000); return (DateTime.UtcNow - last).TotalMilliseconds >= delayMs; } public void ReportRateLimitExceeded(PlatformType platform, TimeSpan? retryAfter = null) { var penalty = retryAfter ?? TimeSpan.FromSeconds(30); _blockedUntil[platform] = DateTime.UtcNow.Add(penalty); } }