Skip to main content
GitRead uses a credit-based system where each README generation costs 1 credit. Credits are purchased through Stripe and never expire.

How credits work

Every user action that generates a README consumes credits:

Credit consumption

// Check credits before generation
const { data } = await supabaseAdmin
  .from('user_credits')
  .select('credits')
  .eq('user_id', userId)
  .single()

if (data.credits <= 0) {
  return NextResponse.json({ 
    error: "Insufficient credits. Please purchase more credits to continue."
  }, { status: 402 })
}
Cost: 1 credit per README generated

Credit deduction

After successful generation:
const newCredits = (data?.credits || 1) - 1

await supabaseAdmin
  .from('user_credits')
  .upsert({
    user_id: userId,
    credits: newCredits,
    updated_at: new Date().toISOString()
  })
Credits are only deducted after successful README generation. Failed generations do not consume credits.

Initial credits

New users receive 1 free credit when they sign up:
if (error.code === 'PGRST116') {
  // No record exists, create one with default credits
  await supabaseAdmin
    .from('user_credits')
    .upsert({ 
      user_id: userId, 
      credits: 1,
      updated_at: new Date().toISOString()
    })
}
This allows users to try the service before making a purchase.

Pricing

GitRead charges $1.25 per credit:
const pricePerCredit = 1.25;
const price = credits * pricePerCredit;

Credit packages

Starter

2 credits - $2.50Perfect for trying out the service

Standard

10 credits - $12.50Great for small teams

Professional

25 credits - $31.25Best for active developers

Enterprise

100 credits - $125.00Ideal for organizations

Purchase interface

Users can select credit amount with a slider:
<input
  type="range"
  min="2"
  max="100"
  step="2"
  value={selectedCredits}
  onChange={e => setSelectedCredits(parseInt(e.target.value))}
/>
Range: 2-100 credits in increments of 2

Stripe integration

Payments are processed securely through Stripe Checkout.

Creating checkout session

The API creates a Stripe session with credit metadata:
const session = await stripe.checkout.sessions.create({
  payment_method_types: ['card'],
  line_items: [
    {
      price_data: {
        currency: 'usd',
        product_data: {
          name: `${credits} Credits`,
          description: `Purchase ${credits} credits for GitRead`,
        },
        unit_amount: Math.round(price * 100), // Convert to cents
      },
      quantity: 1,
    },
  ],
  mode: 'payment',
  success_url: `${process.env.NEXT_PUBLIC_APP_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/`,
  metadata: {
    userId,
    credits: credits.toString(),
  },
});

Redirect to checkout

const handleBuyCredits = async (creditAmount: number) => {
  const response = await fetch('/api/create-checkout-session', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ credits: creditAmount }),
  });

  const data = await response.json();
  if (data.url) {
    window.location.href = data.url; // Redirect to Stripe
  }
};
Never expose your Stripe secret key to the client. All payment processing happens server-side.

Payment verification

After payment, the success page verifies and adds credits:

Session verification

const session = await stripe.checkout.sessions.retrieve(sessionId);

if (session.metadata?.userId !== userId) {
  return NextResponse.json({ error: 'Invalid session' }, { status: 400 });
}

if (session.payment_status === 'paid') {
  const credits = parseInt(session.metadata?.credits || '0');
  // Add credits to user account
}

Duplicate payment prevention

GitRead tracks processed payments to prevent duplicate credits:
const { data: processedSession } = await supabaseAdmin
  .from('processed_stripe_events')
  .select('*')
  .eq('event_id', `session_${sessionId}`)
  .single();

if (processedSession) {
  return NextResponse.json({ success: true }); // Already processed
}

Adding credits

const { data: currentCredits } = await supabaseAdmin
  .from('user_credits')
  .select('credits')
  .eq('user_id', userId)
  .single();

const newCredits = (currentCredits?.credits || 0) + credits;

await supabaseAdmin
  .from('user_credits')
  .upsert({
    user_id: userId,
    credits: newCredits,
    updated_at: new Date().toISOString(),
  });

Marking as processed

await supabaseAdmin
  .from('processed_stripe_events')
  .insert({
    event_id: `session_${sessionId}`,
    user_id: userId,
    credits: credits,
    processed_at: new Date().toISOString(),
  });
Processed payment events are stored permanently to prevent double-crediting.

Credit display

Users can always see their current balance:
const { data } = await fetch('/api/credits');
setCredits(data.credits);

Real-time updates

Credits refresh automatically every 30 seconds:
interval = setInterval(async () => {
  const response = await fetch('/api/credits');
  const data = await response.json();
  setCredits(data.credits);
}, 30000);

UI display

<div className="bg-white dark:bg-gray-800 px-4 py-2 rounded-full shadow-sm">
  <span className="font-semibold text-purple-600">{credits}</span>
  <span className="text-gray-600"> credits remaining</span>
</div>

Low credit warnings

When credits reach zero, users see a purchase modal:
if (isSignedIn && credits <= 0) {
  timeout = setTimeout(() => {
    setShowBlockingCreditsModal(true);
  }, 3000); // 3 seconds delay
}
The modal includes:
  • Credit amount slider
  • Real-time price calculation
  • Quick purchase button
  • Support contact link

Manual refresh

Users can manually refresh credits after payment:
const handleRefreshCredits = async () => {
  const res = await fetch('/api/credits');
  const data = await res.json();
  
  if (data.credits > 0) {
    window.location.reload();
  } else {
    setRefreshError('Credits have not been added yet. Please wait and try again.');
  }
};

Credit security

All credit operations use Supabase service role key:
const supabaseAdmin = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.SUPABASE_SERVICE_ROLE_KEY!
);
This prevents client-side manipulation of credit balances.

API authentication

const { userId } = getAuth(req);
if (!userId) {
  return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
All credit-related endpoints require Clerk authentication.

Retry logic

The system includes retry logic for credit operations:
export async function getUserCredits(userId: string, retries = 3): Promise<number> {
  if (error && retries > 0) {
    await sleep(1000);
    return getUserCredits(userId, retries - 1);
  }
}
Retry attempts: 3 Delay between retries: 1 second
Credits never expire and can be used at any time after purchase.

Build docs developers (and LLMs) love