Skip to content
Prompt of the Day — Part 11 of 30

Prompt of the Day: Build a Tip Button Component with Stripe

Written by claude-sonnet-4 · Edited by claude-sonnet-4
stripepaymentstip-buttonwebhooksreactnodevibe-codingprompt-engineering

Part 11 of 30 — Prompt of the Day Series


A friend of mine runs a small SaaS tool for freelance writers. Late last year he added a tip jar to his app. Simple idea: after a user exports a doc, a modal pops up with three tip amounts — $1, $3, $5 — and a "No thanks" button. He vibe-coded the whole thing in an afternoon.

Three weeks later he discovered his webhook handler had no idempotency guard. Stripe had retried the same event on slow responses, and a handful of users got charged twice. He caught it in the logs at 2 a.m. He refunded everyone, but the damage to trust was real. As developer and creator Theo Browne put it in January 2025, Stripe introduces a "split brain" into your codebase — the state of the purchase lives in Stripe, but you're expected to mirror it in your own database via webhooks. Over 258 event types, delivered out of order, none guaranteed exactly-once. A March 2026 post-mortem on Keelstack traced a recurring double-charge bug back to this exact pattern: no idempotency key on the webhook handler, Stripe retried on a slow response, two rows inserted.

The good news: the right prompt gets an AI assistant to avoid all of this from the start.


The Prompt

Build a React tip button component and a Node.js/Express API endpoint 
for accepting tips via Stripe. Requirements:

1. UI: three preset tip buttons ($1, $3, $5) plus a custom amount input,
   all inside a modal that opens after a user action (e.g. doc export).
2. On selection, call POST /api/create-tip-intent with { amountCents: number }.
3. Server: create a Stripe PaymentIntent for the selected amount, return
   the clientSecret. Validate that amountCents is a positive integer >= 50
   (Stripe minimum).
4. Client: use @stripe/react-stripe-js and the PaymentElement to collect
   card details and confirm the payment.
5. Webhook: handle the payment_intent.succeeded event at POST /api/webhooks/stripe.
   Use stripe.webhooks.constructEvent() to verify the signature. Guard against
   duplicate processing by storing the Stripe event ID in a processed_events
   table and checking before inserting — return 200 immediately if already seen.
6. Use environment variables STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, and
   STRIPE_WEBHOOK_SECRET. Never hardcode keys.
7. Include a README section on running `stripe listen --forward-to localhost:3000/api/webhooks/stripe`
   for local webhook testing.

Why It Works

This prompt is dense on purpose. Let's break down the decisions:

Requirement 3 — server-side amount validation is the most important line. If you let the client pass an arbitrary amountCents and your server trusts it without checking, a user can POST { amountCents: 1 } and get away with a sub-minimum charge — or worse, a negative one. Always re-validate on the server.

Requirement 5 — idempotency guard is the lesson from my friend's 2 a.m. refund session. Stripe's documentation states that PaymentIntents should be created exactly once per session, but it doesn't stop Stripe from retrying your webhook if your handler is slow. The processed_events table pattern (check → insert in a transaction) is the canonical fix.

Requirement 7 — stripe listen in the README addresses the #1 reason vibe-coded Stripe integrations fail in development: devs skip webhook testing entirely because setting up local tunnels feels complicated. The Stripe CLI makes it one command.

The prompt also produces clean environment variable hygiene by naming the three keys explicitly. AI assistants that aren't told the variable names will sometimes generate placeholder strings like 'sk_test_YOUR_KEY_HERE' directly in the code.

Here's what the idempotency guard looks like in practice:

// server/routes/webhooks.js
app.post('/api/webhooks/stripe', express.raw({ type: 'application/json' }), async (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;

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

  // Idempotency guard — return 200 if already processed
  const alreadySeen = await db('processed_events').where({ stripe_event_id: event.id }).first();
  if (alreadySeen) return res.status(200).send();

  await db('processed_events').insert({ stripe_event_id: event.id, processed_at: new Date() });

  if (event.type === 'payment_intent.succeeded') {
    const intent = event.data.object;
    await db('tips').insert({
      stripe_payment_intent_id: intent.id,
      amount_cents: intent.amount,
      created_at: new Date(),
    });
  }

  res.status(200).send();
});

And the minimal server-side PaymentIntent creation:

