dubs-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (304) hide show
  1. package/.claude/settings.local.json +280 -0
  2. package/CLAUDE.md +46 -0
  3. package/CONNECT4_PRODUCTION_DEPLOY.md +155 -0
  4. package/CURRENT_SESSION.md +171 -0
  5. package/CURRENT_SESSION_DRAW.md +516 -0
  6. package/MARCH_MADNESS_SURVIVOR.md +254 -0
  7. package/PANDA.md +166 -0
  8. package/Procfile +4 -0
  9. package/README.md +476 -0
  10. package/controllers/livescoresController.js +376 -0
  11. package/controllers/pickemController.js +554 -0
  12. package/controllers/survivorAdminController.js +887 -0
  13. package/controllers/survivorController.js +623 -0
  14. package/cron/oracleMonitor.js +77 -0
  15. package/cron/pickemOracleMonitor.js +73 -0
  16. package/data/jackpot-history.json +952 -0
  17. package/data/ncaaTeams.js +406 -0
  18. package/documentation/API_SECURITY_GUIDE.md +327 -0
  19. package/documentation/ARCADE_API.md +593 -0
  20. package/documentation/ARCADE_IMPLEMENTATION_SUMMARY.md +399 -0
  21. package/documentation/ARCADE_QUICKSTART.md +242 -0
  22. package/documentation/AUTOMATIC_MODE_ORACLE.md +321 -0
  23. package/documentation/BUG_FIX_COHORT_DATE_DISPLAY.md +171 -0
  24. package/documentation/CLAIM_MIGRATION_INSTRUCTIONS.md +52 -0
  25. package/documentation/CLAIM_STATUS_FIX.md +67 -0
  26. package/documentation/CLI_TOOL_GUIDE.md +372 -0
  27. package/documentation/COHORT_RETENTION_ANALYSIS.md +295 -0
  28. package/documentation/COHORT_RETENTION_IMPLEMENTATION_COMPLETE.md +461 -0
  29. package/documentation/COHORT_RETENTION_SUMMARY.md +204 -0
  30. package/documentation/COMPLETE_PROJECT_SUMMARY.md +490 -0
  31. package/documentation/DATABASE_QUERIES.md +269 -0
  32. package/documentation/DATABASE_RETENTION_POLICY.md +390 -0
  33. package/documentation/DATABASE_SETUP_GUIDE.md +361 -0
  34. package/documentation/DATABASE_SETUP_SUMMARY.md +247 -0
  35. package/documentation/DEMO_API_CURL_COMMANDS.md +656 -0
  36. package/documentation/DEPLOYMENT_SUMMARY.txt +100 -0
  37. package/documentation/DUPLICATE_NOTIFICATIONS_FIXED.md +201 -0
  38. package/documentation/EXCHANGE_RATES_INTEGRATION.md +371 -0
  39. package/documentation/FINAL_API_PROTECTION_TABLE.md +175 -0
  40. package/documentation/GAME_START_NOTIFICATIONS_DEPLOYMENT.md +256 -0
  41. package/documentation/GAME_START_NOTIFICATIONS_INTEGRATION.md +275 -0
  42. package/documentation/HEROKU_DEPLOYMENT.md +134 -0
  43. package/documentation/HEROKU_SCHEDULER_SETUP.md +271 -0
  44. package/documentation/JACKPOT_API.md +521 -0
  45. package/documentation/JACKPOT_DEPLOYMENT_GUIDE.md +362 -0
  46. package/documentation/JWT_IMPLEMENTATION_SUMMARY.md +373 -0
  47. package/documentation/JWT_QUICK_SETUP.md +268 -0
  48. package/documentation/JWT_TESTING_GUIDE.md +404 -0
  49. package/documentation/KEEPER_RECOVERY_GUIDE.md +381 -0
  50. package/documentation/KEEPER_SETUP.md +206 -0
  51. package/documentation/KEEPER_STATE_MACHINE.md +423 -0
  52. package/documentation/LATEST_PRODUCTION_SETUP.md +387 -0
  53. package/documentation/LOCAL_VOTING_TEST.md +279 -0
  54. package/documentation/ORACLE_FIXES_SUMMARY.md +188 -0
  55. package/documentation/ORACLE_POSTGRESQL_UPDATE.md +202 -0
  56. package/documentation/PAYMENT_DEPLOYMENT.md +209 -0
  57. package/documentation/PNL_TRACKING_SETUP.md +189 -0
  58. package/documentation/PREVENTING_LOCKUP_ERRORS.md +472 -0
  59. package/documentation/PRODUCTION_READY_SUMMARY.md +227 -0
  60. package/documentation/PUBLIC_VS_PRIVATE_ENDPOINTS.md +278 -0
  61. package/documentation/QUICK_AUTH_SETUP.md +99 -0
  62. package/documentation/QUICK_DEPLOY.md +224 -0
  63. package/documentation/QUICK_FIX.md +114 -0
  64. package/documentation/QUICK_START.md +152 -0
  65. package/documentation/REFEREE_MODE_GUIDE.md +392 -0
  66. package/documentation/RETENTION_CORE_ACTION_UPDATE.md +313 -0
  67. package/documentation/RETENTION_UPDATE_SUMMARY.md +108 -0
  68. package/documentation/RUN_MIGRATION_NOW.md +39 -0
  69. package/documentation/SCRIPTS_UPDATE_SUMMARY.md +251 -0
  70. package/documentation/SETUP_GUIDE.md +184 -0
  71. package/documentation/STATE_MACHINE_IMPLEMENTATION.md +250 -0
  72. package/documentation/TELEGRAM_NOTIFICATIONS_DIAGNOSIS.md +361 -0
  73. package/documentation/UNIFIED_ARCHITECTURE.md +231 -0
  74. package/documentation/VOTING_DEPLOYMENT_SUMMARY.md +392 -0
  75. package/documentation/WEBSOCKET_ARCHITECTURE.md +881 -0
  76. package/documentation/WHAT_WE_BUILT_TODAY.md +369 -0
  77. package/documentation/latest/LATEST_PRODUCTION_SETUP.md +865 -0
  78. package/ecosystem.config.js +65 -0
  79. package/env.template +125 -0
  80. package/middleware/apiKeyAuth.js +136 -0
  81. package/middleware/authenticate.js +214 -0
  82. package/middleware/developerUserAuth.js +76 -0
  83. package/middleware/socketAuth.js +69 -0
  84. package/package.json +49 -0
  85. package/postman/Dubs-API-v1-With-Voting.postman_collection.json +555 -0
  86. package/postman/Dubs-API-v1.postman_collection.json +205 -0
  87. package/postman/Dubs_Developer_API.postman_collection.json +662 -0
  88. package/postman/QUICKSTART.md +118 -0
  89. package/postman/QUICK_REFERENCE.md +246 -0
  90. package/postman/README.md +71 -0
  91. package/postman/VOTING_API_GUIDE.md +426 -0
  92. package/refactor/Animations.md +148 -0
  93. package/refactor/Chat.md +252 -0
  94. package/routes/actionsRoutes.js +699 -0
  95. package/routes/adminRoutes.js +370 -0
  96. package/routes/analyticsRoutes.js +1262 -0
  97. package/routes/arcadeRoutes.js +557 -0
  98. package/routes/authRoutes.js +2310 -0
  99. package/routes/avatarRoutes.js +85 -0
  100. package/routes/botRoutes.js +211 -0
  101. package/routes/chatRoutes.js +377 -0
  102. package/routes/cryptoPriceRoutes.js +105 -0
  103. package/routes/developerRoutes.js +4201 -0
  104. package/routes/deviceRoutes.js +214 -0
  105. package/routes/dmRoutes.js +167 -0
  106. package/routes/esportsRoutes.js +806 -0
  107. package/routes/exchangeRateRoutes.js +233 -0
  108. package/routes/gamesRoutes.js +3028 -0
  109. package/routes/jackpotRoutes.js +754 -0
  110. package/routes/keeperMonitoringRoutes.js +156 -0
  111. package/routes/keeperWebhookRoutes.js +466 -0
  112. package/routes/livescoresRoutes.js +31 -0
  113. package/routes/pickemAdminRoutes.js +199 -0
  114. package/routes/pickemRoutes.js +231 -0
  115. package/routes/playerStatsRoutes.js +147 -0
  116. package/routes/portfolioRoutes.js +217 -0
  117. package/routes/promoRoutes.js +418 -0
  118. package/routes/referralEarningsRoutes.js +392 -0
  119. package/routes/socialRoutes.js +459 -0
  120. package/routes/sportsRoutes.js +1271 -0
  121. package/routes/survivorAdminRoutes.js +345 -0
  122. package/routes/survivorRoutes.js +756 -0
  123. package/routes/uploadRoutes.js +256 -0
  124. package/routes/userProfileRoutes.js +244 -0
  125. package/routes/whatsNewRoutes.js +331 -0
  126. package/scripts/.claude/settings.local.json +15 -0
  127. package/scripts/README.md +170 -0
  128. package/scripts/RESTART_EVERYTHING.sh +104 -0
  129. package/scripts/add-claim-columns.sql +48 -0
  130. package/scripts/add-crypto-prices-cache.sql +27 -0
  131. package/scripts/add-exchange-rates-cache.sql +40 -0
  132. package/scripts/add-game-invite-column.sql +23 -0
  133. package/scripts/add-game-invite-notification.sql +33 -0
  134. package/scripts/add-game-invite-telegram-pref.sql +16 -0
  135. package/scripts/add-game-joined-notification.sql +16 -0
  136. package/scripts/add-game-joined-pref.js +40 -0
  137. package/scripts/add-game-joined-preference.sql +6 -0
  138. package/scripts/add-game-start-notifications.sql +41 -0
  139. package/scripts/add-notification-flags-to-games.sql +55 -0
  140. package/scripts/add-pending-game-dismissals.sql +19 -0
  141. package/scripts/add-preferred-currency.sql +34 -0
  142. package/scripts/add-winner-columns.js +61 -0
  143. package/scripts/add_mention_system.sql +53 -0
  144. package/scripts/add_payment_system.sql +96 -0
  145. package/scripts/add_sports_event_id_column.sql +22 -0
  146. package/scripts/analyze-cohort-data-heroku.js +276 -0
  147. package/scripts/analyze-cohort-data.js +295 -0
  148. package/scripts/analyze-prod-cohorts.sh +10 -0
  149. package/scripts/backfill-matchup-images.js +245 -0
  150. package/scripts/backfill-missing-signatures.js +175 -0
  151. package/scripts/backfill-referral-earnings.js +202 -0
  152. package/scripts/check-chat-schema.js +130 -0
  153. package/scripts/check-db.sh +14 -0
  154. package/scripts/check_oracle_in_game.js +54 -0
  155. package/scripts/cleanup-database.js +193 -0
  156. package/scripts/clear-notification-cache.js +85 -0
  157. package/scripts/convert-mnemonic.js +50 -0
  158. package/scripts/create-users-table.sql +44 -0
  159. package/scripts/debug-cohort-counts.js +248 -0
  160. package/scripts/debug-winner-calc.js +84 -0
  161. package/scripts/deploy-payment-system.sh +118 -0
  162. package/scripts/deploy-to-heroku.sh +63 -0
  163. package/scripts/diagnose-locked-round.js +143 -0
  164. package/scripts/dubs-cli.js +720 -0
  165. package/scripts/dump-account.js +65 -0
  166. package/scripts/find-vrf-offset.js +48 -0
  167. package/scripts/fix-chat-notifications-constraint.sql +122 -0
  168. package/scripts/fix-claim-columns.js +124 -0
  169. package/scripts/fix-constraint-now.js +44 -0
  170. package/scripts/fix-lock-timestamps.js +96 -0
  171. package/scripts/fix-locked-round.sh +126 -0
  172. package/scripts/fix-missing-badges.sql +91 -0
  173. package/scripts/fix-payment-notifications.sql +41 -0
  174. package/scripts/force-new-round.js +55 -0
  175. package/scripts/force-resolve-and-claim.js +278 -0
  176. package/scripts/important/README.md +115 -0
  177. package/scripts/important/authority-force-lock.js +197 -0
  178. package/scripts/important/authority-resolve-game.js +267 -0
  179. package/scripts/important/check-game-status.js +373 -0
  180. package/scripts/important/list-pending-games-by-version.js +270 -0
  181. package/scripts/important/reconcile-v1-v2-payouts.js +270 -0
  182. package/scripts/initialize-jackpot.js +111 -0
  183. package/scripts/jackpot/.claude/settings.local.json +10 -0
  184. package/scripts/jackpot/force-reset.js +84 -0
  185. package/scripts/jackpot/initialize-mainnet.js +100 -0
  186. package/scripts/jackpot/keeper.js +742 -0
  187. package/scripts/jackpot/status.js +107 -0
  188. package/scripts/jackpot/update-round-duration.js +143 -0
  189. package/scripts/keeper-bot.js +112 -0
  190. package/scripts/list-pending-games.js +131 -0
  191. package/scripts/migrate-chat-v2.js +127 -0
  192. package/scripts/migrate-chat-winners.js +84 -0
  193. package/scripts/migrate-chat.sh +17 -0
  194. package/scripts/migrate-game-invite.js +83 -0
  195. package/scripts/migrate-heroku-game-notifications.sh +159 -0
  196. package/scripts/migrations/001_analytics_tables.sql +422 -0
  197. package/scripts/migrations/002_add_matchup_image_url.sql +14 -0
  198. package/scripts/migrations/003_referral_earnings.sql +208 -0
  199. package/scripts/migrations/004_add_whats_new_notification_type.sql +62 -0
  200. package/scripts/migrations/005_add_connect4_your_turn_notification.sql +61 -0
  201. package/scripts/migrations/005_push_notifications.sql +55 -0
  202. package/scripts/migrations/006_add_draw_team_players.sql +28 -0
  203. package/scripts/migrations/006_add_game_cancelled_notification.sql +62 -0
  204. package/scripts/migrations/007_add_gif_url.sql +8 -0
  205. package/scripts/migrations/008_add_connect4_columns.sql +139 -0
  206. package/scripts/migrations/008_add_pool_tracking.sql +22 -0
  207. package/scripts/migrations/009_create_survivor_pool_tables.sql +174 -0
  208. package/scripts/migrations/010_add_survivor_pool_outcome.sql +28 -0
  209. package/scripts/migrations/011_create_developer_tables.sql +67 -0
  210. package/scripts/migrations/011_fix_keeper_tables.sql +85 -0
  211. package/scripts/migrations/012_create_developer_webhooks.sql +31 -0
  212. package/scripts/migrations/013_add_network_mode.sql +18 -0
  213. package/scripts/migrations/014_create_developer_app_users.sql +19 -0
  214. package/scripts/migrations/015_add_ui_config.sql +4 -0
  215. package/scripts/migrations/016_add_resolution_secret.sql +4 -0
  216. package/scripts/migrations/017_add_external_game_id.sql +3 -0
  217. package/scripts/migrations/018_create_pickem_tables.sql +115 -0
  218. package/scripts/migrations/019_expo_push_tokens.sql +19 -0
  219. package/scripts/migrations/create_whats_new_tables.sql +88 -0
  220. package/scripts/migrations/drop_live_games_tables.sql +34 -0
  221. package/scripts/open-jackpot-round.js +85 -0
  222. package/scripts/purge-all-data.sh +329 -0
  223. package/scripts/purge-all-data.sql +142 -0
  224. package/scripts/purge-heroku-data.sh +149 -0
  225. package/scripts/purge-heroku-data.sql +62 -0
  226. package/scripts/rebuild-heroku-database.sh +113 -0
  227. package/scripts/recover-funds.js +357 -0
  228. package/scripts/regenerate-epl-images.js +278 -0
  229. package/scripts/resize-s3-matchup-images.js +374 -0
  230. package/scripts/resolve-direct.js +88 -0
  231. package/scripts/resolve-mock-game.js +124 -0
  232. package/scripts/resolve-pickem-game.js +55 -0
  233. package/scripts/resolve-round-manual.js +83 -0
  234. package/scripts/resolve-stuck-game.js +382 -0
  235. package/scripts/resolve-stuck-round.js +42 -0
  236. package/scripts/run-connect4-migration.sh +16 -0
  237. package/scripts/run-mention-migration.sh +32 -0
  238. package/scripts/run-payment-migration.sh +51 -0
  239. package/scripts/run-preferred-currency-migration.sh +31 -0
  240. package/scripts/run-referral-earnings-migration.sh +32 -0
  241. package/scripts/run-survivor-outcome-migration.sh +16 -0
  242. package/scripts/seed-test-users.js +346 -0
  243. package/scripts/setup-auth-tables.js +78 -0
  244. package/scripts/setup-complete-database.sql +992 -0
  245. package/scripts/setup-database-fresh.sh +359 -0
  246. package/scripts/setup-heroku-keeper.sh +48 -0
  247. package/scripts/setup-keeper-database.js +83 -0
  248. package/scripts/setup-keeper-state-db.sql +110 -0
  249. package/scripts/setup-oracle.sh +39 -0
  250. package/scripts/setup-pnl-tracking.js +111 -0
  251. package/scripts/start-devnet.sh +14 -0
  252. package/scripts/test-arcade-devnet.sh +160 -0
  253. package/scripts/test-arcade-match.sh +109 -0
  254. package/scripts/test-automatic-mode.sh +239 -0
  255. package/scripts/test-connect4-cancel-claim.js +370 -0
  256. package/scripts/test-connect4-e2e.js +369 -0
  257. package/scripts/test-connect4-resolve.js +369 -0
  258. package/scripts/test-game-state-endpoint.js +136 -0
  259. package/scripts/test-invite-notification.js +86 -0
  260. package/scripts/test-jackpot-api.sh +71 -0
  261. package/scripts/test-poll-confirmation.js +267 -0
  262. package/scripts/test-resolve-game.js +271 -0
  263. package/scripts/test-resolve-signature.js +223 -0
  264. package/scripts/test-signature-preservation.js +124 -0
  265. package/scripts/test-state-machine.js +291 -0
  266. package/scripts/test-webhook-receiver.js +60 -0
  267. package/scripts/update-notification-constraint.js +52 -0
  268. package/scripts/verify-account-layout.js +145 -0
  269. package/scripts/verify-winner-algorithm.js +278 -0
  270. package/server.js +5259 -0
  271. package/services/arcadeMatchService.js +763 -0
  272. package/services/automaticGameOracle.js +1596 -0
  273. package/services/chatService.js +1612 -0
  274. package/services/connect4GameService.js +1049 -0
  275. package/services/connect4NotificationService.js +374 -0
  276. package/services/cryptoPriceService.js +223 -0
  277. package/services/customGameResolver.js +260 -0
  278. package/services/db.js +79 -0
  279. package/services/directMessageService.js +389 -0
  280. package/services/discordNotifications.js +160 -0
  281. package/services/exchangeRateService.js +289 -0
  282. package/services/expoPushService.js +314 -0
  283. package/services/gamesCacheService.js +539 -0
  284. package/services/jackpotHistory.js +331 -0
  285. package/services/jackpotService.js +856 -0
  286. package/services/keeperStateService.js +355 -0
  287. package/services/matchupImageService.js +591 -0
  288. package/services/notificationCacheService.js +407 -0
  289. package/services/pickemOracle.js +440 -0
  290. package/services/playerStatsService.js +389 -0
  291. package/services/portfolioService.js +555 -0
  292. package/services/promoService.js +757 -0
  293. package/services/promoTreasuryService.js +239 -0
  294. package/services/pushNotifications.js +353 -0
  295. package/services/redisService.js +422 -0
  296. package/services/referralEarningsService.js +728 -0
  297. package/services/s3Service.js +396 -0
  298. package/services/socialService.js +1202 -0
  299. package/services/survivorOracle.js +469 -0
  300. package/services/survivorSimulator.js +475 -0
  301. package/services/telegramNotifications.js +461 -0
  302. package/services/userProfileStatsService.js +1185 -0
  303. package/services/whatsNewService.js +388 -0
  304. package/utils/urlHelper.js +95 -0
