Skip to main content

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:

ActionCostDescription
ai_generation3Kinetic email generation (Claude) — charged by /api/generate and /api/generate-email
amp_addon2AMP4Email variant, built by /api/gen/amp after the kinetic email (dual build totals 5)
brand_extractionAI brand builder: scrape a URL into a brand profile
email_sendDefined 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:

CourseModulesModule IDs
Developer6introduction, checkbox-hack, lightswitch, tabbed-elements, engagement-quiz, tracking
Marketing5marketing-why-kinetic, marketing-ecommerce, marketing-subscriptions, marketing-newsletters, marketing-education
Coding 1015coding-html-basics, coding-css-basics, coding-why-tables, coding-mso-conditionals, coding-div-future
AI Prompt Lab5prompting-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:

  1. On page load, checks if any course is fully complete
  2. Calls award_course_completion_tokens() RPC
  3. 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_codes table

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 >= cost to 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:

  1. Calls checkCanAfford() for instant UI feedback (no deduction)
  2. If affordable, executes the action (server deducts credits)
  3. 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();