gamma-tokens.ts 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. /**
  2. * Fetch token IDs from Gamma events API (temperature markets).
  3. * Used as fallback when MarketSnapshot table is empty.
  4. */
  5. import { CITY_ALIASES } from '@poly-weather/core';
  6. const GAMMA_BASE = 'https://gamma-api.polymarket.com';
  7. interface GammaEventMarket {
  8. id: string;
  9. groupItemTitle: string;
  10. clobTokenIds: string;
  11. closed: boolean;
  12. }
  13. interface GammaEvent {
  14. id: string;
  15. title: string;
  16. markets: GammaEventMarket[];
  17. }
  18. export interface TokenPair {
  19. yesTokenId: string;
  20. noTokenId: string;
  21. }
  22. // In-memory cache, refreshed every minute
  23. let cache: Map<string, TokenPair> | null = null;
  24. let cacheTime = 0;
  25. const CACHE_TTL = 60_000;
  26. /**
  27. * Fetch ALL temperature event markets from Gamma.
  28. * Returns a map of "city_slug|bucketLabel" -> { yesTokenId, noTokenId }
  29. */
  30. export async function fetchAllGammaTokens(): Promise<Map<string, TokenPair>> {
  31. if (cache && Date.now() - cacheTime < CACHE_TTL) {
  32. return cache;
  33. }
  34. const tokenMap = new Map<string, TokenPair>();
  35. let offset = 0;
  36. try {
  37. while (offset <= 500) {
  38. const params = new URLSearchParams({
  39. tag_slug: 'temperature',
  40. active: 'true',
  41. closed: 'false',
  42. limit: '50',
  43. offset: offset.toString(),
  44. });
  45. const res = await fetch(`${GAMMA_BASE}/events?${params}`, {
  46. headers: { Accept: 'application/json' },
  47. signal: AbortSignal.timeout(15000),
  48. });
  49. if (!res.ok) break;
  50. const events = (await res.json()) as GammaEvent[];
  51. if (events.length === 0) break;
  52. for (const event of events) {
  53. // Parse city from event title: "Highest temperature in Atlanta on March 18?"
  54. const titleMatch = event.title.match(/temperature in (.+?) on /i);
  55. if (!titleMatch) continue;
  56. const rawCity = titleMatch[1].trim();
  57. // Use CITY_ALIASES to map Gamma titles (e.g. "NYC") to bot slugs (e.g. "new_york")
  58. const citySlug = CITY_ALIASES[rawCity] ?? rawCity.toLowerCase().replace(/\s+/g, '_');
  59. for (const m of event.markets || []) {
  60. if (m.closed) continue;
  61. const bucketLabel = m.groupItemTitle;
  62. if (!bucketLabel) continue;
  63. try {
  64. const tokenIds = JSON.parse(m.clobTokenIds || '[]');
  65. if (tokenIds.length >= 1) {
  66. tokenMap.set(`${citySlug}|${bucketLabel}`, {
  67. yesTokenId: tokenIds[0] || '',
  68. noTokenId: tokenIds[1] || '',
  69. });
  70. }
  71. } catch {
  72. // skip
  73. }
  74. }
  75. }
  76. offset += 50;
  77. }
  78. } catch (err) {
  79. console.error('Gamma events fetch failed:', err);
  80. }
  81. console.log(`[gamma-tokens] Cache loaded: ${tokenMap.size} buckets`);
  82. cache = tokenMap;
  83. cacheTime = Date.now();
  84. return tokenMap;
  85. }
  86. /**
  87. * Look up token pair for a specific city + bucket.
  88. * Tries MarketSnapshot DB first, then Gamma API fallback.
  89. */
  90. export async function lookupTokenPair(
  91. city: string,
  92. bucket: string,
  93. prismaClient: { marketSnapshot: { findFirst: (...args: any[]) => Promise<any> } },
  94. ): Promise<TokenPair | null> {
  95. // Try DB first
  96. const snapshot = await prismaClient.marketSnapshot.findFirst({
  97. where: { city, bucketLabel: bucket },
  98. orderBy: { createdAt: 'desc' },
  99. select: { yesTokenId: true, noTokenId: true },
  100. });
  101. if (snapshot?.yesTokenId) {
  102. return {
  103. yesTokenId: snapshot.yesTokenId,
  104. noTokenId: snapshot.noTokenId || '',
  105. };
  106. }
  107. // Fallback to Gamma
  108. const gammaTokens = await fetchAllGammaTokens();
  109. return gammaTokens.get(`${city}|${bucket}`) || null;
  110. }