Skip to content

Pricing Implementation Plan & Testing Requirements

Transparent Fair Pricing Model - December 2025


📋 OVERVIEW

This document outlines the complete implementation plan for CodeSlick's new transparent pricing model, including: - Technical implementation requirements - Credit management system - Testing requirements - Launch checklist


🎯 PRICING MODEL SUMMARY

Philosophy

"Pay for the platform. Choose your AI model."

Core Principles

  1. Transparency: Users see exact costs (€0.02 per AI fix)
  2. Choice: Users choose AI provider/model
  3. Fairness: Pay for what you use
  4. Control: Real-time credit tracking

Pricing Structure

FREE TIER

  • €0/month
  • 20 PR analyses/month
  • Unlimited pattern-based fixes
  • Bring own API key for unlimited AI

TEAM TIER

  • €39/month base (down from €99)
  • Unlimited analyses & pattern fixes
  • AI Options:
  • 🔑 Own key: +€0/mo (unlimited AI)
  • 📦 Credits: +€10/mo (500 fixes)
  • ♾️ Unlimited: +€60/mo (unlimited server AI)

ENTERPRISE TIER

  • €129/month base (down from €299)
  • Everything in Team + unlimited repos/members
  • AI Options:
  • 🔑 Own key: +€0/mo (unlimited AI)
  • 📦 Credits: +€25/mo (1,000 fixes)
  • ♾️ Unlimited: +€120/mo (unlimited server AI)

🛠️ TECHNICAL IMPLEMENTATION

Phase 1: Database Schema (Week 1)

1.1 New Tables

-- Credit tracking table
CREATE TABLE ai_credits (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
  user_id UUID REFERENCES users(id),

  -- Credit configuration
  credit_type VARCHAR(20) NOT NULL, -- 'own-key', 'pack', 'unlimited'
  monthly_limit INTEGER, -- NULL for unlimited
  credits_used INTEGER DEFAULT 0,
  credits_remaining INTEGER, -- Computed field

  -- Billing period
  period_start TIMESTAMP NOT NULL,
  period_end TIMESTAMP NOT NULL,

  -- API key management (encrypted)
  own_api_key_encrypted TEXT, -- For 'own-key' type
  own_api_provider VARCHAR(50), -- 'openrouter', 'anthropic', etc.
  own_api_model VARCHAR(100),

  -- Timestamps
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW(),

  -- Indexes
  INDEX idx_team_period (team_id, period_start, period_end),
  INDEX idx_user_period (user_id, period_start, period_end)
);

-- Credit usage log (audit trail)
CREATE TABLE ai_credit_usage (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  team_id UUID NOT NULL REFERENCES teams(id),
  user_id UUID REFERENCES users(id),
  credit_record_id UUID REFERENCES ai_credits(id),

  -- Usage details
  file_path VARCHAR(500),
  issue_type VARCHAR(100),
  fix_success BOOLEAN DEFAULT TRUE,
  tokens_used INTEGER,
  cost_eur DECIMAL(10, 4), -- Actual cost in euros

  -- Metadata
  ai_provider VARCHAR(50),
  ai_model VARCHAR(100),
  response_time_ms INTEGER,

  -- Timestamps
  created_at TIMESTAMP DEFAULT NOW(),

  -- Indexes
  INDEX idx_team_date (team_id, created_at),
  INDEX idx_user_date (user_id, created_at)
);

-- Credit alerts (warnings)
CREATE TABLE credit_alerts (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  team_id UUID NOT NULL REFERENCES teams(id),
  alert_type VARCHAR(20) NOT NULL, -- '50_percent', '75_percent', '90_percent', 'exhausted'
  credits_remaining INTEGER,
  sent_at TIMESTAMP DEFAULT NOW(),
  acknowledged BOOLEAN DEFAULT FALSE,

  INDEX idx_team_alerts (team_id, sent_at)
);

1.2 Stripe Product/Price Updates

