Credit Economy
Kinetic Email uses a credit-based economy to gate premium actions like AI generation and email sending. Credits are the internal currency — users earn them through course completion and referrals, and spend them on AI-powered features.
How It Works
User earns credits (course completion, referrals, admin grants)
│
▼
Credit balance stored in Supabase (token_balances table)
│
▼
User triggers premium action (e.g., KineticAI)
│
├── Frontend: useTokenGate() pre-checks balance (read-only)
│ └── If insufficient → blocks action with UI feedback
│
└── Backend: spendTokensServer() deducts credits (atomic)
└── If insufficient → returns 402
Credit Actions & Costs
Action costs are defined in the token_action_costs table:
| Action | Cost | Description |
|---|---|---|
ai_generation | 3 | Kinetic email generation (Claude) — charged by /api/generate and /api/generate-email |
amp_addon | 2 | AMP4Email variant, built by /api/gen/amp after the kinetic email (dual build totals 5) |
brand_extraction | — | AI brand builder: scrape a URL into a brand profile |
email_send | — | Defined in the costs table, but email sending (/api/send-email-mailgun) currently requires auth only and does not deduct credits |
Costs are configurable per action — adjust them in the database without code changes.
Free Regeneration
When a generation's QA score lands below 70, the user is offered one free regeneration. It's genuinely free (verified and consumed server-side, refunds on failure) and replays the full original request — AMP variant, images, products, brand tokens, and locked copy included — while feeding the prior run's failed QA checks back to the model.
Earning Credits
Course Completion
Completing all modules in a course awards bonus credits:
| Course | Modules | Module IDs |
|---|---|---|
| Developer | 6 | introduction, checkbox-hack, lightswitch, tabbed-elements, engagement-quiz, tracking |
| Marketing | 5 | marketing-why-kinetic, marketing-ecommerce, marketing-subscriptions, marketing-newsletters, marketing-education |
| Coding 101 | 5 | coding-html-basics, coding-css-basics, coding-why-tables, coding-mso-conditionals, coding-div-future |
| AI Prompt Lab | 5 | prompting-brand-setup, prompting-kinetic-ai, prompting-minimal, prompting-copy-doc, prompting-refine |
The bonus amount is stored in token_config table (course_completion_bonus key).
Auto-award logic in LearningProgressContext:
- On page load, checks if any course is fully complete
- Calls
award_course_completion_tokens()RPC - SQL function prevents double-awarding (idempotent)
Referrals
Each user gets a unique referral code. When a new user signs up with ?ref=CODE:
- The
process_referral()RPC awards credits to the referrer - Referral codes are stored in the
referral_codestable
Database Schema
token_balances
user_id UUID PRIMARY KEY REFERENCES auth.users(id)
balance INTEGER DEFAULT 0
lifetime_earned INTEGER DEFAULT 0
lifetime_spent INTEGER DEFAULT 0
token_transactions
id UUID PRIMARY KEY
user_id UUID REFERENCES auth.users(id)
amount INTEGER -- positive = credit, negative = debit
balance_after INTEGER
transaction_type TEXT -- 'course_completion', 'referral', 'spend', 'admin_grant'
description TEXT
reference_id TEXT -- e.g., course ID, action name
created_at TIMESTAMPTZ
token_action_costs
action_name TEXT PRIMARY KEY
cost INTEGER
description TEXT
Server-Side Operations
The credit system uses PostgreSQL SECURITY DEFINER functions for all sensitive operations:
- Credit spending — Atomic deduction using
UPDATE ... WHERE balance >= costto prevent negative balances and race conditions - Balance checking — Read-only affordability check for instant frontend UX feedback (no deduction)
- Course awards — Idempotent bonus awards that prevent double-granting
All functions are scoped to the calling user via auth.uid() and are invoked through the Supabase client SDK.
Frontend Integration
useTokenGate() Hook
const { executeWithTokens, isBlocked, errorMessage } = useTokenGate();
// Pre-checks balance, executes action, refreshes balance after
const result = await executeWithTokens('ai_generation', async () => {
return await fetch('/api/generate', { ... });
});
if (result === null) {
// Blocked — insufficient credits
}
The hook:
- Calls
checkCanAfford()for instant UI feedback (no deduction) - If affordable, executes the action (server deducts credits)
- Calls
refreshBalance()to sync the UI with the server-side deduction
TokenContext
const {
balance, // Current credit balance
isLoading, // Balance loading state
spendTokens, // Direct RPC call (used by backend)
checkCanAfford, // Read-only balance check
refreshBalance, // Refresh balance from server
awardCourseCompletion, // Award course completion bonus
getReferralUrl, // Get user's referral link
} = useTokens();