@@ -0,0 +1,881 @@
1
+ # ⚡ Real-Time WebSocket Architecture
2
+
3
+ **Goal:** Instant updates across all connected users
4
+ **Use Case:** Player joins in Japan → Users in USA see it within 100ms
5
+ **Status:** Design Complete, Ready to Implement
6
+
7
+ ---
8
+
9
+ ## 🎯 Why WebSockets are CRITICAL
10
+
11
+ ### **Polling (Current) - NOT GOOD ENOUGH:**
12
+
13
+ ```
14
+ User A joins → Transaction confirms → 0-2 second delay → User B sees it
15
+ ```
16
+
17
+ **Problems:**
18
+ - Feels sluggish
19
+ - Not truly "live"
20
+ - Wastes API calls
21
+ - Battery drain on mobile
22
+
23
+ ### **WebSockets - SMOOTH:**
24
+
25
+ ```
26
+ User A joins → Transaction confirms → <100ms → User B sees it instantly
27
+ ```
28
+
29
+ **Benefits:**
30
+ - ✅ Feels instant and live
31
+ - ✅ True multiplayer experience
32
+ - ✅ Lower server load
33
+ - ✅ Battery friendly
34
+ - ✅ Can push keeper events in real-time
35
+
36
+ ---
37
+
38
+ ## 🏗️ Architecture Design
39
+
40
+ ### **Components:**
41
+
42
+ ```
43
+ ┌─────────────────────────────────────────────────────────────┐
44
+ │ WEBSOCKET FLOW │
45
+ └─────────────────────────────────────────────────────────────┘
46
+
47
+ 1. KEEPER BOT (Server)
48
+ ├─ Processes round (lock/reveal/resolve)
49
+ ├─ Emits events to WebSocket server
50
+ └─ "player_joined", "round_locked", "winner_selected"
51
+
52
+ 2. WEBSOCKET SERVER (dubs-server)
53
+ ├─ Maintains connections to all users
54
+ ├─ Receives events from keeper
55
+ ├─ Broadcasts to all connected clients
56
+ └─ Handles rooms (one per round)
57
+
58
+ 3. CAROUSEL (Frontend)
59
+ ├─ Connects to WebSocket
60
+ ├─ Listens for events
61
+ ├─ Updates UI instantly
62
+ └─ Falls back to polling if disconnected
63
+ ```
64
+
65
+ ---
66
+
67
+ ## 🔌 Implementation Plan
68
+
69
+ ### **Step 1: Add Socket.io to Server (30 min)**
70
+
71
+ **Install:**
72
+ ```bash
73
+ cd dubs-server
74
+ npm install socket.io
75
+ ```
76
+
77
+ **Update `server.js`:**
78
+ ```javascript
79
+ const express = require('express');
80
+ const http = require('http');
81
+ const { Server } = require('socket.io');
82
+
83
+ const app = express();
84
+ const server = http.createServer(app);
85
+
86
+ // Socket.io with CORS
87
+ const io = new Server(server, {
88
+ cors: {
89
+ origin: [
90
+ 'http://localhost:3000',
91
+ 'https://your-netlify-app.netlify.app'
92
+ ],
93
+ methods: ['GET', 'POST']
94
+ }
95
+ });
96
+
97
+ // WebSocket connection handler
98
+ io.on('connection', (socket) => {
99
+ console.log('🔌 Client connected:', socket.id);
100
+
101
+ // Join current round room
102
+ socket.on('join_round', (roundId) => {
103
+ socket.join(`round_${roundId}`);
104
+ console.log(`👥 Client ${socket.id} joined round ${roundId}`);
105
+
106
+ // Send current state immediately
107
+ getCurrentRoundState(roundId).then(state => {
108
+ socket.emit('round_state', state);
109
+ });
110
+ });
111
+
112
+ socket.on('disconnect', () => {
113
+ console.log('🔌 Client disconnected:', socket.id);
114
+ });
115
+ });
116
+
117
+ // Change app.listen to server.listen
118
+ server.listen(PORT, () => {
119
+ console.log(`🚀 Server running on port ${PORT}`);
120
+ console.log(`⚡ WebSocket ready`);
121
+ });
122
+
123
+ // Make io available globally for emitting from keeper
124
+ global.io = io;
125
+ ```
126
+
127
+ ---
128
+
129
+ ### **Step 2: Emit Events from Keeper (15 min)**
130
+
131
+ **Update `jackpot-keeper.js`:**
132
+
133
+ ```javascript
134
+ class JackpotKeeper {
135
+ constructor() {
136
+ // ... existing code ...
137
+ this.io = null; // Will be set via setter
138
+ }
139
+
140
+ setSocketServer(io) {
141
+ this.io = io;
142
+ }
143
+
144
+ async lockRound(roundId) {
145
+ // ... existing lock logic ...
146
+
147
+ // Emit event
148
+ if (this.io) {
149
+ this.io.to(`round_${roundId}`).emit('round_locked', {
150
+ roundId,
151
+ timestamp: Date.now()
152
+ });
153
+ }
154
+
155
+ return true;
156
+ }
157
+
158
+ async resolveRound(roundId) {
159
+ // ... existing resolve logic ...
160
+
161
+ const winner = data.winner;
162
+ const winAmount = Number(totalPot) * 0.95;
163
+
164
+ // Emit winner event
165
+ if (this.io) {
166
+ this.io.to(`round_${roundId}`).emit('winner_selected', {
167
+ roundId,
168
+ winner,
169
+ winAmount,
170
+ timestamp: Date.now()
171
+ });
172
+ }
173
+
174
+ return true;
175
+ }
176
+
177
+ async openOrResetRound(previousRoundId) {
178
+ // ... existing reset logic ...
179
+
180
+ const newRoundId = data.roundId;
181
+
182
+ // Emit new round event
183
+ if (this.io) {
184
+ // Broadcast to all clients
185
+ this.io.emit('round_opened', {
186
+ roundId: newRoundId,
187
+ timestamp: Date.now()
188
+ });
189
+ }
190
+
191
+ return true;
192
+ }
193
+ }
194
+
195
+ // Export keeper instance
196
+ module.exports = JackpotKeeper;
197
+ ```
198
+
199
+ **In keeper startup script:**
200
+ ```javascript
201
+ // server.js or separate keeper init
202
+ const keeper = new JackpotKeeper();
203
+ keeper.setSocketServer(io);
204
+ keeper.run();
205
+ ```
206
+
207
+ ---
208
+
209
+ ### **Step 3: Listen for New Players (Real-Time)**
210
+
211
+ **Add endpoint to emit when player joins:**
212
+
213
+ **Update `jackpotRoutes.js`:**
214
+ ```javascript
215
+ router.post('/enter', async (req, res) => {
216
+ try {
217
+ // ... existing enter logic ...
218
+
219
+ const result = await jackpotService.enterRound(...);
220
+
221
+ // Emit to all users in this round
222
+ if (global.io) {
223
+ global.io.to(`round_${roundId}`).emit('player_joined', {
224
+ roundId,
225
+ player: playerAddress,
226
+ weight: amount,
227
+ entryCount: newEntryCount,
228
+ totalPot: newTotalPot,
229
+ timestamp: Date.now()
230
+ });
231
+ }
232
+
233
+ res.json(result);
234
+ } catch (error) {
235
+ // ... error handling ...
236
+ }
237
+ });
238
+ ```
239
+
240
+ ---
241
+
242
+ ### **Step 4: Frontend WebSocket Integration (30 min)**
243
+
244
+ **Install:**
245
+ ```bash
246
+ cd dubs-jackpot
247
+ npm install socket.io-client
248
+ ```
249
+
250
+ **Create WebSocket Hook:**
251
+
252
+ File: `dubs-jackpot/app/hooks/useJackpotSocket.ts`
253
+
254
+ ```typescript
255
+ import { useEffect, useState, useCallback } from 'react';
256
+ import { io, Socket } from 'socket.io-client';
257
+
258
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
259
+
260
+ export function useJackpotSocket(roundId: string | null) {
261
+ const [socket, setSocket] = useState<Socket | null>(null);
262
+ const [connected, setConnected] = useState(false);
263
+ const [players, setPlayers] = useState<any[]>([]);
264
+ const [roundState, setRoundState] = useState<any>(null);
265
+
266
+ // Connect to WebSocket
267
+ useEffect(() => {
268
+ const newSocket = io(API_BASE, {
269
+ transports: ['websocket', 'polling'],
270
+ reconnection: true,
271
+ reconnectionDelay: 1000,
272
+ reconnectionAttempts: 5
273
+ });
274
+
275
+ newSocket.on('connect', () => {
276
+ console.log('⚡ WebSocket connected');
277
+ setConnected(true);
278
+
279
+ // Join current round room
280
+ if (roundId) {
281
+ newSocket.emit('join_round', roundId);
282
+ }
283
+ });
284
+
285
+ newSocket.on('disconnect', () => {
286
+ console.log('🔌 WebSocket disconnected');
287
+ setConnected(false);
288
+ });
289
+
290
+ setSocket(newSocket);
291
+
292
+ return () => {
293
+ newSocket.close();
294
+ };
295
+ }, []);
296
+
297
+ // Join round room when roundId changes
298
+ useEffect(() => {
299
+ if (socket && roundId) {
300
+ socket.emit('join_round', roundId);
301
+ }
302
+ }, [socket, roundId]);
303
+
304
+ // Listen for player joined
305
+ useEffect(() => {
306
+ if (!socket) return;
307
+
308
+ const handlePlayerJoined = (data: any) => {
309
+ console.log('👤 Player joined:', data);
310
+
311
+ // Add new player to carousel
312
+ setPlayers(prev => {
313
+ // Check if already exists
314
+ if (prev.find(p => p.address === data.player)) {
315
+ return prev;
316
+ }
317
+
318
+ // Add new player
319
+ return [...prev, {
320
+ address: data.player,
321
+ wager: data.weight / 1e9,
322
+ emoji: hashToEmoji(data.player),
323
+ username: data.player.slice(0, 4) + '...',
324
+ timestamp: data.timestamp
325
+ }];
326
+ });
327
+ };
328
+
329
+ socket.on('player_joined', handlePlayerJoined);
330
+
331
+ return () => {
332
+ socket.off('player_joined', handlePlayerJoined);
333
+ };
334
+ }, [socket]);
335
+
336
+ // Listen for round locked
337
+ useEffect(() => {
338
+ if (!socket) return;
339
+
340
+ const handleRoundLocked = (data: any) => {
341
+ console.log('🔒 Round locked:', data);
342
+ setRoundState({ status: 'locked', roundId: data.roundId });
343
+ };
344
+
345
+ socket.on('round_locked', handleRoundLocked);
346
+
347
+ return () => {
348
+ socket.off('round_locked', handleRoundLocked);
349
+ };
350
+ }, [socket]);
351
+
352
+ // Listen for winner selected
353
+ useEffect(() => {
354
+ if (!socket) return;
355
+
356
+ const handleWinnerSelected = (data: any) => {
357
+ console.log('🏆 Winner selected:', data);
358
+
359
+ setRoundState({
360
+ status: 'resolved',
361
+ winner: data.winner,
362
+ winAmount: data.winAmount,
363
+ roundId: data.roundId
364
+ });
365
+ };
366
+
367
+ socket.on('winner_selected', handleWinnerSelected);
368
+
369
+ return () => {
370
+ socket.off('winner_selected', handleWinnerSelected);
371
+ };
372
+ }, [socket]);
373
+
374
+ // Listen for new round
375
+ useEffect(() => {
376
+ if (!socket) return;
377
+
378
+ const handleRoundOpened = (data: any) => {
379
+ console.log('🎰 New round opened:', data);
380
+
381
+ setPlayers([]); // Clear players
382
+ setRoundState({
383
+ status: 'open',
384
+ roundId: data.roundId
385
+ });
386
+ };
387
+
388
+ socket.on('round_opened', handleRoundOpened);
389
+
390
+ return () => {
391
+ socket.off('round_opened', handleRoundOpened);
392
+ };
393
+ }, [socket]);
394
+
395
+ return {
396
+ connected,
397
+ players,
398
+ roundState,
399
+ socket
400
+ };
401
+ }
402
+
403
+ // Helper
404
+ function hashToEmoji(address: string): string {
405
+ const EMOJIS = ['🔥', '⚡', '🌊', '🌿', '🎮', '⭐', '💎', '🎯', '🚀', '🌟'];
406
+ const hash = address.split('').reduce((acc, c) => acc + c.charCodeAt(0), 0);
407
+ return EMOJIS[hash % EMOJIS.length];
408
+ }
409
+ ```
410
+
411
+ ---
412
+
413
+ ### **Step 5: Use in Carousel (10 min)**
414
+
415
+ **Update `JackpotCarousel.tsx`:**
416
+
417
+ ```typescript
418
+ export default function JackpotCarousel() {
419
+ const [currentRound, setCurrentRound] = useState<any>(null);
420
+
421
+ // Get current round ID
422
+ useEffect(() => {
423
+ fetch(`${API_BASE}/jackpot/round/current`)
424
+ .then(r => r.json())
425
+ .then(data => setCurrentRound(data.round));
426
+ }, []);
427
+
428
+ // Use WebSocket hook
429
+ const { connected, players, roundState } = useJackpotSocket(
430
+ currentRound?.roundId
431
+ );
432
+
433
+ // Trigger winner animation when winner selected
434
+ useEffect(() => {
435
+ if (roundState?.status === 'resolved' && roundState.winner) {
436
+ const winnerPlayer = players.find(p => p.address === roundState.winner);
437
+
438
+ if (winnerPlayer) {
439
+ // Trigger carousel animation
440
+ triggerWinnerAnimation(winnerPlayer, roundState.winAmount);
441
+ }
442
+ }
443
+ }, [roundState]);
444
+
445
+ return (
446
+ <div>
447
+ {/* Connection indicator */}
448
+ <div className="connection-status">
449
+ {connected ? '🟢 Live' : '🔴 Reconnecting...'}
450
+ </div>
451
+
452
+ <InfiniteCarousel
453
+ players={players}
454
+ roundState={roundState}
455
+ />
456
+ </div>
457
+ );
458
+ }
459
+ ```
460
+
461
+ ---
462
+
463
+ ## ⚡ Event Types
464
+
465
+ ### **Server → Client Events:**
466
+
467
+ 1. **`player_joined`**
468
+ ```json
469
+ {
470
+ "roundId": "556",
471
+ "player": "Abc...xyz",
472
+ "weight": "50000000",
473
+ "entryCount": 5,
474
+ "totalPot": "250000000",
475
+ "timestamp": 1700000000
476
+ }
477
+ ```
478
+ **UI Action:** Add card to carousel (RIGHT-TO-LEFT animation)
479
+
480
+ 2. **`round_locked`**
481
+ ```json
482
+ {
483
+ "roundId": "556",
484
+ "timestamp": 1700000000
485
+ }
486
+ ```
487
+ **UI Action:** Show "Picking winner...", speed up carousel
488
+
489
+ 3. **`winner_selected`**
490
+ ```json
491
+ {
492
+ "roundId": "556",
493
+ "winner": "Abc...xyz",
494
+ "winAmount": "237500000",
495
+ "timestamp": 1700000000
496
+ }
497
+ ```
498
+ **UI Action:** Scroll to winner, show badge
499
+
500
+ 4. **`round_opened`**
501
+ ```json
502
+ {
503
+ "roundId": "557",
504
+ "timestamp": 1700000000
505
+ }
506
+ ```
507
+ **UI Action:** Reset carousel, clear players
508
+
509
+ 5. **`timer_update`** (every 5 seconds)
510
+ ```json
511
+ {
512
+ "roundId": "556",
513
+ "timeRemaining": 45,
514
+ "status": "open"
515
+ }
516
+ ```
517
+ **UI Action:** Update countdown
518
+
519
+ ---
520
+
521
+ ## 🎯 Implementation Strategy
522
+
523
+ ### **Hybrid Approach (Best of Both Worlds):**
524
+
525
+ **WebSockets for:**
526
+ - ✅ Player joins (instant!)
527
+ - ✅ Round state changes (locked, resolved)
528
+ - ✅ Winner announcements
529
+ - ✅ New round notifications
530
+
531
+ **Polling for:**
532
+ - ✅ Initial load (fetch current state)
533
+ - ✅ Fallback if WebSocket fails
534
+ - ✅ Every 10s to verify sync (catch missed events)
535
+
536
+ **Code:**
537
+ ```typescript
538
+ export function useJackpotData(roundId: string) {
539
+ const socket = useJackpotSocket(roundId);
540
+ const [players, setPlayers] = useState([]);
541
+
542
+ // WebSocket updates (instant)
543
+ useEffect(() => {
544
+ if (socket.players.length > 0) {
545
+ setPlayers(socket.players);
546
+ }
547
+ }, [socket.players]);
548
+
549
+ // Polling fallback (every 10s to verify)
550
+ useEffect(() => {
551
+ const verify = async () => {
552
+ if (!socket.connected) {
553
+ // WebSocket down, use polling
554
+ const data = await fetchEntries(roundId);
555
+ setPlayers(data);
556
+ } else {
557
+ // WebSocket working, just verify sync
558
+ const data = await fetchEntries(roundId);
559
+ if (data.length !== players.length) {
560
+ console.warn('⚠️ Socket/API mismatch, resync');
561
+ setPlayers(data);
562
+ }
563
+ }
564
+ };
565
+
566
+ const interval = setInterval(verify, 10000);
567
+ return () => clearInterval(interval);
568
+ }, [roundId, socket.connected, players.length]);
569
+
570
+ return {
571
+ players,
572
+ connected: socket.connected,
573
+ roundState: socket.roundState
574
+ };
575
+ }
576
+ ```
577
+
578
+ ---
579
+
580
+ ## 🔧 Server-Side Event Emission
581
+
582
+ ### **Where to Emit:**
583
+
584
+ **1. When player enters (API endpoint):**
585
+ ```javascript
586
+ // routes/jackpotRoutes.js
587
+ router.post('/enter', async (req, res) => {
588
+ // ... process entry ...
589
+
590
+ // Emit to all users watching this round
591
+ global.io.to(`round_${roundId}`).emit('player_joined', {
592
+ roundId,
593
+ player: playerAddress,
594
+ weight: amount,
595
+ entryCount: round.entryCount,
596
+ totalPot: round.totalPotLamports
597
+ });
598
+ });
599
+ ```
600
+
601
+ **2. In keeper bot:**
602
+ ```javascript
603
+ // scripts/jackpot-keeper.js
604
+
605
+ async lockRound(roundId) {
606
+ // ... lock logic ...
607
+
608
+ // Broadcast to all users
609
+ this.io.to(`round_${roundId}`).emit('round_locked', { roundId });
610
+ this.io.emit('global_event', { type: 'round_locked', roundId });
611
+ }
612
+
613
+ async resolveRound(roundId) {
614
+ // ... resolve logic ...
615
+
616
+ // Broadcast winner
617
+ this.io.to(`round_${roundId}`).emit('winner_selected', {
618
+ roundId,
619
+ winner,
620
+ winAmount
621
+ });
622
+ }
623
+ ```
624
+
625
+ **3. Timer updates (optional, every 5s):**
626
+ ```javascript
627
+ setInterval(() => {
628
+ getCurrentRound().then(round => {
629
+ if (round) {
630
+ io.to(`round_${round.roundId}`).emit('timer_update', {
631
+ roundId: round.roundId,
632
+ timeRemaining: round.timeRemainingSlots * 0.4,
633
+ totalPot: round.totalPotLamports,
634
+ entryCount: round.entryCount
635
+ });
636
+ }
637
+ });
638
+ }, 5000);
639
+ ```
640
+
641
+ ---
642
+
643
+ ## 🎨 Carousel Integration
644
+
645
+ ### **Updated InfiniteCarousel:**
646
+
647
+ ```typescript
648
+ export default function InfiniteCarousel({
649
+ roundId
650
+ }: { roundId: string | null }) {
651
+ const INITIAL_SLOTS = 7;
652
+ const [players, setPlayers] = useState<(Player | null)[]>(Array(INITIAL_SLOTS).fill(null));
653
+
654
+ // WebSocket hook
655
+ const { connected, players: realPlayers, roundState } = useJackpotSocket(roundId);
656
+
657
+ // Sync real players to carousel
658
+ useEffect(() => {
659
+ if (realPlayers.length > 0) {
660
+ // Map to carousel format
661
+ const carouselPlayers = realPlayers.map(p => ({
662
+ id: p.address,
663
+ username: p.username,
664
+ emoji: p.emoji,
665
+ wager: p.wager,
666
+ address: p.address
667
+ }));
668
+
669
+ // Fill slots right-to-left
670
+ const slots = Array(Math.max(INITIAL_SLOTS, carouselPlayers.length)).fill(null);
671
+ carouselPlayers.forEach((player, i) => {
672
+ slots[slots.length - 1 - i] = player; // Right-to-left
673
+ });
674
+
675
+ setPlayers(slots);
676
+ }
677
+ }, [realPlayers]);
678
+
679
+ // React to round state changes
680
+ useEffect(() => {
681
+ if (roundState?.status === 'locked') {
682
+ // Speed up carousel, show "Picking winner..."
683
+ setIsPickingWinner(true);
684
+ }
685
+
686
+ if (roundState?.status === 'resolved' && roundState.winner) {
687
+ // Trigger winner animation
688
+ const winnerPlayer = players.find(p => p?.address === roundState.winner);
689
+ if (winnerPlayer) {
690
+ handlePickWinner(winnerPlayer);
691
+ }
692
+ }
693
+
694
+ if (roundState?.status === 'open' && roundState.roundId !== currentRound) {
695
+ // New round started
696
+ handleReset();
697
+ }
698
+ }, [roundState]);
699
+
700
+ return (
701
+ <>
702
+ {/* Connection indicator */}
703
+ <div className={`status ${connected ? 'connected' : 'disconnected'}`}>
704
+ {connected ? '⚡ LIVE' : '🔄 Reconnecting...'}
705
+ </div>
706
+
707
+ {/* Carousel */}
708
+ {/* ... rest of carousel UI ... */}
709
+ </>
710
+ );
711
+ }
712
+ ```
713
+
714
+ ---
715
+
716
+ ## 🚀 Performance Optimization
717
+
718
+ ### **1. Debounce Rapid Events:**
719
+
720
+ ```typescript
721
+ // If 5 players join in 1 second, batch them
722
+ const [playerQueue, setPlayerQueue] = useState([]);
723
+
724
+ useEffect(() => {
725
+ socket.on('player_joined', (player) => {
726
+ setPlayerQueue(prev => [...prev, player]);
727
+ });
728
+ }, [socket]);
729
+
730
+ useEffect(() => {
731
+ if (playerQueue.length > 0) {
732
+ const timeout = setTimeout(() => {
733
+ // Add all queued players at once
734
+ setPlayers(prev => [...prev, ...playerQueue]);
735
+ setPlayerQueue([]);
736
+ }, 300); // 300ms debounce
737
+
738
+ return () => clearTimeout(timeout);
739
+ }
740
+ }, [playerQueue]);
741
+ ```
742
+
743
+ ### **2. Efficient Re-renders:**
744
+
745
+ ```typescript
746
+ // Memoize players to prevent unnecessary renders
747
+ const memoizedPlayers = useMemo(() => players, [players.length, players[0]?.id]);
748
+
749
+ // Only re-render carousel when players actually change
750
+ ```
751
+
752
+ ### **3. Background Sync:**
753
+
754
+ ```typescript
755
+ // Verify socket data matches blockchain every 30s
756
+ useEffect(() => {
757
+ const verify = async () => {
758
+ const canonical = await fetchEntries(roundId);
759
+ if (canonical.length !== players.length) {
760
+ console.warn('Resync needed');
761
+ setPlayers(canonical);
762
+ }
763
+ };
764
+
765
+ const interval = setInterval(verify, 30000);
766
+ return () => clearInterval(interval);
767
+ }, [roundId]);
768
+ ```
769
+
770
+ ---
771
+
772
+ ## 🎯 Why This Will Be SMOOTH
773
+
774
+ **Player joins in Tokyo:**
775
+ ```
776
+ 1. Transaction confirms (2-3s)
777
+ 2. Backend emits player_joined (instant)
778
+ 3. WebSocket pushes to all clients (<100ms)
779
+ 4. Carousel in NYC adds card (instant)
780
+ ```
781
+
782
+ **Total delay: <100ms after blockchain confirmation!**
783
+
784
+ **vs Polling:**
785
+ ```
786
+ 1. Transaction confirms (2-3s)
787
+ 2. User in NYC polls API (0-2s wait)
788
+ 3. Carousel updates
789
+ ```
790
+
791
+ **Total delay: 0-2 seconds** 😢
792
+
793
+ ---
794
+
795
+ ## 📋 Implementation Checklist
796
+
797
+ **Backend:**
798
+ - [ ] Install socket.io
799
+ - [ ] Add WebSocket server to server.js
800
+ - [ ] Emit events from keeper
801
+ - [ ] Emit events from API endpoints
802
+ - [ ] Test events with socket.io-client
803
+
804
+ **Frontend:**
805
+ - [ ] Install socket.io-client
806
+ - [ ] Create useJackpotSocket hook
807
+ - [ ] Integrate with carousel
808
+ - [ ] Add connection indicator
809
+ - [ ] Add fallback polling
810
+ - [ ] Test real-time updates
811
+
812
+ **Testing:**
813
+ - [ ] 2 users, different browsers
814
+ - [ ] User A joins → User B sees instantly
815
+ - [ ] Winner selected → Both see animation
816
+ - [ ] Network disconnect → Falls back to polling
817
+ - [ ] Reconnect → Resync automatically
818
+
819
+ ---
820
+
821
+ ## 🎯 Estimated Timeline
822
+
823
+ **Day 1 - Backend WebSocket (3 hours):**
824
+ - Hour 1: Add Socket.io to server
825
+ - Hour 2: Emit events from keeper
826
+ - Hour 3: Test with socket.io-client CLI
827
+
828
+ **Day 2 - Frontend Integration (4 hours):**
829
+ - Hour 1: Create useJackpotSocket hook
830
+ - Hour 2: Integrate with carousel
831
+ - Hour 3: Add fallback & error handling
832
+ - Hour 4: Test with real rounds
833
+
834
+ **Day 3 - Polish & Deploy (2 hours):**
835
+ - Hour 1: Connection indicators, loading states
836
+ - Hour 2: Deploy & monitor
837
+
838
+ **Total: ~9 hours** for real-time multiplayer jackpot! 🚀
839
+
840
+ ---
841
+
842
+ ## 💎 The Result
843
+
844
+ **Users will see:**
845
+
846
+ ```
847
+ [You join from USA]
848
+ → Card appears instantly on your screen
849
+ → Card appears instantly on Tokyo user's screen (<100ms)
850
+
851
+ [Tokyo user joins]
852
+ → Card whooshes in from right on your screen (<100ms)
853
+ → Feels ALIVE and multiplayer
854
+
855
+ [Round locks]
856
+ → Everyone sees carousel speed up (synchronized)
857
+
858
+ [Winner selected]
859
+ → Everyone sees same smooth scroll-to-winner
860
+ → Same winner badge animation
861
+ → True shared experience
862
+ ```
863
+
864
+ **This is what makes it feel like a REAL multiplayer game!** 🎮
865
+
866
+ ---
867
+
868
+ ## 🎯 My Recommendation
869
+
870
+ **YES, absolutely use WebSockets!**
871
+
872
+ **Why:**
873
+ 1. ✅ Minimal extra effort (~3 hours backend, ~4 hours frontend)
874
+ 2. ✅ Massive UX improvement (feels alive!)
875
+ 3. ✅ Required for true multiplayer feel
876
+ 4. ✅ Socket.io handles all edge cases (reconnection, fallback)
877
+ 5. ✅ Can scale to 100s of concurrent users
878
+
879
+ **With your smooth carousel + real-time WebSockets = Magic!** ✨
880
+
881
+ Want me to start implementing the WebSocket system? I can have the backend ready in ~1 hour!