// New Stripe Products
const STRIPE_PRODUCTS = {
  team_base: {
    name: 'CodeSlick Team - Base Platform',
    price: 3900, // €39 in cents
    interval: 'month',
    description: 'Unlimited static analysis + pattern fixes'
  },
  team_credits: {
    name: 'CodeSlick Team - AI Credit Pack',
    price: 1000, // €10 in cents
    interval: 'month',
    description: '500 AI-powered fixes per month'
  },
  team_unlimited: {
    name: 'CodeSlick Team - Unlimited AI',
    price: 6000, // €60 in cents
    interval: 'month',
    description: 'Unlimited server-powered AI fixes'
  },
  enterprise_base: {
    name: 'CodeSlick Enterprise - Base Platform',
    price: 12900, // €129 in cents
    interval: 'month',
    description: 'Unlimited everything + advanced features'
  },
  enterprise_credits: {
    name: 'CodeSlick Enterprise - AI Credit Pack',
    price: 2500, // €25 in cents
    interval: 'month',
    description: '1,000 AI-powered fixes per month'
  },
  enterprise_unlimited: {
    name: 'CodeSlick Enterprise - Unlimited AI',
    price: 12000, // €120 in cents
    interval: 'month',
    description: 'Unlimited server-powered AI fixes'
  }
};

Phase 2: Credit Management API (Week 2)

2.1 Core API Endpoints

// GET /api/teams/[id]/credits
// Returns current credit status
interface CreditStatusResponse {
  creditType: 'own-key' | 'pack' | 'unlimited';
  monthlyLimit: number | null; // null for unlimited
  creditsUsed: number;
  creditsRemaining: number;
  percentageUsed: number;
  periodStart: string;
  periodEnd: string;
  daysRemaining: number;

  // Own-key configuration (if applicable)
  ownKey?: {
    provider: string;
    model: string;
    keyConfigured: boolean; // Don't send actual key
  };

  // Usage this month
  usageThisMonth: {
    totalFixes: number;
    estimatedCost: number; // In euros
    topFiles: Array<{
      path: string;
      fixes: number;
    }>;
  };

  // Warnings
  warnings: Array<{
    type: '50_percent' | '75_percent' | '90_percent' | 'exhausted';
    message: string;
    actionRequired: boolean;
  }>;
}

// POST /api/teams/[id]/credits/configure
// Configure AI option
interface ConfigureCreditsRequest {
  creditType: 'own-key' | 'pack' | 'unlimited';

  // For 'own-key' type
  ownKey?: {
    apiKey: string;
    provider: 'openrouter' | 'anthropic' | 'openai' | 'groq' | 'together';
    model: string;
  };
}

// POST /api/teams/[id]/credits/test-connection
// Test user's own API key
interface TestConnectionRequest {
  apiKey: string;
  provider: string;
  model: string;
}

interface TestConnectionResponse {
  success: boolean;
  message: string;
  estimatedCostPerFix?: number;
}

// GET /api/teams/[id]/credits/usage-history
// Historical usage
interface UsageHistoryResponse {
  history: Array<{
    date: string;
    fixesUsed: number;
    cost: number;
    topFile: string;
  }>;
  totalThisMonth: number;
  averagePerDay: number;
  projectedMonthEnd: number;
}

2.2 Credit Tracking Logic

// src/lib/credits/credit-manager.ts

export class CreditManager {
  /**
   * Check if team has credits available
   * @throws Error if no credits remaining
   */
  async checkCredits(teamId: string): Promise<void> {
    const credits = await this.getCurrentCredits(teamId);

    // Unlimited: always pass
    if (credits.creditType === 'unlimited') {
      return;
    }

    // Own-key: always pass (user pays)
    if (credits.creditType === 'own-key') {
      return;
    }

    // Credit pack: check limit
    if (credits.creditType === 'pack') {
      if (credits.creditsRemaining <= 0) {
        throw new Error(
          `No AI credits remaining. You've used ${credits.creditsUsed}/${credits.monthlyLimit} fixes this month. ` +
          `Options: Wait ${credits.daysRemaining} days for reset, upgrade to Unlimited, or bring your own API key.`
        );
      }

      // Warn if low
      if (credits.creditsRemaining <= 50 && !credits.warned50) {
        await this.sendLowCreditWarning(teamId, '50_percent', credits.creditsRemaining);
      }
    }
  }

