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.