Implement a Credits System with Stripe (my messy notes)

Just a guy who loves to write code and watch anime.
Search for a command to run...

Just a guy who loves to write code and watch anime.
No comments yet. Be the first to comment.
Qualities that make outstanding builders.

Bipedal vs quadrupedal: how many legs This is about the number of legs the creature walks on. Bipedal. Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids. Quadrupedal. Four legs. Do

Intro You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another. The problem

What is Kinematics Kinematics is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inver

Intro Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more. This post is about the less obvious

interface User {
id: string;
credits: number;
// ... other fields
}
const PRICE_IDS = {
"100": process.env.PRICE_ID_100, // 100 credits
"250": process.env.PRICE_ID_250, // 250 credits
"500": process.env.PRICE_ID_500, // 500 credits
} as const;
// Map credits to their price IDs
type Credits = keyof typeof PRICE_IDS;
async function createCheckoutSession(userId: string, credits: Credits) {
const priceId = PRICE_IDS[credits];
if (!priceId) throw new Error("Invalid credits amount");
const session = await stripe.checkout.sessions.create({
mode: "payment",
metadata: {
userId,
credits, // Store credits in metadata for webhook
},
line_items: [
{
price: priceId,
quantity: 1,
},
],
success_url: `${process.env.HOST_URL}/dashboard`,
cancel_url: `${process.env.HOST_URL}/pricing`,
});
return session;
}
async function handleStripeWebhook(body: unknown, signature: string) {
const event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
if (event.type === "checkout.session.completed") {
const session = event.data.object as Stripe.Checkout.Session;
const { userId, credits } = session.metadata;
// Add credits to user
await updateUserCredits(userId, parseInt(credits));
}
return { success: true };
}
// Add credits (after purchase)
async function updateUserCredits(userId: string, amount: number) {
const user = await getUser(userId);
const newCredits = user.credits + amount;
await updateUser(userId, { credits: newCredits });
}
// Use credits (before action)
async function useCredits(userId: string, amount = 1) {
const user = await getUser(userId);
if (user.credits < amount) {
throw new Error("Insufficient credits");
}
const newCredits = user.credits - amount;
await updateUser(userId, { credits: newCredits });
}
User clicks "Buy Credits" button
Create checkout session with metadata
Redirect to Stripe checkout
Webhook triggers on successful payment
Parse metadata and add credits to user
Check credits before expensive operations:
async function generateSomething(userId: string) {
try {
await useCredits(userId);
// Do expensive operation
} catch (error) {
throw new Error("Please purchase more credits");
}
}
Always use database transactions when modifying credits
Add credits field to your auth token/session for quick checks
Consider adding a credit history/log for transparency
Make credit operations idempotent using session IDs