Mini App Builder — Episode 3: A Rejection Letter Became My Product Roadmap

The Telegram Apps Center rejection email was four sentences long:

Enhance the design. Add distinctive features. Improve social mechanics. Consider crypto/Stars payments.

That's it. No list of specific issues, no checklist, no screenshots. Four sentences that turned into the best product roadmap I've ever received.

Telegram Apps Center rejection email listing four feedback points including design enhancement and Stars payment

What This Post Covers

How I translated a vague rejection into four concrete Phase 2 features — difficulty modes, Telegram Stars payments, 1v1 challenges, UI polish — with Claude Code handling the code generation. The iOS bug that nearly killed the multiplayer feature, the payment handler that was silently eating Stars transactions, and the realization that the most elegant solution is sometimes the simplest one.

Four Difficulty Modes

The first feedback item was "add distinctive features." Phase 1 had one mode: Normal, 5x5 grid, 1 to 50. That's it. No reason for a returning player to stay engaged after they'd mastered one configuration.

Phase 2 split the game into four modes:

Easy (4x4 grid, 1 to 32) — free, for casual players and first-timers.
Normal (5x5 grid, 1 to 50) — free, the original game.
Hard (5x6 grid, 1 to 60) — 50 Telegram Stars to unlock.
Chaos (5x5 grid, 50 to 1 in reverse) — 50 Telegram Stars to unlock.

Claude Code refactored the game engine to be config-driven. Grid dimensions, number range, tap direction — all parameters instead of hardcoded values. One data structure, four game modes:

export const CONFIGS = { easy: { cols: 4, rows: 4, cells: 16, target: 32 }, normal: { cols: 5, rows: 5, cells: 25, target: 50 }, hard: { cols: 5, rows: 6, cells: 30, target: 60 }, chaos: { cols: 5, rows: 5, cells: 25, target: 50 }, };

Chaos mode was the most interesting to implement. Same 5x5 grid, but the tap order is reversed — 50 first, then 49, then 48. Initial numbers are 26-50, refill numbers are 1-25. Your muscle memory from Normal mode is completely useless. Players who crush Normal at 18 seconds suddenly struggle to break 40.

SpeedTap difficulty selection screen showing Easy and Normal unlocked with Hard and Chaos locked behind 50 Telegram Stars
The product decision I had to make: should paid modes be behind a hard paywall, or should there be another way to unlock them? I went with both. Pay 50 Stars per mode, OR invite 5 friends to unlock it. Non-paying users get a viral growth path. Paying users get instant access. Same outcome, different price: money or social capital.

Telegram Stars Payments

Telegram Stars is Telegram's built-in virtual currency. 1 Star is roughly $0.013. Users buy them through Apple or Google IAP, and spend them inside Mini Apps. The developer receives Stars in their bot balance, withdrawable after 21 days.

The payment flow Claude Code built:

User taps locked mode → Frontend calls backend for invoice link → Backend calls createInvoiceLink (Bot API) → Returns invoice URL → Frontend calls Telegram.WebApp.openInvoice → Telegram payment UI opens → User pays 50 Stars → Bot receives successful_payment event → Saves purchase to database → Frontend refreshes, mode unlocked

One bug nearly destroyed the whole feature. The first few payments went through on Telegram's side but never unlocked anything in my app. Users were paying and getting nothing. I checked the logs: no successful_payment events were being processed.

Root cause: the bot's message handler checked for text messages before checking for payment events. Since payment confirmations also contain a message object, they hit the generic text handler first and returned before reaching the payment logic. Silent failure — no error, no log, just payments vanishing.

Fix: reorder the handler chain. Pre-checkout queries first, successful payments second, refunded payments third, text messages last. Claude Code rewrote the handler registration in five minutes. The bug had wasted three.

Refunds were another thing I had to handle. Telegram allows users to request refunds up to 90 days after purchase. When a refund happens, the bot receives a refunded_payment event, and the unlocked mode gets re-locked. The payment system has to handle both directions.

The iOS startapp Parameter Saga

This is the bug story that cost me the most time in Phase 2.

The 1v1 challenge feature was supposed to work like this: you finish a game, tap "Challenge a Friend," the app generates a link like t.me/SpeedTapGameBot/speedtap?startapp=duel_abc123, you send it to a friend, they tap it, the app opens and routes them to the exact same board you just played.

Worked perfectly on Telegram Desktop. Completely failed on iOS.

Telegram's iOS app has a long-standing bug where initDataUnsafe.start_param returns undefined for Mini App direct links. I found GitHub issues reporting this going back to 2023. Three years. Still broken.

I tried five different fallback methods:

