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:
-
POST /api/checkoutcreates a Stripe Checkout Session with inlineprice_data. No products are needed in the dashboard. - Stripe hosts the payment page.
-
POST /api/webhookverifies the signature and inserts a row only ifpayment_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_roleonly - 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 listenand the4242card 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. 🚀
