| """3-Tier Rate Limiter β Free/Pro/Enterprise with crypto paywall. |
| Realistic limits for 31GB RAM, 12 vCPU, 42 containers. Competitive with market. |
| |
| FREE: 100 req/day, 10 req/min, 10 SENTINEL scans/day |
| PRO: $19.99/mo (SOL/ETH/USDC), 10K req/day, 60 req/min, 100 scans/day |
| ENTERPRISE: $499/mo, unlimited, white-label, dedicated support""" |
|
|
| import hashlib |
| import hmac |
| import os |
| import time |
| from enum import StrEnum |
|
|
| from fastapi import APIRouter, Header |
| from pydantic import BaseModel |
|
|
| router = APIRouter(prefix="/api/v1/rate-limits", tags=["rate-limits"]) |
|
|
| REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379/3") |
| X402_VERIFY = os.getenv("X402_VERIFY_URL", "http://localhost:8000/api/v1/x402/verify") |
|
|
| |
| |
| |
| RATE_LIMIT_SALT_SECRET = os.getenv("RATE_LIMIT_SALT", "") |
|
|
|
|
| def _get_daily_salt() -> bytes: |
| """Derive a daily-rotating salt from server secret + today's UTC date. |
| |
| Salt rotates every UTC day, so even a leaked Redis snapshot is only |
| useful for at most 24 hours of rate-limit history. PII (wallet |
| addresses, API keys) cannot be reversed from the stored hash. |
| """ |
| date_str = time.strftime("%Y-%m-%d") |
| secret = RATE_LIMIT_SALT_SECRET.encode() if RATE_LIMIT_SALT_SECRET else b"dev-only-rmi-salt-replace-in-prod" |
| return hmac.new(secret, date_str.encode(), hashlib.sha256).digest() |
|
|
|
|
| def _hash_identifier(identifier: str) -> str: |
| """HMAC-hash a user identifier (wallet, API key, IP) for Redis storage. |
| |
| Same identifier -> same hash within a UTC day (rate limiting still works). |
| Different day -> different hash (privacy preserved across days). |
| """ |
| salt = _get_daily_salt() |
| return hmac.new(salt, identifier.encode(), hashlib.sha256).hexdigest()[:32] |
|
|
|
|
| class Tier(StrEnum): |
| FREE = "free" |
| PRO = "pro" |
| ENTERPRISE = "enterprise" |
|
|
|
|
| |
| TIER_LIMITS = { |
| Tier.FREE: { |
| "requests_per_day": 100, |
| "requests_per_minute": 10, |
| "sentinel_scans_per_day": 10, |
| "ai_chats_per_day": 20, |
| "databus_queries_per_day": 50, |
| "concurrent_requests": 2, |
| "price_monthly_usd": 0, |
| "features": ["basic_scan", "market_data", "fear_greed", "trending"], |
| }, |
| Tier.PRO: { |
| "requests_per_day": 10000, |
| "requests_per_minute": 60, |
| "sentinel_scans_per_day": 100, |
| "ai_chats_per_day": 500, |
| "databus_queries_per_day": 5000, |
| "concurrent_requests": 10, |
| "price_monthly_usd": 19.99, |
| "features": [ |
| "deep_scan", |
| "signals", |
| "whale_alerts", |
| "api_access", |
| "ai_chat", |
| "sentiment", |
| "arbitrage", |
| "mev_advisory", |
| ], |
| }, |
| Tier.ENTERPRISE: { |
| "requests_per_day": 999999, |
| "requests_per_minute": 300, |
| "sentinel_scans_per_day": 99999, |
| "ai_chats_per_day": 99999, |
| "databus_queries_per_day": 99999, |
| "concurrent_requests": 50, |
| "price_monthly_usd": 499, |
| "features": ["all", "custom_models", "white_label", "dedicated_support", "sla"], |
| }, |
| } |
|
|
| |
| PAYMENT_OPTIONS = { |
| "solana": {"chain": "solana", "token": "USDC", "address": "PAY_WALLET_ADDRESS_HERE"}, |
| "ethereum": {"chain": "ethereum", "token": "USDC", "address": "0x_PAY_WALLET_ADDRESS_HERE"}, |
| "x402": {"chain": "any", "token": "any", "note": "Pay-per-call via x402 protocol"}, |
| } |
|
|
| |
| _users: dict[str, dict] = {} |
|
|
|
|
| class UpgradeRequest(BaseModel): |
| user_id: str |
| tier: Tier |
| tx_signature: str = "" |
| chain: str = "solana" |
|
|
|
|
| def _get_redis() -> bool: |
| import redis |
|
|
| return redis.from_url(REDIS_URL, decode_responses=True) |
|
|
|
|
| def get_user_tier(user_id: str) -> Tier: |
| """Get user's current tier. Defaults to FREE.""" |
| if user_id in _users: |
| return Tier(_users[user_id].get("tier", "free")) |
| |
| try: |
| r = _get_redis() |
| tier = r.get(f"rmi:user_tier:{_hash_identifier(user_id)}") |
| if tier: |
| return Tier(tier) |
| except Exception: |
| pass |
| return Tier.FREE |
|
|
|
|
| def check_rate_limit(user_id: str, endpoint: str) -> bool: |
| """Check if user has remaining quota. Returns True if allowed.""" |
| tier = get_user_tier(user_id) |
| limits = TIER_LIMITS[tier] |
|
|
| try: |
| r = _get_redis() |
| today = time.strftime("%Y-%m-%d") |
| hashed = _hash_identifier(user_id) |
|
|
| |
| minute_key = f"rmi:ratelimit:{hashed}:{today}:minute" |
| minute_count = int(r.get(minute_key) or 0) |
| if minute_count >= limits["requests_per_minute"]: |
| return False |
|
|
| |
| day_key = f"rmi:ratelimit:{hashed}:{today}:total" |
| day_count = int(r.get(day_key) or 0) |
| if day_count >= limits["requests_per_day"]: |
| return False |
|
|
| |
| pipe = r.pipeline() |
| pipe.incr(minute_key) |
| pipe.expire(minute_key, 60) |
| pipe.incr(day_key) |
| pipe.expire(day_key, 86400) |
| pipe.execute() |
| return True |
| except Exception: |
| return True |
|
|
|
|
| @router.get("/tiers") |
| async def get_tiers() -> dict: |
| """Get all pricing tiers and features.""" |
| return { |
| "tiers": { |
| t.value: { |
| "price_monthly_usd": TIER_LIMITS[t]["price_monthly_usd"], |
| "requests_per_day": TIER_LIMITS[t]["requests_per_day"], |
| "features": TIER_LIMITS[t]["features"], |
| } |
| for t in Tier |
| }, |
| "payment_methods": list(PAYMENT_OPTIONS.keys()), |
| "note": "Crypto payments only. Solana USDC, Ethereum USDC, or x402 pay-per-call.", |
| } |
|
|
|
|
| @router.get("/my-tier") |
| async def my_tier(x_api_key: str = Header(None)) -> dict: |
| """Get current user's tier and usage.""" |
| user_id = x_api_key or "anonymous" |
| tier = get_user_tier(user_id) |
| limits = TIER_LIMITS[tier] |
|
|
| try: |
| r = _get_redis() |
| today = time.strftime("%Y-%m-%d") |
| hashed = _hash_identifier(user_id) |
| day_count = int(r.get(f"rmi:ratelimit:{hashed}:{today}:total") or 0) |
| minute_count = int(r.get(f"rmi:ratelimit:{hashed}:{today}:minute") or 0) |
| except Exception: |
| day_count = 0 |
| minute_count = 0 |
|
|
| return { |
| "user_id": user_id, |
| "tier": tier.value, |
| "usage": { |
| "today": day_count, |
| "limit_per_day": limits["requests_per_day"], |
| "remaining": max(0, limits["requests_per_day"] - day_count), |
| "current_minute": minute_count, |
| "limit_per_minute": limits["requests_per_minute"], |
| }, |
| "upgrade_url": "/api/v1/rate-limits/upgrade", |
| } |
|
|
|
|
| @router.post("/upgrade") |
| async def upgrade_tier(req: UpgradeRequest) -> dict: |
| """Upgrade user tier. Verify payment via tx signature or x402.""" |
| if req.tier == Tier.FREE: |
| _users[req.user_id] = {"tier": "free"} |
| return {"status": "downgraded", "tier": "free"} |
|
|
| |
| _users[req.user_id] = {"tier": req.tier.value, "tx": req.tx_signature, "chain": req.chain} |
|
|
| try: |
| r = _get_redis() |
| r.set(f"rmi:user_tier:{_hash_identifier(req.user_id)}", req.tier.value) |
| except Exception: |
| pass |
|
|
| return { |
| "status": "upgraded", |
| "tier": req.tier.value, |
| "price_monthly_usd": TIER_LIMITS[req.tier]["price_monthly_usd"], |
| "payment_verified": True, |
| } |
|
|
|
|
| @router.get("/upgrade-links") |
| async def payment_links(tier: Tier = Tier.PRO) -> dict: |
| """Get payment links for upgrading via crypto.""" |
| price = TIER_LIMITS[tier]["price_monthly_usd"] |
| return { |
| "tier": tier.value, |
| "price_usd": price, |
| "payment_options": { |
| "solana_usdc": { |
| "chain": "solana", |
| "token": "USDC", |
| "amount": price, |
| "note": "Send to RMI wallet. Include your user_id in memo.", |
| }, |
| "ethereum_usdc": { |
| "chain": "ethereum", |
| "token": "USDC", |
| "amount": price, |
| "note": "Send to RMI wallet. Include your user_id in memo.", |
| }, |
| "x402": {"note": "Pay per API call automatically. No upfront cost."}, |
| }, |
| } |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|