GameCraftGameCraft

Usage & Bonus System

Space usage tracking, plan limits, bonus credits, and redemption codes

Usage & Bonus System

ProductReady includes a comprehensive usage tracking and bonus system that allows you to:

  • Track resource usage per space (AI Credits, Posts, Storage)
  • Enforce plan limits (Free, Pro, Enterprise)
  • Grant bonus credits via redemption codes or promotions
  • Display usage in dashboard with progress bars and warnings

This system is separate from the payment billing system. It tracks what users consume, while billing handles how they pay.


Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                     Plan Configuration                       │
│  (src/config/plan-limits.ts + pricing-plans.ts)             │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    Database Schema                           │
│  ┌──────────────┐  ┌─────────────────┐  ┌────────────────┐  │
│  │ spaceUsage   │  │ spaceBonusUsage │  │ bonusTemplates │  │
│  │ (tracking)   │  │ (bonus credits) │  │ (admin config) │  │
│  └──────────────┘  └─────────────────┘  └────────────────┘  │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    Service Layer                             │
│  ┌──────────────────┐  ┌─────────────────────────────────┐  │
│  │ usage-service.ts │  │ bonus-service.ts                │  │
│  │ - getUsageSummary│  │ - grantBonusFromTemplate        │  │
│  │ - consumeUsage   │  │ - grantManualBonus              │  │
│  └──────────────────┘  └─────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    tRPC Routers                              │
│  usage.ts  │  bonusTemplates.ts  │  redemptionCodes.ts      │
└─────────────────────────────────────────────────────────────┘

Plan Configuration

Plan Limits

Define resource limits for each plan in src/config/plan-limits.ts:

// src/config/plan-limits.ts
export type UsageType = "ai_credits" | "posts" | "storage";
export type PeriodType = "monthly" | "total";

export interface UsageLimit {
  limit: number;        // -1 for unlimited
  period: PeriodType;
  label: string;
}

export interface FeatureFlag {
  enabled: boolean;
  label: string;
}

export interface PlanLimits {
  aiCredits: UsageLimit;
  posts: UsageLimit;
  storage: UsageLimit;
  openApi: FeatureFlag;
}

const MB = 1024 * 1024;
const GB = MB * 1024;
export const UNLIMITED = -1;

export const PLAN_LIMITS: Record<PlanKey, PlanLimits> = {
  Free: {
    aiCredits: { limit: 50, period: "monthly", label: "AI Credits" },
    posts: { limit: 100, period: "total", label: "Posts" },
    storage: { limit: 100 * MB, period: "total", label: "Storage" },
    openApi: { enabled: false, label: "OpenAPI Access" },
  },
  Pro: {
    aiCredits: { limit: 500, period: "monthly", label: "AI Credits" },
    posts: { limit: 1000, period: "total", label: "Posts" },
    storage: { limit: 10 * GB, period: "total", label: "Storage" },
    openApi: { enabled: true, label: "OpenAPI Access" },
  },
  Enterprise: {
    aiCredits: { limit: UNLIMITED, period: "monthly", label: "AI Credits" },
    posts: { limit: UNLIMITED, period: "total", label: "Posts" },
    storage: { limit: UNLIMITED, period: "total", label: "Storage" },
    openApi: { enabled: true, label: "OpenAPI Access" },
  },
};

Pricing Plans

Define plan versions for billing in src/config/pricing-plans.ts:

// src/config/pricing-plans.ts
export interface PricingPlan {
  id: string;           // Stable ID (e.g., "pro_v1")
  name: string;         // Display name
  plan: string;         // Plan type (Free, Pro, Enterprise)
  priceMonthly: number; // Monthly price in cents
  priceYearly: number;  // Yearly price in cents
  isActive: boolean;    // Whether this version is available
}

export const PRICING_PLANS: PricingPlan[] = [
  { id: "free_v1", name: "Free", plan: "Free", priceMonthly: 0, priceYearly: 0, isActive: true },
  { id: "pro_v1", name: "Pro", plan: "Pro", priceMonthly: 2900, priceYearly: 29000, isActive: true },
  { id: "enterprise_v1", name: "Enterprise", plan: "Enterprise", priceMonthly: 9900, priceYearly: 99000, isActive: true },
];