  /**
   * Consume a credit (atomic operation)
   */
  async consumeCredit(teamId: string, userId: string, metadata: CreditUsageMetadata): Promise<void> {
    const credits = await this.getCurrentCredits(teamId);

    // Skip for unlimited and own-key
    if (credits.creditType === 'unlimited' || credits.creditType === 'own-key') {
      // Still log usage for analytics
      await this.logUsage(teamId, userId, metadata);
      return;
    }

    // Atomic increment (prevents race conditions)
    await db.query(`
      UPDATE ai_credits
      SET credits_used = credits_used + 1,
          credits_remaining = monthly_limit - (credits_used + 1),
          updated_at = NOW()
      WHERE team_id = $1
        AND period_start <= NOW()
        AND period_end > NOW()
        AND credits_remaining > 0
      RETURNING *
    `, [teamId]);

    // Log usage
    await this.logUsage(teamId, userId, metadata);
  }

  /**
   * Reset credits at period end (cron job)
   */
  async resetMonthlyCredits(): Promise<void> {
    // Find expired periods
    const expiredCredits = await db.query(`
      SELECT * FROM ai_credits
      WHERE period_end <= NOW()
        AND credit_type = 'pack'
    `);

    for (const credit of expiredCredits) {
      // Create new period
      await db.query(`
        INSERT INTO ai_credits (
          team_id, credit_type, monthly_limit,
          credits_used, credits_remaining,
          period_start, period_end
        ) VALUES (
          $1, $2, $3, 0, $3,
          NOW(), NOW() + INTERVAL '1 month'
        )
      `, [credit.team_id, credit.credit_type, credit.monthly_limit]);
    }
  }
}

Phase 3: Dashboard UI (Week 3)

3.1 Credit Dashboard Component

// src/components/dashboard/CreditDashboard.tsx

export function CreditDashboard() {
  const { data: credits } = useQuery('/api/teams/[id]/credits');

  return (
    <div className="bg-white rounded-lg shadow p-6">
      {/* Header */}
      <div className="flex justify-between items-center mb-6">
        <h2 className="text-2xl font-bold">AI Fix Credits</h2>
        <button onClick={() => setShowConfig(true)}>
          ⚙️ Configure
        </button>
      </div>

      {/* Credit Meter */}
      {credits.creditType === 'pack' && (
        <div className="mb-6">
          <div className="flex justify-between mb-2">
            <span className="font-medium">Credits Remaining</span>
            <span className="font-bold text-blue-600">
              {credits.creditsRemaining} / {credits.monthlyLimit}
            </span>
          </div>

          {/* Progress Bar */}
          <div className="w-full bg-gray-200 rounded-full h-3">
            <div
              className={`h-3 rounded-full transition-all ${
                credits.percentageUsed > 90 ? 'bg-red-600' :
                credits.percentageUsed > 75 ? 'bg-yellow-600' :
                'bg-blue-600'
              }`}
              style={{ width: `${100 - credits.percentageUsed}%` }}
            />
          </div>

          <p className="text-sm text-gray-600 mt-2">
            Resets in {credits.daysRemaining} days
          </p>
        </div>
      )}

      {/* Usage This Month */}
      <div className="grid grid-cols-3 gap-4 mb-6">
        <div className="bg-blue-50 rounded-lg p-4">
          <p className="text-sm text-gray-600">Fixes Used</p>
          <p className="text-2xl font-bold text-blue-600">
            {credits.usageThisMonth.totalFixes}
          </p>
        </div>

        <div className="bg-green-50 rounded-lg p-4">
          <p className="text-sm text-gray-600">Estimated Cost</p>
          <p className="text-2xl font-bold text-green-600">
            {credits.usageThisMonth.estimatedCost.toFixed(2)}
          </p>
        </div>

        <div className="bg-purple-50 rounded-lg p-4">
          <p className="text-sm text-gray-600">Top File</p>
          <p className="text-sm font-mono text-purple-600 truncate">
            {credits.usageThisMonth.topFiles[0]?.path || 'N/A'}
          </p>
        </div>
      </div>

      {/* Warnings */}
      {credits.warnings.length > 0 && (
        <div className="bg-yellow-50 border border-yellow-300 rounded-lg p-4">
          {credits.warnings.map((warning, idx) => (
            <div key={idx} className="flex items-start gap-2">
              <AlertCircle className="text-yellow-600 flex-shrink-0" />
              <div>
                <p className="font-semibold text-yellow-900">{warning.message}</p>
                {warning.actionRequired && (
                  <button className="text-sm text-blue-600 underline mt-1">
                    Upgrade Now
                  </button>
                )}
              </div>
            </div>
          ))}
        </div>
      )}

      {/* Configuration Modal */}
      {showConfig && (
        <CreditConfigModal
          currentType={credits.creditType}
          onClose={() => setShowConfig(false)}
        />
      )}
    </div>
  );
}

