76 KiB
Credits & Limits System - Implementation Plan
Date: January 5, 2026
Status: 🚧 IMPLEMENTATION PLAN - Pending Execution
Prepared For: IGNY8 Platform
Purpose: Comprehensive plan to simplify, optimize, and properly enforce credits and limits system
Executive Summary
This implementation plan addresses the IGNY8 platform's credits and limits system based on complete codebase analysis.
Core Philosophy: SIMPLICITY
Keep ONLY 4 Hard Limits:
- Sites - How many sites per account
- Team Users - How many team members
- Keywords - Total keywords in workspace
- Ahrefs Queries - Monthly research queries (NEW)
Everything Else = Credits - Let users consume credits however they want (content, images, ideas, etc.)
What We're Doing
✅ REMOVE all unnecessary monthly limits (content_ideas, content_words, images_basic, images_premium, image_prompts)
✅ SIMPLIFY to credit-based system only
✅ ENFORCE the 4 hard limits properly
✅ ELIMINATE data duplication across pages
✅ REDESIGN Usage page with multi-dimensional insights
✅ IMPLEMENT Ahrefs keyword research structure
✅ VALIDATE all credit balance checks
✅ USE user-friendly terminology (no "API", "operations", etc.)
1. FINAL SIMPLIFIED MODEL
1.1 The Only Limits That Matter
| Limit | Type | Description | Enforcement |
|---|---|---|---|
| Sites | Hard | Max sites per account | ✅ Keep & Enforce |
| Team Users | Hard | Max team members | ✅ Keep & Enforce |
| Keywords | Hard | Total keywords in workspace | ✅ Keep & Enforce |
| Ahrefs Queries | Monthly | Research queries per month | 🆕 Implement |
1.2 What Gets REMOVED
❌ REMOVE These Limits (Use Credits Instead):
max_content_ideas- Let credits control thismax_content_words- Let credits control thismax_images_basic- Let credits control thismax_images_premium- Let credits control thismax_image_prompts- Let credits control thismax_clusters- Let credits control this (or combine with keywords)usage_content_ideas- Not neededusage_content_words- Not neededusage_images_basic- Not neededusage_images_premium- Not neededusage_image_prompts- Not needed
Why Remove?
- Confusing for users ("I have credits but can't generate content?")
- Double limiting (credits + monthly limits)
- Maintenance overhead
- Credit system already provides control
1.3 Keyword Research Structure (NEW)
Two Ways to Add Keywords:
┌─────────────────────────────────────────────────────────────────┐
│ KEYWORD RESEARCH OPTIONS │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Option 1: IGNY8 Pre-Researched Keywords (FREE) │
│ ──────────────────────────────────────────── │
│ • Access global keyword database │
│ • Filter by industry, sector, country │
│ • Pre-analyzed for search volume, difficulty │
│ • Free to browse and add to workspace │
│ • Limited by: max_keywords (total workspace limit) │
│ │
│ Option 2: Ahrefs Live Research (LIMITED) │
│ ──────────────────────────────────────────── │
│ • Query Ahrefs API directly │
│ • Get fresh, custom keyword data │
│ • Monthly query limit (e.g., 50-500 depending on plan) │
│ • Limited by: max_ahrefs_queries (monthly limit) │
│ • Results can be added to workspace (counts toward max_keywords)│
│ │
└─────────────────────────────────────────────────────────────────┘
Implementation:
- Rename current "Add Keywords" to "Browse Pre-Researched Keywords"
- Add new tab/section "Research with Ahrefs" (with monthly limit indicator)
- Show remaining queries: "You have 42/50 Ahrefs queries remaining this month"
2. Current System Analysis (What We Found)
1.1 Frontend Pages Breakdown
Plans & Billing Dropdown (/account/plans)
| Page | Route | Primary Purpose | Current Data Displayed |
|---|---|---|---|
| Current Plan | /account/plans |
View active subscription | Plan name, price, renewal date, included credits |
| Upgrade Plan | /account/plans/upgrade |
Compare and purchase plans | Pricing table with all plans, features, limits |
| History | /account/plans/history |
Invoices and payments | Invoice list, payment methods, transaction history |
File: frontend/src/pages/account/PlansAndBillingPage.tsx (881 lines)
Tab Structure:
type TabType = 'plan' | 'upgrade' | 'invoices';
Key Data Shown:
- Current plan details (name, price, billing cycle)
- Upgrade options (pricing table with all plans)
- Credit balance (via
getCreditBalance()) - Billing history (invoices, payments)
- Payment methods management
Usage Dropdown (/account/usage)
| Page | Route | Primary Purpose | Current Data Displayed |
|---|---|---|---|
| Limits & Usage | /account/usage |
Track plan limits | Hard limits + monthly limits with progress bars |
| Credit History | /account/usage/credits |
View credit transactions | Transaction log (purchases, deductions, adjustments) |
| Activity | /account/usage/activity |
Monitor API operations | API call statistics by operation type |
File: frontend/src/pages/account/UsageAnalyticsPage.tsx (266 lines)
Tab Structure:
type TabType = 'limits' | 'activity' | 'api';
Key Data Shown:
- Usage summary (hard limits: sites, users, keywords, clusters)
- Monthly limits (content ideas, words, images)
- Credit balance and monthly usage
- Transaction history
- API activity by operation type
1.2 Backend Models
Account Model (backend/igny8_core/auth/models.py)
Credits Field:
credits = models.IntegerField(default=0, validators=[MinValueValidator(0)])
Monthly Usage Tracking Fields:
usage_content_ideas = models.IntegerField(default=0) # Monthly limit tracking
usage_content_words = models.IntegerField(default=0) # Monthly limit tracking
usage_images_basic = models.IntegerField(default=0) # Monthly limit tracking
usage_images_premium = models.IntegerField(default=0) # Monthly limit tracking
usage_image_prompts = models.IntegerField(default=0) # Monthly limit tracking
usage_period_start = models.DateTimeField(null=True) # Billing period tracking
usage_period_end = models.DateTimeField(null=True) # Billing period tracking
Plan Model
Hard Limits (Never Reset):
max_sites = models.IntegerField(default=1) # Sites allowed per account
max_users = models.IntegerField(default=1) # Team members allowed
max_keywords = models.IntegerField(default=1000) # Total keywords allowed
max_clusters = models.IntegerField(default=100) # Total clusters allowed
Monthly Limits (Reset on Billing Cycle):
max_content_ideas = models.IntegerField(default=300) # Ideas per month
max_content_words = models.IntegerField(default=100000) # Words per month
max_images_basic = models.IntegerField(default=300) # Basic images per month
max_images_premium = models.IntegerField(default=60) # Premium images per month
max_image_prompts = models.IntegerField(default=300) # Image prompts per month
Credits:
included_credits = models.IntegerField(default=0) # Monthly credit allocation
extra_credit_price = models.DecimalField(default=0.01) # Price per additional credit
2. Data Flow Analysis
2.1 Credit Deduction Flow
┌─────────────────────────────────────────────────────────────────┐
│ CREDIT DEDUCTION WORKFLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. User triggers AI operation (e.g., generate content) │
│ ↓ │
│ 2. Backend service calls CreditService │
│ ↓ │
│ 3. Check balance: CreditService.check_credits(account, credits)│
│ ↓ │
│ 4. Execute AI operation (OpenAI/Runware/etc.) │
│ ↓ │
│ 5. AI returns tokens used (input + output) │
│ ↓ │
│ 6. Calculate credits: CreditService.calculate_credits_from_tokens()│
│ • Lookup AIModelConfig.tokens_per_credit for model │
│ • credits = ceil(total_tokens / tokens_per_credit) │
│ ↓ │
│ 7. Deduct credits: │
│ • Create CreditTransaction (amount=-credits) │
│ • Create CreditUsageLog (operation details) │
│ • Update Account.credits │
│ ↓ │
│ 8. Return result to user │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Services:
CreditService.check_credits()- Pre-flight balance checkCreditService.calculate_credits_from_tokens()- Token-based calculationCreditService.deduct_credits_for_operation()- Deduct and log
2.2 Limit Enforcement Flow
┌─────────────────────────────────────────────────────────────────┐
│ LIMIT ENFORCEMENT WORKFLOW │
├─────────────────────────────────────────────────────────────────┤
│ │
│ HARD LIMITS (Sites, Users, Keywords, Clusters) │
│ ─────────────────────────────────────────────── │
│ 1. User attempts to create new resource │
│ ↓ │
│ 2. LimitService.check_hard_limit(account, limit_type, count) │
│ • Query database for current count │
│ • Compare: current_count + new_count <= plan.max_XXX │
│ ↓ │
│ 3. If exceeded: Raise HardLimitExceededError │
│ If OK: Allow creation │
│ │
│ MONTHLY LIMITS (Ideas, Words, Images, Prompts) │
│ ───────────────────────────────────────────── │
│ 1. User attempts AI operation │
│ ↓ │
│ 2. LimitService.check_monthly_limit(account, limit_type, amount)│
│ • Read from Account.usage_XXX field │
│ • Compare: current_usage + amount <= plan.max_XXX │
│ ↓ │
│ 3. If exceeded: Raise MonthlyLimitExceededError │
│ If OK: Proceed with operation │
│ ↓ │
│ 4. After operation: LimitService.increment_usage() │
│ • Update Account.usage_XXX field │
│ │
└─────────────────────────────────────────────────────────────────┘
Key Services:
LimitService.check_hard_limit()- Check persistent limitsLimitService.check_monthly_limit()- Check monthly allowancesLimitService.increment_usage()- Update monthly usage countersLimitService.get_usage_summary()- Get all limits and usage
3. Current Limits Configuration
3.1 Items That ARE Limit-Based (User-Requested)
| Item | Type | Plan Field | Enforcement Location | Status |
|---|---|---|---|---|
| Sites | Hard Limit | max_sites |
Account creation, Site model | ✅ Enforced |
| Team Users | Hard Limit | max_users |
User invite, SiteUserAccess model | ✅ Enforced |
| Keywords | Hard Limit | max_keywords |
Keyword import/creation | ⚠️ PARTIAL (see below) |
3.2 Items That Should Have Limits (Future)
| Item | Type | Plan Field | Status | Notes |
|---|---|---|---|---|
| Ahrefs Queries | Monthly Limit | max_ahrefs_queries (new) |
❌ Not Implemented | Future feature for keyword research |
3.3 Items NOT Limit-Based (Credit-Based)
| Item | Enforcement | Notes |
|---|---|---|
| AI Content Generation | Credits | Token-based calculation via AIModelConfig |
| AI Image Generation | Credits | Fixed credits per image (1, 5, or 15) |
| AI Clustering | Credits | Token-based calculation |
| AI Idea Generation | Credits | Token-based calculation |
| Internal Linking | Credits | 8 credits per content piece |
| SEO Optimization | Credits | 1 credit per 200 words |
Important: Monthly limits exist for these (max_content_ideas, max_content_words, max_images_basic, etc.) but they serve as soft limits for cost control, not hard enforcement. Users are primarily constrained by credits.
4. Data Duplication Issues
4.1 Problem: Overlapping Data Across Pages
Current Situation:
| Data Type | Plans & Billing | Usage Analytics | Duplication Level |
|---|---|---|---|
| Credit Balance | ✅ Shown in Current Plan tab | ✅ Shown in all tabs (top cards) | 🔴 HIGH |
| Monthly Usage | ✅ Credits used this month | ✅ Credits used this month | 🔴 HIGH |
| Plan Limits | ✅ Shown in plan details | ✅ Full limits panel with progress bars | 🟡 MEDIUM |
| Credit Transactions | ✅ Billing History tab | ✅ Credit History tab | 🔴 HIGH |
Example of Duplication:
Plans & Billing - Current Plan Tab:
<Card>
<h3>Current Plan: {planName}</h3>
<div>Credits: {creditBalance.credits}</div>
<div>Used This Month: {creditBalance.credits_used_this_month}</div>
<div>Monthly Allowance: {creditBalance.plan_credits_per_month}</div>
</Card>
Usage Analytics - Limits Tab:
<Card>
<div>Credits Left: {creditBalance.credits}</div>
<div>Credits Used This Month: {creditBalance.credits_used_this_month}</div>
<div>Your Monthly Limit: {creditBalance.plan_credits_per_month}</div>
</Card>
4.2 Problem: Confusing User Journey
Current User Experience Issues:
-
Credit Balance appears in BOTH dropdowns
- Plans & Billing → Current Plan → Shows credit balance
- Usage → All tabs → Shows credit balance in top cards
-
Transaction History appears in BOTH dropdowns
- Plans & Billing → History → Shows invoices and payments
- Usage → Credit History → Shows credit transactions
-
Plan Limits appears in BOTH dropdowns
- Plans & Billing → Current Plan → Shows plan features and limits
- Usage → Limits & Usage → Shows detailed progress bars for all limits
User Confusion:
- "Where do I check my credits?" → Two places
- "Where do I see my usage?" → Two places
- "Where do I see my transaction history?" → Two places
5. Enforcement Analysis
5.1 Well-Enforced Limits ✅
Sites (max_sites)
Enforcement Location: backend/igny8_core/auth/models.py (Site model)
# When creating a new site
LimitService.check_hard_limit(account, 'sites', 1)
Status: ✅ FULLY ENFORCED
Works: Users cannot create more sites than their plan allows
Team Users (max_users)
Enforcement Location: User invite flow
# When inviting a new user
LimitService.check_hard_limit(account, 'users', 1)
Status: ✅ FULLY ENFORCED
Works: Users cannot invite more team members than their plan allows
5.2 Partially Enforced Limits ⚠️
Keywords (max_keywords)
Current State:
- ✅ Plan field exists:
Plan.max_keywords - ✅ LimitService has mapping:
'keywords': { 'model': 'planner.Keywords', 'plan_field': 'max_keywords', 'display_name': 'Keywords', 'filter_field': 'account', } - ⚠️ ENFORCEMENT INCONSISTENT
Evidence from Code Search:
Found in backend/igny8_core/business/planning/models.py:
from igny8_core.business.billing.services.limit_service import LimitService
# Only enforced in SOME locations:
LimitService.increment_usage(
account=self.account,
limit_type='keywords',
amount=1
)
Problem:
- Keyword import from SeedKeywords may not check limit
- Keyword creation via API may not check limit consistently
- Manual keyword creation may bypass checks
Impact:
- Users may exceed keyword limits without errors
- Limits shown in UI but not enforced at all entry points
Recommended Fix:
- Add pre-create check to all keyword creation flows:
# Before creating keyword LimitService.check_hard_limit(account, 'keywords', 1) - Locations to add enforcement:
addSeedKeywordsToWorkflowAPI endpoint- Keyword bulk import
- Manual keyword creation form
- CSV import
5.3 Missing Future Limits ❌
Ahrefs Keyword Queries (Planned)
User Requirement:
"in future soon we will have option for user to query and research keywords from ahrefs, which will also have monthly limit"
Current State:
- ❌ No plan field defined
- ❌ No LimitService mapping
- ❌ No enforcement
Current Keywords Situation:
- Existing: Pre-searched keywords from IGNY8's own database (SeedKeywords)
- Limited by:
max_keywords(how many they can add to their workspace) - Future: Ahrefs API queries (need new monthly limit)
Recommended Implementation:
-
Add Plan Field:
# backend/igny8_core/auth/models.py - Plan model max_ahrefs_queries = models.IntegerField( default=50, validators=[MinValueValidator(0)], help_text="Maximum Ahrefs keyword queries per month" ) -
Add Account Usage Field:
# backend/igny8_core/auth/models.py - Account model usage_ahrefs_queries = models.IntegerField( default=0, validators=[MinValueValidator(0)], help_text="Ahrefs queries used this month" ) -
Add LimitService Mapping:
# backend/igny8_core/business/billing/services/limit_service.py MONTHLY_LIMIT_MAPPINGS = { # ... existing mappings ... 'ahrefs_queries': { 'plan_field': 'max_ahrefs_queries', 'usage_field': 'usage_ahrefs_queries', 'display_name': 'Ahrefs Keyword Queries', }, } -
Enforce Before Query:
# In Ahrefs query service def query_ahrefs_keywords(account, query): # Check limit LimitService.check_monthly_limit(account, 'ahrefs_queries', 1) # Execute query results = ahrefs_api.search_keywords(query) # Increment usage LimitService.increment_usage(account, 'ahrefs_queries', 1) return results
6. IMPLEMENTATION PLAN
6.1 Phase 1: Backend Cleanup (Week 1)
1.1 Remove Unused Monthly Limits from Database
File: backend/igny8_core/auth/models.py
Remove from Plan Model:
# REMOVE THESE FIELDS:
max_content_ideas = models.IntegerField(...) # ❌ DELETE
max_content_words = models.IntegerField(...) # ❌ DELETE
max_images_basic = models.IntegerField(...) # ❌ DELETE
max_images_premium = models.IntegerField(...) # ❌ DELETE
max_image_prompts = models.IntegerField(...) # ❌ DELETE
max_clusters = models.IntegerField(...) # ❌ DELETE (or merge with keywords)
Remove from Account Model:
# REMOVE THESE FIELDS:
usage_content_ideas = models.IntegerField(...) # ❌ DELETE
usage_content_words = models.IntegerField(...) # ❌ DELETE
usage_images_basic = models.IntegerField(...) # ❌ DELETE
usage_images_premium = models.IntegerField(...) # ❌ DELETE
usage_image_prompts = models.IntegerField(...) # ❌ DELETE
KEEP ONLY:
# Plan Model - KEEP THESE:
max_sites = models.IntegerField(default=1) # ✅ KEEP
max_users = models.IntegerField(default=1) # ✅ KEEP
max_keywords = models.IntegerField(default=1000) # ✅ KEEP
max_ahrefs_queries = models.IntegerField(default=50) # 🆕 ADD
# Account Model - KEEP THESE:
credits = models.IntegerField(default=0) # ✅ KEEP
usage_ahrefs_queries = models.IntegerField(default=0) # 🆕 ADD
usage_period_start = models.DateTimeField(...) # ✅ KEEP
usage_period_end = models.DateTimeField(...) # ✅ KEEP
Migration Script:
# Create migration: 0XXX_remove_unused_limits.py
operations = [
# Remove from Plan
migrations.RemoveField(model_name='plan', name='max_content_ideas'),
migrations.RemoveField(model_name='plan', name='max_content_words'),
migrations.RemoveField(model_name='plan', name='max_images_basic'),
migrations.RemoveField(model_name='plan', name='max_images_premium'),
migrations.RemoveField(model_name='plan', name='max_image_prompts'),
migrations.RemoveField(model_name='plan', name='max_clusters'),
# Remove from Account
migrations.RemoveField(model_name='account', name='usage_content_ideas'),
migrations.RemoveField(model_name='account', name='usage_content_words'),
migrations.RemoveField(model_name='account', name='usage_images_basic'),
migrations.RemoveField(model_name='account', name='usage_images_premium'),
migrations.RemoveField(model_name='account', name='usage_image_prompts'),
# Add Ahrefs fields
migrations.AddField(
model_name='plan',
name='max_ahrefs_queries',
field=models.IntegerField(default=50, validators=[MinValueValidator(0)])
),
migrations.AddField(
model_name='account',
name='usage_ahrefs_queries',
field=models.IntegerField(default=0, validators=[MinValueValidator(0)])
),
]
1.2 Update LimitService
File: backend/igny8_core/business/billing/services/limit_service.py
Remove from MONTHLY_LIMIT_MAPPINGS:
# DELETE THESE:
'content_ideas': {...}, # ❌ DELETE
'content_words': {...}, # ❌ DELETE
'images_basic': {...}, # ❌ DELETE
'images_premium': {...}, # ❌ DELETE
'image_prompts': {...}, # ❌ DELETE
Keep/Add:
MONTHLY_LIMIT_MAPPINGS = {
# ONLY THIS ONE:
'ahrefs_queries': {
'plan_field': 'max_ahrefs_queries',
'usage_field': 'usage_ahrefs_queries',
'display_name': 'Ahrefs Keyword Research',
},
}
# Remove from HARD_LIMIT_MAPPINGS:
'clusters': {...}, # ❌ DELETE (or keep if needed)
1.3 Update Serializers
File: backend/igny8_core/auth/serializers.py
class PlanSerializer(serializers.ModelSerializer):
class Meta:
fields = [
'id', 'name', 'slug', 'price', 'billing_cycle',
# KEEP ONLY THESE LIMITS:
'max_sites',
'max_users',
'max_keywords',
'max_ahrefs_queries', # NEW
# CREDITS:
'included_credits',
'extra_credit_price',
# REMOVE ALL OTHER max_* fields
]
1.4 Remove Limit Checks from AI Operations
Search and Remove:
# Find all places checking monthly limits
grep -r "check_monthly_limit.*content_ideas" backend/
grep -r "check_monthly_limit.*content_words" backend/
grep -r "check_monthly_limit.*images" backend/
grep -r "increment_usage.*content_ideas" backend/
Replace with credit checks only:
# OLD (REMOVE):
LimitService.check_monthly_limit(account, 'content_ideas', 1)
LimitService.increment_usage(account, 'content_ideas', 1)
# NEW (KEEP):
CreditService.check_credits(account, required_credits)
# Then after operation:
CreditService.deduct_credits_for_operation(...)
1.5 Add Credit Balance Check to Automation
File: backend/igny8_core/business/automation/services.py (or similar)
from igny8_core.business.billing.services.credit_service import CreditService
from igny8_core.business.billing.exceptions import InsufficientCreditsError
def run_automation(automation_config, account):
"""Run automation with credit pre-check"""
# Estimate credits needed
estimated_credits = estimate_automation_cost(automation_config)
# Check balance BEFORE starting
try:
CreditService.check_credits(account, estimated_credits)
except InsufficientCreditsError:
raise AutomationError(
f"Insufficient credits. Need {estimated_credits}, have {account.credits}. "
f"Please add credits to continue."
)
# Run automation stages
for stage in automation_config.stages:
# Each stage checks/deducts its own credits
run_stage(stage, account)
1.6 Enforce Keywords Limit Properly
Files to Update:
backend/igny8_core/business/planning/views.py(keyword creation)backend/igny8_core/api/endpoints/seed_keywords.py(SeedKeyword import)
from igny8_core.business.billing.services.limit_service import LimitService, HardLimitExceededError
# Before creating keywords:
try:
LimitService.check_hard_limit(account, 'keywords', num_keywords_to_add)
except HardLimitExceededError as e:
return Response({
'error': 'keyword_limit_exceeded',
'message': f'You have reached your keyword limit ({plan.max_keywords}). Upgrade your plan to add more.',
'current_count': current_count,
'limit': plan.max_keywords,
'upgrade_url': '/account/plans/upgrade'
}, status=402)
# If check passes, create keywords
keywords = Keywords.objects.bulk_create([...])
6.2 Phase 2: Frontend Cleanup (Week 2)
2.1 Remove Duplicate Data from Plans & Billing Page
File: frontend/src/pages/account/PlansAndBillingPage.tsx
Current Plan Tab - Remove:
// ❌ DELETE: Credit balance display (move to Usage only)
// ❌ DELETE: Usage breakdown charts
// ❌ DELETE: Limit progress bars
// ❌ DELETE: "Credits used this month" details
Current Plan Tab - Keep:
// ✅ KEEP: Plan name, price, billing cycle
// ✅ KEEP: Renewal date
// ✅ KEEP: Brief summary text: "50 Pages/Articles • 2 Sites • 2 Users"
// ✅ KEEP: Upgrade CTA button
2.2 Redesign Usage Page with Multi-Dimensional Insights
File: frontend/src/pages/account/UsageAnalyticsPage.tsx
New Structure:
/**
* Usage & Insights Page - Complete Redesign
*
* Tab 1: Overview (NEW)
* - Credit balance cards
* - Quick stats (sites, users, keywords count)
* - Period selector (7, 30, 90 days)
* - Top metrics
*
* Tab 2: Your Limits
* - Sites, Users, Keywords progress bars
* - Ahrefs queries remaining
*
* Tab 3: Credit Insights (NEW)
* - Credits by Site
* - Credits by Content Type (articles, images, etc.)
* - Credits by Image Quality (basic, quality, premium)
* - Credits by Automation
* - Timeline chart (7/30/90 days)
*
* Tab 4: Activity Log
* - Detailed transaction history
*/
type TabType = 'overview' | 'limits' | 'insights' | 'activity';
Tab 1: Overview (NEW)
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* Credit Balance Card */}
<Card>
<ZapIcon />
<div>Credits Available</div>
<div className="text-3xl">{credits.toLocaleString()}</div>
</Card>
{/* Sites Used Card */}
<Card>
<GlobeIcon />
<div>Sites</div>
<div className="text-3xl">{sitesCount} / {maxSites}</div>
</Card>
{/* Team Members Card */}
<Card>
<UsersIcon />
<div>Team Members</div>
<div className="text-3xl">{usersCount} / {maxUsers}</div>
</Card>
{/* Keywords Card */}
<Card>
<TagIcon />
<div>Keywords</div>
<div className="text-3xl">{keywordsCount.toLocaleString()} / {maxKeywords.toLocaleString()}</div>
</Card>
</div>
{/* Period Selector */}
<div className="flex gap-2">
<Button onClick={() => setPeriod(7)}>Last 7 Days</Button>
<Button onClick={() => setPeriod(30)}>Last 30 Days</Button>
<Button onClick={() => setPeriod(90)}>Last 90 Days</Button>
</div>
Tab 3: Credit Insights (NEW)
{/* Credits by Site */}
<Card>
<h3>Credits by Site</h3>
{sites.map(site => (
<div key={site.id}>
<div>{site.name}</div>
<ProgressBar value={site.credits_used} max={totalCredits} />
<div>{site.credits_used.toLocaleString()} credits</div>
</div>
))}
</Card>
{/* Credits by Content Type */}
<Card>
<h3>Credits by Content Type</h3>
<DonutChart data={[
{ label: 'Articles', value: contentCredits },
{ label: 'Images', value: imageCredits },
{ label: 'Ideas & Planning', value: ideaCredits },
{ label: 'Other', value: otherCredits },
]} />
</Card>
{/* Credits by Image Quality */}
<Card>
<h3>Image Generation by Quality</h3>
{imageQualityBreakdown.map(tier => (
<div key={tier.quality}>
<div>{tier.quality} Quality</div>
<div>{tier.count} images • {tier.credits_used} credits</div>
<div className="text-xs">({tier.credits_per_image} credits each)</div>
</div>
))}
</Card>
{/* Credits by Automation */}
<Card>
<h3>Automation Runs</h3>
{automations.map(auto => (
<div key={auto.id}>
<div>{auto.name}</div>
<div>{auto.runs_count} runs • {auto.credits_used} credits</div>
</div>
))}
</Card>
{/* Timeline Chart */}
<Card>
<h3>Credit Usage Over Time</h3>
<LineChart
data={usageTimeline}
xAxis="date"
yAxis="credits_used"
/>
</Card>
2.3 Update API Interfaces
File: frontend/src/services/billing.api.ts
// REMOVE from Plan interface:
export interface Plan {
id: number;
name: string;
price: number;
// KEEP ONLY THESE LIMITS:
max_sites?: number;
max_users?: number;
max_keywords?: number;
max_ahrefs_queries?: number; // NEW
included_credits?: number;
// REMOVE THESE:
// max_content_ideas?: number; // ❌ DELETE
// max_content_words?: number; // ❌ DELETE
// max_images_basic?: number; // ❌ DELETE
// max_images_premium?: number; // ❌ DELETE
// max_image_prompts?: number; // ❌ DELETE
// max_clusters?: number; // ❌ DELETE
}
// UPDATE UsageSummary interface:
export interface UsageSummary {
account_id: number;
plan_name: string;
period_start: string;
period_end: string;
hard_limits: {
sites?: LimitUsage;
users?: LimitUsage;
keywords?: LimitUsage;
// clusters?: LimitUsage; // ❌ DELETE
};
monthly_limits: {
ahrefs_queries?: LimitUsage; // ONLY THIS ONE
// content_ideas?: LimitUsage; // ❌ DELETE
// content_words?: LimitUsage; // ❌ DELETE
// images_basic?: LimitUsage; // ❌ DELETE
// images_premium?: LimitUsage; // ❌ DELETE
// image_prompts?: LimitUsage; // ❌ DELETE
};
}
// NEW: Multi-dimensional insights
export interface CreditInsights {
period_days: number;
total_credits_used: number;
by_site: Array<{
site_id: number;
site_name: string;
credits_used: number;
percentage: number;
}>;
by_operation: Array<{
operation_type: string;
display_name: string;
credits_used: number;
count: number;
percentage: number;
}>;
by_image_quality: Array<{
quality_tier: 'basic' | 'quality' | 'premium';
model_name: string;
credits_per_image: number;
images_generated: number;
total_credits: number;
}>;
by_automation: Array<{
automation_id: number;
automation_name: string;
runs_count: number;
credits_used: number;
}>;
timeline: Array<{
date: string;
credits_used: number;
}>;
}
// NEW API function
export async function getCreditInsights(days: number = 30): Promise<CreditInsights> {
return fetchAPI(`/v1/billing/credits/insights/?days=${days}`);
}
2.4 Update UsageLimitsPanel Component
File: frontend/src/components/billing/UsageLimitsPanel.tsx
// REMOVE monthly limit configs for deleted fields
const monthlyLimitConfig = {
// ONLY THIS ONE:
ahrefs_queries: {
icon: <SearchIcon className="w-5 h-5" />,
color: 'purple' as const,
description: 'Keyword research queries per month'
},
// DELETE THESE:
// content_ideas: {...}, // ❌ DELETE
// content_words: {...}, // ❌ DELETE
// images_basic: {...}, // ❌ DELETE
// images_premium: {...}, // ❌ DELETE
// image_prompts: {...}, // ❌ DELETE
};
// KEEP hard limits
const hardLimitConfig = {
sites: { icon: <GlobeIcon />, color: 'success' },
users: { icon: <UsersIcon />, color: 'info' },
keywords: { icon: <TagIcon />, color: 'purple' },
// clusters: {...}, // ❌ DELETE if not needed
};
2.5 Use User-Friendly Terminology
Global Search & Replace:
| Technical Term | User-Friendly Term |
|---|---|
| "API Activity" | "Activity Log" or "Recent Actions" |
| "API Operations" | "Actions" or "Activities" |
| "operation_type" | "Action Type" |
| "Monthly Limits" | "Monthly Allowances" |
| "Hard Limits" | "Plan Limits" |
| "Credits Used" | "Credits Spent" |
| "Balance" | "Credits Available" |
6.3 Phase 3: Keyword Research Implementation (Week 3)
3.1 Backend: Ahrefs Service
Create: backend/igny8_core/business/keywords/ahrefs_service.py
from igny8_core.business.billing.services.limit_service import LimitService, MonthlyLimitExceededError
import requests
class AhrefsService:
"""Service for Ahrefs keyword research with monthly limit enforcement"""
@staticmethod
def query_keywords(account, query_params):
"""
Query Ahrefs for keywords with limit enforcement.
Args:
account: Account instance
query_params: dict with search parameters
Returns:
dict: Ahrefs API response with keyword data
Raises:
MonthlyLimitExceededError: If monthly Ahrefs query limit exceeded
"""
# Check monthly limit BEFORE querying
try:
LimitService.check_monthly_limit(account, 'ahrefs_queries', 1)
except MonthlyLimitExceededError as e:
raise AhrefsQueryLimitExceeded(
f"You've used all your Ahrefs queries this month. "
f"Limit: {account.plan.max_ahrefs_queries}. "
f"Resets on {account.usage_period_end.strftime('%B %d, %Y')}. "
f"Upgrade your plan for more queries."
)
# Make Ahrefs API call
try:
response = requests.post(
'https://api.ahrefs.com/v3/site-explorer/keywords',
headers={'Authorization': f'Bearer {settings.AHREFS_API_KEY}'},
json=query_params
)
response.raise_for_status()
results = response.json()
# Increment usage counter
LimitService.increment_usage(account, 'ahrefs_queries', 1)
return results
except requests.RequestException as e:
logger.error(f"Ahrefs API error: {e}")
raise AhrefsAPIError("Failed to fetch keyword data from Ahrefs")
3.2 Frontend: Keyword Research Page
Create: frontend/src/pages/Planner/KeywordResearchPage.tsx
/**
* Keyword Research Page - Two Options
* 1. Browse IGNY8 pre-researched keywords (free)
* 2. Research with Ahrefs (monthly limit)
*/
type ResearchTab = 'browse' | 'ahrefs';
export default function KeywordResearchPage() {
const [activeTab, setActiveTab] = useState<ResearchTab>('browse');
const [ahrefsLimit, setAhrefsLimit] = useState({ used: 0, limit: 50 });
return (
<>
<PageHeader
title="Keyword Research"
description="Find high-opportunity keywords for your content strategy"
/>
{/* Tab Selector */}
<div className="flex gap-4 mb-6">
<Button
variant={activeTab === 'browse' ? 'primary' : 'outline'}
onClick={() => setActiveTab('browse')}
>
<DatabaseIcon className="w-4 h-4" />
Browse Pre-Researched Keywords
</Button>
<Button
variant={activeTab === 'ahrefs' ? 'primary' : 'outline'}
onClick={() => setActiveTab('ahrefs')}
>
<SearchIcon className="w-4 h-4" />
Research with Ahrefs
<Badge variant="soft" tone="purple">
{ahrefsLimit.limit - ahrefsLimit.used} remaining
</Badge>
</Button>
</div>
{/* Tab Content */}
{activeTab === 'browse' && (
<BrowseKeywordsPanel />
)}
{activeTab === 'ahrefs' && (
<AhrefsResearchPanel
limit={ahrefsLimit}
onQuerySuccess={() => {
setAhrefsLimit(prev => ({
...prev,
used: prev.used + 1
}));
}}
/>
)}
</>
);
}
Browse Keywords Panel (Existing SeedKeywords):
function BrowseKeywordsPanel() {
return (
<Card>
<h3>Pre-Researched High-Opportunity Keywords</h3>
<p>Browse thousands of analyzed keywords, ready to use.</p>
{/* Filters */}
<div className="flex gap-4">
<Select label="Industry" options={industries} />
<Select label="Sector" options={sectors} />
<Select label="Country" options={countries} />
<Input placeholder="Search keywords..." />
</div>
{/* Results Table */}
<Table>
<thead>
<tr>
<th>Keyword</th>
<th>Search Volume</th>
<th>Difficulty</th>
<th>Opportunity Score</th>
<th></th>
</tr>
</thead>
<tbody>
{keywords.map(kw => (
<tr key={kw.id}>
<td>{kw.keyword}</td>
<td>{kw.search_volume}</td>
<td><Badge>{kw.difficulty}</Badge></td>
<td>{kw.opportunity_score}/100</td>
<td>
<Button size="sm" onClick={() => addToWorkspace(kw)}>
Add to Workspace
</Button>
</td>
</tr>
))}
</tbody>
</Table>
</Card>
);
}
Ahrefs Research Panel (NEW):
function AhrefsResearchPanel({ limit, onQuerySuccess }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
const remaining = limit.limit - limit.used;
const canQuery = remaining > 0;
const handleSearch = async () => {
if (!canQuery) {
toast.error('You have used all your Ahrefs queries this month.');
return;
}
try {
setLoading(true);
const data = await fetchAPI('/v1/keywords/ahrefs/search/', {
method: 'POST',
body: JSON.stringify({ query })
});
setResults(data.keywords);
onQuerySuccess();
toast.success('Keywords fetched from Ahrefs!');
} catch (error) {
toast.error(error.message);
} finally {
setLoading(false);
}
};
return (
<Card>
<div className="flex items-center justify-between mb-4">
<h3>Live Keyword Research with Ahrefs</h3>
<Badge variant="soft" tone={remaining > 10 ? 'success' : 'warning'}>
{remaining} / {limit.limit} queries remaining
</Badge>
</div>
{!canQuery && (
<Alert tone="warning">
<AlertCircleIcon />
<div>
<div>No queries remaining</div>
<div>Your limit resets on {resetDate}. Upgrade for more queries.</div>
<Button size="sm" onClick={() => navigate('/account/plans/upgrade')}>
Upgrade Plan
</Button>
</div>
</Alert>
)}
{canQuery && (
<>
<div className="flex gap-4 mb-4">
<Input
placeholder="Enter seed keyword (e.g., 'digital marketing')"
value={query}
onChange={e => setQuery(e.target.value)}
onKeyPress={e => e.key === 'Enter' && handleSearch()}
/>
<Button onClick={handleSearch} loading={loading}>
<SearchIcon /> Research Keywords
</Button>
</div>
{/* Results */}
{results.length > 0 && (
<Table>
<thead>
<tr>
<th>Keyword</th>
<th>Volume</th>
<th>Difficulty</th>
<th>CPC</th>
<th></th>
</tr>
</thead>
<tbody>
{results.map((kw, idx) => (
<tr key={idx}>
<td>{kw.keyword}</td>
<td>{kw.volume.toLocaleString()}</td>
<td><Badge>{kw.difficulty}</Badge></td>
<td>${kw.cpc}</td>
<td>
<Button size="sm" onClick={() => addToWorkspace(kw)}>
Add to Workspace
</Button>
</td>
</tr>
))}
</tbody>
</Table>
)}
</>
)}
</Card>
);
}
6.4 Phase 4: Validation & Enforcement (Week 4)
4.1 Credit Balance Validation Checklist
File: Search all AI operation services
# REQUIRED PATTERN for all AI operations:
# 1. BEFORE operation - estimate and check
estimated_credits = estimate_credits_needed(operation_params)
CreditService.check_credits(account, estimated_credits)
# 2. EXECUTE operation
result = ai_service.execute(operation_params)
# 3. AFTER operation - calculate actual and deduct
actual_tokens = result.usage.total_tokens
actual_credits = CreditService.calculate_credits_from_tokens(
operation_type='content_generation',
tokens_input=result.usage.prompt_tokens,
tokens_output=result.usage.completion_tokens
)
CreditService.deduct_credits_for_operation(
account=account,
operation_type='content_generation',
amount=actual_credits,
model=result.model,
tokens_in=result.usage.prompt_tokens,
tokens_out=result.usage.completion_tokens,
metadata={'content_id': content.id}
)
Locations to Verify:
- ✅ Content generation service
- ✅ Image generation service
- ✅ Idea generation service
- ✅ Clustering service
- ✅ Internal linking service
- ✅ Optimization service
- ✅ Automation runner (PRE-CHECK before starting!)
4.2 Automation Credit Pre-Check
File: backend/igny8_core/business/automation/services.py
def estimate_automation_cost(automation_config):
"""Estimate credits needed for full automation run"""
estimated = 0
for stage in automation_config.stages:
if stage.type == 'clustering':
estimated += 10 # Base estimate
elif stage.type == 'idea_generation':
estimated += stage.num_ideas * 2
elif stage.type == 'content_generation':
estimated += stage.num_articles * 50 # Rough estimate
elif stage.type == 'image_generation':
estimated += stage.num_images * stage.credits_per_image
elif stage.type == 'linking':
estimated += stage.num_articles * 8
# Add 20% buffer
return int(estimated * 1.2)
def run_automation(automation_config, account, trigger='manual'):
"""
Run automation with upfront credit validation.
Raises:
InsufficientCreditsError: If not enough credits available
"""
# Estimate total cost
estimated_credits = estimate_automation_cost(automation_config)
# CHECK CREDITS BEFORE STARTING
try:
CreditService.check_credits(account, estimated_credits)
except InsufficientCreditsError:
# Log failed attempt
AutomationRun.objects.create(
automation=automation_config,
account=account,
status='failed',
error_message=f'Insufficient credits. Need {estimated_credits}, have {account.credits}.',
trigger=trigger
)
raise
# Create run record
run = AutomationRun.objects.create(
automation=automation_config,
account=account,
status='running',
estimated_credits=estimated_credits,
trigger=trigger
)
try:
# Execute stages (each deducts credits)
for stage in automation_config.stages:
execute_stage(stage, account, run)
# Mark complete
run.status = 'completed'
run.actual_credits = run.credits_used
run.save()
except Exception as e:
run.status = 'failed'
run.error_message = str(e)
run.save()
raise
4.3 Frontend Credit Check Before Actions
Component: Add to all credit-consuming actions
// Hook for credit validation
function useCreditCheck() {
const { user } = useAuthStore();
const checkCredits = useCallback(async (estimatedCredits: number) => {
if (user.account.credits < estimatedCredits) {
const confirmed = await showConfirmDialog({
title: 'Insufficient Credits',
message: `This action requires approximately ${estimatedCredits} credits, but you only have ${user.account.credits} available.`,
confirmText: 'Add Credits',
cancelText: 'Cancel',
});
if (confirmed) {
navigate('/account/plans');
}
return false;
}
return true;
}, [user]);
return { checkCredits };
}
// Usage example:
function GenerateContentButton({ taskId }) {
const { checkCredits } = useCreditCheck();
const handleGenerate = async () => {
// Estimate credits (rough estimate: 50 for article)
const canProceed = await checkCredits(50);
if (!canProceed) return;
// Proceed with generation
await generateContent(taskId);
};
return <Button onClick={handleGenerate}>Generate Content</Button>;
}
6.5 Phase 5: Testing & Validation (Week 5)
Test Cases
Limit Enforcement:
- Try to create site when at
max_sites→ Should fail with upgrade prompt - Try to invite user when at
max_users→ Should fail with upgrade prompt - Try to add 100 keywords when 50 slots remain and limit is 1000 → Should succeed
- Try to add 100 keywords when 30 slots remain → Should fail
- Try to query Ahrefs when at monthly limit → Should fail with reset date
- Query Ahrefs successfully → Counter increments
- Monthly reset → Ahrefs counter resets to 0
Credit Validation:
- Try to generate content with 0 credits → Should fail immediately
- Try to run automation with insufficient credits → Should fail before starting
- Generate content → Credits deducted correctly based on tokens
- Generate image (basic) → 1 credit deducted
- Generate image (quality) → 5 credits deducted
- Generate image (premium) → 15 credits deducted
- Run automation → All stages check/deduct credits properly
Page Reorganization:
- Plans & Billing → Current Plan: NO credit usage details
- Plans & Billing → Current Plan: Brief summary text only
- Usage → Overview: Shows credit balance, sites/users/keywords count
- Usage → Limits: Shows only 4 limits (sites, users, keywords, ahrefs)
- Usage → Insights: Shows multi-dimensional breakdowns
- No duplicate data between Plans & Usage
Terminology:
- No "API" references in user-facing text
- "Operations" changed to "Actions" or "Activities"
- User-friendly language throughout
7. Updated Page Reorganization
7.1 Page Reorganization (High Priority) 🔥
Problem: Data duplication creates confusion and maintenance overhead.
Proposed Solution: Clear separation of concerns between Plans & Billing vs Usage.
Plans & Billing → Focus on FINANCIAL aspects
Tab 1: Current Plan
- Show: Plan name, price, billing cycle, renewal date
- Show: Brief limits summary (e.g., "50 Pages/Articles, 2 Sites, 2 Users")
- Remove: Detailed limit progress bars (move to Usage)
- Remove: Credits used this month breakdown (move to Usage)
- Action: "Upgrade Plan" button
Tab 2: Upgrade Plan
- Show: Pricing table with plan comparison
- Show: Plan features and limits (static, for comparison)
- Action: Purchase/upgrade plan
Tab 3: Billing History
- Show: Invoices (with download PDF)
- Show: Payment methods management
- Show: Credit package purchases (financial transactions only)
Usage → Focus on CONSUMPTION tracking
Tab 1: Limits & Usage (Keep as is - this is perfect)
- Show: All hard limits with progress bars (sites, users, keywords, clusters)
- Show: All monthly limits with progress bars (ideas, words, images)
- Show: Days until reset for monthly limits
- Show: Credit balance and monthly usage
- Action: "Upgrade for more" CTA when approaching limits
Tab 2: Credit History (Keep as is)
- Show: Credit transaction log (purchases, deductions, adjustments)
- Show: Operation details (what consumed credits)
- Filter: By operation type, date range
Tab 3: Activity Log (Keep as is)
- Show: API operations by type
- Show: Total operations count
- Show: Breakdown by operation type
Summary of Changes
| Page | Current State | Proposed Change |
|---|---|---|
| Plans & Billing → Current Plan | Shows credits, usage, limits | Remove detailed usage, keep financial summary only |
| Plans & Billing → History | Invoices and payments | Keep as is (financial focus) |
| Usage → Limits & Usage | Detailed limits panel | Keep as is (consumption focus) |
| Usage → Credit History | Transaction log | Keep as is (consumption focus) |
Rationale:
- Plans & Billing = "What am I paying for?" (financial/subscription management)
- Usage = "What am I using?" (consumption monitoring)
7.1 Plans & Billing Page (Simplified)
Purpose: Financial management and subscription control
Tab 1: Current Plan
<Card>
<h2>{plan.name} Plan</h2>
<div className="text-3xl">${plan.price}/month</div>
<div>Renews on {renewalDate}</div>
{/* Brief Summary - NO detailed limits */}
<div className="text-sm text-gray-600">
Your plan includes: {plan.included_credits} credits per month •
{plan.max_sites} sites • {plan.max_users} team members
</div>
{/* Upgrade CTA */}
<Button onClick={() => navigate('/account/plans/upgrade')}>
Upgrade Plan
</Button>
</Card>
{/* ❌ REMOVE: Credit usage charts */}
{/* ❌ REMOVE: Limit progress bars */}
{/* ❌ REMOVE: "Credits used this month" */}
Tab 2: Upgrade Plan (No changes needed)
Tab 3: Billing History (No changes needed)
7.2 Usage Page (Multi-Dimensional Insights)
Purpose: Monitor consumption and optimize usage
Tab 1: Overview (NEW)
{/* Quick Stats Cards */}
<div className="grid grid-cols-4 gap-4">
<StatCard
icon={<ZapIcon />}
label="Credits Available"
value={credits.toLocaleString()}
color="brand"
/>
<StatCard
icon={<GlobeIcon />}
label="Sites"
value={`${sitesCount} / ${maxSites}`}
color="success"
/>
<StatCard
icon={<UsersIcon />}
label="Team Members"
value={`${usersCount} / ${maxUsers}`}
color="info"
/>
<StatCard
icon={<TagIcon />}
label="Keywords"
value={`${keywordsCount} / ${maxKeywords}`}
color="purple"
/>
</div>
{/* Period Selector */}
<ButtonGroup>
<Button onClick={() => setPeriod(7)}>Last 7 Days</Button>
<Button onClick={() => setPeriod(30)}>Last 30 Days</Button>
<Button onClick={() => setPeriod(90)}>Last 90 Days</Button>
</ButtonGroup>
{/* Top Metrics for Selected Period */}
<div className="grid grid-cols-3 gap-4">
<Card>
<div>Credits Spent</div>
<div className="text-2xl">{periodCredits.toLocaleString()}</div>
</Card>
<Card>
<div>Articles Created</div>
<div className="text-2xl">{periodArticles}</div>
</Card>
<Card>
<div>Images Generated</div>
<div className="text-2xl">{periodImages}</div>
</Card>
</div>
Tab 2: Your Limits
{/* Only 4 limits total */}
<LimitCard
title="Sites"
icon={<GlobeIcon />}
current={sitesCount}
limit={maxSites}
type="permanent"
/>
<LimitCard
title="Team Members"
icon={<UsersIcon />}
current={usersCount}
limit={maxUsers}
type="permanent"
/>
<LimitCard
title="Keywords in Workspace"
icon={<TagIcon />}
current={keywordsCount}
limit={maxKeywords}
type="permanent"
/>
<LimitCard
title="Ahrefs Research Queries"
icon={<SearchIcon />}
current={ahrefsUsed}
limit={ahrefsLimit}
type="monthly"
daysUntilReset={daysUntilReset}
/>
Tab 3: Credit Insights (NEW)
{/* Multi-dimensional breakdowns */}
{/* By Site */}
<Card>
<h3>Credits by Site</h3>
<div>See which sites consume the most credits</div>
{insights.by_site.map(site => (
<div key={site.id}>
<div className="flex justify-between">
<span>{site.name}</span>
<span>{site.credits_used.toLocaleString()} credits ({site.percentage}%)</span>
</div>
<ProgressBar value={site.percentage} />
</div>
))}
</Card>
{/* By Action Type */}
<Card>
<h3>Credits by Action Type</h3>
<DonutChart data={insights.by_operation} />
<div>
{insights.by_operation.map(op => (
<div key={op.type}>
<span>{op.display_name}</span>
<span>{op.credits_used} credits ({op.count} times)</span>
</div>
))}
</div>
</Card>
{/* By Image Quality */}
<Card>
<h3>Image Generation Breakdown</h3>
<div>Credits vary by quality tier</div>
{insights.by_image_quality.map(tier => (
<div key={tier.quality_tier}>
<div>{tier.quality_tier} Quality</div>
<div>{tier.images_generated} images × {tier.credits_per_image} credits = {tier.total_credits} credits</div>
<Badge>{tier.model_name}</Badge>
</div>
))}
</Card>
{/* By Automation */}
<Card>
<h3>Automation Runs</h3>
{insights.by_automation.map(auto => (
<div key={auto.id}>
<div>{auto.name}</div>
<div>{auto.runs_count} runs • {auto.credits_used} credits total</div>
</div>
))}
</Card>
{/* Timeline Chart */}
<Card>
<h3>Credit Usage Over Time</h3>
<LineChart
data={insights.timeline}
xAxis="date"
yAxis="credits_used"
/>
</Card>
Tab 4: Activity Log (Keep existing, but rename from "API Activity")
8. Backend Changes Summary
8.1 Files to Modify
| File | Action | Description |
|---|---|---|
backend/igny8_core/auth/models.py |
Edit | Remove unused limit fields from Plan & Account |
backend/igny8_core/auth/migrations/0XXX_*.py |
Create | Migration to remove fields |
backend/igny8_core/business/billing/services/limit_service.py |
Edit | Remove unused limit mappings |
backend/igny8_core/auth/serializers.py |
Edit | Remove fields from PlanSerializer |
backend/igny8_core/business/keywords/ahrefs_service.py |
Create | New service for Ahrefs integration |
backend/igny8_core/business/automation/services.py |
Edit | Add credit pre-check |
backend/igny8_core/business/planning/views.py |
Edit | Add keyword limit enforcement |
backend/igny8_core/modules/billing/views.py |
Edit | Add credit insights endpoint |
8.2 New API Endpoints
# Credit Insights (NEW)
GET /api/v1/billing/credits/insights/?days=30
Response: CreditInsights object with multi-dimensional breakdowns
# Ahrefs Search (NEW)
POST /api/v1/keywords/ahrefs/search/
Body: { "query": "digital marketing", "country": "us" }
Response: { "keywords": [...], "queries_remaining": 42 }
9. Frontend Changes Summary
9.1 Files to Modify
| File | Action | Description |
|---|---|---|
frontend/src/pages/account/PlansAndBillingPage.tsx |
Edit | Remove credit usage details from Current Plan tab |
frontend/src/pages/account/UsageAnalyticsPage.tsx |
Rewrite | Add Overview & Credit Insights tabs |
frontend/src/components/billing/UsageLimitsPanel.tsx |
Edit | Remove unused limits, add Ahrefs |
frontend/src/services/billing.api.ts |
Edit | Remove unused fields, add new interfaces |
frontend/src/pages/Planner/KeywordResearchPage.tsx |
Create | New page for keyword research |
frontend/src/components/keywords/AhrefsResearchPanel.tsx |
Create | Ahrefs search component |
9.2 New Components
// Credit Insights Components
CreditInsightsDashboard.tsx
CreditsBySiteWidget.tsx
CreditsByOperationWidget.tsx
CreditsByImageQualityWidget.tsx
CreditsByAutomationWidget.tsx
CreditTimelineChart.tsx
// Ahrefs Research
KeywordResearchPage.tsx
AhrefsResearchPanel.tsx
BrowseKeywordsPanel.tsx
10. Testing Checklist
Problem: Keywords have max_keywords limit defined but enforcement is inconsistent.
Required Changes:
-
Add Pre-Create Checks:
Location:
backend/igny8_core/business/planning/views.py(or wherever keywords are created)# Before creating keywords from igny8_core.business.billing.services.limit_service import LimitService def create_keywords(account, keyword_data): # Check if adding keywords would exceed limit num_new_keywords = len(keyword_data) LimitService.check_hard_limit(account, 'keywords', num_new_keywords) # If check passes, create keywords keywords = Keywords.objects.bulk_create([...]) return keywords -
Add Check to SeedKeyword Import:
Location:
backend/igny8_core/api/endpoints/seed_keywords.py(or similar)# In addSeedKeywordsToWorkflow endpoint def add_seed_keywords_to_workflow(seed_keyword_ids, site_id, sector_id): account = Site.objects.get(id=site_id).account # Check limit BEFORE importing LimitService.check_hard_limit(account, 'keywords', len(seed_keyword_ids)) # Import keywords for seed_kw in SeedKeyword.objects.filter(id__in=seed_keyword_ids): Keywords.objects.create(...) -
Add Check to Bulk Import:
Ensure CSV/Excel keyword imports also check limits before processing.
-
User-Facing Error Messages:
try: LimitService.check_hard_limit(account, 'keywords', 50) except HardLimitExceededError as e: return Response({ 'error': 'keyword_limit_exceeded', 'message': 'You have reached your keyword limit. Upgrade your plan to add more keywords.', 'current': 950, 'limit': 1000, 'upgrade_url': '/account/plans/upgrade' }, status=402)
Testing:
- ✅ Try to import keywords beyond limit → Should fail with clear error
- ✅ Try to create single keyword at limit → Should fail
- ✅ Try to bulk import → Should fail if total exceeds
- ✅ Error message should show current count, limit, and upgrade CTA
6.3 Implement Ahrefs Query Limit (Medium Priority)
Problem: Future feature needs limit definition and enforcement.
Required Changes:
-
Database Migration:
# Create migration: 0XXX_add_ahrefs_query_limits.py operations = [ migrations.AddField( model_name='plan', name='max_ahrefs_queries', field=models.IntegerField(default=50, validators=[MinValueValidator(0)]), ), migrations.AddField( model_name='account', name='usage_ahrefs_queries', field=models.IntegerField(default=0, validators=[MinValueValidator(0)]), ), ] -
Update Plan Admin:
# backend/igny8_core/auth/admin.py - PlanAdmin fieldsets = ( # ... existing fieldsets ... ('Monthly Limits (Reset on Billing Cycle)', { 'fields': ( 'max_content_ideas', 'max_content_words', 'max_images_basic', 'max_images_premium', 'max_image_prompts', 'max_ahrefs_queries', # ADD THIS ), }), ) -
Update LimitService:
# backend/igny8_core/business/billing/services/limit_service.py MONTHLY_LIMIT_MAPPINGS = { # ... existing ... 'ahrefs_queries': { 'plan_field': 'max_ahrefs_queries', 'usage_field': 'usage_ahrefs_queries', 'display_name': 'Ahrefs Keyword Research Queries', }, } -
Add to Plan Serializer:
# backend/igny8_core/auth/serializers.py - PlanSerializer class PlanSerializer(serializers.ModelSerializer): class Meta: fields = [ # ... existing fields ... 'max_ahrefs_queries', # ADD THIS ] -
Enforce in Ahrefs Service:
# Create new service: backend/igny8_core/business/keywords/ahrefs_service.py from igny8_core.business.billing.services.limit_service import LimitService class AhrefsService: @staticmethod def query_keywords(account, query_params): # Check monthly limit LimitService.check_monthly_limit(account, 'ahrefs_queries', 1) # Execute Ahrefs API call results = ahrefs_api.search_keywords(**query_params) # Increment usage counter LimitService.increment_usage(account, 'ahrefs_queries', 1) return results -
Update Frontend:
// frontend/src/components/billing/UsageLimitsPanel.tsx const monthlyLimitConfig = { // ... existing ... ahrefs_queries: { icon: <SearchIcon className="w-5 h-5" />, color: 'purple' as const }, };
Plan Values (Suggested):
| Plan | max_ahrefs_queries/month |
|---|---|
| Free | 0 (no access) |
| Starter | 50 queries |
| Growth | 200 queries |
| Scale | 500 queries |
6.4 Add Cluster Limit Enforcement (Low Priority)
Current State: Clusters have max_clusters limit but may not be consistently enforced.
Recommendation: Apply same enforcement pattern as keywords:
- Check limit before creating clusters
- Add to all cluster creation flows (auto-clustering, manual clustering)
- User-facing error messages
6.5 Monthly Limits Reset Automation (Medium Priority)
Current State: Monthly limits should reset at billing cycle, but automation may not be in place.
Check Required:
- Is there a scheduled task that calls
LimitService.reset_monthly_limits(account)? - When do subscriptions renew?
- How are usage fields reset?
Recommended:
# backend/igny8_core/business/billing/tasks.py (Celery)
from celery import shared_task
from igny8_core.auth.models import Account
from igny8_core.business.billing.services.limit_service import LimitService
@shared_task
def reset_monthly_limits_for_accounts():
"""Reset monthly limits for accounts whose billing period has ended"""
from django.utils import timezone
now = timezone.now()
accounts = Account.objects.filter(
usage_period_end__lte=now,
status='active'
)
for account in accounts:
LimitService.reset_monthly_limits(account)
logger.info(f"Reset monthly limits for account {account.id}")
7. Credit System (Working Well) ✅
No changes needed - the credit system is well-designed:
- Token-Based Calculation: Uses
AIModelConfig.tokens_per_creditfor accurate pricing - Image Fixed Pricing: Uses
AIModelConfig.credits_per_image(1, 5, or 15) - Proper Logging:
CreditUsageLogtracks every operation with metadata - Transaction Ledger:
CreditTransactionmaintains audit trail - Balance Tracking: Account.credits is source of truth
Already Enforced Properly:
- ✅ Pre-flight balance checks before operations
- ✅ Token-based credit calculation after API calls
- ✅ Proper transaction logging
- ✅ Clear error messages (402 Payment Required)
8. Summary of Required Backend Changes
8.1 Database Schema Changes
Add to Plan Model:
max_ahrefs_queries = models.IntegerField(default=50, validators=[MinValueValidator(0)])
Add to Account Model:
usage_ahrefs_queries = models.IntegerField(default=0, validators=[MinValueValidator(0)])
8.2 Enforcement Additions
Locations Needing Limit Checks:
| Location | Limit Type | Method to Add |
|---|---|---|
| Keyword creation | Hard | LimitService.check_hard_limit(account, 'keywords', count) |
| SeedKeyword import | Hard | LimitService.check_hard_limit(account, 'keywords', count) |
| Bulk keyword import | Hard | LimitService.check_hard_limit(account, 'keywords', count) |
| Ahrefs query (future) | Monthly | LimitService.check_monthly_limit(account, 'ahrefs_queries', 1) |
| Cluster creation | Hard | LimitService.check_hard_limit(account, 'clusters', count) |
8.3 Service Updates
LimitService Mappings:
# Add to MONTHLY_LIMIT_MAPPINGS
'ahrefs_queries': {
'plan_field': 'max_ahrefs_queries',
'usage_field': 'usage_ahrefs_queries',
'display_name': 'Ahrefs Keyword Research Queries',
}
9. Summary of Required Frontend Changes
9.1 Page Content Adjustments
Plans & Billing Page:
// Remove from Current Plan tab:
- ❌ Detailed credit usage breakdown (move to Usage)
- ❌ Limit progress bars (move to Usage)
// Keep in Current Plan tab:
- ✅ Plan name, price, renewal date
- ✅ Brief limits summary (text only)
- ✅ Upgrade CTA
// Keep History tab as is
Usage Analytics Page:
// Keep all tabs as is - no changes needed
// This page is perfectly organized
9.2 Component Updates
UsageLimitsPanel.tsx:
// Add Ahrefs queries to monthly limits config
const monthlyLimitConfig = {
// ... existing ...
ahrefs_queries: {
icon: <SearchIcon className="w-5 h-5" />,
color: 'purple' as const
},
};
billing.api.ts:
// Add to Plan interface
export interface Plan {
// ... existing fields ...
max_ahrefs_queries?: number; // ADD THIS
}
// Add to UsageSummary interface
export interface UsageSummary {
// ... existing ...
monthly_limits: {
// ... existing ...
ahrefs_queries?: LimitUsage; // ADD THIS
};
}
10. Implementation Priority
Phase 1: Critical Fixes (Do First) 🔥
-
Enforce Keywords Limit (Backend)
- Add checks to keyword creation flows
- Estimated effort: 4 hours
- Impact: Prevents users from exceeding limits
-
Page Reorganization (Frontend)
- Remove duplicate data from Plans & Billing → Current Plan
- Estimated effort: 2 hours
- Impact: Reduces user confusion
Phase 2: Future Features (Do When Implementing Ahrefs)
- Implement Ahrefs Query Limit (Backend + Frontend)
- Database migration
- LimitService mapping
- Enforcement in Ahrefs service
- Frontend display
- Estimated effort: 6 hours
- Impact: Ready for Ahrefs integration
Phase 3: Nice-to-Have Improvements
-
Enforce Cluster Limit (Backend)
- Similar to keywords enforcement
- Estimated effort: 2 hours
-
Monthly Limits Reset Automation (Backend)
- Celery task for auto-reset
- Estimated effort: 3 hours
11. Migration Strategy
Week-by-Week Rollout
Week 1: Backend Foundation
- Create database migration to remove unused fields
- Update LimitService mappings
- Update serializers
- Add Ahrefs service skeleton
- Deploy to staging
Week 2: Enforcement
- Add keyword limit checks to all entry points
- Add automation credit pre-checks
- Test all validation flows
- Deploy to staging
Week 3: Frontend Cleanup
- Remove duplicate data from Plans & Billing
- Update UsageLimitsPanel
- Update terminology (remove "API", "operations")
- Deploy to staging
Week 4: New Features
- Build Credit Insights tab
- Build Keyword Research page
- Integrate Ahrefs (when ready)
- Add multi-dimensional widgets
- Deploy to staging
Week 5: Testing & Production
- Full regression testing
- User acceptance testing
- Deploy to production
- Monitor for issues
12. Final Limits Configuration
12.1 Database Schema (After Cleanup)
Plan Model - FINAL:
class Plan(models.Model):
# Basic Info
name = models.CharField(max_length=255)
slug = models.SlugField(unique=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
# ONLY 4 LIMITS:
max_sites = models.IntegerField(default=1)
max_users = models.IntegerField(default=1)
max_keywords = models.IntegerField(default=1000)
max_ahrefs_queries = models.IntegerField(default=50)
# Credits
included_credits = models.IntegerField(default=0)
extra_credit_price = models.DecimalField(max_digits=10, decimal_places=2)
Account Model - FINAL:
class Account(models.Model):
# Credits
credits = models.IntegerField(default=0)
# ONLY 1 Usage Tracker:
usage_ahrefs_queries = models.IntegerField(default=0)
# Billing Period
usage_period_start = models.DateTimeField(null=True)
usage_period_end = models.DateTimeField(null=True)
12.2 Suggested Plan Values
| Plan | Price | Included Credits | Sites | Users | Keywords | Ahrefs Queries/mo |
|---|---|---|---|---|---|---|
| Free | $0 | 2,000 | 1 | 1 | 100 | 0 |
| Starter | $49 | 10,000 | 2 | 2 | 1,000 | 50 |
| Growth | $149 | 40,000 | 5 | 3 | 5,000 | 200 |
| Scale | $399 | 120,000 | Unlimited | 5 | 20,000 | 500 |
13. Success Criteria
13.1 Technical Success
- All unused limit fields removed from database
- Migration runs successfully without data loss
- All 4 limits properly enforced
- Credit balance checked before ALL operations
- Automation pre-checks credit balance
- Ahrefs queries counted and limited
- No duplicate data across pages
- User-friendly terminology throughout
13.2 User Experience Success
- Users understand the simple 4-limit model
- Clear separation: Plans & Billing = financial, Usage = consumption
- Multi-dimensional insights provide actionable data
- Keyword research flow is intuitive
- Credit exhaustion messages are clear and actionable
- Upgrade prompts appear at right moments
13.3 Business Success
- Reduced support questions about limits
- Clearer upgrade paths
- Better credit consumption visibility drives upgrades
- Ahrefs integration ready for launch
- System scales without complexity
14. Risks & Mitigation
Risk 1: Data Loss During Migration
Mitigation:
- Backup database before migration
- Test migration on staging with production data clone
- Keep removed fields as comments in code for 1 month
Risk 2: Users Confused by Changes
Mitigation:
- In-app changelog notification
- Update help documentation
- Add tooltips to new UI elements
- Gradual rollout (staging → 10% → 50% → 100%)
Risk 3: Breaking Changes
Mitigation:
- Maintain backward compatibility in API for 2 weeks
- Version API endpoints if needed
- Monitor error logs closely after deployment
15. Post-Launch Monitoring
Metrics to Track
Technical:
- API error rates (especially 402 Insufficient Credits)
- Failed automation runs due to credits
- Keyword limit violations
- Ahrefs query usage patterns
Business:
- Upgrade conversion rate
- Support tickets about limits/credits
- Credit package purchase rate
- User engagement with new Usage insights
User Behavior:
- Time spent on Usage page
- Click-through on upgrade prompts
- Ahrefs query usage distribution
- Most-used insights widgets
16. Documentation Updates Required
- Update
docs/10-MODULES/BILLING.md - Update
docs/40-WORKFLOWS/CREDIT-SYSTEM.md - Create
docs/10-MODULES/KEYWORD-RESEARCH.md - Update API documentation
- Update user help docs
- Update admin guides
17. Summary
Before (Complex)
Keywords Limit Testing
- Try to create single keyword when at limit → Should fail
- Try to import 50 SeedKeywords when 30 slots remain → Should fail
- Try to bulk import CSV with 1000 keywords when at limit → Should fail
- Error message shows current count, limit, and upgrade link
- Upgrade plan → New limit applies immediately
- Delete keywords → Can add more up to new total
Ahrefs Limit Testing (Future)
- Query Ahrefs when at monthly limit → Should fail
- Error message shows queries used, limit, and reset date
- Monthly reset correctly resets counter
- Upgrade plan → New monthly allowance applies
Page Reorganization Testing
- Plans & Billing → Current Plan shows only plan info and brief summary
- Plans & Billing → Current Plan does NOT show detailed usage breakdown
- Usage → Limits & Usage shows all limits with progress bars
- Usage → Credit History shows transaction log
- No duplicate data between the two sections
12. Conclusion
What's Working Well ✅
- Credit System: Token-based calculation is accurate and well-implemented
- Usage Tracking: Monthly limits are properly tracked in Account model
- Sites & Users Limits: Fully enforced with proper error handling
- Frontend UI: UsageLimitsPanel component is excellent
What Needs Fixing ⚠️
- Keywords Enforcement: Limit exists but not consistently checked
- Data Duplication: Credits and usage data shown in both Plans & Usage
- Missing Ahrefs Limit: Future feature needs to be defined
Impact of Changes
| Change | User Benefit | Business Benefit |
|---|---|---|
| Enforce keywords limit | Clear boundaries, upgrade prompts | Prevent unlimited usage, drive upgrades |
| Remove duplication | Less confusion, faster navigation | Easier maintenance, clearer product positioning |
| Add Ahrefs limit | Know usage allowance upfront | Control costs, monetize feature |
Next Steps
- Backend Team: Implement keyword limit enforcement
- Frontend Team: Clean up Plans & Billing page
- Product Team: Define Ahrefs query pricing tiers
- QA Team: Test all limit scenarios
End of Report
For questions or clarification, refer to:
docs/10-MODULES/BILLING.md- Billing system documentationdocs/40-WORKFLOWS/CREDIT-SYSTEM.md- Credit workflowsbackend/igny8_core/business/billing/services/limit_service.py- Limit enforcement code