[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build a Checkout Flow with Stripe

Build a Checkout Flow with Stripe

Build a production checkout flow — cart management, Stripe payment intents, 3D Secure handling, order confirmation, webhook-driven fulfillment, and failed payment recovery.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 6 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

nextjs

v1.0.0

Assists with building production-grade React applications using Next.js. Use when working with the App Router, Server Components, Server Actions, Middleware, or deploying to Vercel or self-hosted environments. Trigger words: nextjs, next.js, app router, server components, server actions, react framework, ssr, isr.

93/100 quality
1.16× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Nadia runs a 20-person e-commerce startup. Their checkout is a single "Buy Now" button that redirects to Stripe Checkout. It works, but they can't customize the flow, offer coupons, show order summaries, or handle cart abandonment. 35% of users abandon at checkout because the redirect feels jarring. They lose $15K/month in failed payments that are never retried. They need an embedded checkout with cart management, real-time price calculation, coupon support, and webhook-driven order processing.

Step 1: Build the Cart and Checkout API

typescript
// src/checkout/cart.ts — Server-side cart with pricing, coupons, and Stripe integration
import Stripe from "stripe";
import { pool } from "../db";
import { Redis } from "ioredis";
import { z } from "zod";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const redis = new Redis(process.env.REDIS_URL!);

interface CartItem {
  productId: string;
  name: string;
  price: number;           // in cents
  quantity: number;
  imageUrl: string;
}

interface Cart {
  id: string;
  userId: string | null;
  items: CartItem[];
  couponCode: string | null;
  discount: number;        // in cents
  subtotal: number;
  tax: number;
  total: number;
  currency: string;
}

const AddItemSchema = z.object({
  productId: z.string(),
  quantity: z.number().int().min(1).max(99),
});

// Get or create cart
export async function getCart(cartId: string): Promise<Cart> {
  const data = await redis.get(`cart:${cartId}`);
  if (data) return JSON.parse(data);

  return {
    id: cartId,
    userId: null,
    items: [],
    couponCode: null,
    discount: 0,
    subtotal: 0,
    tax: 0,
    total: 0,
    currency: "usd",
  };
}

// Add item to cart
export async function addToCart(cartId: string, productId: string, quantity: number): Promise<Cart> {
  const cart = await getCart(cartId);

  // Fetch product from DB
  const { rows: [product] } = await pool.query(
    "SELECT id, name, price_cents, image_url, stock FROM products WHERE id = $1 AND active = true",
    [productId]
  );
  if (!product) throw new Error("Product not found");
  if (product.stock < quantity) throw new Error(`Only ${product.stock} in stock`);

  // Update or add item
  const existing = cart.items.find((i) => i.productId === productId);
  if (existing) {
    existing.quantity = Math.min(existing.quantity + quantity, product.stock);
  } else {
    cart.items.push({
      productId: product.id,
      name: product.name,
      price: product.price_cents,
      quantity,
      imageUrl: product.image_url,
    });
  }

  return recalculateAndSave(cart);
}

// Apply coupon
export async function applyCoupon(cartId: string, code: string): Promise<Cart> {
  const cart = await getCart(cartId);

  const { rows: [coupon] } = await pool.query(
    `SELECT * FROM coupons
     WHERE code = $1 AND active = true
     AND (expires_at IS NULL OR expires_at > NOW())
     AND (max_uses IS NULL OR uses < max_uses)`,
    [code.toUpperCase()]
  );

  if (!coupon) throw new Error("Invalid or expired coupon");

  cart.couponCode = code.toUpperCase();

  if (coupon.type === "percentage") {
    cart.discount = Math.round(cart.subtotal * (coupon.value / 100));
  } else {
    cart.discount = Math.min(coupon.value, cart.subtotal); // don't exceed subtotal
  }

  return recalculateAndSave(cart);
}

// Create Stripe Payment Intent
export async function createPaymentIntent(cartId: string, userId: string): Promise<{
  clientSecret: string;
  paymentIntentId: string;
}> {
  const cart = await getCart(cartId);
  if (cart.items.length === 0) throw new Error("Cart is empty");

  // Get or create Stripe customer
  let { rows: [userRow] } = await pool.query(
    "SELECT stripe_customer_id FROM users WHERE id = $1",
    [userId]
  );

  let customerId = userRow?.stripe_customer_id;
  if (!customerId) {
    const { rows: [user] } = await pool.query("SELECT email, name FROM users WHERE id = $1", [userId]);
    const customer = await stripe.customers.create({ email: user.email, name: user.name });
    customerId = customer.id;
    await pool.query("UPDATE users SET stripe_customer_id = $1 WHERE id = $2", [customerId, userId]);
  }

  // Create payment intent
  const paymentIntent = await stripe.paymentIntents.create({
    amount: cart.total,
    currency: cart.currency,
    customer: customerId,
    metadata: {
      cartId,
      userId,
      couponCode: cart.couponCode || "",
      itemCount: String(cart.items.length),
    },
    automatic_payment_methods: { enabled: true },
  });

  // Store mapping for webhook processing
  await redis.setex(`pi:${paymentIntent.id}`, 3600, JSON.stringify({
    cartId, userId, items: cart.items, total: cart.total,
  }));

  return {
    clientSecret: paymentIntent.client_secret!,
    paymentIntentId: paymentIntent.id,
  };
}

// Webhook handler — processes successful payments
export async function handleStripeWebhook(event: Stripe.Event): Promise<void> {
  switch (event.type) {
    case "payment_intent.succeeded": {
      const pi = event.data.object as Stripe.PaymentIntent;
      const orderData = await redis.get(`pi:${pi.id}`);
      if (!orderData) return;

      const { cartId, userId, items, total } = JSON.parse(orderData);

      // Create order
      const { rows: [order] } = await pool.query(
        `INSERT INTO orders (user_id, items, subtotal, discount, tax, total, stripe_payment_intent_id, status, created_at)
         VALUES ($1, $2, $3, $4, $5, $6, $7, 'confirmed', NOW())
         RETURNING id`,
        [userId, JSON.stringify(items), total, 0, 0, total, pi.id]
      );

      // Decrement stock
      for (const item of items) {
        await pool.query(
          "UPDATE products SET stock = stock - $1 WHERE id = $2",
          [item.quantity, item.productId]
        );
      }

      // Clear cart
      await redis.del(`cart:${cartId}`);

      // Increment coupon usage
      if (pi.metadata.couponCode) {
        await pool.query("UPDATE coupons SET uses = uses + 1 WHERE code = $1", [pi.metadata.couponCode]);
      }

      // Send confirmation email (via queue)
      await redis.rpush("email:queue", JSON.stringify({
        type: "order_confirmation",
        userId,
        orderId: order.id,
      }));

      break;
    }

    case "payment_intent.payment_failed": {
      const pi = event.data.object as Stripe.PaymentIntent;
      // Queue recovery email
      await redis.rpush("email:queue", JSON.stringify({
        type: "payment_failed",
        userId: pi.metadata.userId,
        paymentIntentId: pi.id,
        retryUrl: `${process.env.APP_URL}/checkout/retry/${pi.id}`,
      }));
      break;
    }
  }
}

function recalculateAndSave(cart: Cart): Promise<Cart> {
  cart.subtotal = cart.items.reduce((sum, i) => sum + i.price * i.quantity, 0);
  cart.tax = Math.round(cart.subtotal * 0.08);    // 8% tax
  cart.total = cart.subtotal - cart.discount + cart.tax;
  return redis.setex(`cart:${cart.id}`, 86400 * 7, JSON.stringify(cart)).then(() => cart);
}

Results

  • Checkout abandonment: 35% → 18% — embedded checkout feels native; no jarring redirect; order summary and coupon field visible on one page
  • Failed payment recovery: $0 → $8K/month recovered — webhook catches payment_failed, sends retry email with direct link; 53% of failed payments succeed on retry
  • Coupon system drives $12K/month extra revenue — 15% discount codes for first-time buyers convert at 2.3x the baseline rate
  • 3D Secure handled automatically — Stripe's automatic_payment_methods handles SCA compliance; European customers authenticate seamlessly
  • Order processing is reliable — webhook-driven fulfillment means orders are never lost even if the user's browser crashes after payment