✅ TESTING REQUIREMENTS

Test Suite 1: Credit Tracking (Critical)

describe('Credit Management', () => {
  it('should consume 1 credit per AI fix', async () => {
    const team = await createTestTeam({ creditType: 'pack', monthlyLimit: 500 });

    // Use 1 fix
    await generateAIFix(team.id, mockIssue);

    const credits = await getCredits(team.id);
    expect(credits.creditsUsed).toBe(1);
    expect(credits.creditsRemaining).toBe(499);
  });

  it('should prevent fix when credits exhausted', async () => {
    const team = await createTestTeam({ creditType: 'pack', monthlyLimit: 1 });

    // Use the only credit
    await generateAIFix(team.id, mockIssue);

    // Attempt 2nd fix
    await expect(
      generateAIFix(team.id, mockIssue)
    ).rejects.toThrow('No AI credits remaining');
  });

  it('should allow unlimited fixes for own-key users', async () => {
    const team = await createTestTeam({
      creditType: 'own-key',
      ownApiKey: 'test-key'
    });

    // Use 1000 fixes
    for (let i = 0; i < 1000; i++) {
      await generateAIFix(team.id, mockIssue);
    }

    const credits = await getCredits(team.id);
    expect(credits.creditsRemaining).toBe(null); // Unlimited
  });

  it('should reset credits on new period', async () => {
    const team = await createTestTeam({ creditType: 'pack', monthlyLimit: 500 });

    // Use all credits
    for (let i = 0; i < 500; i++) {
      await generateAIFix(team.id, mockIssue);
    }

    // Simulate period end
    await timeTravelToNextMonth();
    await resetMonthlyCredits();

    const credits = await getCredits(team.id);
    expect(credits.creditsUsed).toBe(0);
    expect(credits.creditsRemaining).toBe(500);
  });

  it('should send warning at 50% usage', async () => {
    const team = await createTestTeam({ creditType: 'pack', monthlyLimit: 100 });

    // Use 50 credits
    for (let i = 0; i < 50; i++) {
      await generateAIFix(team.id, mockIssue);
    }

    const alerts = await getAlerts(team.id);
    expect(alerts).toContainEqual(
      expect.objectContaining({ alert_type: '50_percent' })
    );
  });
});

Test Suite 2: Own API Key Management

describe('Own API Key', () => {
  it('should encrypt API key in database', async () => {
    const team = await createTestTeam();

    await configureOwnKey(team.id, {
      apiKey: 'sk-test-12345',
      provider: 'openrouter',
      model: 'anthropic/claude-3.5-sonnet'
    });

    // Check database directly
    const record = await db.query('SELECT * FROM ai_credits WHERE team_id = $1', [team.id]);
    expect(record.own_api_key_encrypted).not.toBe('sk-test-12345');
    expect(record.own_api_key_encrypted).toContain('encrypted:');
  });

  it('should validate API key before saving', async () => {
    const team = await createTestTeam();

    await expect(
      configureOwnKey(team.id, {
        apiKey: 'invalid-key',
        provider: 'openrouter',
        model: 'test'
      })
    ).rejects.toThrow('Invalid API key');
  });

  it('should use user API key for fixes', async () => {
    const team = await createTestTeam({
      creditType: 'own-key',
      ownApiKey: 'sk-test-user-key'
    });

    const mockFetch = jest.spyOn(global, 'fetch');

    await generateAIFix(team.id, mockIssue);

    expect(mockFetch).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        headers: expect.objectContaining({
          'Authorization': 'Bearer sk-test-user-key'
        })
      })
    );
  });
});

