A post went viral on X: "there should be an app where you upload your bills and bored billionaires swipe right to pay them."

Everyone joked. I wanted to see how fast I could ship a real one. So here's Pay My Mortgage 🏠, running on Helpeasant, an open-source, one-page "joke-but-real" payoff site.

Yes, it's a joke. Yes, the button works.

👉 Live: helpeasant.com
👉 Repo: github.com/mariache/helpeasant

What it does

  • Live progress counter (% only, 4 decimals, so even $5 visibly moves it)
  • Quick buttons: ☕ $5 · 🍕 $20 · 🏠 $100, plus any amount
  • A "Pay it all off 🤯" button that politely explains card limits
  • A Wall of Weirdos for supporters, with emoji tiers instead of amounts
  • Milestones with confetti: "Paid for a doorknob" → "The garage is basically mine"
  • "Can't chip in? Send it to Elon 🚀" share button
  • Installable PWA, dark mode

Stack

  • Next.js 15 (App Router), one page and three API routes
  • Stripe Checkout + a signed webhook
  • Supabase (Postgres), server-side only
  • Vercel for hosting, Cloudflare for DNS

Design decisions I liked

1. The counter only trusts webhooks

The UI never updates the total on a button click. The flow is:

  1. POST /api/checkout creates a Stripe Checkout Session with inline price_data. No products are needed in the dashboard.
  2. Stripe hosts the payment page.
  3. POST /api/webhook verifies the signature and inserts a row only if payment_status === "paid".

Inserts are idempotent on the Stripe session ID, so webhook retries never double-count:

await supabase.from("donations").upsert( { stripe_session_id: session.id, amount_cents: session.amount_total, name, message }, { onConflict: "stripe_session_id", ignoreDuplicates: true } ); 

2. Privacy by design

The real balance lives in a server-only env var. The browser only ever gets a rounded percentage, and the wall shows tiers (☕ 🍕 🛋️ 🏠) instead of amounts. Otherwise total ÷ % would reveal the balance.

3. A locked-down database

  • Row-level security on, "auto-expose new tables" off
  • Explicit grants to service_role only
  • The client never talks to Supabase directly; everything goes through API routes

4. Config over code

Title, emoji, tagline, owner name and city are all NEXT_PUBLIC_* env vars. A fork is a new campaign with zero code changes: Pay My Car Loan, Pay My Student Debt, you name it.

5. Native share with a fallback

navigator.share on mobile opens WhatsApp, Messages and so on. On desktop it falls back to an X intent with @elonmusk pre-filled.

What actually took the time

The code took maybe an hour. Most of the evening went into setup:

  • Domain + Cloudflare DNS (tip: set records to DNS only, grey cloud, or Vercel complains about the proxy)
  • Stripe verification (it needs a live, reachable site that matches your business name)
  • Test-mode end-to-end run with stripe listen and the 4242 card before switching to live keys

Fork it

git clone https://github.com/mariache/helpeasant cd helpeasant && npm install cp .env.example .env.local npm run dev 

The README walks through Supabase, Stripe and Vercel setup in about 15 minutes.

Feedback welcome, especially on abuse and fraud edge cases I haven't covered. And if you can't chip in... you know who to send it to. 🚀