diff --git a/apps/kalshi-feed/.gitignore b/apps/kalshi-feed/.gitignore new file mode 100644 index 00000000..93a92483 --- /dev/null +++ b/apps/kalshi-feed/.gitignore @@ -0,0 +1,4 @@ +dist/ +.output/ +.wxt/ +node_modules/ diff --git a/apps/kalshi-feed/SCHEMA.md b/apps/kalshi-feed/SCHEMA.md new file mode 100644 index 00000000..2a4a9163 --- /dev/null +++ b/apps/kalshi-feed/SCHEMA.md @@ -0,0 +1,95 @@ +# Kalshi Feed — Backend API Schema + +The frontend expects data from a Supabase backend. Below is the exact schema the backend should populate. + +## Supabase Table: `markets` + +| Column | Type | Description | +|--------|------|-------------| +| `id` | `uuid` | Primary key | +| `ticker` | `text` | Kalshi market ticker (e.g. `KXFEDRATE-26MAR-B4.5`) | +| `title` | `text` | Market question (e.g. "Will the Fed cut rates in March 2026?") | +| `subtitle` | `text` | Short context line | +| `category` | `text` | One of: `politics`, `sports`, `crypto`, `economics`, `entertainment`, `science`, `weather`, `tech` | +| `yes_price` | `integer` | Current YES price in cents (0-100) | +| `no_price` | `integer` | Current NO price in cents (0-100, should equal 100 - yes_price) | +| `volume` | `bigint` | Total volume in cents | +| `volume_24h` | `bigint` | 24-hour volume in cents | +| `open_interest` | `bigint` | Open interest in cents | +| `close_time` | `timestamptz` | When the market closes | +| `image_url` | `text` | Optional market image URL | +| `kalshi_url` | `text` | Direct link to market on kalshi.com | +| `status` | `text` | One of: `open`, `closed`, `settled` | +| `last_price` | `integer` | Last trade price in cents | +| `price_change_24h` | `integer` | Price change in last 24h (percentage points) | +| `traders_count` | `integer` | Number of unique traders | +| `created_at` | `timestamptz` | When the market was created on Kalshi | + +## API Endpoint: `GET /feed` + +### Query Parameters +| Param | Type | Default | Description | +|-------|------|---------|-------------| +| `category` | `string` | `trending` | Filter by category or `trending`/`all` | +| `cursor` | `string` | `null` | Pagination cursor | +| `limit` | `integer` | `20` | Number of markets per page | + +### Response Shape +```json +{ + "markets": [ + { + "id": "uuid", + "ticker": "KXFEDRATE-26MAR-B4.5", + "title": "Will the Fed cut rates in March 2026?", + "subtitle": "Federal Reserve interest rate decision", + "category": "economics", + "yes_price": 67, + "no_price": 33, + "volume": 2847293, + "volume_24h": 482100, + "open_interest": 1200000, + "close_time": "2026-03-19T18:00:00Z", + "image_url": null, + "kalshi_url": "https://kalshi.com/markets/kxfedrate", + "status": "open", + "last_price": 67, + "price_change_24h": 3, + "traders_count": 18432, + "created_at": "2026-01-15T00:00:00Z" + } + ], + "cursor": "next_page_token_or_null", + "has_more": true +} +``` + +## Feed Ranking Algorithm (for `trending`) + +``` +score = ( + volume_24h * 0.3 + + controversy_score * 0.25 + + recency_boost * 0.2 + + closing_soon_boost * 0.15 + + category_diversity * 0.1 +) + +controversy_score = 1 - abs(yes_price - 50) / 50 +``` + +Markets closest to 50/50 with high volume rank highest. + +## Kalshi Public API (data source for scraper) + +Base URL: `https://api.elections.kalshi.com/trade-api/v2` + +No auth required for market data: +- `GET /markets?status=open&limit=200` — all open markets +- `GET /events/{event_ticker}` — event details +- `GET /markets/{ticker}/orderbook` — live orderbook +- `GET /markets/{ticker}/trades` — trade history + +## TypeScript Types + +See `lib/types/market.ts` for the exact TypeScript interface the frontend uses. diff --git a/apps/kalshi-feed/biome.json b/apps/kalshi-feed/biome.json new file mode 100644 index 00000000..496933c9 --- /dev/null +++ b/apps/kalshi-feed/biome.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.14/schema.json", + "root": false, + "extends": "//", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedImports": "error", + "noUnusedVariables": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "asNeeded" + } + }, + "css": { + "parser": { + "tailwindDirectives": true + } + } +} diff --git a/apps/kalshi-feed/entrypoints/background/index.ts b/apps/kalshi-feed/entrypoints/background/index.ts new file mode 100644 index 00000000..2951e52e --- /dev/null +++ b/apps/kalshi-feed/entrypoints/background/index.ts @@ -0,0 +1,3 @@ +export default defineBackground(() => { + chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }) +}) diff --git a/apps/kalshi-feed/entrypoints/sidepanel/App.tsx b/apps/kalshi-feed/entrypoints/sidepanel/App.tsx new file mode 100644 index 00000000..fb9d3387 --- /dev/null +++ b/apps/kalshi-feed/entrypoints/sidepanel/App.tsx @@ -0,0 +1,6 @@ +import type { FC } from 'react' +import { Feed } from './components/Feed' + +export const App: FC = () => { + return +} diff --git a/apps/kalshi-feed/entrypoints/sidepanel/components/BottomNav.tsx b/apps/kalshi-feed/entrypoints/sidepanel/components/BottomNav.tsx new file mode 100644 index 00000000..3668f28c --- /dev/null +++ b/apps/kalshi-feed/entrypoints/sidepanel/components/BottomNav.tsx @@ -0,0 +1,43 @@ +import { Home, Play, PlusSquare, Trophy, User } from 'lucide-react' +import type { FC } from 'react' + +const NAV_ITEMS = [ + { icon: Home, label: 'Home' }, + { icon: Play, label: 'Feed', active: true }, + { icon: PlusSquare, label: '', isCenter: true }, + { icon: Trophy, label: 'Ranking' }, + { icon: User, label: 'Profile' }, +] as const + +export const BottomNav: FC = () => { + return ( +
+ {NAV_ITEMS.map((item) => + 'isCenter' in item && item.isCenter ? ( + + ) : ( + + ), + )} +
+ ) +} diff --git a/apps/kalshi-feed/entrypoints/sidepanel/components/Feed.tsx b/apps/kalshi-feed/entrypoints/sidepanel/components/Feed.tsx new file mode 100644 index 00000000..8f2888ea --- /dev/null +++ b/apps/kalshi-feed/entrypoints/sidepanel/components/Feed.tsx @@ -0,0 +1,45 @@ +import { type FC, useRef, useState } from 'react' +import { FAKE_MARKETS } from '@/lib/data/fake-markets' +import { BottomNav } from './BottomNav' +import { MarketCard } from './MarketCard' +import { TopBar } from './TopBar' + +export const Feed: FC = () => { + const [currentIndex, setCurrentIndex] = useState(0) + const feedRef = useRef(null) + + const currentMarket = FAKE_MARKETS[currentIndex] ?? FAKE_MARKETS[0] + + const handleScroll = () => { + if (!feedRef.current) return + const scrollTop = feedRef.current.scrollTop + const cardHeight = feedRef.current.clientHeight + const index = Math.round(scrollTop / cardHeight) + if (index !== currentIndex && index >= 0 && index < FAKE_MARKETS.length) { + setCurrentIndex(index) + } + } + + return ( +
+ {/* Top bar overlay */} +
+ +
+ + {/* Scrollable feed */} +
+ {FAKE_MARKETS.map((market) => ( + + ))} +
+ + {/* Bottom nav */} + +
+ ) +} diff --git a/apps/kalshi-feed/entrypoints/sidepanel/components/MarketCard.tsx b/apps/kalshi-feed/entrypoints/sidepanel/components/MarketCard.tsx new file mode 100644 index 00000000..d0820745 --- /dev/null +++ b/apps/kalshi-feed/entrypoints/sidepanel/components/MarketCard.tsx @@ -0,0 +1,134 @@ +import { + Bookmark, + CheckCircle, + Clock, + Flame, + Heart, + MessageCircle, + Share2, + TrendingUp, + Users, + XCircle, +} from 'lucide-react' +import type { FC } from 'react' +import type { Market } from '@/lib/types/market' +import { formatCompact, formatDaysRemaining } from '@/lib/utils/format' + +interface MarketCardProps { + market: Market +} + +export const MarketCard: FC = ({ market }) => { + const handleBet = () => { + window.open(market.kalshi_url, '_blank') + } + + return ( +
+ {market.image_url && ( + + )} +
+ + {/* Right side action bar */} +
+ {market.is_hot && ( +
+
+ +
+ + HOT + +
+ )} +
+ + + {formatCompact(market.likes_count)} + +
+
+ + + {formatCompact(market.comments_count)} + +
+
+ + + {formatCompact(market.shares_count)} + +
+
+ + Save +
+
+ + {/* Bottom content overlay */} +
+ {/* Trending badge */} + {market.is_trending && ( +
+ + + TRENDING + +
+ )} + + {/* Title */} +

+ {market.title} +

+ + {/* Stats row */} +
+
+ + + {market.yes_price}% YES + +
+
+ + + {formatDaysRemaining(market.close_time)} + +
+
+ + + {formatCompact(market.traders_count)} + +
+
+ + {/* YES / NO buttons */} +
+ + +
+
+
+ ) +} diff --git a/apps/kalshi-feed/entrypoints/sidepanel/components/TopBar.tsx b/apps/kalshi-feed/entrypoints/sidepanel/components/TopBar.tsx new file mode 100644 index 00000000..4e33ea0b --- /dev/null +++ b/apps/kalshi-feed/entrypoints/sidepanel/components/TopBar.tsx @@ -0,0 +1,27 @@ +import { Bell, Eye, Search } from 'lucide-react' +import type { FC } from 'react' +import type { MarketCategory } from '@/lib/types/market' +import { getCategoryLabel } from '@/lib/utils/gradients' + +interface TopBarProps { + category: MarketCategory +} + +export const TopBar: FC = ({ category }) => { + return ( +
+ +
+ + + {getCategoryLabel(category)} + +
+ +
+ ) +} diff --git a/apps/kalshi-feed/entrypoints/sidepanel/index.html b/apps/kalshi-feed/entrypoints/sidepanel/index.html new file mode 100644 index 00000000..75703aea --- /dev/null +++ b/apps/kalshi-feed/entrypoints/sidepanel/index.html @@ -0,0 +1,12 @@ + + + + + + Kalshi Feed + + +
+ + + diff --git a/apps/kalshi-feed/entrypoints/sidepanel/main.tsx b/apps/kalshi-feed/entrypoints/sidepanel/main.tsx new file mode 100644 index 00000000..b0cde1b4 --- /dev/null +++ b/apps/kalshi-feed/entrypoints/sidepanel/main.tsx @@ -0,0 +1,14 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import '@/styles/global.css' +import { App } from './App' + +const root = document.getElementById('root') + +if (root) { + ReactDOM.createRoot(root).render( + + + , + ) +} diff --git a/apps/kalshi-feed/lib/data/fake-markets.ts b/apps/kalshi-feed/lib/data/fake-markets.ts new file mode 100644 index 00000000..4bff787f --- /dev/null +++ b/apps/kalshi-feed/lib/data/fake-markets.ts @@ -0,0 +1,316 @@ +import type { Market } from '@/lib/types/market' + +export const FAKE_MARKETS: Market[] = [ + { + id: '1', + ticker: 'KXFEDRATE-26MAR-B4.5', + title: 'Will the Fed cut rates in March 2026?', + subtitle: 'Federal Reserve interest rate decision', + category: 'economics', + yes_price: 67, + no_price: 33, + volume: 2_847_293, + volume_24h: 482_100, + open_interest: 1_200_000, + close_time: '2026-03-19T18:00:00Z', + image_url: + 'https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxfedrate', + status: 'open', + last_price: 67, + price_change_24h: 3, + traders_count: 18_432, + likes_count: 287_000, + comments_count: 142_000, + shares_count: 98_500, + is_hot: true, + is_trending: true, + created_at: '2026-01-15T00:00:00Z', + }, + { + id: '2', + ticker: 'KXBTC-26FEB-B100K', + title: 'Will Bitcoin hit $100K by end of February?', + subtitle: 'BTC price prediction', + category: 'crypto', + yes_price: 42, + no_price: 58, + volume: 5_120_000, + volume_24h: 890_000, + open_interest: 3_200_000, + close_time: '2026-02-28T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1518546305927-5a555bb7020d?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxbtc', + status: 'open', + last_price: 42, + price_change_24h: -5, + traders_count: 32_100, + likes_count: 412_000, + comments_count: 89_000, + shares_count: 156_000, + is_hot: true, + is_trending: true, + created_at: '2026-01-01T00:00:00Z', + }, + { + id: '3', + ticker: 'KXSB-26FEB-CHIEFS', + title: 'Will the Chiefs win Super Bowl LX?', + subtitle: 'NFL Championship 2026', + category: 'sports', + yes_price: 55, + no_price: 45, + volume: 8_450_000, + volume_24h: 1_200_000, + open_interest: 4_500_000, + close_time: '2026-02-08T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1508098682722-e99c643e7f1b?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxsb', + status: 'open', + last_price: 55, + price_change_24h: 2, + traders_count: 45_000, + likes_count: 534_000, + comments_count: 201_000, + shares_count: 178_000, + is_hot: false, + is_trending: true, + created_at: '2025-09-01T00:00:00Z', + }, + { + id: '4', + ticker: 'KXELECTION-28NOV', + title: 'Will the Democratic candidate win in 2028?', + subtitle: 'US Presidential Election 2028', + category: 'politics', + yes_price: 48, + no_price: 52, + volume: 12_300_000, + volume_24h: 2_100_000, + open_interest: 8_000_000, + close_time: '2028-11-03T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1540910419892-4a36d2c3266c?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxelection', + status: 'open', + last_price: 48, + price_change_24h: -1, + traders_count: 89_000, + likes_count: 892_000, + comments_count: 445_000, + shares_count: 312_000, + is_hot: true, + is_trending: true, + created_at: '2025-11-15T00:00:00Z', + }, + { + id: '5', + ticker: 'KXOSCARS-26MAR', + title: 'Will "The Brutalist" win Best Picture at the Oscars?', + subtitle: '98th Academy Awards', + category: 'entertainment', + yes_price: 38, + no_price: 62, + volume: 1_890_000, + volume_24h: 345_000, + open_interest: 900_000, + close_time: '2026-03-02T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1478720568477-152d9b164e26?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxoscars', + status: 'open', + last_price: 38, + price_change_24h: 5, + traders_count: 12_400, + likes_count: 156_000, + comments_count: 67_000, + shares_count: 43_000, + is_hot: false, + is_trending: false, + created_at: '2026-01-20T00:00:00Z', + }, + { + id: '6', + ticker: 'KXHIGHNY-26FEB22-B50', + title: 'NYC high temp above 50\u00B0F today?', + subtitle: 'New York City weather', + category: 'weather', + yes_price: 72, + no_price: 28, + volume: 420_000, + volume_24h: 89_000, + open_interest: 150_000, + close_time: '2026-02-22T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1534430480872-3498386e7856?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxhighny', + status: 'open', + last_price: 72, + price_change_24h: 8, + traders_count: 3_200, + likes_count: 23_000, + comments_count: 8_400, + shares_count: 5_200, + is_hot: false, + is_trending: false, + created_at: '2026-02-22T00:00:00Z', + }, + { + id: '7', + ticker: 'KXETH-26MAR-B5K', + title: 'Will Ethereum hit $5,000 by end of March?', + subtitle: 'ETH price prediction', + category: 'crypto', + yes_price: 23, + no_price: 77, + volume: 3_200_000, + volume_24h: 560_000, + open_interest: 1_800_000, + close_time: '2026-03-31T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1639762681485-074b7f938ba0?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxeth', + status: 'open', + last_price: 23, + price_change_24h: -2, + traders_count: 15_600, + likes_count: 198_000, + comments_count: 54_000, + shares_count: 72_000, + is_hot: false, + is_trending: false, + created_at: '2026-01-10T00:00:00Z', + }, + { + id: '8', + ticker: 'KXAI-26Q1-AGI', + title: 'Will OpenAI release GPT-5 in Q1 2026?', + subtitle: 'AI model release prediction', + category: 'tech', + yes_price: 31, + no_price: 69, + volume: 4_100_000, + volume_24h: 720_000, + open_interest: 2_200_000, + close_time: '2026-03-31T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1677442136019-21780ecad995?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxai', + status: 'open', + last_price: 31, + price_change_24h: -8, + traders_count: 28_000, + likes_count: 345_000, + comments_count: 167_000, + shares_count: 123_000, + is_hot: true, + is_trending: true, + created_at: '2025-12-01T00:00:00Z', + }, + { + id: '9', + ticker: 'KXGDP-26Q1-B3', + title: 'Will US GDP growth exceed 3% in Q1 2026?', + subtitle: 'Economic growth forecast', + category: 'economics', + yes_price: 35, + no_price: 65, + volume: 1_950_000, + volume_24h: 280_000, + open_interest: 1_100_000, + close_time: '2026-04-30T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxgdp', + status: 'open', + last_price: 35, + price_change_24h: 1, + traders_count: 8_900, + likes_count: 67_000, + comments_count: 23_000, + shares_count: 15_000, + is_hot: false, + is_trending: false, + created_at: '2026-01-05T00:00:00Z', + }, + { + id: '10', + ticker: 'KXMMA-26MAR-JONES', + title: 'Will Jon Jones retain UFC Heavyweight title?', + subtitle: 'UFC Championship bout', + category: 'sports', + yes_price: 62, + no_price: 38, + volume: 6_700_000, + volume_24h: 1_450_000, + open_interest: 3_800_000, + close_time: '2026-03-15T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1549719386-74dfcbf7dbed?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxmma', + status: 'open', + last_price: 62, + price_change_24h: 4, + traders_count: 52_000, + likes_count: 423_000, + comments_count: 189_000, + shares_count: 134_000, + is_hot: true, + is_trending: true, + created_at: '2026-01-20T00:00:00Z', + }, + { + id: '11', + ticker: 'KXSPACEX-26H1', + title: 'Will SpaceX complete a Starship orbital flight in H1 2026?', + subtitle: 'Space exploration milestone', + category: 'science', + yes_price: 78, + no_price: 22, + volume: 2_340_000, + volume_24h: 410_000, + open_interest: 1_400_000, + close_time: '2026-06-30T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1541185933-ef5d8ed016c2?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxspacex', + status: 'open', + last_price: 78, + price_change_24h: 2, + traders_count: 19_500, + likes_count: 267_000, + comments_count: 98_000, + shares_count: 87_000, + is_hot: false, + is_trending: true, + created_at: '2025-12-15T00:00:00Z', + }, + { + id: '12', + ticker: 'KXTIKTOK-26MAR', + title: 'Will TikTok be banned in the US by March 2026?', + subtitle: 'Social media regulation', + category: 'politics', + yes_price: 15, + no_price: 85, + volume: 9_800_000, + volume_24h: 1_800_000, + open_interest: 5_500_000, + close_time: '2026-03-31T23:59:00Z', + image_url: + 'https://images.unsplash.com/photo-1611162617474-5b21e879e113?w=800&h=1200&fit=crop', + kalshi_url: 'https://kalshi.com/markets/kxtiktok', + status: 'open', + last_price: 15, + price_change_24h: -3, + traders_count: 67_000, + likes_count: 756_000, + comments_count: 334_000, + shares_count: 245_000, + is_hot: true, + is_trending: true, + created_at: '2025-10-01T00:00:00Z', + }, +] diff --git a/apps/kalshi-feed/lib/types/market.ts b/apps/kalshi-feed/lib/types/market.ts new file mode 100644 index 00000000..b15ee5ac --- /dev/null +++ b/apps/kalshi-feed/lib/types/market.ts @@ -0,0 +1,50 @@ +export type MarketCategory = + | 'politics' + | 'sports' + | 'crypto' + | 'economics' + | 'entertainment' + | 'science' + | 'weather' + | 'tech' + +export interface Market { + id: string + ticker: string + title: string + subtitle: string + category: MarketCategory + yes_price: number + no_price: number + volume: number + volume_24h: number + open_interest: number + close_time: string + image_url: string | null + kalshi_url: string + status: 'open' | 'closed' | 'settled' + last_price: number + price_change_24h: number + traders_count: number + likes_count: number + comments_count: number + shares_count: number + is_hot: boolean + is_trending: boolean + created_at: string +} + +export interface FeedResponse { + markets: Market[] + cursor: string | null + has_more: boolean +} + +export type FeedCategory = + | 'trending' + | 'politics' + | 'sports' + | 'crypto' + | 'economics' + | 'entertainment' + | 'all' diff --git a/apps/kalshi-feed/lib/utils/cn.ts b/apps/kalshi-feed/lib/utils/cn.ts new file mode 100644 index 00000000..d32b0fe6 --- /dev/null +++ b/apps/kalshi-feed/lib/utils/cn.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from 'clsx' +import { twMerge } from 'tailwind-merge' + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/apps/kalshi-feed/lib/utils/format.ts b/apps/kalshi-feed/lib/utils/format.ts new file mode 100644 index 00000000..00915718 --- /dev/null +++ b/apps/kalshi-feed/lib/utils/format.ts @@ -0,0 +1,25 @@ +export function formatCompact(n: number): string { + if (n >= 1_000_000) { + return `${(n / 1_000_000).toFixed(1)}M` + } + if (n >= 1_000) { + return `${(n / 1_000).toFixed(1)}K` + } + return `${n}` +} + +export function formatDaysRemaining(closeTime: string): string { + const now = new Date() + const close = new Date(closeTime) + const diffMs = close.getTime() - now.getTime() + + if (diffMs < 0) return '0d' + + const diffHours = Math.floor(diffMs / (1000 * 60 * 60)) + const diffDays = Math.floor(diffHours / 24) + + if (diffDays > 0) return `${diffDays}d` + if (diffHours > 0) return `${diffHours}h` + const diffMinutes = Math.floor(diffMs / (1000 * 60)) + return `${diffMinutes}m` +} diff --git a/apps/kalshi-feed/lib/utils/gradients.ts b/apps/kalshi-feed/lib/utils/gradients.ts new file mode 100644 index 00000000..7c16dab5 --- /dev/null +++ b/apps/kalshi-feed/lib/utils/gradients.ts @@ -0,0 +1,15 @@ +import type { MarketCategory } from '@/lib/types/market' + +export function getCategoryLabel(category: MarketCategory): string { + const labels: Record = { + politics: 'Politics', + sports: 'Sports', + crypto: 'Crypto', + economics: 'Economics', + entertainment: 'Entertainment', + science: 'Science', + weather: 'Weather', + tech: 'Tech', + } + return labels[category] ?? category +} diff --git a/apps/kalshi-feed/package.json b/apps/kalshi-feed/package.json new file mode 100644 index 00000000..9636154f --- /dev/null +++ b/apps/kalshi-feed/package.json @@ -0,0 +1,34 @@ +{ + "name": "@browseros/kalshi-feed", + "description": "TikTok-style prediction market feed for Kalshi", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "wxt", + "build": "wxt build", + "build:dev": "wxt build --mode development", + "zip": "wxt zip", + "typecheck": "tsc --noEmit", + "lint": "bunx biome check", + "lint:fix": "bunx biome check --write --unsafe" + }, + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.562.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tailwind-merge": "^3.4.0", + "tw-animate-css": "^1.4.0" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.17", + "@types/chrome": "^0.1.28", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@wxt-dev/module-react": "^1.1.3", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.2", + "wxt": "^0.20.6" + } +} diff --git a/apps/kalshi-feed/styles/global.css b/apps/kalshi-feed/styles/global.css new file mode 100644 index 00000000..94757193 --- /dev/null +++ b/apps/kalshi-feed/styles/global.css @@ -0,0 +1,107 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: #000000; + --foreground: #ffffff; + --card: #111111; + --card-foreground: #ffffff; + --muted: #1a1a1a; + --muted-foreground: #888888; + --border: #222222; + --yes-green: #00c853; + --yes-green-dark: #00a844; + --no-red: #ff1744; + --no-red-dark: #d50000; + --accent-blue: #2979ff; + --accent-purple: #7c4dff; + --accent-orange: #ff6d00; + --accent-cyan: #00e5ff; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-border: var(--border); + --color-yes-green: var(--yes-green); + --color-yes-green-dark: var(--yes-green-dark); + --color-no-red: var(--no-red); + --color-no-red-dark: var(--no-red-dark); + --color-accent-blue: var(--accent-blue); + --color-accent-purple: var(--accent-purple); + --color-accent-orange: var(--accent-orange); + --color-accent-cyan: var(--accent-cyan); + + --animate-slide-up: slide-up 0.3s ease-out; + --animate-fade-in: fade-in 0.2s ease-out; + --animate-pulse-green: pulse-green 0.6s ease-out; + --animate-pulse-red: pulse-red 0.6s ease-out; +} + +@layer base { + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + + body { + background: var(--background); + color: var(--foreground); + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + overflow: hidden; + width: 100%; + height: 100vh; + } + + html { + overflow: hidden; + } + + @keyframes slide-up { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } + } + + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + + @keyframes pulse-green { + 0% { + box-shadow: 0 0 0 0 rgba(0, 200, 83, 0.6); + } + 100% { + box-shadow: 0 0 0 20px rgba(0, 200, 83, 0); + } + } + + @keyframes pulse-red { + 0% { + box-shadow: 0 0 0 0 rgba(255, 23, 68, 0.6); + } + 100% { + box-shadow: 0 0 0 20px rgba(255, 23, 68, 0); + } + } +} diff --git a/apps/kalshi-feed/tsconfig.json b/apps/kalshi-feed/tsconfig.json new file mode 100644 index 00000000..f2cb3db6 --- /dev/null +++ b/apps/kalshi-feed/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./.wxt/tsconfig.json", + "compilerOptions": { + "types": ["chrome"], + "allowImportingTsExtensions": true, + "jsx": "react-jsx", + "baseUrl": ".", + "paths": { + "@/*": ["./*"] + } + } +} diff --git a/apps/kalshi-feed/wxt.config.ts b/apps/kalshi-feed/wxt.config.ts new file mode 100644 index 00000000..abc7586d --- /dev/null +++ b/apps/kalshi-feed/wxt.config.ts @@ -0,0 +1,21 @@ +import tailwindcss from '@tailwindcss/vite' +import { defineConfig } from 'wxt' + +export default defineConfig({ + outDir: 'dist', + modules: ['@wxt-dev/module-react'], + manifest: { + name: 'Kalshi Feed', + description: 'TikTok-style prediction market feed powered by Kalshi', + action: { + default_title: 'Kalshi Feed', + }, + permissions: ['sidePanel', 'storage'], + side_panel: { + default_path: 'sidepanel.html', + }, + }, + vite: () => ({ + plugins: [tailwindcss()], + }), +}) diff --git a/apps/kalshi-scraper/drizzle.config.ts b/apps/kalshi-scraper/drizzle.config.ts new file mode 100644 index 00000000..c884c61b --- /dev/null +++ b/apps/kalshi-scraper/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit' + +export default defineConfig({ + schema: './src/db/schema.ts', + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { + url: process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL ?? '', + }, +}) diff --git a/apps/kalshi-scraper/package.json b/apps/kalshi-scraper/package.json new file mode 100644 index 00000000..889972d5 --- /dev/null +++ b/apps/kalshi-scraper/package.json @@ -0,0 +1,28 @@ +{ + "name": "@browseros/kalshi-scraper", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "bun run --watch src/index.ts", + "start": "bun run src/index.ts", + "typecheck": "tsc --noEmit", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push", + "db:studio": "drizzle-kit studio" + }, + "dependencies": { + "@browserbasehq/sdk": "^2.0.0", + "drizzle-orm": "^0.45.1", + "hono": "^4.6.0", + "playwright-core": "^1.52.0", + "postgres": "^3.4.7", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/bun": "latest", + "drizzle-kit": "^0.31.8", + "typescript": "^5.9.3" + } +} diff --git a/apps/kalshi-scraper/scripts/test-api.ts b/apps/kalshi-scraper/scripts/test-api.ts new file mode 100644 index 00000000..76039475 --- /dev/null +++ b/apps/kalshi-scraper/scripts/test-api.ts @@ -0,0 +1,77 @@ +import { mapCategory } from '../src/services/category-mapper' +import { + dollarsToCents, + fetchAllMarkets, + fetchEvents, +} from '../src/services/kalshi-api' +import { + computeFeedScore, + generateEngagementCounts, + isHotMarket, +} from '../src/services/ranking' + +async function main() { + console.log('Fetching markets from Kalshi API...') + const markets = await fetchAllMarkets() + console.log(`Fetched ${markets.length} open markets`) + + console.log('\nFetching events...') + const eventMap = await fetchEvents() + console.log(`Fetched ${eventMap.size} events`) + + // Show first 5 markets with transformed data + console.log('\n--- Top 5 markets by volume_24h ---\n') + const sorted = markets.sort((a, b) => b.volume_24h - a.volume_24h).slice(0, 5) + + for (const m of sorted) { + const event = eventMap.get(m.event_ticker) + const category = mapCategory(event?.category ?? '', [], m.title) + const yesPrice = dollarsToCents(m.yes_bid_dollars) + + const market = { + ticker: m.ticker, + title: m.title, + yesPrice, + noPrice: 100 - yesPrice, + volume24h: m.volume_24h, + closeTime: m.close_time ? new Date(m.close_time) : null, + createdAt: new Date(), + } + + const score = computeFeedScore({ + market: market as any, + isInTrendingList: false, + }) + const hot = isHotMarket(market as any) + const engagement = generateEngagementCounts(m.volume_24h, m.ticker) + + console.log(`${m.ticker}`) + console.log(` Title: ${m.title}`) + console.log(` Category: ${event?.category ?? 'unknown'} → ${category}`) + console.log(` YES: ${yesPrice}¢ Volume 24h: ${m.volume_24h}`) + console.log( + ` Price change: ${dollarsToCents(m.last_price_dollars) - dollarsToCents(m.previous_price_dollars)}`, + ) + console.log(` Feed score: ${score} Hot: ${hot}`) + console.log( + ` Engagement: likes=${engagement.likesCount} comments=${engagement.commentsCount}`, + ) + console.log() + } + + // Show category distribution + const categories = new Map() + for (const m of markets) { + const event = eventMap.get(m.event_ticker) + const cat = mapCategory(event?.category ?? '', [], m.title) + categories.set(cat, (categories.get(cat) ?? 0) + 1) + } + console.log('--- Category distribution ---') + for (const [cat, count] of [...categories.entries()].sort( + (a, b) => b[1] - a[1], + )) { + console.log(` ${cat}: ${count}`) + } +} + +main().catch(console.error) diff --git a/apps/kalshi-scraper/src/config.ts b/apps/kalshi-scraper/src/config.ts new file mode 100644 index 00000000..bc8db05f --- /dev/null +++ b/apps/kalshi-scraper/src/config.ts @@ -0,0 +1,26 @@ +import { z } from 'zod' + +const envSchema = z.object({ + DATABASE_URL: z.string(), + BROWSERBASE_API_KEY: z.string(), + BROWSERBASE_PROJECT_ID: z.string(), + SCRAPE_SECRET: z.string().default('dev-secret'), + PORT: z.coerce.number().default(3001), +}) + +function parseConfig() { + const result = envSchema.safeParse(process.env) + + if (!result.success) { + console.error('Invalid environment variables:') + for (const issue of result.error.issues) { + console.error(` ${issue.path.join('.')}: ${issue.message}`) + } + process.exit(1) + } + + return result.data +} + +export const config = parseConfig() +export type Config = z.infer diff --git a/apps/kalshi-scraper/src/db/index.ts b/apps/kalshi-scraper/src/db/index.ts new file mode 100644 index 00000000..0ab6ded3 --- /dev/null +++ b/apps/kalshi-scraper/src/db/index.ts @@ -0,0 +1,10 @@ +import { drizzle } from 'drizzle-orm/postgres-js' +import postgres from 'postgres' +import { config } from '../config' +import * as schema from './schema' + +const client = postgres(config.DATABASE_URL) + +export const db = drizzle(client, { schema }) + +export * from './schema' diff --git a/apps/kalshi-scraper/src/db/schema.ts b/apps/kalshi-scraper/src/db/schema.ts new file mode 100644 index 00000000..2c2f9a68 --- /dev/null +++ b/apps/kalshi-scraper/src/db/schema.ts @@ -0,0 +1,41 @@ +import { + bigint, + boolean, + integer, + pgTable, + text, + timestamp, + uuid, +} from 'drizzle-orm/pg-core' + +export const markets = pgTable('markets', { + id: uuid('id').defaultRandom().primaryKey(), + ticker: text('ticker').notNull().unique(), + title: text('title').notNull(), + subtitle: text('subtitle').default(''), + category: text('category').notNull(), + yesPrice: integer('yes_price').notNull(), + noPrice: integer('no_price').notNull(), + volume: bigint('volume', { mode: 'number' }).default(0), + volume24h: bigint('volume_24h', { mode: 'number' }).default(0), + openInterest: bigint('open_interest', { mode: 'number' }).default(0), + closeTime: timestamp('close_time', { withTimezone: true }), + imageUrl: text('image_url'), + kalshiUrl: text('kalshi_url').notNull(), + status: text('status').notNull().default('open'), + lastPrice: integer('last_price').default(0), + priceChange24h: integer('price_change_24h').default(0), + tradersCount: integer('traders_count').default(0), + isHot: boolean('is_hot').default(false), + isTrending: boolean('is_trending').default(false), + feedScore: integer('feed_score').default(0), + likesCount: integer('likes_count').default(0), + commentsCount: integer('comments_count').default(0), + sharesCount: integer('shares_count').default(0), + eventTicker: text('event_ticker'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow(), +}) + +export type Market = typeof markets.$inferSelect +export type NewMarket = typeof markets.$inferInsert diff --git a/apps/kalshi-scraper/src/index.ts b/apps/kalshi-scraper/src/index.ts new file mode 100644 index 00000000..7abb8765 --- /dev/null +++ b/apps/kalshi-scraper/src/index.ts @@ -0,0 +1,40 @@ +import { Hono } from 'hono' +import { cors } from 'hono/cors' +import { logger } from 'hono/logger' +import { config } from './config' +import { feedRoute } from './routes/feed' +import { scrapeRoute } from './routes/scrape' + +const app = new Hono() + .use(logger()) + .use('*', cors()) + .get('/', (c) => + c.json({ service: '@browseros/kalshi-scraper', version: '0.1.0' }), + ) + .get('/health', (c) => c.json({ status: 'ok' })) + .route('/feed', feedRoute) + .route('/scrape', scrapeRoute) + .notFound((c) => c.json({ error: 'Not found' }, 404)) + .onError((err, c) => { + console.error('Unhandled error:', err) + return c.json({ error: 'Internal server error' }, 500) + }) + +console.log(` +@browseros/kalshi-scraper v0.1.0 + + PORT: ${config.PORT} + DATABASE_URL: *** + BROWSERBASE: *** + +Endpoints: + GET / - Service info + GET /health - Health check + GET /feed - Market feed (category, cursor, limit) + POST /scrape - Trigger scrape cycle (requires x-scrape-secret) +`) + +export default { + port: config.PORT, + fetch: app.fetch, +} diff --git a/apps/kalshi-scraper/src/routes/feed.ts b/apps/kalshi-scraper/src/routes/feed.ts new file mode 100644 index 00000000..ef828bb3 --- /dev/null +++ b/apps/kalshi-scraper/src/routes/feed.ts @@ -0,0 +1,74 @@ +import { and, desc, eq, lt } from 'drizzle-orm' +import { Hono } from 'hono' +import { db, markets } from '../db' + +export const feedRoute = new Hono().get('/', async (c) => { + const category = c.req.query('category') ?? 'trending' + const cursor = c.req.query('cursor') ?? null + const limit = Math.min( + Number.parseInt(c.req.query('limit') ?? '20', 10) || 20, + 100, + ) + + // Build where conditions + const conditions = [eq(markets.status, 'open')] + + // Category filter + if (category === 'trending') { + conditions.push(eq(markets.isTrending, true)) + } else if (category !== 'all') { + conditions.push(eq(markets.category, category)) + } + + // Cursor-based pagination (cursor = feed_score of last item) + if (cursor) { + conditions.push(lt(markets.feedScore, Number.parseInt(cursor, 10))) + } + + const rows = await db + .select() + .from(markets) + .where(and(...conditions)) + .orderBy(desc(markets.feedScore)) + .limit(limit + 1) + + const hasMore = rows.length > limit + const pageRows = hasMore ? rows.slice(0, limit) : rows + const nextCursor = + hasMore && pageRows.length > 0 + ? String(pageRows[pageRows.length - 1].feedScore) + : null + + // Map DB rows to the FeedResponse shape the frontend expects + const feedMarkets = pageRows.map((row) => ({ + id: row.id, + ticker: row.ticker, + title: row.title, + subtitle: row.subtitle ?? '', + category: row.category, + yes_price: row.yesPrice, + no_price: row.noPrice, + volume: row.volume ?? 0, + volume_24h: row.volume24h ?? 0, + open_interest: row.openInterest ?? 0, + close_time: row.closeTime?.toISOString() ?? '', + image_url: row.imageUrl, + kalshi_url: row.kalshiUrl, + status: row.status, + last_price: row.lastPrice ?? 0, + price_change_24h: row.priceChange24h ?? 0, + traders_count: row.tradersCount ?? 0, + likes_count: row.likesCount ?? 0, + comments_count: row.commentsCount ?? 0, + shares_count: row.sharesCount ?? 0, + is_hot: row.isHot ?? false, + is_trending: row.isTrending ?? false, + created_at: row.createdAt?.toISOString() ?? '', + })) + + return c.json({ + markets: feedMarkets, + cursor: nextCursor, + has_more: hasMore, + }) +}) diff --git a/apps/kalshi-scraper/src/routes/scrape.ts b/apps/kalshi-scraper/src/routes/scrape.ts new file mode 100644 index 00000000..7ca3e5a8 --- /dev/null +++ b/apps/kalshi-scraper/src/routes/scrape.ts @@ -0,0 +1,189 @@ +import { sql } from 'drizzle-orm' +import { Hono } from 'hono' +import { config } from '../config' +import { db, markets } from '../db' +import type { NewMarket } from '../db/schema' +import { + type ScrapedRanking, + scrapeKalshiRankings, +} from '../services/browserbase-scraper' +import { mapCategory } from '../services/category-mapper' +import { + dollarsToCents, + fetchAllMarkets, + fetchEvents, + type KalshiEvent, + type KalshiMarket, +} from '../services/kalshi-api' +import { + computeFeedScore, + generateEngagementCounts, + isHotMarket, + isTrendingMarket, +} from '../services/ranking' + +export const scrapeRoute = new Hono().post('/', async (c) => { + // Protect endpoint with secret + const secret = c.req.header('x-scrape-secret') ?? c.req.query('secret') + if (secret !== config.SCRAPE_SECRET) { + return c.json({ error: 'Unauthorized' }, 401) + } + + const startTime = Date.now() + const results = { + marketsIngested: 0, + eventsLoaded: 0, + scraped: false, + error: null as string | null, + } + + try { + // Run API pull and Browserbase scrape in parallel + const [kalshiMarkets, eventMap, scrapedRankings] = await Promise.all([ + fetchAllMarkets(), + fetchEvents(), + scrapeKalshiRankingsOrFallback(), + ]) + + results.eventsLoaded = eventMap.size + results.scraped = scrapedRankings.length > 0 + + // Build set of trending titles from scraped data + const trendingTitles = buildTrendingTitles(scrapedRankings) + + // Transform and upsert each market + const marketRows = kalshiMarkets.map((km) => + transformMarket(km, eventMap, trendingTitles), + ) + + // Batch upsert in chunks of 100 + for (let i = 0; i < marketRows.length; i += 100) { + const chunk = marketRows.slice(i, i + 100) + await upsertMarkets(chunk) + } + + results.marketsIngested = marketRows.length + } catch (err) { + results.error = err instanceof Error ? err.message : String(err) + console.error('Scrape failed:', results.error) + } + + const duration = Date.now() - startTime + return c.json({ ...results, durationMs: duration }) +}) + +// -- Helpers -- + +async function scrapeKalshiRankingsOrFallback(): Promise { + try { + return await scrapeKalshiRankings() + } catch (err) { + console.error('Browserbase scrape failed, continuing with API-only:', err) + return [] + } +} + +function buildTrendingTitles(rankings: ScrapedRanking[]): Set { + const titles = new Set() + for (const ranking of rankings) { + if (ranking.feedType === 'trending' || ranking.feedType === 'top_movers') { + for (const item of ranking.items) { + titles.add(item.title.toLowerCase()) + } + } + } + return titles +} + +function transformMarket( + km: KalshiMarket, + eventMap: Map, + trendingTitles: Set, +): NewMarket { + const event = eventMap.get(km.event_ticker) + const category = mapCategory(event?.category ?? '', [], km.title) + + // Convert prices from FixedPointDollars to cents + const yesPrice = dollarsToCents(km.yes_bid_dollars) + const noPrice = 100 - yesPrice + const lastPrice = dollarsToCents(km.last_price_dollars) + const previousPrice = dollarsToCents(km.previous_price_dollars) + const priceChange24h = lastPrice - previousPrice + + // Build the kalshi URL from the event ticker + const seriesTicker = event?.series_ticker ?? km.event_ticker + const kalshiUrl = `https://kalshi.com/markets/${seriesTicker.toLowerCase()}` + + // Check if this market appears in scraped trending list + const isInTrendingList = trendingTitles.has(km.title.toLowerCase()) + + const engagement = generateEngagementCounts(km.volume_24h, km.ticker) + + const market: NewMarket = { + ticker: km.ticker, + title: km.title, + subtitle: event?.sub_title ?? km.subtitle ?? '', + category, + yesPrice, + noPrice, + volume: km.volume, + volume24h: km.volume_24h, + openInterest: km.open_interest, + closeTime: km.close_time ? new Date(km.close_time) : null, + imageUrl: null, + kalshiUrl, + status: 'open', + lastPrice, + priceChange24h, + tradersCount: Math.round(km.volume_24h / 50), + isHot: false, + isTrending: false, + feedScore: 0, + likesCount: engagement.likesCount, + commentsCount: engagement.commentsCount, + sharesCount: engagement.sharesCount, + eventTicker: km.event_ticker, + } + + // Compute ranking fields + market.feedScore = computeFeedScore({ market, isInTrendingList }) + market.isHot = isHotMarket(market) + market.isTrending = isTrendingMarket(isInTrendingList, market.feedScore ?? 0) + + return market +} + +async function upsertMarkets(rows: NewMarket[]) { + if (rows.length === 0) return + + await db + .insert(markets) + .values(rows) + .onConflictDoUpdate({ + target: markets.ticker, + set: { + title: sql`excluded.title`, + subtitle: sql`excluded.subtitle`, + category: sql`excluded.category`, + yesPrice: sql`excluded.yes_price`, + noPrice: sql`excluded.no_price`, + volume: sql`excluded.volume`, + volume24h: sql`excluded.volume_24h`, + openInterest: sql`excluded.open_interest`, + closeTime: sql`excluded.close_time`, + kalshiUrl: sql`excluded.kalshi_url`, + status: sql`excluded.status`, + lastPrice: sql`excluded.last_price`, + priceChange24h: sql`excluded.price_change_24h`, + tradersCount: sql`excluded.traders_count`, + isHot: sql`excluded.is_hot`, + isTrending: sql`excluded.is_trending`, + feedScore: sql`excluded.feed_score`, + likesCount: sql`excluded.likes_count`, + commentsCount: sql`excluded.comments_count`, + sharesCount: sql`excluded.shares_count`, + eventTicker: sql`excluded.event_ticker`, + updatedAt: sql`now()`, + }, + }) +} diff --git a/apps/kalshi-scraper/src/services/browserbase-scraper.ts b/apps/kalshi-scraper/src/services/browserbase-scraper.ts new file mode 100644 index 00000000..f7d33884 --- /dev/null +++ b/apps/kalshi-scraper/src/services/browserbase-scraper.ts @@ -0,0 +1,135 @@ +import Browserbase from '@browserbasehq/sdk' +import { chromium } from 'playwright-core' +import { config } from '../config' + +export type FeedType = 'trending' | 'top_movers' | 'new' | 'highest_volume' + +export interface ScrapedItem { + title: string + subtitle: string + pricePercent: number + priceChange: number + rank: number +} + +export interface ScrapedRanking { + feedType: FeedType + items: ScrapedItem[] +} + +const SECTION_NAMES: Array<{ label: string; feedType: FeedType }> = [ + { label: 'Trending', feedType: 'trending' }, + { label: 'Top movers', feedType: 'top_movers' }, + { label: 'Top Movers', feedType: 'top_movers' }, + { label: 'New', feedType: 'new' }, + { label: 'Highest volume', feedType: 'highest_volume' }, + { label: 'Highest Volume', feedType: 'highest_volume' }, +] + +export async function scrapeKalshiRankings(): Promise { + const bb = new Browserbase({ apiKey: config.BROWSERBASE_API_KEY }) + + const session = await bb.sessions.create({ + projectId: config.BROWSERBASE_PROJECT_ID, + browserSettings: { blockAds: true }, + }) + + const browser = await chromium.connectOverCDP(session.connectUrl) + const context = browser.contexts()[0] + const page = context.pages()[0] + + try { + await page.goto('https://kalshi.com/markets', { + waitUntil: 'networkidle', + timeout: 30_000, + }) + await page.waitForTimeout(3000) + + // Extract each section separately to keep complexity low + const rankings: ScrapedRanking[] = [] + for (const { label, feedType } of SECTION_NAMES) { + const items = await extractSection(page, label) + if (items.length > 0) { + rankings.push({ feedType, items }) + } + } + + // Deduplicate by feedType (e.g. "Top movers" vs "Top Movers") + return deduplicateRankings(rankings) + } finally { + await browser.close() + } +} + +async function extractSection( + page: import('playwright-core').Page, + sectionLabel: string, +): Promise { + return page.evaluate((label) => { + const headings = document.querySelectorAll( + 'h2, h3, [class*="heading"], [class*="title"]', + ) + + // Find the heading matching this section + let section: Element | null = null + for (const h of headings) { + if (h.textContent?.trim() === label) { + section = h.closest('section') ?? h.parentElement + break + } + } + if (!section) return [] + + const items: Array<{ + title: string + subtitle: string + pricePercent: number + priceChange: number + rank: number + }> = [] + + const listItems = section.querySelectorAll( + 'a, [class*="item"], [class*="card"], [class*="row"]', + ) + + let rank = 1 + for (const item of listItems) { + if (rank > 10) break + + const titleEl = item.querySelector( + '[class*="title"], [class*="name"], h4, h5, span', + ) + const title = titleEl?.textContent?.trim() ?? '' + if (!title || title === label) continue + + const subtitleEl = item.querySelector( + '[class*="subtitle"], [class*="description"], [class*="sub"]', + ) + const subtitle = subtitleEl?.textContent?.trim() ?? '' + + const priceMatch = item.textContent?.match(/(\d{1,3})%/) + const pricePercent = priceMatch ? Number.parseInt(priceMatch[1], 10) : 0 + + const changeMatch = item.textContent?.match(/[▲▼△▽]?\s*(\d+)/) + const isNeg = + item.textContent?.includes('▼') || item.textContent?.includes('▽') + const priceChange = changeMatch + ? Number.parseInt(changeMatch[1], 10) * (isNeg ? -1 : 1) + : 0 + + items.push({ title, subtitle, pricePercent, priceChange, rank }) + rank++ + } + + return items + }, sectionLabel) +} + +function deduplicateRankings(rankings: ScrapedRanking[]): ScrapedRanking[] { + const seen = new Set() + return rankings.filter((r) => { + if (seen.has(r.feedType)) return false + seen.add(r.feedType) + return true + }) +} diff --git a/apps/kalshi-scraper/src/services/category-mapper.ts b/apps/kalshi-scraper/src/services/category-mapper.ts new file mode 100644 index 00000000..4d38fa4b --- /dev/null +++ b/apps/kalshi-scraper/src/services/category-mapper.ts @@ -0,0 +1,70 @@ +type FrontendCategory = + | 'politics' + | 'sports' + | 'crypto' + | 'economics' + | 'entertainment' + | 'science' + | 'weather' + | 'tech' + +const CATEGORY_MAP: Record = { + Politics: 'politics', + 'US Politics': 'politics', + World: 'politics', + 'World Politics': 'politics', + Sports: 'sports', + 'Climate and Weather': 'weather', + Climate: 'weather', + Weather: 'weather', + Economics: 'economics', + Economy: 'economics', + Financial: 'economics', + Financials: 'economics', + Finance: 'economics', + Tech: 'tech', + Technology: 'tech', + AI: 'tech', + Entertainment: 'entertainment', + Culture: 'entertainment', + Science: 'science', + Crypto: 'crypto', + Cryptocurrency: 'crypto', +} + +// Map tags that hint at crypto markets +const CRYPTO_TAGS = [ + 'bitcoin', + 'btc', + 'ethereum', + 'eth', + 'crypto', + 'defi', + 'solana', +] + +export function mapCategory( + kalshiCategory: string, + tags: string[] = [], + title = '', +): FrontendCategory { + // Check tags for crypto signals first + const lowerTags = tags.map((t) => t.toLowerCase()) + if (lowerTags.some((t) => CRYPTO_TAGS.includes(t))) return 'crypto' + + // Check title for crypto keywords + const lowerTitle = title.toLowerCase() + if (CRYPTO_TAGS.some((k) => lowerTitle.includes(k))) return 'crypto' + + // Direct category mapping + const mapped = CATEGORY_MAP[kalshiCategory] + if (mapped) return mapped + + // Case-insensitive fallback + const lowerCategory = kalshiCategory.toLowerCase() + for (const [key, value] of Object.entries(CATEGORY_MAP)) { + if (key.toLowerCase() === lowerCategory) return value + } + + return 'economics' +} diff --git a/apps/kalshi-scraper/src/services/kalshi-api.ts b/apps/kalshi-scraper/src/services/kalshi-api.ts new file mode 100644 index 00000000..261c2dd7 --- /dev/null +++ b/apps/kalshi-scraper/src/services/kalshi-api.ts @@ -0,0 +1,145 @@ +const KALSHI_API_BASE = 'https://api.elections.kalshi.com/trade-api/v2' +const MAX_RETRIES = 3 +const MAX_MARKET_PAGES = 50 +const MAX_EVENT_PAGES = 30 +const PAGE_DELAY_MS = 300 + +// -- Kalshi API response types -- + +export interface KalshiMarket { + ticker: string + event_ticker: string + market_type: string + title: string + subtitle: string + yes_sub_title: string + no_sub_title: string + yes_bid: number + yes_ask: number + yes_bid_dollars: string + yes_ask_dollars: string + last_price: number + last_price_dollars: string + previous_price: number + previous_price_dollars: string + volume: number + volume_24h: number + volume_24h_fp: string + open_interest: number + open_interest_fp: string + close_time: string + status: string + result: string +} + +export interface KalshiEvent { + event_ticker: string + series_ticker: string + title: string + sub_title: string + category: string +} + +interface MarketsResponse { + cursor: string + markets: KalshiMarket[] +} + +interface EventsResponse { + cursor: string + events: KalshiEvent[] +} + +// -- Helpers -- + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +async function fetchJson(url: string): Promise { + for (let attempt = 0; attempt < MAX_RETRIES; attempt++) { + const res = await fetch(url, { + headers: { Accept: 'application/json' }, + }) + + if (res.status === 429) { + const backoff = 2 ** (attempt + 1) * 1000 + console.warn(`Rate limited, retrying in ${backoff}ms...`) + await sleep(backoff) + continue + } + + if (!res.ok) { + throw new Error(`Kalshi API ${res.status}: ${url}`) + } + + return res.json() as Promise + } + + throw new Error(`Kalshi API: max retries exceeded for ${url}`) +} + +// Convert FixedPointDollars string to integer cents (0-100) +export function dollarsToCents(dollars: string): number { + const value = parseFloat(dollars || '0') + return Math.round(value * 100) +} + +// -- Public API -- + +export async function fetchAllMarkets(): Promise { + const allMarkets: KalshiMarket[] = [] + let cursor = '' + let pages = 0 + + // Fetch non-multivariate markets with pagination + while (pages < MAX_MARKET_PAGES) { + const params = new URLSearchParams({ + status: 'open', + limit: '1000', + mve_filter: 'exclude', + }) + if (cursor) params.set('cursor', cursor) + + const data = await fetchJson( + `${KALSHI_API_BASE}/markets?${params}`, + ) + allMarkets.push(...data.markets) + pages++ + + if (!data.cursor) break + cursor = data.cursor + await sleep(PAGE_DELAY_MS) + } + + // Keep only markets with trading activity + return allMarkets.filter((m) => m.volume_24h > 0 || m.open_interest > 0) +} + +export async function fetchEvents(): Promise> { + const eventMap = new Map() + let cursor = '' + let pages = 0 + + while (pages < MAX_EVENT_PAGES) { + const params = new URLSearchParams({ + status: 'open', + limit: '200', + }) + if (cursor) params.set('cursor', cursor) + + const data = await fetchJson( + `${KALSHI_API_BASE}/events?${params}`, + ) + for (const event of data.events) { + eventMap.set(event.event_ticker, event) + } + pages++ + + if (!data.cursor) break + cursor = data.cursor + await sleep(PAGE_DELAY_MS) + } + + return eventMap +} diff --git a/apps/kalshi-scraper/src/services/ranking.ts b/apps/kalshi-scraper/src/services/ranking.ts new file mode 100644 index 00000000..56628470 --- /dev/null +++ b/apps/kalshi-scraper/src/services/ranking.ts @@ -0,0 +1,91 @@ +import type { NewMarket } from '../db/schema' + +interface RankingInput { + market: NewMarket + isInTrendingList: boolean +} + +// Compute a feed score (0-1000) for ranking markets in the TikTok feed +export function computeFeedScore(input: RankingInput): number { + const { market, isInTrendingList } = input + + // Volume component — normalize to 0-1 range (cap at 2M for 24h volume) + const volume24h = market.volume24h ?? 0 + const volumeScore = Math.min(volume24h / 2_000_000, 1) + + // Controversy — markets near 50/50 are most engaging + const yesPrice = market.yesPrice ?? 50 + const controversyScore = 1 - Math.abs(yesPrice - 50) / 50 + + // Recency — newer markets get a boost (decay over 30 days) + const createdMs = market.createdAt + ? new Date(market.createdAt).getTime() + : Date.now() - 30 * 24 * 60 * 60 * 1000 + const ageMs = Date.now() - createdMs + const ageDays = ageMs / (24 * 60 * 60 * 1000) + const recencyScore = Math.max(0, 1 - ageDays / 30) + + // Closing soon — urgency factor (peak when closing in next 24h) + const closeMs = market.closeTime + ? new Date(market.closeTime).getTime() + : Date.now() + 365 * 24 * 60 * 60 * 1000 + const timeToCloseHours = Math.max( + 0, + (closeMs - Date.now()) / (60 * 60 * 1000), + ) + const closingSoonScore = + timeToCloseHours < 24 + ? 1 - timeToCloseHours / 24 + : timeToCloseHours < 168 + ? 0.3 + : 0 + + // Kalshi trending boost + const trendingBoost = isInTrendingList ? 1 : 0 + + // Weighted sum + const score = + volumeScore * 0.3 + + controversyScore * 0.25 + + recencyScore * 0.2 + + closingSoonScore * 0.15 + + trendingBoost * 0.1 + + return Math.round(score * 1000) +} + +// Determine if a market should be flagged as "hot" +export function isHotMarket(market: NewMarket): boolean { + const volume24h = market.volume24h ?? 0 + const yesPrice = market.yesPrice ?? 50 + const controversyScore = 1 - Math.abs(yesPrice - 50) / 50 + return volume24h > 500_000 && controversyScore > 0.5 +} + +// Determine if a market should be flagged as "trending" +export function isTrendingMarket( + isInTrendingList: boolean, + feedScore: number, +): boolean { + return isInTrendingList || feedScore > 500 +} + +// Simple deterministic hash for a string, returns value between 0 and 1 +function deterministicHash(str: string): number { + let hash = 0 + for (let i = 0; i < str.length; i++) { + hash = (hash * 31 + str.charCodeAt(i)) | 0 + } + return Math.abs(hash % 1000) / 1000 +} + +// Generate synthetic engagement counts based on volume and ticker +export function generateEngagementCounts(volume24h: number, ticker: string) { + const base = Math.max(100, volume24h / 10) + const h = deterministicHash(ticker) + return { + likesCount: Math.round(base * (0.8 + h * 0.4)), + commentsCount: Math.round(base * (0.3 + h * 0.2)), + sharesCount: Math.round(base * (0.2 + h * 0.15)), + } +} diff --git a/apps/kalshi-scraper/tsconfig.json b/apps/kalshi-scraper/tsconfig.json new file mode 100644 index 00000000..754a1643 --- /dev/null +++ b/apps/kalshi-scraper/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ESNext", + "module": "preserve", + "moduleResolution": "bundler", + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "types": ["@types/bun"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "drizzle"] +} diff --git a/bun.lock b/bun.lock index d455ab7d..3165301a 100644 --- a/bun.lock +++ b/bun.lock @@ -136,6 +136,45 @@ "ws": "^8.18.3", }, }, + "apps/kalshi-feed": { + "name": "@browseros/kalshi-feed", + "version": "0.0.1", + "dependencies": { + "clsx": "^2.1.1", + "lucide-react": "^0.562.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "tailwind-merge": "^3.4.0", + "tw-animate-css": "^1.4.0", + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.17", + "@types/chrome": "^0.1.28", + "@types/react": "^19.1.12", + "@types/react-dom": "^19.1.9", + "@wxt-dev/module-react": "^1.1.3", + "tailwindcss": "^4.1.17", + "typescript": "^5.9.2", + "wxt": "^0.20.6", + }, + }, + "apps/kalshi-scraper": { + "name": "@browseros/kalshi-scraper", + "version": "0.1.0", + "dependencies": { + "@browserbasehq/sdk": "^2.0.0", + "drizzle-orm": "^0.45.1", + "hono": "^4.6.0", + "playwright-core": "^1.52.0", + "postgres": "^3.4.7", + "zod": "^3.24.1", + }, + "devDependencies": { + "@types/bun": "latest", + "drizzle-kit": "^0.31.8", + "typescript": "^5.9.3", + }, + }, "apps/server": { "name": "@browseros/server", "version": "0.0.52", @@ -345,10 +384,16 @@ "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.1", "", {}, "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw=="], + "@browserbasehq/sdk": ["@browserbasehq/sdk@2.6.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-83iXP5D7xMm8Wyn66TUaUrgoByCmAJuoMoZQI3sGg3JAiMlTfnCIMqyVBoNSaItaPIkaCnrsj6LiusmXV2X9YA=="], + "@browseros-ai/agent-sdk": ["@browseros-ai/agent-sdk@workspace:packages/agent-sdk"], "@browseros/agent": ["@browseros/agent@workspace:apps/agent"], + "@browseros/kalshi-feed": ["@browseros/kalshi-feed@workspace:apps/kalshi-feed"], + + "@browseros/kalshi-scraper": ["@browseros/kalshi-scraper@workspace:apps/kalshi-scraper"], + "@browseros/server": ["@browseros/server@workspace:apps/server"], "@browseros/shared": ["@browseros/shared@workspace:packages/shared"], @@ -385,6 +430,8 @@ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + "@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], @@ -425,6 +472,10 @@ "@envelop/types": ["@envelop/types@5.2.1", "", { "dependencies": { "@whatwg-node/promise-helpers": "^1.0.0", "tslib": "^2.5.0" } }, "sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg=="], + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.2", "", { "os": "android", "cpu": "arm" }, "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA=="], @@ -1443,6 +1494,8 @@ "@types/node": ["@types/node@24.10.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw=="], + "@types/node-fetch": ["@types/node-fetch@2.6.13", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.4" } }, "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw=="], + "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], "@types/parse-json": ["@types/parse-json@4.0.2", "", {}, "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw=="], @@ -1563,6 +1616,8 @@ "agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "ahooks": ["ahooks@3.9.6", "", { "dependencies": { "@babel/runtime": "^7.21.0", "@types/js-cookie": "^3.0.6", "dayjs": "^1.9.1", "intersection-observer": "^0.12.0", "js-cookie": "^3.0.5", "lodash": "^4.17.21", "react-fast-compare": "^3.2.2", "resize-observer-polyfill": "^1.5.1", "screenfull": "^5.0.0", "tslib": "^2.4.1" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Mr7f05swd5SmKlR9SZo5U6M0LsL4ErweLzpdgXjA1JPmnZ78Vr6wzx0jUtvoxrcqGKYnX0Yjc02iEASVxHFPjQ=="], "ai": ["ai@5.0.121", "", { "dependencies": { "@ai-sdk/gateway": "2.0.27", "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.20", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-3iYPdARKGLryC/7OA9RgBUaym1gynvWS7UPy8NwoRNCKP52lshldtHB5xcEfVviw7liWH2zJlW9yEzsDglcIEQ=="], @@ -2011,6 +2066,10 @@ "downshift": ["downshift@9.0.13", "", { "dependencies": { "@babel/runtime": "^7.24.5", "compute-scroll-into-view": "^3.1.0", "prop-types": "^15.8.1", "react-is": "18.2.0", "tslib": "^2.6.2" }, "peerDependencies": { "react": ">=16.12.0" } }, "sha512-fPV+K5jwEzfEAhNhprgCmpWQ23MKwKNzdbtK0QQFiw4hbFcKhMeGB+ccorfWJzmsLR5Dty+CmLDduWlIs74G/w=="], + "drizzle-kit": ["drizzle-kit@0.31.9", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg=="], + + "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -2071,6 +2130,8 @@ "esbuild": ["esbuild@0.27.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.2", "@esbuild/android-arm": "0.27.2", "@esbuild/android-arm64": "0.27.2", "@esbuild/android-x64": "0.27.2", "@esbuild/darwin-arm64": "0.27.2", "@esbuild/darwin-x64": "0.27.2", "@esbuild/freebsd-arm64": "0.27.2", "@esbuild/freebsd-x64": "0.27.2", "@esbuild/linux-arm": "0.27.2", "@esbuild/linux-arm64": "0.27.2", "@esbuild/linux-ia32": "0.27.2", "@esbuild/linux-loong64": "0.27.2", "@esbuild/linux-mips64el": "0.27.2", "@esbuild/linux-ppc64": "0.27.2", "@esbuild/linux-riscv64": "0.27.2", "@esbuild/linux-s390x": "0.27.2", "@esbuild/linux-x64": "0.27.2", "@esbuild/netbsd-arm64": "0.27.2", "@esbuild/netbsd-x64": "0.27.2", "@esbuild/openbsd-arm64": "0.27.2", "@esbuild/openbsd-x64": "0.27.2", "@esbuild/openharmony-arm64": "0.27.2", "@esbuild/sunos-x64": "0.27.2", "@esbuild/win32-arm64": "0.27.2", "@esbuild/win32-ia32": "0.27.2", "@esbuild/win32-x64": "0.27.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw=="], + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="], @@ -2199,7 +2260,7 @@ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - "form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="], + "form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="], "formdata-node": ["formdata-node@6.0.3", "", {}, "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg=="], @@ -2243,6 +2304,8 @@ "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], "get-value": ["get-value@2.0.6", "", {}, "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="], @@ -2375,6 +2438,8 @@ "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -3047,6 +3112,8 @@ "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], + "playwright-core": ["playwright-core@1.58.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg=="], + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], @@ -3057,6 +3124,8 @@ "postcss-selector-parser": ["postcss-selector-parser@6.0.10", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w=="], + "postgres": ["postgres@3.4.8", "", {}, "sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg=="], + "postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="], "postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="], @@ -3345,6 +3414,8 @@ "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "responselike": ["responselike@4.0.2", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-cGk8IbWEAnaCpdAt1BHzJ3Ahz5ewDJa0KseTsE3qIRMJ3C698W8psM7byCeWVpd/Ha7FUYzuRVzXoKoM6nRUbA=="], "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], @@ -3749,7 +3820,7 @@ "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "web-streams-polyfill": ["web-streams-polyfill@4.0.0-beta.3", "", {}, "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug=="], "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], @@ -3873,8 +3944,14 @@ "@better-auth/core/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "@browserbasehq/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + + "@browserbasehq/sdk/formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "@browseros/agent/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "@browseros/kalshi-scraper/@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + "@browseros/server/@types/bun": ["@types/bun@1.3.5", "", { "dependencies": { "bun-types": "1.3.5" } }, "sha512-RnygCqNrd3srIPEWBd5LFeUYG7plCoH2Yw9WaZGyNmdTEei+gWaHqydbaIRkIkcbXwhBT94q78QljxN0Sk838w=="], "@chevrotain/cst-dts-gen/lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], @@ -3901,6 +3978,8 @@ "@emotion/serialize/@emotion/unitless": ["@emotion/unitless@0.10.0", "", {}, "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="], + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + "@google-cloud/logging/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], "@google/gemini-cli-core/@google/genai": ["@google/genai@1.16.0", "", { "dependencies": { "google-auth-library": "^9.14.2", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.11.4" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-hdTYu39QgDFxv+FB6BK2zi4UIJGWhx2iPc0pHQ0C5Q/RCi+m+4gsryIzTGO+riqWcUA8/WGYp6hpqckdOBNysw=="], @@ -4325,6 +4404,8 @@ "dotenv-expand/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "drizzle-kit/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "duplexify/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "ecdsa-sig-formatter/safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -4349,6 +4430,8 @@ "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "fetch-blob/web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "figures/is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], "find-up/path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -4379,6 +4462,8 @@ "googleapis-common/uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="], + "got/form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="], + "got/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "graphql-config/@graphql-tools/url-loader": ["@graphql-tools/url-loader@8.0.33", "", { "dependencies": { "@graphql-tools/executor-graphql-ws": "^2.0.1", "@graphql-tools/executor-http": "^1.1.9", "@graphql-tools/executor-legacy-ws": "^1.1.19", "@graphql-tools/utils": "^10.9.1", "@graphql-tools/wrap": "^10.0.16", "@types/ws": "^8.0.0", "@whatwg-node/fetch": "^0.10.0", "@whatwg-node/promise-helpers": "^1.0.0", "isomorphic-ws": "^5.0.0", "sync-fetch": "0.6.0-2", "tslib": "^2.4.0", "ws": "^8.17.1" }, "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" } }, "sha512-Fu626qcNHcqAj8uYd7QRarcJn5XZ863kmxsg1sm0fyjyfBJnsvC7ddFt6Hayz5kxVKfsnjxiDfPMXanvsQVBKw=="], @@ -4465,6 +4550,8 @@ "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + "publish-browser-extension/form-data-encoder": ["form-data-encoder@4.1.0", "", {}, "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw=="], + "publish-browser-extension/listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="], "publish-browser-extension/zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], @@ -4569,8 +4656,56 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@browserbasehq/sdk/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + + "@browseros/kalshi-scraper/@types/bun/bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + "@browseros/server/@types/bun/bun-types": ["bun-types@1.3.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-inmAYe2PFLs0SUbFOWSVD24sg1jFlMPxOjOSSCYqUgn4Hsc3rDc7dFvfVYjFPNHtov6kgUeulV4SxbuIV/stPw=="], + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + "@google/gemini-cli-core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.203.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ=="], "@google/gemini-cli-core/@opentelemetry/exporter-logs-otlp-http/@opentelemetry/core": ["@opentelemetry/core@2.0.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw=="], @@ -4793,6 +4928,58 @@ "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], + "drizzle-kit/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "drizzle-kit/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "drizzle-kit/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "drizzle-kit/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "drizzle-kit/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "drizzle-kit/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "drizzle-kit/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "drizzle-kit/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "drizzle-kit/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "drizzle-kit/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "drizzle-kit/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "drizzle-kit/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "drizzle-kit/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "drizzle-kit/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "drizzle-kit/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "drizzle-kit/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "drizzle-kit/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "drizzle-kit/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "drizzle-kit/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "drizzle-kit/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "drizzle-kit/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "drizzle-kit/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "fx-runner/which/is-absolute": ["is-absolute@0.1.7", "", { "dependencies": { "is-relative": "^0.1.0" } }, "sha512-Xi9/ZSn4NFapG8RP98iNPMOeaV3mXPisxKxzKtHVqr3g56j/fBn+yZmnxSVAA8lmZbl2J9b/a4kJvfU3hqQYgA=="],