Test Suite 3: Stripe Integration

describe('Stripe Billing', () => {
  it('should create subscription with base + credits', async () => {
    const team = await createTestTeam();

    const subscription = await createSubscription(team.id, {
      tier: 'team',
      aiOption: 'credits'
    });

    expect(subscription.items).toHaveLength(2);
    expect(subscription.items[0].price.id).toBe(STRIPE_PRODUCTS.team_base.priceId);
    expect(subscription.items[1].price.id).toBe(STRIPE_PRODUCTS.team_credits.priceId);
    expect(subscription.amount_total).toBe(4900); // €49
  });

  it('should update subscription when changing AI option', async () => {
    const team = await createTestTeam({ tier: 'team', aiOption: 'credits' });

    // Switch to unlimited
    await updateSubscription(team.id, { aiOption: 'unlimited' });

    const subscription = await getSubscription(team.id);
    expect(subscription.amount_total).toBe(9900); // €99
  });

  it('should handle downgrade to own-key', async () => {
    const team = await createTestTeam({ tier: 'team', aiOption: 'unlimited' });

    // Switch to own-key
    await updateSubscription(team.id, { aiOption: 'own-key' });

    const subscription = await getSubscription(team.id);
    expect(subscription.items).toHaveLength(1); // Only base
    expect(subscription.amount_total).toBe(3900); // €39
  });
});

Test Suite 4: Edge Cases

describe('Edge Cases', () => {
  it('should handle concurrent fix requests (race condition)', async () => {
    const team = await createTestTeam({ creditType: 'pack', monthlyLimit: 10 });

    // Attempt 20 simultaneous fixes
    const promises = Array(20).fill(null).map(() =>
      generateAIFix(team.id, mockIssue)
    );

    const results = await Promise.allSettled(promises);

    // Only 10 should succeed
    const successful = results.filter(r => r.status === 'fulfilled');
    expect(successful.length).toBe(10);

    const credits = await getCredits(team.id);
    expect(credits.creditsUsed).toBe(10);
  });

  it('should handle API key rotation', async () => {
    const team = await createTestTeam({
      creditType: 'own-key',
      ownApiKey: 'sk-old-key'
    });

    // Update to new key
    await configureOwnKey(team.id, {
      apiKey: 'sk-new-key',
      provider: 'openrouter',
      model: 'anthropic/claude-3.5-sonnet'
    });

    // Next fix should use new key
    const mockFetch = jest.spyOn(global, 'fetch');
    await generateAIFix(team.id, mockIssue);

    expect(mockFetch).toHaveBeenCalledWith(
      expect.any(String),
      expect.objectContaining({
        headers: expect.objectContaining({
          'Authorization': 'Bearer sk-new-key'
        })
      })
    );
  });

  it('should handle period transition gracefully', async () => {
    const team = await createTestTeam({ creditType: 'pack', monthlyLimit: 100 });

    // Use 99 credits
    for (let i = 0; i < 99; i++) {
      await generateAIFix(team.id, mockIssue);
    }

    // Time travel to last day of period
    await timeTravelToDays(29);

    // Use last credit
    await generateAIFix(team.id, mockIssue);

    // Should fail
    await expect(
      generateAIFix(team.id, mockIssue)
    ).rejects.toThrow();

    // Time travel to new period
    await timeTravelToDays(2);
    await resetMonthlyCredits();

    // Should succeed now
    await expect(
      generateAIFix(team.id, mockIssue)
    ).resolves.toBeDefined();
  });
});

🚀 LAUNCH CHECKLIST

Pre-Launch (Week 4)

  • Database Migration
  • Create ai_credits table
  • Create ai_credit_usage table
  • Create credit_alerts table
  • Add indexes for performance
  • Test rollback procedure

  • Stripe Configuration

  • Create new products in Stripe
  • Create price points (€39, €10, €60, €129, €25, €120)
  • Test webhook handling for subscription changes
  • Configure prorated billing
  • Test annual billing (17% discount)

  • API Development

  • Implement credit checking middleware
  • Build credit configuration endpoints
  • Create usage tracking API
  • Add real-time credit updates (WebSocket)
  • Implement alert system

  • Dashboard UI

  • Build credit dashboard component
  • Create configuration modal (own-key setup)
  • Add usage charts/analytics
  • Implement low-credit warnings
  • Add upgrade/downgrade flows

  • Testing

  • Run 568+ existing tests (ensure no regressions)
  • Add 50+ new credit management tests
  • Load test: 1,000 concurrent credit checks
  • Security audit: API key encryption
  • End-to-end user flows

