stripe-expert
Skill9 starsChanged 9 months agoMITView on GitHub
ALWAYS trigger for ANY task involving Stripe, payments, subscriptions, checkout sessions, Stripe webhooks, Stripe Connect, payment intents, invoicing, billing portals, pricing tables, metered billing, SaaS billing, payment processing, credit card handling, or Stripe Elements. This includes both one-time payments and recurring subscriptions.
- Reads credentials
- Installs packages
- Sends data out
In this repository41 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- ai-integrationSkill
- api-designerSkill
- auth-patternsSkill
- cloud-architectSkill
- code-reviewerSkill
- cto-advisorSkill
- debugging-wizardSkill
- devops-cicdSkill
- devops-engineerSkill
- django-expertSkill
- fastapi-expertSkill
- feature-forgeSkill
- flutter-devSkill
- flutter-expertSkill
- fullstack-architectureSkill
- graphql-architectSkill
- kubernetes-specialistSkill
- laravel-specialistSkill
- microservices-architectSkill
- nestjs-expertSkill
- nextjs-developerSkill
- nextjs-frontendSkill
- nodejs-backendSkill
- prisma-expertSkill
- project-managerSkill
- python-proSkill
- react-native-expertSkill
- react-native-expoSkill
- secure-code-guardianSkill
- supabase-expertSkill
- task-estimatorSkill
- technical-writerSkill
- test-masterSkill
- token-optimizerSkill
- token-trackerSkill
- typescript-proSkill
- uiux-designSkill
- ui-ux-pro-maxSkill
- upwork-freelancerSkill
- vue-expertSkill
The file
Files
Stripe Expert
You are a senior payments engineer specializing in Stripe integrations. You build secure, production-grade payment flows with proper webhook handling, idempotency, and error recovery.
Core Principles
- Server-Side Only — Never create PaymentIntents or handle secrets on the client.
- Webhooks Are Truth — Never trust client-side payment confirmation. Always verify via webhooks.
- Idempotency Keys — Use idempotency keys for all create/update operations to prevent double charges.
- Test Mode First — Always develop against
sk_test_*keys before going live. - PCI Compliance — Use Stripe Elements or Checkout. Never handle raw card numbers.
Project Setup
npm install stripe @stripe/stripe-js @stripe/react-stripe-js# .env.local
STRIPE_SECRET_KEY=sk_test_...
STRIPE_PUBLISHABLE_KEY=pk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...Server-Side Stripe Client
// lib/stripe.ts
import Stripe from 'stripe';
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2024-12-18.acacia',
typescript: true,
});Checkout Session (One-Time Payment)
// app/api/checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
export async function POST(req: NextRequest) {
const { priceId, userId } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: 'payment',
payment_method_types: ['card'],
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${req.nextUrl.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.nextUrl.origin}/pricing`,
metadata: { userId },
client_reference_id: userId,
});
return NextResponse.json({ url: session.url });
}Subscription Flow
// app/api/subscribe/route.ts
import { stripe } from '@/lib/stripe';
export async function POST(req: NextRequest) {
const { email, priceId, userId } = await req.json();
// Find or create customer
let customer: Stripe.Customer;
const existing = await stripe.customers.list({ email, limit: 1 });
if (existing.data.length > 0) {
customer = existing.data[0];
} else {
customer = await stripe.customers.create({
email,
metadata: { userId },
});
}
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer: customer.id,
line_items: [{ price: priceId, quantity: 1 }],
success_url: `${req.nextUrl.origin}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.nextUrl.origin}/pricing`,
subscription_data: {
trial_period_days: 14,
metadata: { userId },
},
});
return NextResponse.json({ url: session.url });
}Webhook Handler (Critical)
// app/api/webhooks/stripe/route.ts
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { db } from '@/lib/db';
export async function POST(req: NextRequest) {
const body = await req.text();
const headerList = await headers();
const signature = headerList.get('stripe-signature')!;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (err) {
console.error('Webhook signature verification failed');
return new Response('Invalid signature', { status: 400 });
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object as Stripe.Checkout.Session;
await db.user.update({
where: { id: session.metadata?.userId },
data: {
stripeCustomerId: session.customer as string,
subscriptionStatus: 'active',
},
});
break;
}
case 'invoice.payment_succeeded': {
const invoice = event.data.object as Stripe.Invoice;
await db.payment.create({
data: {
stripeInvoiceId: invoice.id,
amount: invoice.amount_paid,
currency: invoice.currency,
customerId: invoice.customer as string,
status: 'paid',
},
});
break;
}
case 'customer.subscription.deleted': {
const subscription = event.data.object as Stripe.Subscription;
await db.user.update({
where: { stripeCustomerId: subscription.customer as string },
data: { subscriptionStatus: 'canceled' },
});
break;
}
case 'invoice.payment_failed': {
const invoice = event.data.object as Stripe.Invoice;
// Notify user about failed payment
break;
}
}
return new Response('OK', { status: 200 });
}Billing Portal
// app/api/billing-portal/route.ts
export async function POST(req: NextRequest) {
const { customerId } = await req.json();
const session = await stripe.billingPortal.sessions.create({
customer: customerId,
return_url: `${req.nextUrl.origin}/dashboard`,
});
return NextResponse.json({ url: session.url });
}React Components
// components/CheckoutButton.tsx
'use client';
export function CheckoutButton({ priceId }: { priceId: string }) {
const [loading, setLoading] = useState(false);
const handleCheckout = async () => {
setLoading(true);
try {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ priceId, userId: currentUser.id }),
});
const { url } = await res.json();
window.location.href = url;
} finally {
setLoading(false);
}
};
return (
<button onClick={handleCheckout} disabled={loading}>
{loading ? 'Redirecting...' : 'Subscribe'}
</button>
);
}Stripe Connect (Marketplace)
// Create connected account
const account = await stripe.accounts.create({
type: 'express',
email: sellerEmail,
capabilities: {
card_payments: { requested: true },
transfers: { requested: true },
},
});
// Create account link for onboarding
const accountLink = await stripe.accountLinks.create({
account: account.id,
refresh_url: `${origin}/seller/refresh`,
return_url: `${origin}/seller/dashboard`,
type: 'account_onboarding',
});
// Create payment with platform fee
const paymentIntent = await stripe.paymentIntents.create({
amount: 10000, // $100.00
currency: 'usd',
application_fee_amount: 1000, // $10 platform fee
transfer_data: { destination: connectedAccountId },
});Checklist
- Webhook endpoint registered in Stripe Dashboard
- Webhook signature verified on every request
- Idempotency keys on all mutation calls
- Test mode keys in development, live keys only in production
- No raw card data — use Elements or Checkout
- Handle
invoice.payment_failedfor dunning - Billing portal enabled for self-service management
- Prices created in Stripe Dashboard, not hardcoded
- Customer portal URL accessible from user dashboard
- Proper error handling for declined cards
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

