Markdown Converter
Agent skill for markdown-converter
Telegram Mini App: vertical platformer where a monkey bounces on balloons.
Sign in to like and favorite skills
Telegram Mini App: vertical platformer where a monkey bounces on balloons.
The app is being rewritten from plain HTML/JS/Phaser to a React + Rive platform. The old Phaser scenes (menu, profile, leaderboard, shop, etc.) are being replaced by Rive artboards in the React layer. The Phaser game engine itself (
new/public/src/index.js) stays for core gameplay but the surrounding UI is moving to Rive.
Old files kept for backward compatibility:
src/index.js — original game (reference, not served)new/public/src/index.js — copy of old game, still used by game.htmlAPI server is ESM (
"type": "module" in package.json). All .js files use import/export, no require(). The API server uses Node's --experimental-strip-types (requires Node ≥ 22.6.0) to run .ts files directly — no build step. server-api.js stays JS and imports .ts files with explicit extension (import routesNew from './routes-new.ts'). Rules for .ts files: type-only annotations only — no enum, namespace, or parameter properties (those need --experimental-transform-types).
Drizzle ORM is integrated for type-safe queries. Schema pulled from existing DB into
schema.ts. New routes in routes-new.ts use Drizzle; legacy routes in server-api.js still use raw pool.query(). Migrations tracked in drizzle/ folder — baseline migration (0000) is a no-op since tables already existed.
React (Rive lobby) ──hard reload──▸ game.html (Phaser 3) │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ Solo (local) 1v1 (Socket.IO) Duel (HTTP poll) port 3000 port 3001
new/)game.htmlserver.js — 1v1 matchmaking (port 3000)monkey-flipper-api/server-api.js — Express + PostgreSQL (port 3001)Navigation between React lobby and Phaser game uses hard reloads (
window.location.href), not SPA routing.
server.js, port 3000)Real-time 1v1 matchmaking and gameplay relay. All state is in-memory (no DB, no Redis — lost on restart).
Data structures:
matchmakingQueue — FIFO array of waiting playersgameRooms — Map of GameRoom instances (stores both players' state, seed, scores)Socket events:
| Event | Direction | Description |
|---|---|---|
| client → server | Join matchmaking queue with userId/username |
| server → client | Acknowledgement, waiting for opponent |
| server → client | Match found — sends roomId, seed, opponent info |
| server → client | 3-second countdown before game starts |
| client → server | Send position (x, y), isAlive, score |
| server → client | Relayed opponent position/score |
| server → client | Game over — reason (fall/timeout/double_fall), winner, scores |
| client → server | Leave matchmaking queue |
| server → client | Opponent dropped, you win |
Game room lifecycle: countdown (1s) → playing (2 min max) → finished → room deleted (10s cleanup).
REST endpoints:
GET /api/stats (queue size, active games), GET /api/health.
monkey-flipper-api/server-api.js, port 3001)Express + PostgreSQL. Handles everything persistent. Auth via JWT (24h) + Telegram initData HMAC validation.
Database tables:
users, player_scores, duels, wallets, transactions, purchases, audit_log, referrals, tournaments, daily_rewards, achievements
Key route groups:
| Group | Routes | Description |
|---|---|---|
| Scoring | , | Save game results, server-side score recalculation, coin rewards, referral bonuses |
| Leaderboard | , | Top 100 players, per-player stats (games, scores, rank, wallet) |
| Duels | , , , , , , | Async challenge links (24h expiry), position polling for ghost opponent, timeout logic |
| Wallet | , , | Monkey coin, STARS, TON balances and transaction history |
| TON | , , | TON wallet connection/disconnection |
| STARS | , , | STARS wallet, Telegram Stars (XTR) balance |
| Shop | , , , , , , | Item catalog, purchase with monkey coins / STARS / TON |
| Equipment | , , , | Equip skins, consume boosts |
| Tournaments | , , , | Join, play, leaderboard, prize distribution |
| Daily Rewards | , | Streak-based daily rewards with weekly multiplier |
| Achievements | , , | Progress-based achievements with coin rewards |
| Referrals | , , | Referral codes, fraud detection, auto-paid bonus on first game |
| Admin | , , , | Admin dashboard, STARS refunds, purchase analytics |
Security: Rate limiting (5 req/min on score submission), Telegram initData HMAC-SHA256 validation, JWT auth, CORS whitelist (Telegram origins + Vercel domain).
Static HTML page at
/admin-stats.html, served via express.static(__dirname) (line 62 of server-api.js). Live at https://monkey-flipper-djm1.onrender.com/admin-stats.html.
Login:
POST /api/admin/login with { password }. Password is process.env.ADMIN_PASSWORD with fallback admin123. Returns a 24h JWT. The login page itself displays the default password in plain text.
Features:
Admin endpoints (JWT-protected via
):validateAdmin
GET /api/admin/stars-transactions — Telegram Stars transactions with refund statusPOST /api/admin/refund-by-payload — refund Stars by transaction IDPOST /api/admin/refund-stars — refund Stars by purchase IDGET /api/admin/purchases-stats — total purchases, unique users, TON received, recent purchasesGET /api/admin/stars-purchases — all Stars purchases with refund eligibility
risk: Serves every file in express.static(__dirname)
monkey-flipper-api/ publicly — including server-api.js source code, shop-items.json, and any other files in that directory. .env is typically not committed but anything else in the folder is accessible.
Almost all endpoints are unauthenticated. The
validateTelegram middleware exists but is never applied globally — and even when present, it skips requests with no X-Telegram-Init-Data header ("backward compatibility"). Only a few endpoints enforce auth:
validateJWT: /api/stars/balancevalidateAdmin: /api/admin/stars-transactions, /api/admin/refund-*, /api/admin/purchases-stats, /api/admin/stars-purchasesvalidateShopAuth: /api/shop/create-stars-invoice, /api/shop/create-ton-transaction, /api/shop/confirm-ton-paymentEverything else is wide open — no auth required, exploitable with plain
curl:
| Endpoint | Risk |
|---|---|
| Client sends score directly, server trusts it. Only rate-limited (5/min). Anyone can submit fake scores for any userId and farm coins ( per request) |
| Add coins to any user's wallet directly |
| Admin action with hardcoded key () instead of proper auth |
| No rate limiting on login attempts, default password shown on the login page |
| Serves all files in directory publicly (source code, configs) |
All GET endpoints with | Any user's data (wallet, stats, duels, achievements, referrals) is readable by anyone |
exists as a safer alternative (server recalculates score from events) but the game client still calls POST /api/game-events
save-score instead.
| File | Description |
|---|---|
| React app — Rive lobby, tab navigation (Game/Profile/Top/Shop) |
| Entry point — Telegram SDK init |
| Server URLs (localhost vs Render prod) |
| Telegram WebApp SDK wrapper |
| Phaser game container (loads CDN scripts + ) |
| Main game — 7625 lines, 14 Phaser scenes (legacy, being partially replaced by Rive) |
| Socket.IO server for 1v1 matchmaking (port 3000) |
| API server (port 3001), ESM |
| New API routes using Drizzle (mounted at , no auth) |
| Drizzle schema (pulled from DB, 14 tables) |
| Drizzle db factory () |
| Drizzle Kit config |
| Migration SQL files + journal |
| Original game (reference only, not served) |
| Rive animation file (Lobby, Profile, Top, Shop artboards) |
new/public/src/index.js)?start_param=duel_<id>)Auto-mode from URL:
?mode=solo|1v1|tournament skips MenuScene.
?start_param=duel_<id>), other player accepts and plays whenever (24h expiry). Ghost opponent via 500ms HTTP polling instead of WebSocket. 60s timeout after one player finishes. Results persisted in PostgreSQL duels table."Lobby" with swipeable carousel"Left " (trailing space), "Right"singleGame → solo, duelGame → 1v1, tournamentGame → tournamenttonCoins, starsCoins, gameCoinsnpm run dev:all # Starts all 4: Vite (5173) + Socket.IO (3000) + API (3001) + legacy HTTP (8000)
Or individually:
cd new && npm run dev # Vite dev server (5173) npm run dev # Socket.IO server (3000) cd monkey-flipper-api && npm start # API server (3001)
Local PvP testing: open two tabs with
?test=1 to get unique anonymous IDs.
main, builds new/dist)monkey-flipper-djm1.onrender.com) — manual deploy. Health: GET /api/new/healthmonkey-flipper-1v1.onrender.com) — manual deploy. Health: GET /api/healthSchema changes: Edit
schema.ts, then npx drizzle-kit generate to create a migration in drizzle/. Apply with npx drizzle-kit migrate. Never edit migration files after they've been applied.
src/index.js — don't rewritewindow.location.href = '/' (hard reload to React)GAME_MODES array in App.tsx must match Rive carousel order: ['solo', 'tournament', '1v1']