Launch Day

  • Deploy
  • Deploy database migrations
  • Deploy API changes
  • Deploy UI updates
  • Monitor error rates

  • Communication

  • Email existing users (pricing update)
  • Publish blog post: "Fair Pricing Revolution"
  • Update pricing page
  • Update documentation

  • Monitoring

  • Set up credit usage alerts
  • Monitor API error rates
  • Track conversion rates (free → paid)
  • Monitor Stripe webhooks

Post-Launch (First Week)

  • Metrics Tracking
  • Own-key adoption rate (target: 60%)
  • Credit pack adoption (target: 30%)
  • Unlimited AI adoption (target: 10%)
  • Average revenue per user (target: €48/mo)
  • LTV:CAC ratio (target: >4x)

  • User Feedback

  • Survey users on pricing clarity (target: >90% "fair")
  • Monitor NPS score (target: >60)
  • Track support tickets about pricing
  • Collect testimonials

  • A/B Testing

  • Test €39 vs €49 base price
  • Test credit pack sizes (500 vs 750 vs 1,000)
  • Test free tier limits (20 vs 30 analyses/month)

📊 SUCCESS METRICS

Week 1 Goals

  • 100% existing users migrated to new pricing
  • <5% churn rate (users canceling due to pricing)
  • >50% of new signups choose own-key option
  • Zero critical bugs in credit tracking

Month 1 Goals

  • 200 paying customers (up from 0)
  • €9,600 MRR (200 teams × €48 avg)
  • 60% own-key adoption
  • >90% "pricing is fair" survey rating
  • <2% monthly churn

Month 3 Goals

  • 500 paying customers
  • €24,000 MRR
  • 4.5x LTV:CAC ratio
  • >65 NPS score
  • 1% monthly churn (sticky pricing)

🔒 SECURITY REQUIREMENTS

API Key Encryption

// Use AES-256-GCM for API key encryption
import crypto from 'crypto';

const ENCRYPTION_KEY = process.env.CREDIT_ENCRYPTION_KEY; // 32 bytes
const ALGORITHM = 'aes-256-gcm';

export function encryptAPIKey(apiKey: string): string {
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(ALGORITHM, ENCRYPTION_KEY, iv);

  let encrypted = cipher.update(apiKey, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  return `encrypted:${iv.toString('hex')}:${encrypted}:${authTag.toString('hex')}`;
}

export function decryptAPIKey(encryptedKey: string): string {
  const [prefix, ivHex, encrypted, authTagHex] = encryptedKey.split(':');

  if (prefix !== 'encrypted') {
    throw new Error('Invalid encrypted key format');
  }

  const iv = Buffer.from(ivHex, 'hex');
  const authTag = Buffer.from(authTagHex, 'hex');
  const decipher = crypto.createDecipheriv(ALGORITHM, ENCRYPTION_KEY, iv);

  decipher.setAuthTag(authTag);

  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');

  return decrypted;
}

Rate Limiting

// Prevent credit abuse
const RATE_LIMITS = {
  ai_fix: {
    max: 10, // Max 10 AI fixes per minute
    window: 60 * 1000 // 1 minute
  },
  credit_check: {
    max: 100, // Max 100 credit checks per minute
    window: 60 * 1000
  }
};

🎯 NEXT STEPS

  1. Week 1: Implement database schema + Stripe products
  2. Week 2: Build credit management API
  3. Week 3: Create dashboard UI + testing
  4. Week 4: QA, security audit, launch preparation
  5. Week 5: Soft launch to beta users
  6. Week 6: Full public launch + marketing

Created: December 7, 2025 Status: Ready for implementation Owner: Vitor (Founder)