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,469 @@
1
+ /**
2
+ * Survivor Pool Oracle Service
3
+ * Monitors tournament games and resolves rounds based on ESPN results
4
+ */
5
+
6
+ const axios = require('axios');
7
+ const { pool } = require('./db');
8
+
9
+ // ESPN NCAAB scoreboard URL
10
+ const ESPN_NCAAB_URL = 'https://site.api.espn.com/apis/site/v2/sports/basketball/mens-college-basketball/scoreboard';
11
+
12
+ // Round names for logging
13
+ const ROUND_NAMES = {
14
+ 1: 'First Four',
15
+ 2: 'Round of 64',
16
+ 3: 'Round of 32',
17
+ 4: 'Sweet 16',
18
+ 5: 'Elite 8',
19
+ 6: 'Final Four',
20
+ 7: 'Championship'
21
+ };
22
+
23
+ class SurvivorOracle {
24
+ constructor(config = {}) {
25
+ this.checkIntervalMs = config.checkIntervalMs || 5 * 60 * 1000; // 5 minutes default
26
+ this.isRunning = false;
27
+ this.intervalId = null;
28
+ }
29
+
30
+ /**
31
+ * Start the oracle monitor
32
+ */
33
+ start() {
34
+ if (this.isRunning) {
35
+ console.log('[SurvivorOracle] Already running');
36
+ return;
37
+ }
38
+
39
+ console.log('[SurvivorOracle] Starting...');
40
+ console.log(` Check interval: ${this.checkIntervalMs / 1000}s`);
41
+
42
+ this.isRunning = true;
43
+
44
+ // Run immediately
45
+ this.checkActivePools();
46
+
47
+ // Then run on interval
48
+ this.intervalId = setInterval(() => {
49
+ this.checkActivePools();
50
+ }, this.checkIntervalMs);
51
+ }
52
+
53
+ /**
54
+ * Stop the oracle
55
+ */
56
+ stop() {
57
+ if (this.intervalId) {
58
+ clearInterval(this.intervalId);
59
+ this.intervalId = null;
60
+ }
61
+ this.isRunning = false;
62
+ console.log('[SurvivorOracle] Stopped');
63
+ }
64
+
65
+ /**
66
+ * Check all active survivor pools
67
+ */
68
+ async checkActivePools() {
69
+ try {
70
+ console.log(`\n[SurvivorOracle] [${new Date().toISOString()}] Checking active pools...`);
71
+
72
+ // Get all active pools
73
+ const result = await pool.query(
74
+ "SELECT * FROM survivor_pools WHERE status = 'active'"
75
+ );
76
+
77
+ const activePools = result.rows;
78
+ console.log(`[SurvivorOracle] Found ${activePools.length} active pool(s)`);
79
+
80
+ for (const survivorPool of activePools) {
81
+ await this.processPool(survivorPool);
82
+ }
83
+ } catch (error) {
84
+ console.error('[SurvivorOracle] Error checking pools:', error.message);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Process a single pool
90
+ */
91
+ async processPool(survivorPool) {
92
+ try {
93
+ console.log(`[SurvivorOracle] Processing pool ${survivorPool.id}: ${survivorPool.name}`);
94
+ console.log(` Current round: ${survivorPool.current_round} (${ROUND_NAMES[survivorPool.current_round]})`);
95
+
96
+ // Check if round deadline has passed
97
+ if (survivorPool.round_deadline && new Date() < new Date(survivorPool.round_deadline)) {
98
+ console.log(` Deadline not yet passed, skipping`);
99
+ return;
100
+ }
101
+
102
+ // Get unresolved games for current round
103
+ const gamesResult = await pool.query(
104
+ `SELECT * FROM survivor_tournament_games
105
+ WHERE pool_id = $1 AND round = $2 AND status != 'final'`,
106
+ [survivorPool.id, survivorPool.current_round]
107
+ );
108
+
109
+ const unresolvedGames = gamesResult.rows;
110
+ console.log(` Unresolved games: ${unresolvedGames.length}`);
111
+
112
+ if (unresolvedGames.length > 0) {
113
+ // Fetch ESPN scores and update games
114
+ await this.updateGameScores(survivorPool.id, unresolvedGames);
115
+ }
116
+
117
+ // Check if all games in current round are final
118
+ const allGamesResult = await pool.query(
119
+ `SELECT
120
+ COUNT(*) as total,
121
+ COUNT(*) FILTER (WHERE status = 'final') as final_count
122
+ FROM survivor_tournament_games
123
+ WHERE pool_id = $1 AND round = $2`,
124
+ [survivorPool.id, survivorPool.current_round]
125
+ );
126
+
127
+ const { total, final_count } = allGamesResult.rows[0];
128
+ console.log(` Games status: ${final_count}/${total} final`);
129
+
130
+ if (parseInt(total) > 0 && parseInt(final_count) === parseInt(total)) {
131
+ // All games final - resolve the round
132
+ console.log(` All games final - resolving round`);
133
+ await this.resolveCurrentRound(survivorPool.id);
134
+ }
135
+ } catch (error) {
136
+ console.error(`[SurvivorOracle] Error processing pool ${survivorPool.id}:`, error.message);
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Fetch ESPN scores and update tournament games
142
+ */
143
+ async updateGameScores(poolId, games) {
144
+ try {
145
+ console.log(`[SurvivorOracle] Fetching ESPN scores...`);
146
+
147
+ const { data } = await axios.get(ESPN_NCAAB_URL);
148
+
149
+ if (!data.events) {
150
+ console.log('[SurvivorOracle] No events in ESPN response');
151
+ return;
152
+ }
153
+
154
+ // Create a map of ESPN game IDs to results
155
+ const espnResults = new Map();
156
+
157
+ for (const event of data.events) {
158
+ const competition = event.competitions?.[0];
159
+ if (!competition) continue;
160
+
161
+ const gameId = event.id;
162
+ const status = event.status?.type?.description;
163
+ const competitors = competition.competitors || [];
164
+
165
+ if (competitors.length !== 2) continue;
166
+
167
+ const team1 = competitors.find(c => c.homeAway === 'home') || competitors[0];
168
+ const team2 = competitors.find(c => c.homeAway === 'away') || competitors[1];
169
+
170
+ espnResults.set(gameId, {
171
+ status,
172
+ isFinal: status === 'Final' || event.status?.type?.completed === true,
173
+ team1Score: parseInt(team1.score) || 0,
174
+ team2Score: parseInt(team2.score) || 0,
175
+ team1Id: team1.team?.id,
176
+ team2Id: team2.team?.id,
177
+ winnerId: team1.winner ? team1.team?.id : (team2.winner ? team2.team?.id : null)
178
+ });
179
+ }
180
+
181
+ // Update each tournament game with ESPN results
182
+ for (const game of games) {
183
+ const result = espnResults.get(game.espn_game_id);
184
+
185
+ if (!result) {
186
+ console.log(` Game ${game.espn_game_id}: Not found in ESPN data`);
187
+ continue;
188
+ }
189
+
190
+ if (result.isFinal) {
191
+ console.log(` Game ${game.espn_game_id}: FINAL - ${result.team1Score} vs ${result.team2Score}`);
192
+
193
+ // Determine winner
194
+ let winnerId = result.winnerId;
195
+ if (!winnerId) {
196
+ // Fallback: higher score wins
197
+ winnerId = result.team1Score > result.team2Score ? game.team1_id : game.team2_id;
198
+ }
199
+
200
+ await pool.query(
201
+ `UPDATE survivor_tournament_games
202
+ SET winner_id = $3, team1_score = $4, team2_score = $5, status = 'final', updated_at = NOW()
203
+ WHERE pool_id = $1 AND espn_game_id = $2`,
204
+ [poolId, game.espn_game_id, winnerId, result.team1Score, result.team2Score]
205
+ );
206
+ } else {
207
+ console.log(` Game ${game.espn_game_id}: ${result.status || 'In Progress'}`);
208
+
209
+ // Update live scores
210
+ const newStatus = result.status === 'In Progress' ? 'live' : 'scheduled';
211
+ await pool.query(
212
+ `UPDATE survivor_tournament_games
213
+ SET team1_score = $3, team2_score = $4, status = $5, updated_at = NOW()
214
+ WHERE pool_id = $1 AND espn_game_id = $2`,
215
+ [poolId, game.espn_game_id, result.team1Score, result.team2Score, newStatus]
216
+ );
217
+ }
218
+ }
219
+ } catch (error) {
220
+ console.error('[SurvivorOracle] Error fetching ESPN scores:', error.message);
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Resolve the current round - update picks and eliminate losers
226
+ */
227
+ async resolveCurrentRound(poolId) {
228
+ const client = await pool.connect();
229
+
230
+ try {
231
+ await client.query('BEGIN');
232
+
233
+ // Get pool info
234
+ const poolResult = await client.query(
235
+ 'SELECT * FROM survivor_pools WHERE id = $1 FOR UPDATE',
236
+ [poolId]
237
+ );
238
+
239
+ if (poolResult.rows.length === 0) {
240
+ throw new Error('Pool not found');
241
+ }
242
+
243
+ const survivorPool = poolResult.rows[0];
244
+ const currentRound = survivorPool.current_round;
245
+
246
+ console.log(`[SurvivorOracle] Resolving round ${currentRound} for pool ${poolId}`);
247
+
248
+ // Get all final games for this round
249
+ const gamesResult = await client.query(
250
+ `SELECT * FROM survivor_tournament_games
251
+ WHERE pool_id = $1 AND round = $2 AND status = 'final'`,
252
+ [poolId, currentRound]
253
+ );
254
+
255
+ const finalGames = gamesResult.rows;
256
+ const winnerMap = new Map();
257
+
258
+ for (const game of finalGames) {
259
+ // Map both team IDs to whether they won
260
+ winnerMap.set(game.team1_id, game.winner_id === game.team1_id);
261
+ winnerMap.set(game.team2_id, game.winner_id === game.team2_id);
262
+ }
263
+
264
+ // Get all picks for this round
265
+ const picksResult = await client.query(
266
+ `SELECT sp.*, se.id as entry_id, se.user_id
267
+ FROM survivor_picks sp
268
+ JOIN survivor_entries se ON se.id = sp.entry_id
269
+ WHERE se.pool_id = $1 AND sp.round = $2 AND se.is_alive = true`,
270
+ [poolId, currentRound]
271
+ );
272
+
273
+ const picks = picksResult.rows;
274
+ let eliminatedCount = 0;
275
+ let survivedCount = 0;
276
+
277
+ // Process each pick
278
+ for (const pick of picks) {
279
+ const teamWon = winnerMap.get(pick.team_id);
280
+
281
+ if (teamWon === true) {
282
+ // Team won - mark pick as won
283
+ await client.query(
284
+ `UPDATE survivor_picks SET result = 'won', updated_at = NOW() WHERE id = $1`,
285
+ [pick.id]
286
+ );
287
+ survivedCount++;
288
+ console.log(` Entry ${pick.entry_id}: ${pick.team_name} WON - survived`);
289
+ } else if (teamWon === false) {
290
+ // Team lost - mark pick as lost and eliminate entry
291
+ await client.query(
292
+ `UPDATE survivor_picks SET result = 'lost', updated_at = NOW() WHERE id = $1`,
293
+ [pick.id]
294
+ );
295
+ await client.query(
296
+ `UPDATE survivor_entries
297
+ SET is_alive = false, eliminated_at_round = $2, updated_at = NOW()
298
+ WHERE id = $1`,
299
+ [pick.entry_id, currentRound]
300
+ );
301
+ eliminatedCount++;
302
+ console.log(` Entry ${pick.entry_id}: ${pick.team_name} LOST - ELIMINATED`);
303
+ } else {
304
+ // Game result not found (shouldn't happen if all games are final)
305
+ console.warn(` Entry ${pick.entry_id}: Team ${pick.team_id} result not found`);
306
+ }
307
+ }
308
+
309
+ // Eliminate entries that didn't submit a pick (auto-elimination)
310
+ const missingPicksResult = await client.query(
311
+ `UPDATE survivor_entries
312
+ SET is_alive = false, eliminated_at_round = $2, updated_at = NOW()
313
+ WHERE pool_id = $1
314
+ AND is_alive = true
315
+ AND id NOT IN (
316
+ SELECT entry_id FROM survivor_picks WHERE round = $2
317
+ )
318
+ RETURNING id`,
319
+ [poolId, currentRound]
320
+ );
321
+
322
+ const autoEliminatedCount = missingPicksResult.rowCount;
323
+ if (autoEliminatedCount > 0) {
324
+ console.log(` Auto-eliminated ${autoEliminatedCount} entries (no pick submitted)`);
325
+ }
326
+
327
+ // Check remaining survivors
328
+ const aliveResult = await client.query(
329
+ 'SELECT COUNT(*) as count FROM survivor_entries WHERE pool_id = $1 AND is_alive = true',
330
+ [poolId]
331
+ );
332
+ const aliveCount = parseInt(aliveResult.rows[0].count);
333
+
334
+ console.log(`[SurvivorOracle] Round ${currentRound} resolved:`);
335
+ console.log(` Survived: ${survivedCount}`);
336
+ console.log(` Eliminated: ${eliminatedCount}`);
337
+ console.log(` Auto-eliminated: ${autoEliminatedCount}`);
338
+ console.log(` Still alive: ${aliveCount}`);
339
+
340
+ // Determine next action
341
+ if (currentRound === 7 || aliveCount === 0) {
342
+ // Championship finished or everyone eliminated - complete the pool
343
+ await client.query(
344
+ `UPDATE survivor_pools SET status = 'complete', updated_at = NOW() WHERE id = $1`,
345
+ [poolId]
346
+ );
347
+ console.log(`[SurvivorOracle] Pool ${poolId} COMPLETE`);
348
+
349
+ // TODO: Trigger payout distribution
350
+ // This will call the Solana contract to distribute winnings
351
+
352
+ } else if (aliveCount === 1) {
353
+ // Only one survivor - they win!
354
+ await client.query(
355
+ `UPDATE survivor_pools SET status = 'complete', updated_at = NOW() WHERE id = $1`,
356
+ [poolId]
357
+ );
358
+ console.log(`[SurvivorOracle] Pool ${poolId} COMPLETE - Single winner!`);
359
+
360
+ // TODO: Trigger payout to single winner
361
+
362
+ } else {
363
+ // Advance to next round
364
+ const nextRound = currentRound + 1;
365
+ await client.query(
366
+ `UPDATE survivor_pools
367
+ SET current_round = $2, round_deadline = NULL, updated_at = NOW()
368
+ WHERE id = $1`,
369
+ [poolId, nextRound]
370
+ );
371
+ console.log(`[SurvivorOracle] Pool ${poolId} advanced to round ${nextRound} (${ROUND_NAMES[nextRound]})`);
372
+ }
373
+
374
+ await client.query('COMMIT');
375
+
376
+ return {
377
+ poolId,
378
+ round: currentRound,
379
+ survived: survivedCount,
380
+ eliminated: eliminatedCount,
381
+ autoEliminated: autoEliminatedCount,
382
+ stillAlive: aliveCount,
383
+ isComplete: currentRound === 7 || aliveCount <= 1
384
+ };
385
+ } catch (error) {
386
+ await client.query('ROLLBACK');
387
+ console.error('[SurvivorOracle] Error resolving round:', error.message);
388
+ throw error;
389
+ } finally {
390
+ client.release();
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Get survivors for payout (called when pool is complete)
396
+ */
397
+ async getSurvivorsForPayout(poolId) {
398
+ const result = await pool.query(
399
+ `SELECT se.*, u.wallet_address, u.username
400
+ FROM survivor_entries se
401
+ JOIN users u ON u.id = se.user_id
402
+ WHERE se.pool_id = $1 AND se.is_alive = true`,
403
+ [poolId]
404
+ );
405
+
406
+ return result.rows;
407
+ }
408
+
409
+ /**
410
+ * Calculate payouts for survivors
411
+ */
412
+ async calculatePayouts(poolId) {
413
+ // Get pool info
414
+ const poolResult = await pool.query(
415
+ 'SELECT * FROM survivor_pools WHERE id = $1',
416
+ [poolId]
417
+ );
418
+
419
+ if (poolResult.rows.length === 0) {
420
+ throw new Error('Pool not found');
421
+ }
422
+
423
+ const survivorPool = poolResult.rows[0];
424
+
425
+ // Get all entries for total pot
426
+ const entriesResult = await pool.query(
427
+ 'SELECT COUNT(*) as count FROM survivor_entries WHERE pool_id = $1',
428
+ [poolId]
429
+ );
430
+ const totalEntries = parseInt(entriesResult.rows[0].count);
431
+
432
+ // Get survivors
433
+ const survivors = await this.getSurvivorsForPayout(poolId);
434
+ const survivorCount = survivors.length;
435
+
436
+ // Calculate payouts
437
+ const buyInLamports = BigInt(survivorPool.buy_in_lamports);
438
+ const totalPotLamports = buyInLamports * BigInt(totalEntries);
439
+ const feePercentage = 6; // 6% total
440
+ const feeLamports = totalPotLamports * BigInt(feePercentage) / BigInt(100);
441
+ const netPotLamports = totalPotLamports - feeLamports;
442
+ const perSurvivorLamports = survivorCount > 0
443
+ ? netPotLamports / BigInt(survivorCount)
444
+ : BigInt(0);
445
+
446
+ return {
447
+ poolId,
448
+ totalEntries,
449
+ survivorCount,
450
+ totalPotLamports: totalPotLamports.toString(),
451
+ feeLamports: feeLamports.toString(),
452
+ netPotLamports: netPotLamports.toString(),
453
+ perSurvivorLamports: perSurvivorLamports.toString(),
454
+ survivors: survivors.map(s => ({
455
+ entryId: s.id,
456
+ userId: s.user_id,
457
+ walletAddress: s.wallet_address,
458
+ username: s.username,
459
+ payoutLamports: perSurvivorLamports.toString()
460
+ }))
461
+ };
462
+ }
463
+ }
464
+
465
+ // Export singleton instance
466
+ const survivorOracle = new SurvivorOracle();
467
+
468
+ module.exports = survivorOracle;
469
+ module.exports.SurvivorOracle = SurvivorOracle;