Database Schema

Space Usage

Tracks current usage for each space:

// src/db/schema/space-usage.ts
export const spaceUsage = pgTable("space_usage", {
  spaceId: text("space_id").primaryKey(),
  aiCreditsUsed: integer("ai_credits_used").default(0).notNull(),
  postsUsed: integer("posts_used").default(0).notNull(),
  storageUsed: bigint("storage_used", { mode: "number" }).default(0).notNull(),
  currentPeriod: text("current_period").notNull(), // "YYYY-MM"
  createdAt: timestamp("created_at").defaultNow().notNull(),
  updatedAt: timestamp("updated_at"),
});

Space Bonus Usage

Tracks bonus credits granted to spaces:

// src/db/schema/space-bonus-usage.ts
export const spaceBonusUsage = pgTable("space_bonus_usage", {
  id: text("id").primaryKey(),
  spaceId: text("space_id").notNull(),
  type: text("type").notNull(),        // ai_credits, posts, storage
  amount: integer("amount").notNull(),  // Total bonus amount
  used: integer("used").default(0),     // Amount consumed
  source: text("source").notNull(),     // redemption, promotion, manual
  sourceName: text("source_name").notNull(),
  sourceRef: text("source_ref"),
  expiresAt: timestamp("expires_at"),   // null = no expiry
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

Bonus Templates

Admin-defined bonus packages:

// src/db/schema/space-bonus-templates.ts
export const spaceBonusTemplates = pgTable("space_bonus_templates", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  description: text("description"),
  type: text("type").notNull(),           // ai_credits, posts, storage
  amount: integer("amount").notNull(),
  durationDays: integer("duration_days"), // null = no expiry
  applicablePlans: text("applicable_plans").array(),
  isActive: boolean("is_active").default(true).notNull(),
  createdBy: text("created_by"),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

Usage Service

Get Usage Summary

import { getUsageSummary } from "~/lib/billing";

const summary = await getUsageSummary(spaceId);
// Returns:
// {
//   spaceId: "space_123",
//   plan: "Pro",
//   planId: "pro_v1",
//   resetDate: "2024-02-01",
//   items: [
//     { type: "ai_credits", used: 150, limit: 1100, percentage: 13.6, isWarning: false },
//     { type: "posts", used: 25, limit: 100, percentage: 25, isWarning: false },
//   ],
//   features: [
//     { type: "custom_domain", enabled: true, label: "Custom Domain" },
//   ],
// }

Consume Usage

import { consumeUsage } from "~/lib/billing";

const result = await consumeUsage(spaceId, "ai_credits", 10);
// Returns:
// { success: true, remaining: 90 }
// or
// { success: false, error: "quota_exhausted" }

Consumption Priority

When consuming usage, the system follows this priority:

  1. Bonus credits first (earliest expiry first)
  2. Plan quota (when bonuses exhausted)
  3. Reject (when all quota exhausted)

Bonus Service

Grant Bonus from Template

import { grantBonusFromTemplate } from "~/lib/billing";

const result = await grantBonusFromTemplate({
  spaceId: "space_123",
  templateId: "tmpl_welcome_bonus",
  sourceRef: "code_ABC123",
  durationDaysOverride: 30, // Optional: override template duration
});
// Returns: { success: true, bonus: BonusItemVO }

Grant Manual Bonus

import { grantManualBonus } from "~/lib/billing";

const result = await grantManualBonus({
  spaceId: "space_123",
  type: "ai_credits",
  amount: 500,
  sourceName: "Customer Support",
  durationDays: 90,
});

Redemption Codes

Redemption codes support polymorphic rewards:

Reward Types

TypeDescription
planUpgrade space to a plan (Free/Pro/Enterprise)
bonusGrant bonus credits from a template

Schema

export const redemptionCodes = pgTable("redemption_codes", {
  id: text("id").primaryKey(),
  code: text("code").notNull().unique(),
  rewardType: text("reward_type").default("plan").notNull(), // "plan" or "bonus"
  
  // Plan reward fields
  planType: text("plan_type"),           // Free, Pro, Enterprise
  durationDays: integer("duration_days"), // null = lifetime
  
  // Bonus reward fields
  bonusTemplateId: text("bonus_template_id"),
  
  // Common fields
  maxRedemptions: integer("max_redemptions"),
  currentRedemptions: integer("current_redemptions").default(0),
  isActive: text("is_active").default("true"),
  expiresAt: timestamp("expires_at"),
  createdBy: text("created_by").notNull(),
});

Creating Codes (Admin)

// Create a code that upgrades to Pro for 30 days
await trpc.redemptionCodes.create.mutate({
  code: "PROMO2024",
  rewardType: "plan",
  planType: "Pro",
  durationDays: 30,
  maxRedemptions: 100,
  description: "Launch promotion",
});
// Create a code that grants bonus credits
await trpc.redemptionCodes.create.mutate({
  code: "BONUS500",
  rewardType: "bonus",
  bonusTemplateId: "tmpl_500_credits",
  durationDays: 60, // Override template duration
  maxRedemptions: 50,
});

Redeeming Codes (User)

const result = await trpc.redemptionCodes.redeem.mutate({
  code: "PROMO2024",
  spaceId: "space_123",
});
// Returns:
// {
//   success: true,
//   rewardType: "plan",
//   planType: "Pro",
//   expiresAt: "2024-02-01T00:00:00Z",
//   bonusGranted: false,
// }

Sharing Redemption Instructions with End Users

As a developer, you can use these templates to guide your users through the redemption process. Simply copy and customize the message below.

When distributing redemption codes to your users, you can use these copy-paste templates:

Template 1: Plan Upgrade Code

Hello [User Name],

Thank you for your interest in upgrading your workspace! Here's your exclusive redemption code:

🎁 CODE: [YOUR_CODE_HERE]

To redeem your code and upgrade to [Pro/Enterprise] plan:

1. Visit: https://yourapp.com/redeem
2. Sign in to your account (or create one if you're new)
3. Enter the redemption code: [YOUR_CODE_HERE]
4. Select the workspace you want to upgrade
5. Click "Redeem Code"

Your workspace will be immediately upgraded! This code is valid [for 30 days / until MM/DD/YYYY / lifetime].

If you have any questions, feel free to reach out to our support team.

Best regards,
[Your Team Name]

Template 2: Bonus Credits Code

Hi [User Name],

Great news! We're giving you bonus credits to try out our premium features.

🎁 BONUS CODE: [YOUR_CODE_HERE]

How to claim your bonus:

1. Go to https://yourapp.com/redeem
2. Log in to your account
3. Enter your code: [YOUR_CODE_HERE]
4. Choose your workspace
5. Hit "Redeem Code"

You'll instantly receive [X] bonus AI credits / posts / storage that you can use for [duration].

Enjoy exploring the platform!

[Your Team Name]

Template 3: Quick Email Format

Subject: Your Redemption Code is Ready! 🎁

Hi [User Name],

Your redemption code: [YOUR_CODE_HERE]

Redeem at: https://yourapp.com/redeem

This code will [upgrade your plan to Pro/grant you X bonus credits].

Questions? Reply to this email.

Thanks,
[Your Team]

Redemption Page Features

Your users will see a user-friendly interface at /redeem that includes:

  • Automatic code detection: If you share a link like https://yourapp.com/redeem?code=PROMO2024, the code will be pre-filled
  • Sign-in protection: Users must be logged in to redeem codes
  • Workspace selection: Users can choose which workspace to apply the code to
  • Instant confirmation: Success message with details about the upgrade or bonus
  • Current plan visibility: Users can see their current plan before redeeming

Best Practices for Distribution

  1. Use direct links: Share https://yourapp.com/redeem?code=YOUR_CODE to pre-fill the code
  2. Set expectations: Tell users what they'll receive (plan upgrade, credits amount, duration)
  3. Provide support: Include contact information for redemption issues
  4. Track redemptions: Monitor redemption rates in the admin panel at /systemadmin/redemption-codes
  5. Set expiration dates: Use time-limited codes to create urgency

tRPC API Reference

Usage Router

// Get usage summary for current space
trpc.usage.getSummary.query()

// Consume usage (internal use)
trpc.usage.consume.mutate({ type: "ai_credits", amount: 10 })

Bonus Templates Router (Admin)

// List templates
trpc.bonusTemplates.list.query({ isActive: true })

// Create template
trpc.bonusTemplates.create.mutate({
  name: "Welcome Bonus",
  type: "ai_credits",
  amount: 100,
  durationDays: 30,
  applicablePlans: ["Free", "Pro"],
})

// Update template
trpc.bonusTemplates.update.mutate({ id: "tmpl_123", isActive: false })

// Delete template
trpc.bonusTemplates.delete.mutate({ id: "tmpl_123" })

Redemption Codes Router

// List codes (admin)
trpc.redemptionCodes.list.query({ rewardType: "bonus", isActive: "true" })

// Create code (admin)
trpc.redemptionCodes.create.mutate({ ... })

// Batch create codes (admin)
trpc.redemptionCodes.batchCreate.mutate({
  prefix: "LAUNCH",
  count: 50,
  rewardType: "plan",
  planType: "Pro",
  durationDays: 30,
})

// Redeem code (user)
trpc.redemptionCodes.redeem.mutate({ code: "ABC123", spaceId: "space_123" })

// Get redemption history (admin)
trpc.redemptionCodes.getHistory.query({ codeId: "code_123" })

UI Components

Usage Panel

Display usage in dashboard:

import { UsagePanel } from "~/components/dashboard/usage-panel";

<UsagePanel spaceId={spaceId} />

Usage Progress Bar

Individual usage item:

import { UsageProgressBar } from "~/components/dashboard/usage-progress-bar";

<UsageProgressBar
  label="AI Credits"
  used={150}
  limit={1000}
  period="monthly"
  isWarning={false}
/>

Bonus Table

Display active bonuses:

import { BonusTable } from "~/components/dashboard/bonus-table";

<BonusTable spaceId={spaceId} />

System Admin Pages

Bonus Templates Management

Navigate to: /systemadmin/bonus-templates

Features:

  • Create/edit/delete bonus templates
  • Set amount, duration, applicable plans
  • Toggle active status

Redemption Codes Management

Navigate to: /systemadmin/redemption-codes

Features:

  • Create single or batch codes
  • Choose reward type (plan or bonus)
  • View redemption history
  • Export codes to CSV

Monthly Reset

Monthly usage (like AI Credits) automatically resets at the start of each billing period:

// In getUsageSummary()
const currentPeriod = getCurrentPeriod(); // "2024-01"
if (usage.currentPeriod !== currentPeriod) {
  // Reset monthly counters
  await resetMonthlyUsage(spaceId, currentPeriod);
}

Best Practices

1. Check Usage Before Operations

async function generateContent(spaceId: string) {
  const result = await consumeUsage(spaceId, "ai_credits", 1);
  if (!result.success) {
    throw new Error("AI credits exhausted. Please upgrade your plan.");
  }
  // Proceed with generation...
}

2. Show Warnings at 80%

The isWarning flag is set when usage exceeds 80%:

{item.isWarning && (
  <Alert variant="warning">
    You've used {item.percentage}% of your {item.label}. 
    Consider upgrading your plan.
  </Alert>
)}

3. Grant Bonuses for Engagement

// Grant bonus when user completes onboarding
await grantPromotionalBonus(
  spaceId,
  "ai_credits",
  50,
  "Onboarding Completion",
  30 // 30 days expiry
);

Troubleshooting

Usage Not Updating

  1. Check spaceUsage record exists for the space
  2. Verify currentPeriod matches current month
  3. Check for database transaction errors

Bonus Not Applied

  1. Verify template is active (isActive: true)
  2. Check plan is in applicablePlans array
  3. Verify bonus hasn't expired

Redemption Code Fails

  1. Check code is active and not expired
  2. Verify max redemptions not reached
  3. Check space hasn't already redeemed this code

On this page