1. Standard start_param — broken on iOS.
2. Parsing the raw initData URL-encoded string — still nothing.
3. window.location.search query parameters — officially documented for iOS, didn't work.
4. window.location.hash fragment — officially documented for Android, also didn't work.
5. Nested tgWebAppData inside the hash — nope.

All five failed on real iOS devices in testing. I was watching a production feature die in real time.

Plan B: bot deep links. Switch to t.me/SpeedTapGameBot?start=duel_xxx. This routes through the bot chat first, where /start parameters are 100% reliable. The bot registers the opponent and sends a message with a "Play Now" button.

This worked for brand new users who had never opened the bot before. But users who already had the bot in their chat list? Telegram skipped the bot chat entirely and opened the Mini App directly — and the bot never received the /start command. Fail case covered half of my users.

Plan C: manual challenge codes. I gave up on URL routing entirely:

Creator plays a game → Server generates 4-character code (e.g., "XKQP") → Creator copies code, shares via message → Opponent opens app → Taps Challenge tab → Taps "Enter Code" → Types: XKQP → Server returns challenge data → Opponent plays same board → VS result screen

Every platform. Every device. Every version of Telegram. No dependency on startapp parameters, no reliance on bot deep links. The opponent types four characters and it just works.

Sometimes the most elegant solution is the simplest one. I burned a week on URL routing before accepting that four manual keystrokes was the correct answer.

SpeedTap 1v1 challenge VS result screen comparing two players times and winner

Both players get the same board through a deterministic seed (same concept as the daily challenge). The seed determines initial number positions and refill order. Two clients with the same seed produce identical games. Fair competition without any server-side game state synchronization.

Rematch logic has a small psychological twist. Only the loser can request a rematch. The winner doesn't need revenge — they already won. The loser does. Giving them the first-move privilege on the rematch felt right. Anti-spam rules keep this from being abused: same pair limited to 10 duels per day, one-hour cooldown after a decline.

UI Polish and Social Glue

The remaining feedback was "enhance the design." Claude Code added:

Confetti. A CSS particle system that rains 50 to 150 colored squares and circles from the top of the screen. Triggers on game completion (50 particles), new personal record (150 particles), and challenge win (80 particles). The 150-particle new-record burst is satisfying in a way that's hard to describe.

Screen transitions. 0.2-second fade with a slight upward slide on every screen change. Sounds small. Makes everything feel connected.

Ranking medals. 🥇🥈🥉 for top 3 with highlighted backgrounds. Ranking tabs switch between difficulty modes.

Notification system. Three types, all via Telegram bot messages (never in-app popups — those would interrupt gameplay). 24-hour and 72-hour return reminders (lifetime max of 2 per user — no spam). Daily challenge reminders for users who played yesterday's challenge. Challenge result notifications when an opponent finishes your duel.

Admin dashboard at /admin/stats — three tabs (Today / Weekly / Monthly), calendar view, metrics for DAU, new users, share clicks, duels created, Stars revenue, D1 retention. All labels in Korean since I'm the only admin using it. Claude Code built the entire server-rendered dashboard in one go.

The Numbers

Phase 2 feature count:

Difficulty modes: 4 (Easy, Normal, Hard, Chaos).
Monetization: Telegram Stars (50 Stars per mode) + referral unlock (5 friends per mode).
Multiplayer: 1v1 challenges via 4-character codes, rematch system, global leaderboards per difficulty.
New database tables: 12 total (up from the MVP's 3).
Notification types: 3 automated triggers.
New frontend modules: +1 (confetti.js).
New backend routes: +5 (duels, purchases, referrals, settings, broadcast).
Apps Center: Resubmitted, under review.

What's Next

Phase 2 is live. The app has everything the Apps Center asked for. Resubmission is pending review. If it gets approved, SpeedTap lands in the official Telegram Mini App directory with 178K monthly browsers. If it gets rejected again, I'll get more specific feedback to build Phase 3 from.

But feature lists don't actually matter if nobody plays. The real question is the one this whole series has been building toward: what do the actual numbers look like after all this work? Next episode covers the honest data across Phase 1 and Phase 2 combined — DAU, share rates, ad revenue, Telegram Stars earnings, user acquisition costs, retention. The numbers that tell you whether building on Telegram Mini Apps is worth your time.

← Previous: Episode 2: I Built a Telegram Game in One Day With Claude Code       Next: Episode 4:The Honest Numbers After Two Weeks. →


More updates on the way. If you're working on something similar or found a smarter way to do it, drop it in the comments — the more we share, the faster we all move.

Disclaimer: This blog documents my personal learning journey. Nothing here is financial advice.

Comments