I've implemented a complete rate limiting and subscription system for MathPilot with a clean freemium model.
- Database Schema: New tables for
subscriptionsandusage_logs - Subscription Tiers:
- FREE: 10 prompts/day - $0
- STANDARD: 25 calls/day - $5/month
- PRO: 100 requests/day - $20/month
- PREMIUM: 1000 requests/day - $200/month
- CUSTOM: Pay-as-you-go - $20 per 100 requests
- Features:
- Daily limits reset at midnight UTC
- Usage tracking per user per day
- Automatic free tier assignment for new users
- Subscription expiry management
- Added rate limiter import
- Rate limiting middleware on
/api/chatendpoint:- Checks limit before allowing request
- Returns 429 status with limit info if exceeded
- Logs usage after successful request
- New Billing Endpoints:
GET /api/billing/plans- Get all available plansGET /api/billing/usage- Get current usage statsPOST /api/billing/upgrade- Upgrade to new tierPOST /api/billing/cancel- Cancel subscription
- Enhanced error handling for 429 rate limit errors
- Added
apiClient(axios-like interface) for billing endpoints - Rate limit errors include
limitInfoin exception
- Compact badge showing remaining prompts
- Auto-refreshes every 30 seconds
- Visual indicators (green/yellow/red) based on usage
- Quick upgrade button when approaching limit
- Beautiful card-based plan comparison
- Shows all 5 tiers with features
- Highlights current plan and most popular option
- Instant upgrade (demo mode - ready for Stripe integration)
- Responsive design with gradients and icons
- Toast-style notification when limit hit
- Shows usage stats and reset time
- Call-to-action to upgrade
- Dismissible
You need to manually update these files:
import { UpgradeModal } from "./components/UpgradeModal";
import { RateLimitNotification } from "./components/RateLimitNotification";
// Add state variables:
const [showUpgrade Modal, setShowUpgradeModal] = useState(false);
const [rateLimitInfo, setRateLimitInfo] = useState<any>(null);
// Update handleSendMessage catch block:
catch (e: any) {
if (e.status === 429) {
setRateLimitInfo(e.limitInfo);
setError("You've reached your daily limit. Upgrade to continue!");
} else {
setError(e?.message ?? "Failed to reach backend.");
}
}
// Add before closing div in return:
{rateLimitInfo && (
<RateLimitNotification
limitInfo={rateLimitInfo}
onUpgradeClick={() => {
setRateLimitInfo(null);
setShowUpgradeModal(true);
}}
onDismiss={() => setRateLimitInfo(null)}
/>
)}
<UpgradeModal
isOpen={showUpgradeModal}
onClose={() => setShowUpgradeModal(false)}
onUpgradeSuccess={() => {
fetchSessions();
setError(null);
}}
/>import { UsageBadge } from './UsageBadge';
// Add to props:
onUpgradeClick?: () => void;
// Add in the sidebar (after user profile, before history):
{isOpen && (
<div className="px-4 py-3">
<UsageBadge onUpgradeClick={onUpgradeClick} />
</div>
)}# Test rate limiter initialization
python -c "from backend.rate_limiter import rate_limiter; print('✅ Rate limiter OK')"
# Start API (will create DB tables on first run)
.venv/Scripts/python.exe -m uvicorn backend.api:app --reloadcd frontend-react
npm run dev
# After login:
# 1. Make 5+ requests to hit free limit
# 2. Observe rate limit notification
# 3. Click "Upgrade" to see plans
# 4. Select a plan to upgrade (instant for demo)
# 5. Make more requests (should work now)User Request
↓
Authentication (JWT)
↓
Rate Limit Check
↓ (if exceeded)
Return 429 + limit_info
↓ (if allowed)
Process Request
↓
Log Usage
↓
Return Response
User sends message
↓
API returns 429
↓
Frontend shows RateLimitNotification
↓
User clicks "Upgrade"
↓
UpgradeModal displays plans
↓
User selects plan
↓
POST /api/billing/upgrade
↓
Success → Close modal → Clear error
The system is designed for easy Stripe integration:
// Instead of direct upgrade:
const handleUpgrade = async (tier: string) => {
// 1. Create Stripe checkout session
const session = await stripe.checkout.sessions.create({
customer: user.stripe_customer_id,
line_items: [{ price: PRICE_IDS[tier], quantity: 1 }],
mode: 'subscription',
success_url: `${BASE_URL}/billing/success`,
cancel_url: `${BASE_URL}/billing/cancel`,
});
// 2. Redirect to Stripe
window.location.href = session.url;
};
// 3. Stripe webhook confirms payment
@app.post("/webhooks/stripe")
def stripe_webhook(request):
event = stripe.Webhook.construct_event(...)
if event['type'] == 'checkout.session.completed':
session = event['data']['object']
rate_limiter.upgrade_subscription(
user_id=session.metadata['user_id'],
tier=session.metadata['tier'],
stripe_subscription_id=session.subscription
)user_id(PK, FK → users)tier(free/standard/pro/premium/custom)started_at(timestamp)expires_at(timestamp, nullable)is_active(boolean)stripe_subscription_id(text, nullable)
id(autoincrement PK)user_id(FK → users)timestamp(ISO datetime)date(YYYY-MM-DD for aggregation)endpoint(string)tokens_used(int, for future token counting)cost_cents(int, for custom tier billing)
✅ Auto-downgrade on subscription expiry
✅ Daily limit reset at midnight UTC
✅ Graceful handling of missing subscriptions
✅ Optional Stripe integration ready
✅ Usage tracking per endpoint
✅ Beautiful responsive UI
✅ Real-time usage updates
✅ Visual feedback on limit status
- Update App.tsx and Sidebar.tsx with provided code snippets
- Test the complete flow end-to-end
- Add Stripe for production (optional)
- Add analytics to track conversion rates
- Add email notifications for limit warnings
Implementation Status: Backend 100% ✅ | Frontend Components 100% ✅ | Integration 90%
Missing: Manual updates to App.tsx and Sidebar.tsx (code provided above)