I'm building Kordhub, a tool that lets creators sell access to their Discord server or Telegram group. Someone pays with Stripe, the bot gives them a role. They cancel, the role is removed.
Sounds simple. Here are 5 things that broke while testing it with real payments.
1. Stripe sends the same cancellation twice
When a member cancels from the Stripe customer portal, Stripe fires two customer.subscription.updated events almost at the same time (the second one carries the cancellation survey answer). Both read the old state before either wrote, so I logged the cancellation twice.
Fix: make the update conditional ("only if it's not already canceling") and ask for the updated rows back. If nothing comes back, another event already handled it, so skip the log and the notification.
2. The bot can't assign a role above its own
Discord returns 403 (code 50013) if the bot's role is below the role you're trying to give. New servers hit this all the time, because the bot's role lands at the bottom when it joins.
Fix: catch that exact code and tell the creator in plain words: "drag the Kordhub role above the role you're selling, then tap Re-send access". Turning a cryptic error into one instruction saved a support ticket.
3. Telegram won't remove the group owner
When a subscription expired, my Telegram ban call failed with Bad Request: can't remove chat owner. I was testing with my own account, which owns the group.
Lesson: owners and admins can't be removed by a bot. Treat that error as "nothing to do", not as something to retry forever.
4. Don't DM people directly from your webhook
Discord rate limits are global per bot. Sending DMs straight from the payment webhook works in testing and falls over with real traffic.
Fix: every DM goes into a queue (a Postgres table + a small worker), so a burst of sales never fights the rate limit and failures are logged per job.
5. Expiration needs its own clock
Stripe tells you when a subscription is deleted, but not every access type has a Stripe subscription (one-time passes, manual grants). A scheduled job (pg_cron every 5 minutes) checks expires_at, removes access, and cancels anything still open in Stripe.
Stack
Supabase (Postgres, Auth, Edge Functions, pg_cron), Stripe Connect with direct charges, Cloudflare Pages, vanilla JS.
If you're building something similar, I hope this saves you a few hours. And if you run a paid community, you can try it free at kordhub.com.
Top comments (0)