Skip to main content

Billing & Tokens

Garmint uses a token-based billing system with subscription tiers and pay-as-you-go options.

Subscription Plans

Trial

100 tokens/month
  • ~8 fast generations
  • 1 workspace
  • Basic garment library

Starter

2,500 tokens/month — $29/mo
  • ~200 fast generations
  • Full garment library
  • Email support

Pro

10,000 tokens/month — $99/mo
  • 3 workspaces
  • Priority queue
  • Token rollover

Studio

40,000 tokens/month — $299/mo
  • Unlimited workspaces
  • Production dashboard
  • Dedicated support

Token Costs

Generation costs vary by model and operation:
OperationModelTokens
Fast generationnano-banana12
Quality generationnano-banana-pro45
Print-ready 4Knano-banana-pro-4k90
Model photomodel-photo23
Background removalremove-bg3
Image upscaleupscale1
// lib/billing/tokens.ts
export const TOKEN_COSTS = {
  "nano-banana": 12,         // Fast generation
  "nano-banana-pro": 45,     // Quality generation
  "nano-banana-pro-4k": 90,  // Print-ready 4K
  "model-photo": 23,         // Model photo (FASHN)
  "remove-bg": 3,            // Background removal
  "upscale": 1,              // Image upscaling
} as const;

Token Packs (PAYG)

Buy additional tokens anytime through the billing portal.

Database Schema

billing_plans

create table public.billing_plans (
  id uuid primary key,
  slug text unique,           -- 'free', 'pro', 'studio'
  name text,                  -- 'Free', 'Pro', 'Studio'
  billing_model text,         -- 'subscription' | 'payg'
  monthly_tokens integer,     -- tokens granted per billing cycle
  rollover boolean,           -- unused tokens carry over?
  price_monthly integer,      -- price in cents
  price_per_token numeric,    -- for PAYG plans
  is_active boolean
);

token_grants

Each grant represents tokens added to a user’s balance:
create table public.token_grants (
  id uuid primary key,
  user_id uuid references profiles(id),
  subscription_id uuid references subscriptions(id),
  source text,      -- 'system' | 'plan' | 'payg_purchase' | 'manual_adjustment'
  tokens integer,
  expires_at timestamptz,
  metadata jsonb
);

usage_ledgers

Every token consumption is logged:
create table public.usage_ledgers (
  id bigserial primary key,
  user_id uuid references profiles(id),
  event_type text,  -- 'ai_generation' | 'mockup' | 'upload'
  tokens integer,
  model text,       -- which AI model was used
  notes text,
  metadata jsonb
);

Balance Calculation

// lib/billing/tokens.ts
export async function getTokenBalance(userId: string) {
  const supabase = getSupabaseServiceRoleClient();
  
  const { data } = await supabase
    .from('user_token_balances')
    .select('*')
    .eq('user_id', userId)
    .single();
  
  return {
    available: data?.tokens_available ?? 0,
    spent: data?.tokens_spent ?? 0,
    remaining: data?.tokens_remaining ?? 0,
  };
}
The user_token_balances view calculates:
tokens_remaining = tokens_available - tokens_spent

Checking Balance

Before generation, verify sufficient tokens:
// In API routes
export async function POST(req: Request) {
  const { userId, model } = await getContext();
  
  const balance = await getTokenBalance(userId);
  const cost = TOKEN_COSTS.ai_generation[model];
  
  if (balance.remaining < cost) {
    return Response.json(
      { error: "Insufficient tokens" },
      { status: 402 }
    );
  }
  
  // Proceed with generation...
}

Recording Usage

After successful generation:
export async function recordUsage(
  userId: string,
  eventType: string,
  tokens: number,
  model?: string
) {
  const supabase = getSupabaseServiceRoleClient();
  
  await supabase
    .from('usage_ledgers')
    .insert({
      user_id: userId,
      event_type: eventType,
      tokens,
      model,
      metadata: { timestamp: new Date().toISOString() },
    });
}

Plan Detection

// lib/billing/plan.ts
export async function getCurrentPlan(userId: string) {
  const supabase = getSupabaseServiceRoleClient();
  
  const { data: subscription } = await supabase
    .from('subscriptions')
    .select('*, plan:billing_plans(*)')
    .eq('user_id', userId)
    .in('status', ['active', 'trialing'])
    .single();
  
  if (!subscription) {
    // Return free plan
    const { data: freePlan } = await supabase
      .from('billing_plans')
      .select('*')
      .eq('slug', 'free')
      .single();
    
    return freePlan;
  }
  
  return subscription.plan;
}

Stripe Integration

Checkout Session

// app/api/stripe/checkout/route.ts
export async function POST(req: Request) {
  const { priceId, userId } = await req.json();
  
  const session = await stripe.checkout.sessions.create({
    mode: 'subscription',
    line_items: [{ price: priceId, quantity: 1 }],
    metadata: { userId },
    success_url: `${origin}/sub-success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${origin}/pricing`,
  });
  
  return Response.json({ url: session.url });
}

Webhook Handler

// app/api/stripe/webhook/route.ts
export async function POST(req: Request) {
  const event = stripe.webhooks.constructEvent(
    await req.text(),
    req.headers.get('stripe-signature')!,
    process.env.STRIPE_WEBHOOK_SECRET!
  );

  switch (event.type) {
    case 'checkout.session.completed': {
      const session = event.data.object;
      await createSubscription(session);
      break;
    }
    case 'invoice.paid': {
      const invoice = event.data.object;
      await grantMonthlyTokens(invoice);
      break;
    }
    case 'customer.subscription.deleted': {
      const subscription = event.data.object;
      await cancelSubscription(subscription);
      break;
    }
  }

  return Response.json({ received: true });
}

Granting Tokens

On subscription payment:
async function grantMonthlyTokens(invoice: Stripe.Invoice) {
  const userId = invoice.metadata?.userId;
  const subscriptionId = invoice.subscription;
  
  // Get plan token amount
  const { data: sub } = await supabase
    .from('subscriptions')
    .select('*, plan:billing_plans(*)')
    .eq('external_reference', subscriptionId)
    .single();
  
  // Grant tokens
  await supabase
    .from('token_grants')
    .insert({
      user_id: userId,
      subscription_id: sub.id,
      source: 'plan',
      tokens: sub.plan.monthly_tokens,
      expires_at: sub.plan.rollover ? null : sub.current_period_end,
    });
}

UI Components

Token Display

// In header or sidebar
<div className="flex items-center gap-2">
  <Coins className="h-4 w-4" />
  <span>{tokenBalance.remaining} tokens</span>
</div>

Upgrade Prompt

{tokenBalance.remaining < 10 && (
  <Alert variant="warning">
    <AlertTitle>Low on tokens</AlertTitle>
    <AlertDescription>
      <Link href="/pricing">Upgrade your plan</Link> or buy a token pack.
    </AlertDescription>
  </Alert>
)}

Seeding Plans

Initial plans in supabase/seed.sql:
insert into public.billing_plans 
  (slug, name, billing_model, monthly_tokens, price_monthly)
values
  ('free', 'Free', 'subscription', 25, 0),
  ('pro', 'Pro', 'subscription', 500, 2900),
  ('studio', 'Studio', 'subscription', 2000, 9900);