agentsclimarketplace

Stripe payments

Skill medy-gribkov/arcana/skills/stripe-payments

Universal AI development toolkit. 74 production-ready skills for every coding agent. Works with Claude Code, Cursor, Codex.

Install
npx -y skills add medy-gribkov/arcana --skill stripe-payments

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Stripe integration covering Checkout, Payment Intents, subscriptions, webhook verification, idempotency, and SCA

SKILL.md

9.3 KB, as published. Nobody here has run it

Stripe Payments Integration

Secure payment flows with webhook handling, idempotency, and SCA support.

Checkout Sessions

BAD: Client-side price, no idempotency

// ❌ Trusting client data, XSS risk
app.post('/checkout', async (req, res) => {
  const { priceId, amount } = req.body; // Never trust client
  const session = await stripe.checkout.sessions.create({
    line_items: [{ price: priceId, quantity: 1 }],
    success_url: req.body.successUrl, // XSS risk
  });
  res.json({ url: session.url });
});

GOOD: Server-side price lookup, idempotency, metadata

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2024-12-18.acacia',
});

app.post('/checkout', async (req, res) => {
  const { productId, userId } = req.body;

  const prices = await stripe.prices.list({ product: productId, active: true });
  if (!prices.data.length) return res.status(400).json({ error: 'Invalid product' });

  const session = await stripe.checkout.sessions.create({
    line_items: [{ price: prices.data[0].id, quantity: 1 }],
    mode: 'payment',
    success_url: `${process.env.BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
    cancel_url: `${process.env.BASE_URL}/cancel`,
    metadata: { userId, productId },
  }, {
    idempotencyKey: `checkout_${userId}_${productId}_${Date.now()}`,
  });

  res.json({ url: session.url });
});

Payment Intents with SCA

BAD: Deprecated Charges API, no 3D Secure

// ❌ Deprecated, no SCA support
app.post('/charge', async (req, res) => {
  await stripe.charges.create({
    amount: req.body.amount,
    source: req.body.token, // Deprecated
  });
});

GOOD: Payment Intent with automatic SCA, React Elements

// Server-side
app.post('/payment-intent', async (req, res) => {
  const order = await db.orders.findUnique({ where: { id: req.body.orderId } });
  if (!order) return res.status(404).json({ error: 'Order not found' });

  const paymentIntent = await stripe.paymentIntents.create({
    amount: order.totalCents,
    currency: 'usd',
    automatic_payment_methods: { enabled: true },
    metadata: { orderId: order.id },
  }, { idempotencyKey: `pi_${order.id}` });

  res.json({ clientSecret: paymentIntent.client_secret });
});

// Client-side React
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';

function CheckoutForm() {
  const stripe = useStripe();
  const elements = useElements();

  return (
    <form onSubmit={async (e) => {
      e.preventDefault();
      if (!stripe || !elements) return;
      await stripe.confirmPayment({
        elements,
        confirmParams: { return_url: `${window.location.origin}/complete` },
      });
    }}>
      <PaymentElement />
      <button disabled={!stripe}>Pay</button>
    </form>
  );
}

Subscriptions

BAD: Duplicate customers, no proration

// ❌ Creates duplicate customers
app.post('/subscribe', async (req, res) => {
  const customer = await stripe.customers.create({ email: req.body.email });
  const sub = await stripe.subscriptions.create({ customer: customer.id, items: [{ price: req.body.priceId }] });
  res.json(sub);
});

GOOD: Idempotent customer, trials, proration

async function subscribe(userId: string, priceId: string, trialDays?: number) {
  const user = await db.users.findUnique({ where: { id: userId } });

  const customer = user.stripeCustomerId
    ? await stripe.customers.retrieve(user.stripeCustomerId) as Stripe.Customer
    : await stripe.customers.create(
        { email: user.email, metadata: { userId } },
        { idempotencyKey: `customer_${userId}` }
      );

  if (!user.stripeCustomerId) {
    await db.users.update({ where: { id: userId }, data: { stripeCustomerId: customer.id } });
  }

  return await stripe.subscriptions.create({
    customer: customer.id,
    items: [{ price: priceId }],
    trial_period_days: trialDays,
    payment_settings: { save_default_payment_method: 'on_subscription' },
    metadata: { userId },
  }, { idempotencyKey: `sub_${userId}_${priceId}` });
}

// Upgrade/downgrade with proration
async function changePlan(subId: string, newPriceId: string) {
  const sub = await stripe.subscriptions.retrieve(subId);
  return await stripe.subscriptions.update(subId, {
    items: [{ id: sub.items.data[0].id, price: newPriceId }],
    proration_behavior: 'create_prorations',
  });
}

Webhook Verification (Critical)

BAD: No signature verification

// ❌ CRITICAL SECURITY FLAW
app.post('/webhook', async (req, res) => {
  const event = req.body;
  await fulfillOrder(event.data.object.metadata.orderId); // Attacker-controlled!
});

GOOD: Signature verification, idempotent processing

import { buffer } from 'micro';

export const config = { api: { bodyParser: false } }; // Next.js

export default async function webhook(req, res) {
  const buf = await buffer(req);
  const sig = req.headers['stripe-signature']!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(buf, sig, process.env.STRIPE_WEBHOOK_SECRET!);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Idempotency check
  const existing = await db.webhookEvents.findUnique({ where: { stripeEventId: event.id } });
  if (existing) return res.json({ received: true, duplicate: true });

  await db.webhookEvents.create({ data: { stripeEventId: event.id, type: event.type } });

  switch (event.type) {
    case 'payment_intent.succeeded':
      await handlePayment(event.data.object as Stripe.PaymentIntent);
      break;
    case 'customer.subscription.updated':
      await syncSubscription(event.data.object as Stripe.Subscription);
      break;
  }

  res.json({ received: true });
}

Customer Portal and Refunds

// Self-service billing portal
app.post('/portal', async (req, res) => {
  const user = await db.users.findUnique({ where: { id: req.body.userId } });
  if (!user?.stripeCustomerId) return res.status(400).json({ error: 'No customer' });

  const session = await stripe.billingPortal.sessions.create({
    customer: user.stripeCustomerId,
    return_url: `${process.env.BASE_URL}/account`,
  });
  res.json({ url: session.url });
});

// Refunds with idempotency
async function refund(paymentIntentId: string) {
  return await stripe.refunds.create({
    payment_intent: paymentIntentId,
  }, { idempotencyKey: `refund_${paymentIntentId}` });
}

// Cancel subscription
async function cancelSub(subId: string, immediately = false) {
  return immediately
    ? await stripe.subscriptions.cancel(subId)
    : await stripe.subscriptions.update(subId, { cancel_at_period_end: true });
}

Error Handling

BAD: Generic errors, no retry

// ❌ No error specificity
try {
  await stripe.paymentIntents.create({ /* ... */ });
} catch (err) {
  res.status(500).json({ error: 'Something went wrong' });
}

GOOD: Type-specific errors with retry logic

async function createPayment(params: Stripe.PaymentIntentCreateParams, retries = 3) {
  for (let i = 0; i < retries; i++) {
    try {
      return await stripe.paymentIntents.create(params, {
        idempotencyKey: params.metadata?.orderId,
      });
    } catch (err) {
      if (err instanceof Stripe.errors.StripeCardError) {
        throw new Error(`Card declined: ${err.message}`); // Don't retry
      } else if (err instanceof Stripe.errors.StripeRateLimitError) {
        if (i < retries - 1) {
          await new Promise(r => setTimeout(r, 1000 * (i + 1)));
          continue;
        }
      } else if (err instanceof Stripe.errors.StripeAuthenticationError) {
        throw new Error('Invalid API key'); // Don't retry
      }
      throw err;
    }
  }
  throw new Error('Payment failed after retries');
}

Testing

// Test cards
const TEST_CARDS = {
  success: '4242424242424242',
  declined: '4000000000000002',
  requiresAuth: '4000002500003155', // 3D Secure
  insufficientFunds: '4000000000009995',
};

// Test clocks for subscription testing
async function createTestClock(customerId: string) {
  const clock = await stripe.testHelpers.testClocks.create({
    frozen_time: Math.floor(Date.now() / 1000),
  });
  await stripe.customers.update(customerId, { test_clock: clock.id });
  return clock;
}

// Advance time
async function advanceTime(clockId: string, days: number) {
  const clock = await stripe.testHelpers.testClocks.retrieve(clockId);
  return await stripe.testHelpers.testClocks.advance(clockId, {
    frozen_time: clock.frozen_time + (days * 86400),
  });
}

Configuration

# .env.local
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
// Validate on startup
if (!process.env.STRIPE_SECRET_KEY) throw new Error('STRIPE_SECRET_KEY required');
if (!process.env.STRIPE_WEBHOOK_SECRET) console.warn('Webhooks will fail without STRIPE_WEBHOOK_SECRET');

const isTestMode = process.env.STRIPE_SECRET_KEY.startsWith('sk_test_');
console.log(`Stripe: ${isTestMode ? 'TEST' : 'LIVE'} mode`);

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.