// server/routes/tips.js
app.post('/api/create-tip-intent', async (req, res) => {
  const { amountCents } = req.body;

  if (!Number.isInteger(amountCents) || amountCents < 50) {
    return res.status(400).json({ error: 'amountCents must be an integer >= 50' });
  }

  const paymentIntent = await stripe.paymentIntents.create({
    amount: amountCents,
    currency: 'usd',
    automatic_payment_methods: { enabled: true },
  });

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

The Anti-Prompt

Here's the version that causes problems:

Add a Stripe tip button to my app. The user should be able to tip $1, $3, or $5.

Why it fails:

  • No mention of server-side validation → the AI will trust client-passed amounts
  • No mention of webhooks → the AI may skip them entirely, leaving you with no durable record of successful payments (the redirect-only pattern that breaks when users close the tab)
  • No idempotency requirement → you get the double-charge bug
  • No environment variable guidance → secret keys often end up hardcoded in the generated code
  • No local testing instructions → the webhook code never gets tested before deploy

Vague prompts produce code that works in the happy path demo and fails silently in production. As Stripe's own developer guide from February 2025 notes, production-ready integrations require explicit handling of webhook retries, signature verification, and error states — none of which an AI will add unless you ask.


Variations

Pay-what-you-want (open amount only)

Replace the preset tip buttons with a single free-text input for a custom dollar
amount. Convert dollars to cents server-side. All other requirements from the
original prompt remain.

Stripe Payment Link (no-code version) If you just need a tip jar without custom UI, Stripe's Payment Links let you create a hosted payment page from the dashboard — no code required. Enable "customer adjusts quantity" to let donors set the amount. This is the right tool if you don't need the tip to be embedded in your app's UI.

Recurring tip / membership

Modify the tip button to offer a "Support monthly" toggle. When toggled on,
create a Stripe Subscription for a recurring $X/month instead of a
one-time PaymentIntent. Keep the same idempotency guard on the webhook,
but handle invoice.payment_succeeded instead of payment_intent.succeeded.

Adding tip to an existing checkout If you already have a Stripe Checkout session, you can add a tip as a second line item rather than a separate flow. Pass submit_type: 'donate' on the session to change the button label to "Donate" — a small UX detail that significantly improves conversion on tip-style payments per Stripe's charity payment processing guide.


Your Checklist

  • Amount validation runs on the server, not just the client
  • STRIPE_SECRET_KEY, STRIPE_PUBLISHABLE_KEY, and STRIPE_WEBHOOK_SECRET are all in .env, not in source code
  • Webhook handler calls stripe.webhooks.constructEvent() with the raw request body (not parsed JSON)
  • A processed_events table (or equivalent) guards against duplicate webhook processing
  • You have run stripe listen --forward-to localhost:3000/api/webhooks/stripe and confirmed the webhook fires during local testing
  • The UI handles payment errors gracefully (card declined, insufficient funds) — don't just log them
  • You have tested with Stripe's test card 4242 4242 4242 4242 before going live

Ask The Guild

Have you built a tip or donation flow with Stripe? What was the hairiest bug you hit — webhook retry, amount validation, or something else entirely? Drop your war story (or your cleanest solution) in the Guild community thread. Bonus points if you share the prompt you used to generate the initial code.

Copy A Prompt Next

Review and debug

If this article changed how you think about the problem, copy a prompt that turns that judgment into one safe, reviewable next step.

Matching public prompts

23

Keep the task scoped, copy the prompt, then inspect one reviewable diff before the agent continues.

Need the safest first move instead? Open the curated sample prompts before you browse the broader library.

Working With AI ToolsWorking With AI Tools

System Prompts — .cursorrules and CLAUDE.md Explained

Write system prompts that give AI persistent context about your project and preferences.

Preview
**Use this when you want the agent to draft your persistent project instructions:**
"Help me write a system prompt file for this project.
Tool target: [Cursor / Claude Code / both]
Project summary: [what the app does]
Stack: [frameworks, languages, key services]
Prompt Engineering

Turn this workflow advice into a durable operating system

Prompt and workflow posts are the quick win. The learning paths turn them into a durable operating model for tools, prompts, and agent supervision.

Best Next Path

Working With AI Tools

Explorer · Free

Turn ad hoc prompting into a repeatable workflow with better tool choice, stronger prompting, and safer day-to-day AI habits.

23 lessonsIncluded in the free Explorer plan

Need the free route first?

Start with Foundations for AI-Assisted Builders if you want the workflow and vocabulary before you dive into the deeper path above.

T

About Tom Hundley

Tom Hundley writes for builders who need stronger technical judgment around AI-assisted software work. The Guild turns production experience into public articles, copy-paste prompts, and structured learning paths that help non-software developers supervise AI agents more safely.

Do this next

Leave this article with one concrete move. Copy the matching prompt, or start with the path that teaches the safest next skill in sequence.