├── terminal ├── public │ ├── logo.jpg │ ├── predict-os-banner.png │ ├── okbet.svg │ └── dome-icon-light.svg ├── src │ ├── app │ │ ├── page.tsx │ │ ├── market-analysis │ │ │ └── page.tsx │ │ ├── betting-bots │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── api │ │ │ ├── analyze-event-markets │ │ │ │ └── route.ts │ │ │ └── limit-order-bot │ │ │ │ └── route.ts │ │ └── globals.css │ ├── lib │ │ └── utils.ts │ ├── types │ │ ├── betting-bot.ts │ │ └── api.ts │ └── components │ │ ├── Terminal.tsx │ │ ├── AnalysisOutput.tsx │ │ ├── Sidebar.tsx │ │ ├── TerminalInput.tsx │ │ └── BettingBotTerminal.tsx ├── postcss.config.mjs ├── next.config.mjs ├── eslint.config.mjs ├── .gitignore ├── tsconfig.json ├── .env.example ├── package.json └── tailwind.config.ts ├── supabase ├── .gitignore ├── .env.example ├── functions │ ├── _shared │ │ ├── polymarket │ │ │ ├── utils.ts │ │ │ ├── types.ts │ │ │ └── client.ts │ │ ├── dome │ │ │ ├── types.ts │ │ │ ├── client.ts │ │ │ └── endpoints.ts │ │ └── ai │ │ │ ├── types.ts │ │ │ ├── callOpenAI.ts │ │ │ ├── callGrok.ts │ │ │ └── prompts │ │ │ └── analyzeEventMarkets.ts │ ├── polymarket-up-down-15-markets-limit-order-bot │ │ ├── types.ts │ │ └── index.ts │ └── analyze-event-markets │ │ ├── types.ts │ │ └── index.ts └── config.toml ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── docs └── features │ ├── market-analysis.md │ └── betting-bots.md └── README.md /terminal/public/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictionXBT/PredictOS/HEAD/terminal/public/logo.jpg -------------------------------------------------------------------------------- /supabase/.gitignore: -------------------------------------------------------------------------------- 1 | # Supabase 2 | .branches 3 | .temp 4 | 5 | # dotenvx 6 | .env.keys 7 | .env.local 8 | .env.*.local 9 | -------------------------------------------------------------------------------- /terminal/public/predict-os-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PredictionXBT/PredictOS/HEAD/terminal/public/predict-os-banner.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "denoland.vscode-deno", 4 | "bradlc.vscode-tailwindcss" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /terminal/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | export default function Home() { 4 | redirect("/market-analysis"); 5 | } 6 | -------------------------------------------------------------------------------- /terminal/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /terminal/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | /* config options here */ 4 | }; 5 | 6 | export default nextConfig; 7 | 8 | -------------------------------------------------------------------------------- /terminal/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | -------------------------------------------------------------------------------- /terminal/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from "eslint/config"; 2 | import nextVitals from "eslint-config-next/core-web-vitals"; 3 | import nextTs from "eslint-config-next/typescript"; 4 | 5 | const eslintConfig = defineConfig([ 6 | ...nextVitals, 7 | ...nextTs, 8 | // Override default ignores of eslint-config-next. 9 | globalIgnores([ 10 | // Default ignores of eslint-config-next: 11 | ".next/**", 12 | "out/**", 13 | "build/**", 14 | "next-env.d.ts", 15 | ]), 16 | ]); 17 | 18 | export default eslintConfig; 19 | -------------------------------------------------------------------------------- /terminal/src/app/market-analysis/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Terminal from "@/components/Terminal"; 4 | import Sidebar from "@/components/Sidebar"; 5 | 6 | export default function MarketAnalysisPage() { 7 | return ( 8 |
9 | {/* Sidebar Navigation */} 10 |
11 | 12 |
13 | 14 | {/* Main Content */} 15 |
16 | 17 |
18 |
19 | ); 20 | } 21 | 22 | -------------------------------------------------------------------------------- /terminal/src/app/betting-bots/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import BettingBotTerminal from "@/components/BettingBotTerminal"; 4 | import Sidebar from "@/components/Sidebar"; 5 | 6 | export default function BettingBotsPage() { 7 | return ( 8 |
9 | {/* Sidebar Navigation */} 10 |
11 | 12 |
13 | 14 | {/* Main Content */} 15 |
16 | 17 |
18 |
19 | ); 20 | } 21 | 22 | -------------------------------------------------------------------------------- /terminal/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enablePaths": [ 3 | "supabase/functions" 4 | ], 5 | "deno.lint": true, 6 | "deno.unstable": [ 7 | "bare-node-builtins", 8 | "byonm", 9 | "sloppy-imports", 10 | "unsafe-proto", 11 | "webgpu", 12 | "broadcast-channel", 13 | "worker-options", 14 | "cron", 15 | "kv", 16 | "ffi", 17 | "fs", 18 | "http", 19 | "net" 20 | ], 21 | "[typescript]": { 22 | "editor.defaultFormatter": "denoland.vscode-deno" 23 | }, 24 | "css.validate": false, 25 | "scss.validate": false, 26 | "less.validate": false, 27 | "css.lint.unknownAtRules": "ignore", 28 | "tailwindCSS.includeLanguages": { 29 | "typescript": "javascript", 30 | "typescriptreact": "javascript" 31 | }, 32 | "files.associations": { 33 | "*.css": "tailwindcss" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /terminal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./src/*" 28 | ] 29 | } 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts", 36 | ".next/dev/types/**/*.ts", 37 | "**/*.mts" 38 | ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 PredictionXBT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /terminal/.env.example: -------------------------------------------------------------------------------- 1 | # PredictOS Terminal Environment Variables 2 | # Copy this file to .env and update the values if needed 3 | 4 | # ========================================================================================= 5 | # SUPABASE CONFIGURATION ## run `supabase status` to get these values 6 | # ========================================================================================= 7 | # For local development, use the default Supabase local values: 8 | SUPABASE_URL=http://127.0.0.1:54321 9 | SUPABASE_ANON_KEY=your_anon_key_here 10 | 11 | 12 | # ========================================================================================= 13 | # EDGE FUNCTION URLS 14 | # ========================================================================================= 15 | # Full URLs to the Supabase Edge Functions 16 | # update base url based on SUPABASE_URL 17 | SUPABASE_EDGE_FUNCTION_ANALYZE_EVENT_MARKETS=http://127.0.0.1:54321/functions/v1/analyze-event-markets 18 | SUPABASE_EDGE_FUNCTION_BETTING_BOT=http://127.0.0.1:54321/functions/v1/polymarket-up-down-15-markets 19 | 20 | # Note: The terminal uses these to call the Supabase edge functions. 21 | # Make sure to deploy the edge functions first with: supabase functions deploy 22 | -------------------------------------------------------------------------------- /terminal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "predictos-terminal", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Open-source AI-powered prediction market analysis terminal for Kalshi and Polymarket", 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "class-variance-authority": "^0.7.1", 14 | "clsx": "^2.1.1", 15 | "lucide-react": "^0.462.0", 16 | "next": "^14.2.33", 17 | "react": "^18.3.1", 18 | "react-dom": "^18.3.1", 19 | "tailwind-merge": "^2.6.0" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^22.16.5", 23 | "@types/react": "^18.3.23", 24 | "@types/react-dom": "^18.3.7", 25 | "autoprefixer": "^10.4.21", 26 | "postcss": "^8.5.6", 27 | "tailwindcss": "^3.4.17", 28 | "typescript": "^5.8.3" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/predictionxbt/predictos" 33 | }, 34 | "keywords": [ 35 | "prediction-markets", 36 | "kalshi", 37 | "polymarket", 38 | "trading", 39 | "alpha", 40 | "ai-analysis", 41 | "grok", 42 | "next.js" 43 | ], 44 | "license": "MIT" 45 | } 46 | -------------------------------------------------------------------------------- /supabase/.env.example: -------------------------------------------------------------------------------- 1 | # PredictOS Supabase Edge Functions Environment Variables 2 | # Copy this file to .env and fill in your values 3 | 4 | # ============================================================================= 5 | # DOME API (Required for Market Analysis) 6 | # ============================================================================= 7 | # Get your API key from https://dashboard.domeapi.io 8 | DOME_API_KEY=your_dome_api_key_here 9 | 10 | # ============================================================================= 11 | # AI PROVIDERS (Required for Market Analysis) 12 | # ============================================================================= 13 | # xAI Grok API Key - https://console.x.ai/ 14 | XAI_API_KEY=your_xai_api_key_here 15 | 16 | # OpenAI API Key (optional) - https://platform.openai.com/ 17 | OPENAI_API_KEY=your_openai_api_key_here 18 | 19 | # ============================================================================= 20 | # POLYMARKET TRADING (Required for Betting Bot) 21 | # ============================================================================= 22 | # Your wallet's private key for signing transactions 23 | # WARNING: Keep this secure! Never commit this to version control. 24 | POLYMARKET_WALLET_PRIVATE_KEY=your_private_key_here 25 | 26 | # Your Polymarket proxy wallet address 27 | # This is the address shown in your Polymarket account settings 28 | POLYMARKET_PROXY_WALLET_ADDRESS=your_proxy_wallet_address_here 29 | -------------------------------------------------------------------------------- /terminal/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | 4 | export const metadata: Metadata = { 5 | title: "PredictOS - The OpenSource All-In-One Prediction Market Framework", 6 | description: "The all-in-one open-source framework for prediction markets. Build agents, analyze markets, and trade smarter with AI-powered tools for Kalshi, Polymarket, and beyond.", 7 | keywords: ["prediction markets", "kalshi", "polymarket", "trading framework", "AI analysis", "open source", "trading agents", "market analytics", "SDK"], 8 | icons: { 9 | icon: "/logo.jpg", 10 | apple: "/logo.jpg", 11 | }, 12 | openGraph: { 13 | title: "PredictOS - The OpenSource All-In-One Prediction Market Framework", 14 | description: "The all-in-one open-source framework for prediction markets. Terminal, Agents, Analytics, and SDK.", 15 | type: "website", 16 | }, 17 | }; 18 | 19 | export default function RootLayout({ 20 | children, 21 | }: Readonly<{ 22 | children: React.ReactNode; 23 | }>) { 24 | return ( 25 | 26 | 27 | {/* Background Effects */} 28 |
29 |
30 | 31 | {/* Main Content */} 32 |
33 | {children} 34 |
35 | 36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /terminal/src/types/betting-bot.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types for the Limit Order Bot API 3 | */ 4 | 5 | /** 6 | * Supported assets for 15-minute up/down markets 7 | */ 8 | export type SupportedAsset = "BTC" | "SOL" | "ETH" | "XRP"; 9 | 10 | /** 11 | * Log entry from the bot 12 | */ 13 | export interface BotLogEntry { 14 | timestamp: string; 15 | level: "INFO" | "WARN" | "ERROR" | "SUCCESS"; 16 | message: string; 17 | details?: Record; 18 | } 19 | 20 | /** 21 | * Order response from Polymarket 22 | */ 23 | export interface OrderResponse { 24 | success: boolean; 25 | orderId?: string; 26 | errorMsg?: string; 27 | transactionHash?: string; 28 | status?: string; 29 | } 30 | 31 | /** 32 | * Market order result from the limit order bot 33 | */ 34 | export interface MarketOrderResult { 35 | marketSlug: string; 36 | marketTitle?: string; 37 | marketStartTime: string; 38 | targetTimestamp: number; 39 | ordersPlaced?: { 40 | up?: OrderResponse; 41 | down?: OrderResponse; 42 | }; 43 | error?: string; 44 | } 45 | 46 | /** 47 | * Request body for the limit-order-bot endpoint 48 | */ 49 | export interface LimitOrderBotRequest { 50 | asset: SupportedAsset; 51 | /** Order price as a percentage (e.g., 48 for 48%). Optional, defaults to 48% */ 52 | price?: number; 53 | /** Order size in USD total. Optional, defaults to $25 */ 54 | sizeUsd?: number; 55 | } 56 | 57 | /** 58 | * Response from the limit-order-bot endpoint 59 | */ 60 | export interface LimitOrderBotResponse { 61 | success: boolean; 62 | data?: { 63 | asset: SupportedAsset; 64 | pricePercent: number; 65 | sizeUsd: number; 66 | market: MarketOrderResult; 67 | }; 68 | logs: BotLogEntry[]; 69 | error?: string; 70 | } 71 | -------------------------------------------------------------------------------- /supabase/functions/_shared/polymarket/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Polymarket Utility Functions 3 | */ 4 | 5 | import type { SupportedAsset, BotLogEntry } from "./types.ts"; 6 | 7 | /** 8 | * Asset slug prefixes for 15-minute up/down markets 9 | */ 10 | export const ASSET_SLUG_PREFIXES: Record = { 11 | BTC: "btc-updown-15m-", 12 | SOL: "sol-updown-15m-", 13 | ETH: "eth-updown-15m-", 14 | XRP: "xrp-updown-15m-", 15 | }; 16 | 17 | /** 18 | * Get the market slug for a given asset and timestamp 19 | */ 20 | export function buildMarketSlug(asset: SupportedAsset, timestamp: number): string { 21 | const prefix = ASSET_SLUG_PREFIXES[asset]; 22 | return `${prefix}${timestamp}`; 23 | } 24 | 25 | /** 26 | * Format a Unix timestamp to a short time string (HH:MM:SS UTC) 27 | */ 28 | export function formatTimeShort(ts: number): string { 29 | const dt = new Date(ts * 1000); 30 | return dt.toISOString().slice(11, 19) + " UTC"; 31 | } 32 | 33 | /** 34 | * Create a log entry with current timestamp 35 | */ 36 | export function createLogEntry( 37 | level: BotLogEntry["level"], 38 | message: string, 39 | details?: Record 40 | ): BotLogEntry { 41 | return { 42 | timestamp: new Date().toISOString(), 43 | level, 44 | message, 45 | details, 46 | }; 47 | } 48 | 49 | /** 50 | * Parse token IDs from market data 51 | * Returns [upTokenId, downTokenId] 52 | */ 53 | export function parseTokenIds(clobTokenIdsStr: string): [string, string] { 54 | const tokenIds = JSON.parse(clobTokenIdsStr); 55 | if (!Array.isArray(tokenIds) || tokenIds.length < 2) { 56 | throw new Error(`Expected 2 token IDs, got ${tokenIds.length}`); 57 | } 58 | // First token is Up, second is Down (based on outcomes: ["Up", "Down"]) 59 | return [tokenIds[0], tokenIds[1]]; 60 | } 61 | -------------------------------------------------------------------------------- /supabase/functions/_shared/dome/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type definitions for Dome API responses 3 | * @see https://docs.domeapi.io/ 4 | */ 5 | 6 | // ============================================================================ 7 | // Polymarket Types 8 | // ============================================================================ 9 | 10 | export interface PolymarketMarket { 11 | token_id: string; 12 | condition_id: string; 13 | question: string; 14 | slug: string; 15 | end_date: string; 16 | description?: string; 17 | outcomes: string[]; 18 | outcome_prices: number[]; 19 | volume: number; 20 | liquidity: number; 21 | active: boolean; 22 | closed: boolean; 23 | image?: string; 24 | icon?: string; 25 | } 26 | 27 | export interface PolymarketMarketsResponse { 28 | markets: PolymarketMarket[]; 29 | next_cursor?: string; 30 | } 31 | 32 | // ============================================================================ 33 | // Kalshi Types 34 | // ============================================================================ 35 | 36 | export interface KalshiMarket { 37 | ticker: string; 38 | event_ticker: string; 39 | title: string; 40 | subtitle?: string; 41 | status: string; 42 | close_time: string; 43 | yes_bid: number; 44 | yes_ask: number; 45 | no_bid: number; 46 | no_ask: number; 47 | last_price: number; 48 | volume: number; 49 | volume_24h: number; 50 | liquidity: number; 51 | open_interest: number; 52 | } 53 | 54 | export interface KalshiMarketsResponse { 55 | markets: KalshiMarket[]; 56 | cursor?: string; 57 | } 58 | 59 | // ============================================================================ 60 | // Common Types 61 | // ============================================================================ 62 | 63 | export interface PaginationParams { 64 | cursor?: string; 65 | limit?: number; 66 | } 67 | -------------------------------------------------------------------------------- /supabase/functions/_shared/dome/client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Base HTTP client for Dome API 3 | * @see https://docs.domeapi.io/ 4 | */ 5 | 6 | const DOME_API_BASE_URL = 'https://api.domeapi.io/v1'; 7 | 8 | export interface RequestOptions { 9 | method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; 10 | headers?: Record; 11 | params?: Record; 12 | } 13 | 14 | /** 15 | * Gets the Dome API key from environment variables 16 | */ 17 | function getApiKey(): string { 18 | const apiKey = Deno.env.get('DOME_API_KEY'); 19 | if (!apiKey) { 20 | throw new Error('DOME_API_KEY environment variable is not set'); 21 | } 22 | return apiKey; 23 | } 24 | 25 | /** 26 | * Makes a request to the Dome API 27 | * @param endpoint The API endpoint (e.g., '/polymarket/markets') 28 | * @param options Request options including method, headers, and params 29 | * @returns Promise resolving to the parsed JSON response 30 | */ 31 | export async function request( 32 | endpoint: string, 33 | options: RequestOptions = {} 34 | ): Promise { 35 | const { method = 'GET', headers = {}, params = {} } = options; 36 | 37 | // Build query string from params 38 | const queryParams = new URLSearchParams(); 39 | Object.entries(params).forEach(([key, value]) => { 40 | if (value !== undefined && value !== null) { 41 | queryParams.append(key, String(value)); 42 | } 43 | }); 44 | 45 | const queryString = queryParams.toString(); 46 | const url = `${DOME_API_BASE_URL}${endpoint}${queryString ? `?${queryString}` : ''}`; 47 | 48 | const response = await fetch(url, { 49 | method, 50 | headers: { 51 | 'Content-Type': 'application/json', 52 | 'Authorization': `Bearer ${getApiKey()}`, 53 | ...headers, 54 | }, 55 | }); 56 | 57 | if (!response.ok) { 58 | const errorText = await response.text(); 59 | throw new Error( 60 | `Dome API error: ${response.status} ${response.statusText} - ${errorText}` 61 | ); 62 | } 63 | 64 | return response.json(); 65 | } 66 | -------------------------------------------------------------------------------- /supabase/functions/_shared/polymarket/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Polymarket Types for Trading Bot 3 | */ 4 | 5 | /** 6 | * Supported assets for 15-minute up/down markets 7 | */ 8 | export type SupportedAsset = "BTC" | "SOL" | "ETH" | "XRP"; 9 | 10 | /** 11 | * Market data from Gamma API 12 | */ 13 | export interface PolymarketMarket { 14 | id: string; 15 | question: string; 16 | conditionId: string; 17 | slug: string; 18 | title: string; 19 | description: string; 20 | outcomes: string; 21 | outcomePrices: string; 22 | volume: string; 23 | volume24hr: number; 24 | clobTokenIds: string; 25 | acceptingOrders: boolean; 26 | active: boolean; 27 | closed: boolean; 28 | endDate: string; 29 | startDate: string; 30 | } 31 | 32 | /** 33 | * Parsed token IDs for Up and Down outcomes 34 | */ 35 | export interface TokenIds { 36 | up: string; 37 | down: string; 38 | } 39 | 40 | /** 41 | * Order side type 42 | */ 43 | export type OrderSideType = "BUY" | "SELL"; 44 | 45 | /** 46 | * Order arguments for creating a new order 47 | */ 48 | export interface OrderArgs { 49 | tokenId: string; 50 | price: number; 51 | size: number; 52 | side: OrderSideType; 53 | } 54 | 55 | /** 56 | * Order response from the CLOB API 57 | */ 58 | export interface OrderResponse { 59 | success: boolean; 60 | orderId?: string; 61 | errorMsg?: string; 62 | transactionHash?: string; 63 | status?: string; 64 | } 65 | 66 | /** 67 | * Log entry for bot execution 68 | */ 69 | export interface BotLogEntry { 70 | timestamp: string; 71 | level: "INFO" | "WARN" | "ERROR" | "SUCCESS"; 72 | message: string; 73 | details?: Record; 74 | } 75 | 76 | /** 77 | * Polymarket client configuration 78 | */ 79 | export interface PolymarketClientConfig { 80 | /** Private key for signing orders */ 81 | privateKey: string; 82 | /** Proxy/funder address (shown under profile pic on Polymarket) */ 83 | proxyAddress: string; 84 | /** Signature type: 0 = EOA, 1 = Magic/Email, 2 = Browser Wallet */ 85 | signatureType?: number; 86 | } 87 | -------------------------------------------------------------------------------- /supabase/functions/polymarket-up-down-15-markets-limit-order-bot/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type definitions for polymarket-up-down-15-markets-limit-order-bot edge function 3 | */ 4 | 5 | import type { SupportedAsset, BotLogEntry } from "../_shared/polymarket/types.ts"; 6 | 7 | /** 8 | * Request body for the limit order bot 9 | */ 10 | export interface LimitOrderBotRequest { 11 | /** Asset to trade (BTC, SOL, ETH, XRP) */ 12 | asset: SupportedAsset; 13 | /** Order price as percentage (e.g., 48 for 48%). Defaults to 48% */ 14 | price?: number; 15 | /** Order size in USD total per side. Defaults to $25 */ 16 | sizeUsd?: number; 17 | } 18 | 19 | /** 20 | * Order placement result for a single side 21 | */ 22 | export interface OrderPlacementResult { 23 | success: boolean; 24 | orderId?: string; 25 | errorMsg?: string; 26 | status?: string; 27 | } 28 | 29 | /** 30 | * Result for a single market's order placement 31 | */ 32 | export interface MarketOrderResult { 33 | /** Market slug identifier */ 34 | marketSlug: string; 35 | /** Market title/question */ 36 | marketTitle?: string; 37 | /** Market start time (formatted) */ 38 | marketStartTime: string; 39 | /** Unix timestamp of market start */ 40 | targetTimestamp: number; 41 | /** Orders placed for Up and Down sides */ 42 | ordersPlaced?: { 43 | up?: OrderPlacementResult; 44 | down?: OrderPlacementResult; 45 | }; 46 | /** Error message if market processing failed */ 47 | error?: string; 48 | } 49 | 50 | /** 51 | * Response from the limit order bot 52 | */ 53 | export interface LimitOrderBotResponse { 54 | /** Whether the request was successful */ 55 | success: boolean; 56 | /** Response data (only present on success) */ 57 | data?: { 58 | /** Asset traded */ 59 | asset: SupportedAsset; 60 | /** Order price as percentage */ 61 | pricePercent: number; 62 | /** Order size in USD total */ 63 | sizeUsd: number; 64 | /** Result for the market */ 65 | market: MarketOrderResult; 66 | }; 67 | /** Log entries from the bot execution */ 68 | logs: BotLogEntry[]; 69 | /** Error message (only present on failure) */ 70 | error?: string; 71 | } 72 | -------------------------------------------------------------------------------- /terminal/src/app/api/analyze-event-markets/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import type { AnalyzeMarketRequest, AnalyzeMarketResponse } from "@/types/api"; 3 | 4 | /** 5 | * Server-side API route to proxy requests to the Supabase Edge Function. 6 | * This keeps the Supabase URL and keys secure on the server. 7 | */ 8 | export async function POST(request: NextRequest) { 9 | try { 10 | // Read environment variables server-side 11 | const supabaseUrl = process.env.SUPABASE_URL; 12 | const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; 13 | 14 | if (!supabaseUrl || !supabaseAnonKey) { 15 | return NextResponse.json( 16 | { 17 | success: false, 18 | error: "Server configuration error: Missing Supabase credentials", 19 | }, 20 | { status: 500 } 21 | ); 22 | } 23 | 24 | // Parse request body 25 | const body: AnalyzeMarketRequest = await request.json(); 26 | 27 | // Validate required fields 28 | if (!body.url) { 29 | return NextResponse.json( 30 | { 31 | success: false, 32 | error: "Missing required field: url", 33 | }, 34 | { status: 400 } 35 | ); 36 | } 37 | 38 | if (!body.question) { 39 | return NextResponse.json( 40 | { 41 | success: false, 42 | error: "Missing required field: question", 43 | }, 44 | { status: 400 } 45 | ); 46 | } 47 | 48 | // Auto-detect pmType based on URL if not provided 49 | const pmType = body.pmType || (body.url.includes("kalshi.com") ? "Kalshi" : "Polymarket"); 50 | 51 | // Call the Supabase Edge Function 52 | const edgeFunctionUrl = process.env.SUPABASE_EDGE_FUNCTION_ANALYZE_EVENT_MARKETS 53 | || `${supabaseUrl}/functions/v1/analyze-event-markets`; 54 | 55 | const response = await fetch(edgeFunctionUrl, { 56 | method: "POST", 57 | headers: { 58 | "Content-Type": "application/json", 59 | Authorization: `Bearer ${supabaseAnonKey}`, 60 | apikey: supabaseAnonKey, 61 | }, 62 | body: JSON.stringify({ 63 | url: body.url, 64 | question: body.question, 65 | pmType, 66 | model: body.model, 67 | }), 68 | }); 69 | 70 | const data: AnalyzeMarketResponse = await response.json(); 71 | 72 | return NextResponse.json(data, { status: response.status }); 73 | } catch (error) { 74 | console.error("Error in analyze-event-markets API route:", error); 75 | return NextResponse.json( 76 | { 77 | success: false, 78 | error: error instanceof Error ? error.message : "An unexpected error occurred", 79 | }, 80 | { status: 500 } 81 | ); 82 | } 83 | } 84 | 85 | -------------------------------------------------------------------------------- /terminal/src/types/api.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Request body for the analyze-market endpoint 3 | */ 4 | export interface AnalyzeMarketRequest { 5 | /** Prediction market URL - the ticker will be extracted from the last path segment */ 6 | url: string; 7 | /** User's question about this event/markets */ 8 | question: string; 9 | /** Prediction market type (e.g., "Kalshi", "Polymarket") - auto-detected if not provided */ 10 | pmType?: string; 11 | /** Grok model to use for analysis */ 12 | model?: string; 13 | } 14 | 15 | /** 16 | * AI analysis result for an event's markets 17 | */ 18 | export interface MarketAnalysis { 19 | /** Event ticker identifier */ 20 | event_ticker: string; 21 | /** Market ticker with the best alpha opportunity (if any) */ 22 | ticker: string; 23 | /** Market title/question */ 24 | title: string; 25 | /** Current market probability (0-100) */ 26 | marketProbability: number; 27 | /** AI's estimated actual probability (0-100) */ 28 | estimatedActualProbability: number; 29 | /** Difference between estimated and market probability (positive = buy yes, negative = buy no) */ 30 | alphaOpportunity: number; 31 | /** Whether there is meaningful alpha opportunity */ 32 | hasAlpha: boolean; 33 | /** Which side the AI predicts will win */ 34 | predictedWinner: "YES" | "NO"; 35 | /** Confidence that the predicted winner will win (0-100) */ 36 | winnerConfidence: number; 37 | /** Recommended trading action */ 38 | recommendedAction: "BUY YES" | "BUY NO" | "NO TRADE"; 39 | /** Detailed explanation of the analysis */ 40 | reasoning: string; 41 | /** AI's confidence in this overall assessment (0-100) */ 42 | confidence: number; 43 | /** Key factors influencing the assessment */ 44 | keyFactors: string[]; 45 | /** Risks that could affect the prediction */ 46 | risks: string[]; 47 | /** Direct answer to the user's specific question */ 48 | questionAnswer: string; 49 | /** Brief summary of the analysis findings (under 270 characters) */ 50 | analysisSummary: string; 51 | } 52 | 53 | /** 54 | * Metadata included in every response 55 | */ 56 | export interface ResponseMetadata { 57 | /** Unique identifier for this request */ 58 | requestId: string; 59 | /** ISO timestamp of the response */ 60 | timestamp: string; 61 | /** Event ticker analyzed */ 62 | eventTicker: string; 63 | /** Number of markets found for the event */ 64 | marketsCount: number; 65 | /** Question asked about the event */ 66 | question: string; 67 | /** Total processing time in milliseconds */ 68 | processingTimeMs: number; 69 | /** Grok model used for analysis */ 70 | grokModel?: string; 71 | /** Total tokens consumed by Grok */ 72 | grokTokensUsed?: number; 73 | } 74 | 75 | /** 76 | * Response from the analyze-market endpoint 77 | */ 78 | export interface AnalyzeMarketResponse { 79 | /** Whether the request was successful */ 80 | success: boolean; 81 | /** Analysis result (only present on success) */ 82 | data?: MarketAnalysis; 83 | /** Request metadata */ 84 | metadata: ResponseMetadata; 85 | /** Direct URL to the market (only present on success) */ 86 | "pm-market-url"?: string; 87 | /** Error message (only present on failure) */ 88 | error?: string; 89 | } 90 | 91 | -------------------------------------------------------------------------------- /supabase/functions/analyze-event-markets/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Type definitions for analyze-event-markets edge function 3 | */ 4 | 5 | /** 6 | * Request body for the analyze-market endpoint 7 | */ 8 | export interface AnalyzeMarketRequest { 9 | /** Prediction market URL - the ticker will be extracted from the last path segment */ 10 | url: string; 11 | /** User's question about this event/markets */ 12 | question: string; 13 | /** Prediction market type (e.g., "Kalshi", "Polymarket") */ 14 | pmType?: string; 15 | /** Grok model to use for analysis (default: grok-4-1-fast-reasoning) */ 16 | model?: string; 17 | } 18 | 19 | /** 20 | * AI analysis result for an event's markets 21 | */ 22 | export interface MarketAnalysis { 23 | /** Event ticker identifier */ 24 | event_ticker: string; 25 | /** Market ticker with the best alpha opportunity (if any) */ 26 | ticker: string; 27 | /** Market title/question */ 28 | title: string; 29 | /** Current market probability (0-100) */ 30 | marketProbability: number; 31 | /** AI's estimated actual probability (0-100) */ 32 | estimatedActualProbability: number; 33 | /** Difference between estimated and market probability (positive = buy yes, negative = buy no) */ 34 | alphaOpportunity: number; 35 | /** Whether there is meaningful alpha opportunity */ 36 | hasAlpha: boolean; 37 | /** Which side the AI predicts will win */ 38 | predictedWinner: "YES" | "NO"; 39 | /** Confidence that the predicted winner will win (0-100) */ 40 | winnerConfidence: number; 41 | /** Recommended trading action */ 42 | recommendedAction: "BUY YES" | "BUY NO" | "NO TRADE"; 43 | /** Detailed explanation of the analysis */ 44 | reasoning: string; 45 | /** AI's confidence in this overall assessment (0-100) */ 46 | confidence: number; 47 | /** Key factors influencing the assessment */ 48 | keyFactors: string[]; 49 | /** Risks that could affect the prediction */ 50 | risks: string[]; 51 | /** Direct answer to the user's specific question */ 52 | questionAnswer: string; 53 | /** Brief summary of the analysis findings (under 270 characters) */ 54 | analysisSummary: string; 55 | } 56 | 57 | /** 58 | * Metadata included in every response 59 | */ 60 | export interface ResponseMetadata { 61 | /** Unique identifier for this request */ 62 | requestId: string; 63 | /** ISO timestamp of the response */ 64 | timestamp: string; 65 | /** Event ticker analyzed */ 66 | eventTicker: string; 67 | /** Number of markets found for the event */ 68 | marketsCount: number; 69 | /** Question asked about the event */ 70 | question: string; 71 | /** Total processing time in milliseconds */ 72 | processingTimeMs: number; 73 | /** Grok model used for analysis */ 74 | grokModel?: string; 75 | /** Total tokens consumed by Grok */ 76 | grokTokensUsed?: number; 77 | } 78 | 79 | /** 80 | * Response from the analyze-market endpoint 81 | */ 82 | export interface AnalyzeMarketResponse { 83 | /** Whether the request was successful */ 84 | success: boolean; 85 | /** Analysis result (only present on success) */ 86 | data?: MarketAnalysis; 87 | /** Request metadata */ 88 | metadata: ResponseMetadata; 89 | /** Direct URL to the market (only present on success) */ 90 | "pm-market-url"?: string; 91 | /** Error message (only present on failure) */ 92 | error?: string; 93 | } 94 | 95 | -------------------------------------------------------------------------------- /supabase/functions/_shared/dome/endpoints.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Dome API endpoint functions 3 | * @see https://docs.domeapi.io/ 4 | */ 5 | 6 | import { request } from './client.ts'; 7 | import type { 8 | PolymarketMarketsResponse, 9 | KalshiMarketsResponse, 10 | KalshiMarket, 11 | PaginationParams, 12 | } from './types.ts'; 13 | 14 | // ============================================================================ 15 | // Polymarket Endpoints 16 | // ============================================================================ 17 | 18 | /** 19 | * Gets Polymarket markets 20 | * @param params Pagination and filter parameters 21 | * @returns Promise resolving to markets list 22 | */ 23 | export async function getPolymarketMarkets( 24 | params?: PaginationParams & { 25 | active?: boolean; 26 | closed?: boolean; 27 | market_slug?: string; 28 | } 29 | ): Promise { 30 | return request('/polymarket/markets', { 31 | params: params as Record, 32 | }); 33 | } 34 | 35 | // ============================================================================ 36 | // Kalshi Endpoints 37 | // ============================================================================ 38 | 39 | /** 40 | * Gets Kalshi markets 41 | * @param params Pagination and filter parameters 42 | * @returns Promise resolving to markets list 43 | */ 44 | async function getKalshiMarkets( 45 | params?: PaginationParams & { 46 | eventTicker?: string; 47 | status?: 'open' | 'closed' | 'settled'; 48 | } 49 | ): Promise { 50 | return request('/kalshi/markets', { 51 | params: { 52 | cursor: params?.cursor, 53 | limit: params?.limit, 54 | event_ticker: params?.eventTicker, 55 | status: params?.status, 56 | }, 57 | }); 58 | } 59 | 60 | /** 61 | * Gets Kalshi markets by event ticker 62 | * @param eventTicker Event ticker identifier 63 | * @param status Market status filter 64 | * @returns Promise resolving to markets list 65 | * 66 | * @example 67 | * getKalshiMarketsByEvent("KXBTC-25DEC") 68 | */ 69 | export async function getKalshiMarketsByEvent( 70 | eventTicker: string, 71 | status: 'open' | 'closed' | 'settled' = 'open' 72 | ): Promise { 73 | const response = await getKalshiMarkets({ 74 | eventTicker, 75 | status, 76 | limit: 100, // Dome API max limit is 100 77 | }); 78 | return response.markets; 79 | } 80 | 81 | // ============================================================================ 82 | // Helper Functions 83 | // ============================================================================ 84 | 85 | /** 86 | * Builds a Kalshi market URL from a market ticker 87 | * Extracts the first segment before "-" and constructs the URL 88 | * 89 | * @example 90 | * buildKalshiMarketUrl("KXBTCD-25DEC1217-T89999.99") // returns "https://kalshi.com/markets/KXBTCD" 91 | * 92 | * @param ticker Market ticker string 93 | * @returns Kalshi market URL 94 | */ 95 | export function buildKalshiMarketUrl(ticker: string): string { 96 | const firstElement = ticker.split("-")[0]; 97 | return `https://kalshi.com/markets/${firstElement}`; 98 | } 99 | 100 | /** 101 | * Builds a Polymarket event URL from a market slug 102 | * @param slug Event slug identifier 103 | * @returns Polymarket event URL 104 | */ 105 | export function buildPolymarketUrl(slug: string): string { 106 | return `https://polymarket.com/event/${slug}`; 107 | } 108 | -------------------------------------------------------------------------------- /docs/features/market-analysis.md: -------------------------------------------------------------------------------- 1 | # Market Analysis Setup 2 | 3 | This document explains how to configure the environment variables required for the **AI Market Analysis** feature in PredictOS. 4 | 5 | ## Overview 6 | 7 | The Market Analysis feature allows you to paste a Kalshi or Polymarket URL and get instant AI-powered analysis with probability estimates, confidence scores, and trading recommendations. 8 | 9 | ## Required Environment Variables 10 | 11 | Add these to your `supabase/.env.local` file: 12 | 13 | ### 1. Dome API Key (Required) 14 | 15 | ```env 16 | DOME_API_KEY=your_dome_api_key 17 | ``` 18 | 19 | **What it's for:** Dome API provides unified access to prediction market data from Kalshi, Polymarket, and other platforms. 20 | 21 | **How to get it:** 22 | 1. Go to [https://dashboard.domeapi.io](https://dashboard.domeapi.io) 23 | 2. Create an account or sign in 24 | 3. Navigate to API Keys section 25 | 4. Generate a new API key 26 | 27 | ### 2. AI Provider API Key (One Required) 28 | 29 | You need **at least one** of the following AI provider keys: 30 | 31 | #### Option A: xAI Grok (Recommended) 32 | 33 | ```env 34 | XAI_API_KEY=your_xai_api_key 35 | ``` 36 | 37 | **How to get it:** 38 | 1. Go to [https://x.ai](https://x.ai) 39 | 2. Create an account or sign in 40 | 3. Navigate to API section 41 | 4. Generate a new API key 42 | 43 | #### Option B: OpenAI GPT 44 | 45 | ```env 46 | OPENAI_API_KEY=your_openai_api_key 47 | ``` 48 | 49 | **How to get it:** 50 | 1. Go to [https://platform.openai.com](https://platform.openai.com) 51 | 2. Create an account or sign in 52 | 3. Navigate to API Keys 53 | 4. Generate a new API key 54 | 55 | > 💡 **Note:** You can configure both providers to switch between them. If both are configured, the system will use xAI Grok by default. 56 | 57 | ## Complete Example 58 | 59 | Your `supabase/.env.local` file should look like this: 60 | 61 | ```env 62 | # Dome API - Required for market data 63 | DOME_API_KEY=your_dome_api_key 64 | 65 | # AI Provider - At least one is required 66 | XAI_API_KEY=your_xai_api_key # Option A: xAI Grok 67 | OPENAI_API_KEY=your_openai_api_key # Option B: OpenAI GPT 68 | ``` 69 | 70 | ## Frontend Environment Variables 71 | 72 | In addition to the backend variables above, you need to configure the frontend (`terminal/.env`): 73 | 74 | ```env 75 | SUPABASE_URL= 76 | SUPABASE_ANON_KEY= 77 | 78 | # Edge Function URL (for local development) 79 | SUPABASE_EDGE_FUNCTION_ANALYZE_EVENT_MARKETS=http://127.0.0.1:54321/functions/v1/analyze-event-markets 80 | ``` 81 | 82 | ## Verification 83 | 84 | After setting up your environment variables: 85 | 86 | 1. Start the Supabase services: 87 | ```bash 88 | cd supabase 89 | supabase start 90 | supabase functions serve --env-file .env.local 91 | ``` 92 | 93 | 2. Start the frontend: 94 | ```bash 95 | cd terminal 96 | npm run dev 97 | ``` 98 | 99 | 3. Navigate to [http://localhost:3000/market-analysis](http://localhost:3000/market-analysis) 100 | 101 | 4. Try pasting a Kalshi or Polymarket URL to test the analysis feature 102 | 103 | ## Troubleshooting 104 | 105 | | Error | Solution | 106 | |-------|----------| 107 | | "DOME_API_KEY is not configured" | Add your Dome API key to `.env.local` | 108 | | "No AI provider configured" | Add either XAI_API_KEY or OPENAI_API_KEY | 109 | | "Invalid API key" | Double-check your API keys are correct and active | 110 | | "Rate limit exceeded" | Wait a few minutes or upgrade your API plan | 111 | 112 | --- 113 | 114 | ← [Back to main README](../../README.md) 115 | 116 | -------------------------------------------------------------------------------- /terminal/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | prefix: "", 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | fontFamily: { 21 | sans: ["Space Grotesk", "system-ui", "sans-serif"], 22 | mono: ["JetBrains Mono", "monospace"], 23 | display: ["Orbitron", "sans-serif"], 24 | }, 25 | colors: { 26 | border: "hsl(var(--border))", 27 | input: "hsl(var(--input))", 28 | ring: "hsl(var(--ring))", 29 | background: "hsl(var(--background))", 30 | foreground: "hsl(var(--foreground))", 31 | primary: { 32 | DEFAULT: "hsl(var(--primary))", 33 | foreground: "hsl(var(--primary-foreground))", 34 | }, 35 | secondary: { 36 | DEFAULT: "hsl(var(--secondary))", 37 | foreground: "hsl(var(--secondary-foreground))", 38 | }, 39 | destructive: { 40 | DEFAULT: "hsl(var(--destructive))", 41 | foreground: "hsl(var(--destructive-foreground))", 42 | }, 43 | success: { 44 | DEFAULT: "hsl(var(--success))", 45 | foreground: "hsl(var(--success-foreground))", 46 | }, 47 | danger: { 48 | DEFAULT: "hsl(var(--danger))", 49 | foreground: "hsl(var(--danger-foreground))", 50 | }, 51 | warning: { 52 | DEFAULT: "hsl(var(--warning))", 53 | foreground: "hsl(var(--warning-foreground))", 54 | }, 55 | muted: { 56 | DEFAULT: "hsl(var(--muted))", 57 | foreground: "hsl(var(--muted-foreground))", 58 | }, 59 | accent: { 60 | DEFAULT: "hsl(var(--accent))", 61 | foreground: "hsl(var(--accent-foreground))", 62 | }, 63 | popover: { 64 | DEFAULT: "hsl(var(--popover))", 65 | foreground: "hsl(var(--popover-foreground))", 66 | }, 67 | card: { 68 | DEFAULT: "hsl(var(--card))", 69 | foreground: "hsl(var(--card-foreground))", 70 | }, 71 | }, 72 | borderRadius: { 73 | lg: "var(--radius)", 74 | md: "calc(var(--radius) - 2px)", 75 | sm: "calc(var(--radius) - 4px)", 76 | }, 77 | keyframes: { 78 | "fade-in": { 79 | from: { opacity: "0" }, 80 | to: { opacity: "1" }, 81 | }, 82 | "fade-in-up": { 83 | from: { opacity: "0", transform: "translateY(10px)" }, 84 | to: { opacity: "1", transform: "translateY(0)" }, 85 | }, 86 | "slide-up": { 87 | from: { opacity: "0", transform: "translateY(20px)" }, 88 | to: { opacity: "1", transform: "translateY(0)" }, 89 | }, 90 | "pulse-live": { 91 | "0%, 100%": { opacity: "1" }, 92 | "50%": { opacity: "0.5" }, 93 | }, 94 | "pulse-glow": { 95 | "0%, 100%": { boxShadow: "0 0 10px hsl(var(--primary) / 0.4)" }, 96 | "50%": { boxShadow: "0 0 25px hsl(var(--primary) / 0.7)" }, 97 | }, 98 | }, 99 | animation: { 100 | "fade-in": "fade-in 0.6s ease-out forwards", 101 | "fade-in-up": "fade-in-up 0.6s ease-out forwards", 102 | "slide-up": "slide-up 0.5s ease-out forwards", 103 | "pulse-live": "pulse-live 2s cubic-bezier(0.4, 0, 0.6, 1) infinite", 104 | "pulse-glow": "pulse-glow 2s ease-in-out infinite", 105 | }, 106 | }, 107 | }, 108 | plugins: [], 109 | }; 110 | 111 | export default config; 112 | 113 | -------------------------------------------------------------------------------- /supabase/functions/_shared/ai/types.ts: -------------------------------------------------------------------------------- 1 | // Types for Grok API requests and responses 2 | export interface GrokRequestInput { 3 | role: "user" | "assistant" | "system"; 4 | content: string; 5 | } 6 | 7 | export interface GrokRequestTool { 8 | type: "web_search" | "x_search" 9 | } 10 | 11 | export interface GrokRequestPayload { 12 | model: string; 13 | input: GrokRequestInput[]; 14 | tools?: GrokRequestTool[]; 15 | tool_choice?: "auto" | "none" | "required"; 16 | temperature?: number | null; 17 | top_p?: number | null; 18 | max_output_tokens?: number | null; 19 | parallel_tool_calls?: boolean; 20 | store?: boolean; 21 | previous_response_id?: string | null; 22 | response_format?: { 23 | type: string; 24 | }; 25 | } 26 | 27 | // Types for OpenAI API requests and responses 28 | export interface OpenAIRequestInput { 29 | role: "user" | "assistant" | "system"; 30 | content: string; 31 | } 32 | 33 | export interface OpenAIRequestPayload { 34 | model: string; 35 | input: OpenAIRequestInput[]; 36 | temperature?: number | null; 37 | top_p?: number | null; 38 | max_output_tokens?: number | null; 39 | store?: boolean; 40 | text?: { 41 | format: { 42 | type: string; 43 | }; 44 | }; 45 | } 46 | 47 | export interface OpenAIUsage { 48 | input_tokens: number; 49 | input_tokens_details?: { 50 | cached_tokens?: number; 51 | }; 52 | output_tokens: number; 53 | output_tokens_details?: { 54 | reasoning_tokens?: number; 55 | }; 56 | total_tokens: number; 57 | } 58 | 59 | export interface OpenAIOutputText { 60 | type: "output_text"; 61 | text: string; 62 | annotations?: unknown[]; 63 | } 64 | 65 | export interface OpenAIMessage { 66 | content: OpenAIOutputText[]; 67 | id: string; 68 | role: "assistant"; 69 | type: "message"; 70 | status: "completed" | "failed" | "pending"; 71 | } 72 | 73 | export type OpenAIOutputItem = OpenAIMessage; 74 | 75 | export interface OpenAIResponseResult { 76 | created_at: number; 77 | id: string; 78 | model: string; 79 | object: "response"; 80 | output: OpenAIOutputItem[]; 81 | temperature: number | null; 82 | top_p: number | null; 83 | usage: OpenAIUsage; 84 | status: "completed" | "failed" | "pending"; 85 | } 86 | 87 | export interface GrokUsage { 88 | input_tokens: number; 89 | input_tokens_details?: { 90 | cached_tokens?: number; 91 | }; 92 | output_tokens: number; 93 | output_tokens_details?: { 94 | reasoning_tokens?: number; 95 | }; 96 | total_tokens: number; 97 | num_sources_used?: number; 98 | num_server_side_tools_used?: number; 99 | server_side_tool_usage_details?: { 100 | web_search_calls?: number; 101 | x_search_calls?: number; 102 | code_interpreter_calls?: number; 103 | file_search_calls?: number; 104 | mcp_calls?: number; 105 | document_search_calls?: number; 106 | }; 107 | } 108 | 109 | export interface GrokReasoning { 110 | effort?: string | null; 111 | summary?: string | null; 112 | } 113 | 114 | export interface GrokTextFormat { 115 | format: { 116 | type: "text"; 117 | }; 118 | } 119 | 120 | export interface GrokAnnotation { 121 | type: "url_citation"; 122 | url: string; 123 | } 124 | 125 | export interface GrokOutputText { 126 | type: "output_text"; 127 | text: string; 128 | logprobs?: unknown[]; 129 | annotations?: GrokAnnotation[]; 130 | } 131 | 132 | export interface GrokCustomToolCall { 133 | call_id: string; 134 | input: string; 135 | name: string; 136 | type: "custom_tool_call"; 137 | id: string; 138 | status: "completed" | "failed" | "pending"; 139 | } 140 | 141 | export interface GrokMessage { 142 | content: GrokOutputText[]; 143 | id: string; 144 | role: "assistant"; 145 | type: "message"; 146 | status: "completed" | "failed" | "pending"; 147 | } 148 | 149 | export type GrokOutputItem = GrokCustomToolCall | GrokMessage; 150 | 151 | export interface GrokResponseResult { 152 | created_at: number; 153 | id: string; 154 | max_output_tokens: number | null; 155 | model: string; 156 | object: "response"; 157 | output: GrokOutputItem[]; 158 | parallel_tool_calls: boolean; 159 | previous_response_id: string | null; 160 | reasoning: GrokReasoning; 161 | temperature: number | null; 162 | text: GrokTextFormat; 163 | tool_choice: "auto" | "none" | "required"; 164 | tools: GrokRequestTool[]; 165 | top_p: number | null; 166 | usage: GrokUsage; 167 | user: string | null; 168 | incomplete_details: unknown | null; 169 | status: "completed" | "failed" | "pending"; 170 | store: boolean; 171 | metadata: Record; 172 | } 173 | 174 | -------------------------------------------------------------------------------- /terminal/src/components/Terminal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import Image from "next/image"; 5 | import TerminalInput, { type AIModel } from "./TerminalInput"; 6 | import AnalysisOutput from "./AnalysisOutput"; 7 | import type { MarketAnalysis, AnalyzeMarketResponse } from "@/types/api"; 8 | 9 | interface AnalysisResult { 10 | id: string; 11 | analysis: MarketAnalysis; 12 | timestamp: Date; 13 | marketUrl?: string; 14 | } 15 | 16 | /** 17 | * Validate that the URL is a supported prediction market URL (Kalshi or Polymarket) 18 | */ 19 | function isPredictionMarketUrl(url: string): boolean { 20 | try { 21 | const urlObj = new URL(url); 22 | return urlObj.hostname.includes("kalshi") || urlObj.hostname.includes("polymarket"); 23 | } catch { 24 | return false; 25 | } 26 | } 27 | 28 | const Terminal = () => { 29 | const [analyses, setAnalyses] = useState([]); 30 | const [isLoading, setIsLoading] = useState(false); 31 | const [error, setError] = useState(null); 32 | const [shouldClearInput, setShouldClearInput] = useState(false); 33 | 34 | const handleSubmit = async (url: string, model: AIModel) => { 35 | setIsLoading(true); 36 | setError(null); 37 | setShouldClearInput(false); 38 | 39 | try { 40 | // Validate the URL 41 | if (!isPredictionMarketUrl(url)) { 42 | throw new Error("Invalid URL. Please paste a valid Kalshi or Polymarket URL."); 43 | } 44 | 45 | // Call our server-side API 46 | const response = await fetch("/api/analyze-event-markets", { 47 | method: "POST", 48 | headers: { 49 | "Content-Type": "application/json", 50 | }, 51 | body: JSON.stringify({ 52 | url, 53 | question: "What is the best trading opportunity in this market? Analyze the probability and provide a recommendation.", 54 | model, 55 | }), 56 | }); 57 | 58 | const data: AnalyzeMarketResponse = await response.json(); 59 | 60 | if (!data.success || !data.data) { 61 | throw new Error(data.error || "Failed to analyze market"); 62 | } 63 | 64 | const result: AnalysisResult = { 65 | id: data.metadata.requestId, 66 | analysis: data.data, 67 | timestamp: new Date(data.metadata.timestamp), 68 | marketUrl: data["pm-market-url"], 69 | }; 70 | 71 | setAnalyses(prev => [result, ...prev]); 72 | setShouldClearInput(true); 73 | } catch (err) { 74 | setError(err instanceof Error ? err.message : "An unexpected error occurred"); 75 | } finally { 76 | setIsLoading(false); 77 | } 78 | }; 79 | 80 | return ( 81 |
82 |
83 |
84 | {/* Header - Always visible */} 85 |
86 | {/* AI Market Analysis */} 87 |
88 |

89 | AI Market Analysis 90 |

91 |

92 | Paste a Kalshi or Polymarket URL to get instant AI-powered analysis 93 | with probability estimates and alpha opportunities. 94 |

95 |
96 | 97 | {/* Powered by Dome */} 98 | 115 |
116 | 117 | {/* Error Display */} 118 | {error && ( 119 |
120 |

{`> Error: ${error}`}

121 |
122 | )} 123 | 124 | {/* Input */} 125 | 126 | 127 | {/* Analysis Results */} 128 |
129 | {analyses.map((result) => ( 130 | 136 | ))} 137 |
138 |
139 |
140 |
141 | ); 142 | }; 143 | 144 | export default Terminal; 145 | 146 | -------------------------------------------------------------------------------- /supabase/functions/_shared/ai/callOpenAI.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | OpenAIRequestPayload, 3 | OpenAIResponseResult, 4 | } from "./types.ts"; 5 | 6 | /** 7 | * Check if an error is retryable (network errors, timeouts, etc.) 8 | */ 9 | function isRetryableError(error: unknown): boolean { 10 | if (error instanceof Error) { 11 | const message = error.message.toLowerCase(); 12 | return ( 13 | message.includes("connection") || 14 | message.includes("tls") || 15 | message.includes("timeout") || 16 | message.includes("eof") || 17 | message.includes("network") || 18 | message.includes("fetch failed") 19 | ); 20 | } 21 | return false; 22 | } 23 | 24 | /** 25 | * Sleep for a given number of milliseconds 26 | */ 27 | function sleep(ms: number): Promise { 28 | return new Promise((resolve) => setTimeout(resolve, ms)); 29 | } 30 | 31 | /** 32 | * Make a fetch request with timeout 33 | */ 34 | async function fetchWithTimeout( 35 | url: string, 36 | options: RequestInit, 37 | timeoutMs: number = 120000, // 2 minutes default timeout 38 | ): Promise { 39 | const controller = new AbortController(); 40 | const timeoutId = setTimeout(() => controller.abort(), timeoutMs); 41 | 42 | try { 43 | const response = await fetch(url, { 44 | ...options, 45 | signal: controller.signal, 46 | }); 47 | clearTimeout(timeoutId); 48 | return response; 49 | } catch (error) { 50 | clearTimeout(timeoutId); 51 | if (error instanceof Error && error.name === "AbortError") { 52 | throw new Error(`Request timeout after ${timeoutMs}ms`); 53 | } 54 | throw error; 55 | } 56 | } 57 | 58 | /** 59 | * Call OpenAI API with the Responses API 60 | * 61 | * @param message User message to send 62 | * @param systemPrompt System prompt for the AI 63 | * @param responseFormat Response format type (e.g., "json_object") 64 | * @param model OpenAI model to use 65 | * @param maxRetries Maximum number of retries on failure 66 | * @returns OpenAI response result 67 | */ 68 | export async function callOpenAIResponses( 69 | message: string, 70 | systemPrompt: string, 71 | responseFormat: string, 72 | model: string = "gpt-4.1", 73 | maxRetries: number = 3, 74 | ): Promise { 75 | const apiKey = Deno.env.get("OPENAI_API_KEY"); 76 | if (!apiKey) { 77 | throw new Error("OPENAI_API_KEY environment variable is not set"); 78 | } 79 | 80 | const payload: OpenAIRequestPayload = { 81 | model, 82 | input: [ 83 | { 84 | role: "system", 85 | content: systemPrompt, 86 | }, 87 | { 88 | role: "user", 89 | content: message, 90 | }, 91 | ], 92 | text: { 93 | format: { 94 | type: responseFormat, 95 | }, 96 | }, 97 | }; 98 | 99 | let lastError: Error | null = null; 100 | 101 | // Retry logic with exponential backoff 102 | for (let attempt = 0; attempt <= maxRetries; attempt++) { 103 | try { 104 | const response = await fetchWithTimeout( 105 | "https://api.openai.com/v1/responses", 106 | { 107 | method: "POST", 108 | headers: { 109 | "Content-Type": "application/json", 110 | Authorization: `Bearer ${apiKey}`, 111 | }, 112 | body: JSON.stringify(payload), 113 | }, 114 | 120000, // 2 minute timeout 115 | ); 116 | 117 | if (!response.ok) { 118 | const errorText = await response.text(); 119 | const error = new Error( 120 | `OpenAI API error: ${response.status} ${response.statusText} - ${errorText}`, 121 | ); 122 | 123 | // Don't retry on client errors (4xx) except for 429 (rate limit) 124 | if (response.status >= 400 && response.status < 500 && response.status !== 429) { 125 | throw error; 126 | } 127 | 128 | // Retry on server errors (5xx) and rate limits (429) 129 | lastError = error; 130 | if (attempt < maxRetries) { 131 | const backoffMs = Math.min(1000 * Math.pow(2, attempt), 10000); // Max 10 seconds 132 | console.warn( 133 | `OpenAI API error (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${backoffMs}ms...`, 134 | ); 135 | await sleep(backoffMs); 136 | continue; 137 | } 138 | throw error; 139 | } 140 | 141 | const rawResponse: OpenAIResponseResult = await response.json(); 142 | return rawResponse; 143 | } catch (error) { 144 | lastError = error instanceof Error ? error : new Error(String(error)); 145 | 146 | // If it's not a retryable error, throw immediately 147 | if (!isRetryableError(error) && attempt === 0) { 148 | throw lastError; 149 | } 150 | 151 | // If we've exhausted retries, throw the last error 152 | if (attempt >= maxRetries) { 153 | throw lastError; 154 | } 155 | 156 | // Exponential backoff: 1s, 2s, 4s, etc., max 10s 157 | const backoffMs = Math.min(1000 * Math.pow(2, attempt), 10000); 158 | console.warn( 159 | `Network error (attempt ${attempt + 1}/${maxRetries + 1}): ${lastError.message}. Retrying in ${backoffMs}ms...`, 160 | ); 161 | await sleep(backoffMs); 162 | } 163 | } 164 | 165 | // This should never be reached, but TypeScript needs it 166 | throw lastError || new Error("Unknown error occurred"); 167 | } 168 | 169 | -------------------------------------------------------------------------------- /supabase/functions/_shared/ai/callGrok.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GrokRequestPayload, 3 | GrokResponseResult, 4 | } from "./types.ts"; 5 | 6 | /** 7 | * Check if an error is retryable (network errors, timeouts, etc.) 8 | */ 9 | function isRetryableError(error: unknown): boolean { 10 | if (error instanceof Error) { 11 | const message = error.message.toLowerCase(); 12 | return ( 13 | message.includes("connection") || 14 | message.includes("tls") || 15 | message.includes("timeout") || 16 | message.includes("eof") || 17 | message.includes("network") || 18 | message.includes("fetch failed") 19 | ); 20 | } 21 | return false; 22 | } 23 | 24 | /** 25 | * Sleep for a given number of milliseconds 26 | */ 27 | function sleep(ms: number): Promise { 28 | return new Promise((resolve) => setTimeout(resolve, ms)); 29 | } 30 | 31 | /** 32 | * Make a fetch request with timeout 33 | */ 34 | async function fetchWithTimeout( 35 | url: string, 36 | options: RequestInit, 37 | timeoutMs: number = 120000, // 2 minutes default timeout 38 | ): Promise { 39 | const controller = new AbortController(); 40 | const timeoutId = setTimeout(() => controller.abort(), timeoutMs); 41 | 42 | try { 43 | const response = await fetch(url, { 44 | ...options, 45 | signal: controller.signal, 46 | }); 47 | clearTimeout(timeoutId); 48 | return response; 49 | } catch (error) { 50 | clearTimeout(timeoutId); 51 | if (error instanceof Error && error.name === "AbortError") { 52 | throw new Error(`Request timeout after ${timeoutMs}ms`); 53 | } 54 | throw error; 55 | } 56 | } 57 | 58 | /** 59 | * Call Grok AI with the Responses API 60 | * 61 | * @param message User message to send 62 | * @param systemPrompt System prompt for the AI 63 | * @param responseFormat Response format type (e.g., "json_object") 64 | * @param model Grok model to use 65 | * @param maxRetries Maximum number of retries on failure 66 | * @returns Grok response result 67 | */ 68 | export async function callGrokResponses( 69 | message: string, 70 | systemPrompt: string, 71 | responseFormat: string, 72 | model: string = "grok-4-1-fast-reasoning", 73 | maxRetries: number = 3, 74 | ): Promise { 75 | const apiKey = Deno.env.get("XAI_API_KEY"); 76 | if (!apiKey) { 77 | throw new Error("XAI_API_KEY environment variable is not set"); 78 | } 79 | 80 | const payload: GrokRequestPayload = { 81 | model, 82 | input: [ 83 | { 84 | role: "system", 85 | content: systemPrompt, 86 | }, 87 | { 88 | role: "user", 89 | content: message, 90 | }, 91 | ], 92 | tools: [ 93 | { 94 | type: "x_search", 95 | }, 96 | ], 97 | response_format: { 98 | type: responseFormat, 99 | } 100 | }; 101 | 102 | let lastError: Error | null = null; 103 | 104 | // Retry logic with exponential backoff 105 | for (let attempt = 0; attempt <= maxRetries; attempt++) { 106 | try { 107 | const response = await fetchWithTimeout( 108 | "https://api.x.ai/v1/responses", 109 | { 110 | method: "POST", 111 | headers: { 112 | "Content-Type": "application/json", 113 | Authorization: `Bearer ${apiKey}`, 114 | }, 115 | body: JSON.stringify(payload), 116 | }, 117 | 120000, // 2 minute timeout 118 | ); 119 | 120 | if (!response.ok) { 121 | const errorText = await response.text(); 122 | const error = new Error( 123 | `Grok API error: ${response.status} ${response.statusText} - ${errorText}`, 124 | ); 125 | 126 | // Don't retry on client errors (4xx) except for 429 (rate limit) 127 | if (response.status >= 400 && response.status < 500 && response.status !== 429) { 128 | throw error; 129 | } 130 | 131 | // Retry on server errors (5xx) and rate limits (429) 132 | lastError = error; 133 | if (attempt < maxRetries) { 134 | const backoffMs = Math.min(1000 * Math.pow(2, attempt), 10000); // Max 10 seconds 135 | console.warn( 136 | `Grok API error (attempt ${attempt + 1}/${maxRetries + 1}), retrying in ${backoffMs}ms...`, 137 | ); 138 | await sleep(backoffMs); 139 | continue; 140 | } 141 | throw error; 142 | } 143 | 144 | const rawResponse: GrokResponseResult = await response.json(); 145 | return rawResponse; 146 | } catch (error) { 147 | lastError = error instanceof Error ? error : new Error(String(error)); 148 | 149 | // If it's not a retryable error, throw immediately 150 | if (!isRetryableError(error) && attempt === 0) { 151 | throw lastError; 152 | } 153 | 154 | // If we've exhausted retries, throw the last error 155 | if (attempt >= maxRetries) { 156 | throw lastError; 157 | } 158 | 159 | // Exponential backoff: 1s, 2s, 4s, etc., max 10s 160 | const backoffMs = Math.min(1000 * Math.pow(2, attempt), 10000); 161 | console.warn( 162 | `Network error (attempt ${attempt + 1}/${maxRetries + 1}): ${lastError.message}. Retrying in ${backoffMs}ms...`, 163 | ); 164 | await sleep(backoffMs); 165 | } 166 | } 167 | 168 | // This should never be reached, but TypeScript needs it 169 | throw lastError || new Error("Unknown error occurred"); 170 | } 171 | 172 | -------------------------------------------------------------------------------- /supabase/functions/_shared/ai/prompts/analyzeEventMarkets.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Generates prompts for analyzing event markets from any prediction market platform. 3 | * Accepts raw market JSON data (Kalshi, Polymarket, etc.) without normalization. 4 | */ 5 | export function analyzeEventMarketsPrompt( 6 | markets: unknown[], 7 | eventIdentifier: string, 8 | question: string, 9 | pmType: string 10 | ): { 11 | systemPrompt: string; 12 | userPrompt: string; 13 | } { 14 | const systemPrompt = `You are a financial analyst expert in the field of prediction markets (posted on ${pmType}) that understands the latest news, events, and market trends. 15 | Your expertise lies in deeply analyzing prediction markets for a specific event, identifying if there's alpha (mispricing) opportunity, and providing a clear recommendation on which side (YES or NO) is more likely to win based on your analysis. 16 | You always provide a short analysisSummary of your findings, less than 270 characters, that is very conversational and understandable by a non-expert who just wants to understand which side it might make more sense to buy into. 17 | 18 | Your output is ALWAYS in JSON format and you are VERY STRICT about it. You must return valid JSON that matches the exact schema specified.`; 19 | 20 | const userPrompt = `# Task: Deep Analysis of an Event's Prediction Markets 21 | 22 | You are analyzing all markets for a specific event (${eventIdentifier}) to determine: 23 | 1. Whether there is an alpha (mispricing) opportunity in any of the markets 24 | 2. Which market has the best alpha opportunity (if any) 25 | 3. Which side (YES or NO) is more likely to win for that market 26 | 4. Your confidence level in this assessment 27 | 28 | ## User's query/input/question About This Event 29 | ${question} 30 | 31 | ## Platform: ${pmType} 32 | 33 | ## Event Markets (${markets.length} market${markets.length !== 1 ? 's' : ''}) 34 | 35 | ${JSON.stringify(markets, null, 2)} 36 | 37 | ## Your Analysis Process 38 | 39 | 1. **Understand the event**: Review all markets to understand the overall event and how the markets relate to each other 40 | 2. **Analyze each market**: For each market, understand the title/question, description, outcomes, and resolution rules 41 | 3. **Assess current pricing**: 42 | - For Kalshi: \`last_price\` field (0-100) represents the current implied probability for YES 43 | - For Polymarket: \`outcomePrices\` field contains an array like ["0.21", "0.79"] where the first value is the YES probability (multiply by 100 to get percentage) 44 | 4. **Research and assess**: Use your knowledge of current events, news, trends, and fundamental factors to estimate actual probabilities 45 | 5. **Find the best opportunity**: Identify which market (if any) has the best alpha opportunity 46 | 6. **Consider context**: Take into account liquidity, volume, open interest, expiration/end dates, and market status 47 | 7. **Answer the user's question**: Address the specific question asked about this event 48 | 49 | ## What is Alpha? 50 | 51 | Alpha in prediction markets occurs when there's a mispricing between: 52 | - **Market Probability**: The current market price represents the market's collective assessment 53 | - **Actual Probability**: Your assessment based on the latest information, trends, and analysis 54 | 55 | ### Example: 56 | If the market prices an event at 30% (YES side), but your analysis suggests it should be 60%, there's a 30-point alpha opportunity in buying YES. 57 | Conversely, if the market prices at 70% but you believe it should be 40%, there's alpha in buying NO. 58 | 59 | ## Output Format 60 | 61 | Return your analysis in JSON format with the following fields. Focus on the SINGLE BEST market opportunity (or the most relevant market if no alpha exists): 62 | 63 | { 64 | "event_ticker": "string - event ticker identifier", 65 | "ticker": "string - market ticker identifier for the best opportunity", 66 | "title": "string - market title", 67 | "marketProbability": number - current market probability (0-100), 68 | "estimatedActualProbability": number - your estimated actual probability (0-100), 69 | "alphaOpportunity": number - the difference (positive = buy yes, negative = buy no), 70 | "hasAlpha": boolean - whether you believe there is meaningful alpha (|alphaOpportunity| >= 5), 71 | "predictedWinner": "string - either 'YES' or 'NO' - which side you think will win", 72 | "winnerConfidence": number - confidence that your predicted winner will win (0-100), 73 | "recommendedAction": "string - either 'BUY YES', 'BUY NO', or 'NO TRADE' if no alpha", 74 | "reasoning": "string - detailed explanation of your analysis, including relevant trends, news, and factors. If there are multiple markets, explain why you chose this one.", 75 | "confidence": number - your confidence in this overall assessment (0-100), 76 | "keyFactors": ["string"] - array of key factors influencing your assessment, 77 | "risks": ["string"] - array of risks that could affect your prediction, 78 | "questionAnswer": "string - direct answer to the user's specific query/input/question", 79 | "analysisSummary": "string - brief summary of your findings under 270 characters" 80 | } 81 | 82 | ## Important Notes 83 | 84 | - Be thorough in your analysis - consider all markets for this event 85 | - If there are multiple markets, explain why you selected the one you did as the best opportunity 86 | - Consider both sides of the argument before making your recommendation 87 | - If you don't find meaningful alpha (|alphaOpportunity| < 5) in ANY market, set hasAlpha to false and recommendedAction to "NO TRADE" 88 | - Your predictedWinner should be the side you think will ultimately win (YES or NO), regardless of alpha 89 | - winnerConfidence is specifically about which side wins, while confidence is about your overall analysis quality 90 | - Be conservative with confidence scores - only assign high confidence when you have strong evidence 91 | - Your reasoning should be specific and reference actual trends, news, or data when possible 92 | - Address the user's query/input/question directly in the questionAnswer field 93 | - The analysisSummary should be conversational and to-the-point, no hype or emojis 94 | 95 | Now analyze these markets and provide your assessment. Return your response in the exact JSON format specified above.`; 96 | 97 | return { 98 | systemPrompt, 99 | userPrompt, 100 | }; 101 | } 102 | 103 | -------------------------------------------------------------------------------- /docs/features/betting-bots.md: -------------------------------------------------------------------------------- 1 | # Betting Bots Setup 2 | 3 | This document explains how to configure the environment variables required for the **Betting Bots** feature in PredictOS. 4 | 5 | ## Overview 6 | 7 | The Betting Bots feature currently includes the **Polymarket 15 Minute Up/Down Arbitrage Bot**, which automatically places limit orders on Polymarket's 15-minute up/down markets to capture arbitrage opportunities. 8 | 9 | > 🚀 More bots coming soon! 10 | 11 | ## Why It Works 12 | 13 | > 📖 Reference: [x.com/hanakoxbt/status/1999149407955308699](https://x.com/hanakoxbt/status/1999149407955308699) 14 | 15 | This strategy exploits a simple arbitrage opportunity in binary prediction markets: 16 | 17 | 1. **Find a 15m crypto market with high liquidity** 18 | 2. **Place limit orders:** buy YES at 48 cents and NO at 48 cents 19 | 3. **Wait until both orders are filled** 20 | 4. **Total cost:** $0.96 for shares on both sides 21 | 22 | **Regardless of the outcome**, one side always pays out $1.00 — guaranteeing a **~4% profit** per trade when both orders fill. 23 | 24 | ### The Math 25 | 26 | | Scenario | Cost | Payout | Profit | 27 | |----------|------|--------|--------| 28 | | "Yes" wins | $0.48 (Yes) + $0.48 (No) = $0.96 | $1.00 | +$0.04 (~4.2%) | 29 | | "No" wins | $0.48 (Yes) + $0.48 (No) = $0.96 | $1.00 | +$0.04 (~4.2%) | 30 | 31 | The bot automates this process every 15 minutes, placing straddle limit orders on both sides of the market to capture this arbitrage when both orders fill. 32 | 33 | ## Required Environment Variables 34 | 35 | Add these to your `supabase/.env.local` file: 36 | 37 | ### 1. Polymarket Wallet Private Key (Required) 38 | 39 | ```env 40 | POLYMARKET_WALLET_PRIVATE_KEY=your_wallet_private_key 41 | ``` 42 | 43 | **What it's for:** This is the private key of your Ethereum wallet that will be used to sign transactions on Polymarket. 44 | 45 | **How to get it:** 46 | 1. Create an account on P [https://polymarket.com](https://polymarket.com) 47 | 2. `profile drop-down` -> `settings` -> `Export Private Key` 48 | 3. **⚠️ IMPORTANT:** Never share your private key or commit it to version control 49 | 50 | > 🔒 **Security Best Practice:** Create a dedicated wallet for bot trading with only the funds you're willing to risk. Never use your main wallet's private key. 51 | 52 | ### 2. Polymarket Proxy Wallet Address (Required) 53 | 54 | ```env 55 | POLYMARKET_PROXY_WALLET_ADDRESS=your_proxy_wallet_address 56 | ``` 57 | 58 | **What it's for:** This is your Polymarket proxy wallet address, which is used for placing orders on Polymarket's CLOB (Central Limit Order Book). 59 | 60 | **How to get it:** 61 | 1. Create an account on [https://polymarket.com](https://polymarket.com) 62 | 2. Your proxy wallet will be created automatically 63 | 3. `profile drop-down` --> `under username` --> `click copy` 64 | 65 | 66 | > 💡 **Note:** The proxy wallet is different from your main wallet. It's a smart contract wallet that Polymarket creates for you to interact with their order book. 67 | 68 | ## Complete Example 69 | 70 | Your `supabase/.env.local` file should include these for betting bots: 71 | 72 | ```env 73 | # Polymarket Bot Configuration - Required for Betting Bots 74 | POLYMARKET_WALLET_PRIVATE_KEY=0x...your_private_key_here 75 | POLYMARKET_PROXY_WALLET_ADDRESS=0x...your_proxy_wallet_address_here 76 | ``` 77 | 78 | ## Frontend Environment Variables 79 | 80 | In addition to the backend variables above, you need to configure the frontend (`terminal/.env`): 81 | 82 | ```env 83 | SUPABASE_URL= 84 | SUPABASE_ANON_KEY= 85 | 86 | # Edge Function URL (for local development) 87 | SUPABASE_EDGE_FUNCTION_BETTING_BOT=http://127.0.0.1:54321/functions/v1/polymarket-up-down-15-markets 88 | ``` 89 | 90 | ## Full Environment File 91 | 92 | If you're using both Market Analysis and Betting Bots, your complete `supabase/.env.local` should look like: 93 | 94 | ```env 95 | # ============================================ 96 | # Market Analysis Configuration 97 | # ============================================ 98 | 99 | # Dome API - Required for market data 100 | DOME_API_KEY=your_dome_api_key 101 | 102 | # AI Provider - At least one is required 103 | XAI_API_KEY=your_xai_api_key 104 | OPENAI_API_KEY=your_openai_api_key 105 | 106 | # ============================================ 107 | # Betting Bots Configuration 108 | # ============================================ 109 | 110 | # Polymarket Bot - Required for Betting Bots 111 | POLYMARKET_WALLET_PRIVATE_KEY=0x...your_private_key 112 | POLYMARKET_PROXY_WALLET_ADDRESS=0x...your_proxy_wallet 113 | ``` 114 | 115 | ## Verification 116 | 117 | After setting up your environment variables: 118 | 119 | 1. Start the Supabase services: 120 | ```bash 121 | cd supabase 122 | supabase start 123 | supabase functions serve --env-file .env.local 124 | ``` 125 | 126 | 2. Start the frontend: 127 | ```bash 128 | cd terminal 129 | npm run dev 130 | ``` 131 | 132 | 3. Navigate to [http://localhost:3000/betting-bots](http://localhost:3000/betting-bots) 133 | 134 | 4. Configure your bot parameters and start the bot to test 135 | 136 | ## Bot Parameters 137 | 138 | The Polymarket 15 Minute Up/Down Bot accepts the following parameters: 139 | 140 | | Parameter | Description | Default | 141 | |-----------|-------------|---------| 142 | | Asset Symbol | The cryptocurrency to trade (e.g., BTC, ETH, SOL) | BTC | 143 | | Bet Amount | Amount in USDC per bet on each side | 25 | 144 | | Price Threshold | Price target for each side | 0.48 | 145 | 146 | ## Troubleshooting 147 | 148 | | Error | Solution | 149 | |-------|----------| 150 | | "Private key not configured" | Add POLYMARKET_WALLET_PRIVATE_KEY to `.env.local` | 151 | | "Proxy wallet not configured" | Add POLYMARKET_PROXY_WALLET_ADDRESS to `.env.local` | 152 | | "Invalid private key" | Ensure your private key is correctly formatted (with or without 0x prefix) | 153 | | "Insufficient balance" | Fund your Polymarket wallet with USDC | 154 | | "Order failed" | Check that your proxy wallet is properly set up on Polymarket | 155 | 156 | ## Security Considerations 157 | 158 | ⚠️ **Important Security Notes:** 159 | 160 | 1. **Never commit your private key** to version control 161 | 2. **Use a dedicated trading wallet** with limited funds 162 | 3. **Keep your `.env.local` file** in `.gitignore` 163 | 4. **Monitor your bot** regularly for unexpected behavior 164 | 5. **Start with small amounts** until you're confident in the bot's behavior 165 | 166 | --- 167 | 168 | ← [Back to main README](../../README.md) 169 | 170 | -------------------------------------------------------------------------------- /terminal/src/components/AnalysisOutput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | import { TrendingUp, TrendingDown, Minus, BarChart3 } from "lucide-react"; 5 | import type { MarketAnalysis } from "@/types/api"; 6 | 7 | interface AnalysisOutputProps { 8 | analysis: MarketAnalysis; 9 | timestamp: Date; 10 | marketUrl?: string; 11 | } 12 | 13 | const AnalysisOutput = ({ analysis, timestamp, marketUrl }: AnalysisOutputProps) => { 14 | const [displayedLines, setDisplayedLines] = useState(0); 15 | 16 | const getVerdict = (): "bullish" | "bearish" | "neutral" => { 17 | if (analysis.recommendedAction === "BUY YES") return "bullish"; 18 | if (analysis.recommendedAction === "BUY NO") return "bearish"; 19 | return "neutral"; 20 | }; 21 | 22 | const verdict = getVerdict(); 23 | 24 | const allLines = [ 25 | { type: "header", content: `MARKET: ${analysis.title}` }, 26 | { type: "info", content: `EVENT: ${analysis.event_ticker}` }, 27 | { type: "info", content: `TICKER: ${analysis.ticker}` }, 28 | ...(marketUrl ? [{ type: "info", content: `URL: ${marketUrl}` }] : []), 29 | { type: "divider", content: "─".repeat(50) }, 30 | { type: "price", content: `MARKET PROBABILITY: ${analysis.marketProbability.toFixed(1)}%` }, 31 | { type: "price", content: `ESTIMATED ACTUAL: ${analysis.estimatedActualProbability.toFixed(1)}%` }, 32 | { type: "edge", content: `ALPHA OPPORTUNITY: ${analysis.alphaOpportunity > 0 ? "+" : ""}${analysis.alphaOpportunity.toFixed(1)}%` }, 33 | { type: "confidence", content: `CONFIDENCE: ${analysis.confidence}%` }, 34 | { type: "divider", content: "─".repeat(50) }, 35 | { type: "verdict-label", content: "VERDICT:" }, 36 | { type: "verdict", content: `${analysis.recommendedAction} (${analysis.predictedWinner} @ ${analysis.winnerConfidence}% confidence)` }, 37 | { type: "divider", content: "─".repeat(50) }, 38 | ...(analysis.questionAnswer ? [ 39 | { type: "section", content: "QUESTION ANSWER:" }, 40 | { type: "answer", content: analysis.questionAnswer }, 41 | { type: "divider", content: "─".repeat(50) }, 42 | ] : []), 43 | { type: "section", content: "KEY FACTORS:" }, 44 | ...analysis.keyFactors.map(f => ({ type: "factor", content: `• ${f}` })), 45 | { type: "divider", content: "─".repeat(50) }, 46 | { type: "section", content: "RISKS:" }, 47 | ...analysis.risks.map(r => ({ type: "risk", content: `⚠ ${r}` })), 48 | { type: "divider", content: "─".repeat(50) }, 49 | { type: "recommendation-label", content: "SUMMARY:" }, 50 | { type: "recommendation", content: analysis.analysisSummary }, 51 | ]; 52 | 53 | useEffect(() => { 54 | setDisplayedLines(0); 55 | const interval = setInterval(() => { 56 | setDisplayedLines(prev => { 57 | if (prev >= allLines.length) { 58 | clearInterval(interval); 59 | return prev; 60 | } 61 | return prev + 1; 62 | }); 63 | }, 50); 64 | 65 | return () => clearInterval(interval); 66 | }, [analysis.ticker, allLines.length]); 67 | 68 | const getVerdictIcon = () => { 69 | switch (verdict) { 70 | case "bullish": 71 | return ; 72 | case "bearish": 73 | return ; 74 | default: 75 | return ; 76 | } 77 | }; 78 | 79 | const getVerdictColor = () => { 80 | switch (verdict) { 81 | case "bullish": 82 | return "text-success"; 83 | case "bearish": 84 | return "text-destructive"; 85 | default: 86 | return "text-warning"; 87 | } 88 | }; 89 | 90 | const getLineStyle = (type: string) => { 91 | switch (type) { 92 | case "header": 93 | return "text-primary font-bold text-lg"; 94 | case "info": 95 | return "text-muted-foreground text-sm"; 96 | case "divider": 97 | return "text-border/50"; 98 | case "price": 99 | return "text-foreground"; 100 | case "edge": 101 | return analysis.alphaOpportunity > 0 ? "text-success font-semibold" : analysis.alphaOpportunity < 0 ? "text-destructive font-semibold" : "text-warning font-semibold"; 102 | case "confidence": 103 | return analysis.confidence >= 70 ? "text-success" : analysis.confidence >= 40 ? "text-warning" : "text-destructive"; 104 | case "verdict-label": 105 | case "section": 106 | case "recommendation-label": 107 | return "text-primary font-semibold mt-2"; 108 | case "verdict": 109 | return `${getVerdictColor()} font-bold text-xl`; 110 | case "answer": 111 | return "text-secondary-foreground pl-2 whitespace-pre-wrap"; 112 | case "factor": 113 | return "text-secondary-foreground pl-2"; 114 | case "risk": 115 | return "text-warning/80 pl-2"; 116 | case "recommendation": 117 | return "text-foreground font-medium"; 118 | default: 119 | return "text-foreground"; 120 | } 121 | }; 122 | 123 | return ( 124 |
125 |
126 |
127 | 128 | 129 | ANALYSIS OUTPUT 130 | 131 |
132 |
133 | {getVerdictIcon()} 134 | 135 | {analysis.recommendedAction} 136 | 137 |
138 |
139 | 140 |
141 | {allLines.slice(0, displayedLines).map((line, index) => ( 142 |
143 | {line.type === "verdict" && getVerdictIcon()} 144 | {line.content} 145 |
146 | ))} 147 | {displayedLines < allLines.length && ( 148 | 149 | )} 150 |
151 | 152 |
153 | Analyzed at {timestamp.toLocaleTimeString()} 154 | PredictOS 155 |
156 |
157 | ); 158 | }; 159 | 160 | export default AnalysisOutput; 161 | 162 | -------------------------------------------------------------------------------- /terminal/src/app/api/limit-order-bot/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import type { LimitOrderBotRequest, LimitOrderBotResponse } from "@/types/betting-bot"; 3 | 4 | const MAX_RETRIES = 3; 5 | const RETRY_DELAY_MS = 2000; // 2 seconds between retries 6 | 7 | /** 8 | * Helper to delay execution 9 | */ 10 | function delay(ms: number): Promise { 11 | return new Promise(resolve => setTimeout(resolve, ms)); 12 | } 13 | 14 | /** 15 | * Call the Supabase Edge Function with retry logic for cold starts 16 | */ 17 | async function callEdgeFunction( 18 | url: string, 19 | headers: Record, 20 | body: object, 21 | attempt: number = 1 22 | ): Promise<{ response: Response; isRetry: boolean }> { 23 | const response = await fetch(url, { 24 | method: "POST", 25 | headers: { 26 | "Content-Type": "application/json", 27 | ...headers, 28 | }, 29 | body: JSON.stringify(body), 30 | }); 31 | 32 | // Check if we got a non-JSON response (likely a timeout/error page) 33 | const contentType = response.headers.get("content-type"); 34 | const isJsonResponse = contentType && contentType.includes("application/json"); 35 | 36 | // If non-JSON response and we have retries left, retry (handles cold start timeouts) 37 | if (!isJsonResponse && attempt < MAX_RETRIES) { 38 | console.log(`Edge function returned non-JSON (attempt ${attempt}/${MAX_RETRIES}), retrying in ${RETRY_DELAY_MS}ms...`); 39 | await delay(RETRY_DELAY_MS); 40 | return callEdgeFunction(url, headers, body, attempt + 1); 41 | } 42 | 43 | return { response, isRetry: attempt > 1 }; 44 | } 45 | 46 | /** 47 | * Server-side API route to proxy requests to the Supabase Edge Function (polymarket-up-down-15-markets-limit-order-bot). 48 | * This keeps the Supabase URL and keys secure on the server. 49 | * Includes retry logic to handle cold start timeouts. 50 | */ 51 | export async function POST(request: NextRequest) { 52 | try { 53 | // Read environment variables server-side 54 | const supabaseUrl = process.env.SUPABASE_URL; 55 | const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; 56 | 57 | if (!supabaseUrl || !supabaseAnonKey) { 58 | return NextResponse.json( 59 | { 60 | success: false, 61 | error: "Server configuration error: Missing Supabase credentials", 62 | logs: [{ 63 | timestamp: new Date().toISOString(), 64 | level: "ERROR", 65 | message: "Server configuration error: Missing Supabase credentials", 66 | }], 67 | } as LimitOrderBotResponse, 68 | { status: 500 } 69 | ); 70 | } 71 | 72 | // Parse request body 73 | let body: LimitOrderBotRequest; 74 | try { 75 | body = await request.json(); 76 | } catch { 77 | return NextResponse.json( 78 | { 79 | success: false, 80 | error: "Invalid JSON in request body", 81 | logs: [{ 82 | timestamp: new Date().toISOString(), 83 | level: "ERROR", 84 | message: "Invalid JSON in request body", 85 | }], 86 | } as LimitOrderBotResponse, 87 | { status: 400 } 88 | ); 89 | } 90 | 91 | // Validate required fields 92 | if (!body.asset) { 93 | return NextResponse.json( 94 | { 95 | success: false, 96 | error: "Missing required field: asset", 97 | logs: [{ 98 | timestamp: new Date().toISOString(), 99 | level: "ERROR", 100 | message: "Missing required field: asset", 101 | }], 102 | } as LimitOrderBotResponse, 103 | { status: 400 } 104 | ); 105 | } 106 | 107 | // Validate asset value 108 | const validAssets = ["BTC", "SOL", "ETH", "XRP"]; 109 | if (!validAssets.includes(body.asset.toUpperCase())) { 110 | return NextResponse.json( 111 | { 112 | success: false, 113 | error: `Invalid asset. Must be one of: ${validAssets.join(", ")}`, 114 | logs: [{ 115 | timestamp: new Date().toISOString(), 116 | level: "ERROR", 117 | message: `Invalid asset: ${body.asset}`, 118 | }], 119 | } as LimitOrderBotResponse, 120 | { status: 400 } 121 | ); 122 | } 123 | 124 | // Call the Supabase Edge Function with retry logic 125 | const edgeFunctionUrl = process.env.SUPABASE_EDGE_FUNCTION_LIMIT_ORDER_BOT 126 | || `${supabaseUrl}/functions/v1/polymarket-up-down-15-markets-limit-order-bot`; 127 | 128 | const { response, isRetry } = await callEdgeFunction( 129 | edgeFunctionUrl, 130 | { 131 | Authorization: `Bearer ${supabaseAnonKey}`, 132 | apikey: supabaseAnonKey, 133 | }, 134 | { 135 | asset: body.asset.toUpperCase(), 136 | price: body.price, 137 | sizeUsd: body.sizeUsd, 138 | } 139 | ); 140 | 141 | // Check if response is JSON before parsing 142 | const contentType = response.headers.get("content-type"); 143 | if (!contentType || !contentType.includes("application/json")) { 144 | const text = await response.text(); 145 | console.error("Non-JSON response from edge function after retries:", text.substring(0, 500)); 146 | return NextResponse.json( 147 | { 148 | success: false, 149 | error: `Edge function error (${response.status}): Server returned non-JSON response after ${MAX_RETRIES} attempts. The function may be timing out.`, 150 | logs: [{ 151 | timestamp: new Date().toISOString(), 152 | level: "ERROR", 153 | message: `Edge function returned status ${response.status} with non-JSON response`, 154 | }], 155 | } as LimitOrderBotResponse, 156 | { status: 502 } 157 | ); 158 | } 159 | 160 | const data: LimitOrderBotResponse = await response.json(); 161 | 162 | // Add a note if we had to retry 163 | if (isRetry && data.logs) { 164 | data.logs.unshift({ 165 | timestamp: new Date().toISOString(), 166 | level: "INFO", 167 | message: "Request succeeded after retry (cold start recovery)", 168 | }); 169 | } 170 | 171 | return NextResponse.json(data, { status: response.status }); 172 | } catch (error) { 173 | console.error("Error in limit-order-bot API route:", error); 174 | return NextResponse.json( 175 | { 176 | success: false, 177 | error: error instanceof Error ? error.message : "An unexpected error occurred", 178 | logs: [{ 179 | timestamp: new Date().toISOString(), 180 | level: "ERROR", 181 | message: error instanceof Error ? error.message : "An unexpected error occurred", 182 | }], 183 | } as LimitOrderBotResponse, 184 | { status: 500 } 185 | ); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /terminal/public/okbet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /terminal/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap'); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | @layer base { 8 | :root { 9 | /* PredictOS Terminal Theme - Cyan/Teal Dark Terminal */ 10 | --background: 200 20% 6%; 11 | --foreground: 180 100% 95%; 12 | 13 | --card: 200 25% 8%; 14 | --card-foreground: 180 100% 95%; 15 | 16 | --popover: 200 25% 8%; 17 | --popover-foreground: 180 100% 95%; 18 | 19 | /* Teal/Cyan Primary */ 20 | --primary: 174 100% 41%; 21 | --primary-foreground: 200 20% 6%; 22 | 23 | --secondary: 200 20% 12%; 24 | --secondary-foreground: 180 100% 90%; 25 | 26 | --muted: 200 15% 15%; 27 | --muted-foreground: 200 10% 55%; 28 | 29 | --accent: 174 100% 41%; 30 | --accent-foreground: 200 20% 6%; 31 | 32 | --destructive: 0 84% 60%; 33 | --destructive-foreground: 0 0% 100%; 34 | 35 | /* Success (for Yes/Buy recommendations) */ 36 | --success: 142 76% 36%; 37 | --success-foreground: 0 0% 100%; 38 | 39 | /* Danger (for No/Sell recommendations) */ 40 | --danger: 0 84% 60%; 41 | --danger-foreground: 0 0% 100%; 42 | 43 | /* Warning */ 44 | --warning: 38 92% 50%; 45 | --warning-foreground: 0 0% 0%; 46 | 47 | --border: 200 20% 18%; 48 | --input: 200 20% 12%; 49 | --ring: 174 100% 41%; 50 | 51 | --radius: 0.5rem; 52 | 53 | /* Sidebar */ 54 | --sidebar-background: 200 20% 7%; 55 | --sidebar-foreground: 180 100% 95%; 56 | --sidebar-primary: 174 100% 41%; 57 | --sidebar-primary-foreground: 200 20% 6%; 58 | --sidebar-accent: 200 20% 12%; 59 | --sidebar-accent-foreground: 180 100% 90%; 60 | --sidebar-border: 174 40% 20%; 61 | --sidebar-ring: 174 100% 41%; 62 | 63 | /* Glow Effects */ 64 | --glow-primary: 0 0 30px hsl(174 100% 41% / 0.4); 65 | --glow-success: 0 0 20px hsl(142 76% 36% / 0.4); 66 | --glow-danger: 0 0 20px hsl(0 84% 60% / 0.4); 67 | } 68 | } 69 | 70 | @layer base { 71 | * { 72 | @apply border-border; 73 | } 74 | 75 | html { 76 | scroll-behavior: smooth; 77 | overflow-y: scroll; 78 | } 79 | 80 | body { 81 | @apply bg-background text-foreground antialiased; 82 | font-family: 'Space Grotesk', 'JetBrains Mono', system-ui, sans-serif; 83 | font-feature-settings: "rlig" 1, "calt" 1; 84 | } 85 | 86 | ::selection { 87 | @apply bg-primary/30 text-foreground; 88 | } 89 | 90 | /* Custom Scrollbar */ 91 | ::-webkit-scrollbar { 92 | width: 8px; 93 | height: 8px; 94 | } 95 | 96 | ::-webkit-scrollbar-track { 97 | @apply bg-background; 98 | } 99 | 100 | ::-webkit-scrollbar-thumb { 101 | @apply bg-muted rounded-full; 102 | } 103 | 104 | ::-webkit-scrollbar-thumb:hover { 105 | @apply bg-muted-foreground/30; 106 | } 107 | } 108 | 109 | @layer components { 110 | /* Terminal Glow Text */ 111 | .glow-text { 112 | text-shadow: 0 0 10px hsl(var(--primary) / 0.5), 0 0 20px hsl(var(--primary) / 0.3); 113 | } 114 | 115 | .glow-text-success { 116 | text-shadow: 0 0 10px hsl(var(--success) / 0.5), 0 0 20px hsl(var(--success) / 0.3); 117 | } 118 | 119 | .glow-text-danger { 120 | text-shadow: 0 0 10px hsl(var(--danger) / 0.5), 0 0 20px hsl(var(--danger) / 0.3); 121 | } 122 | 123 | /* Glow Box Effects */ 124 | .glow-box { 125 | box-shadow: 0 0 20px hsl(var(--primary) / 0.2), inset 0 0 20px hsl(var(--primary) / 0.05); 126 | } 127 | 128 | .glow-box-hover:hover { 129 | box-shadow: 0 0 30px hsl(var(--primary) / 0.3), inset 0 0 30px hsl(var(--primary) / 0.08); 130 | } 131 | 132 | /* Terminal Border */ 133 | .terminal-border { 134 | @apply border border-primary/30; 135 | box-shadow: 0 0 10px hsl(var(--primary) / 0.1); 136 | } 137 | 138 | .terminal-border-glow { 139 | border: 1px solid hsl(var(--primary) / 0.5); 140 | box-shadow: 0 0 10px hsl(var(--primary) / 0.2), inset 0 0 10px hsl(var(--primary) / 0.05); 141 | } 142 | 143 | /* Sidebar */ 144 | .bg-sidebar { 145 | background-color: hsl(var(--sidebar-background)); 146 | } 147 | 148 | /* Gradient backgrounds */ 149 | .gradient-radial { 150 | background: radial-gradient(ellipse at center, hsl(var(--primary) / 0.1) 0%, transparent 70%); 151 | } 152 | 153 | /* Pulse animation for live indicators */ 154 | .pulse-live { 155 | animation: pulse-live 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 156 | } 157 | 158 | @keyframes pulse-live { 159 | 0%, 100% { 160 | opacity: 1; 161 | } 162 | 50% { 163 | opacity: 0.5; 164 | } 165 | } 166 | 167 | /* Scanline effect for retro terminal feel */ 168 | .scanlines::after { 169 | content: ''; 170 | position: absolute; 171 | inset: 0; 172 | background: repeating-linear-gradient( 173 | 0deg, 174 | transparent, 175 | transparent 2px, 176 | hsl(var(--primary) / 0.03) 2px, 177 | hsl(var(--primary) / 0.03) 4px 178 | ); 179 | pointer-events: none; 180 | } 181 | } 182 | 183 | @layer utilities { 184 | /* Font utilities */ 185 | .font-display { 186 | font-family: 'Orbitron', sans-serif; 187 | } 188 | 189 | .font-mono { 190 | font-family: 'JetBrains Mono', monospace; 191 | } 192 | 193 | /* Hide scrollbar but keep functionality */ 194 | .scrollbar-hide { 195 | -ms-overflow-style: none; 196 | scrollbar-width: none; 197 | } 198 | 199 | .scrollbar-hide::-webkit-scrollbar { 200 | display: none; 201 | } 202 | 203 | /* Text gradient */ 204 | .text-gradient-primary { 205 | @apply bg-gradient-to-r from-primary via-primary/80 to-primary bg-clip-text text-transparent; 206 | } 207 | 208 | /* Glow utilities */ 209 | .glow-primary { 210 | box-shadow: var(--glow-primary); 211 | } 212 | 213 | .glow-strong { 214 | box-shadow: 0 0 40px hsl(var(--primary) / 0.7); 215 | } 216 | 217 | .text-glow { 218 | text-shadow: 0 0 10px hsl(var(--primary) / 0.7), 0 0 20px hsl(var(--primary) / 0.5); 219 | } 220 | 221 | .text-glow-strong { 222 | text-shadow: 0 0 10px hsl(var(--primary) / 0.9), 0 0 30px hsl(var(--primary) / 0.7), 0 0 50px hsl(var(--primary) / 0.5); 223 | } 224 | 225 | .border-glow { 226 | box-shadow: inset 0 0 20px hsl(var(--primary) / 0.1), 0 0 20px hsl(var(--primary) / 0.2); 227 | } 228 | 229 | /* Pulse glow */ 230 | .pulse-glow { 231 | animation: pulse-glow 2s ease-in-out infinite; 232 | } 233 | 234 | @keyframes pulse-glow { 235 | 0%, 100% { box-shadow: 0 0 10px hsl(var(--primary) / 0.4); } 236 | 50% { box-shadow: 0 0 25px hsl(var(--primary) / 0.7); } 237 | } 238 | 239 | /* Animation classes */ 240 | .animate-fade-in { 241 | animation: fadeIn 0.6s ease-out forwards; 242 | } 243 | 244 | .animate-fade-in-up { 245 | animation: fadeInUp 0.6s ease-out forwards; 246 | } 247 | 248 | @keyframes fadeIn { 249 | from { opacity: 0; } 250 | to { opacity: 1; } 251 | } 252 | 253 | @keyframes fadeInUp { 254 | from { opacity: 0; transform: translateY(10px); } 255 | to { opacity: 1; transform: translateY(0); } 256 | } 257 | 258 | .fade-in { 259 | animation: fadeIn 0.6s ease-out forwards; 260 | } 261 | 262 | .slide-up { 263 | animation: slideUp 0.5s ease-out forwards; 264 | } 265 | 266 | @keyframes slideUp { 267 | from { opacity: 0; transform: translateY(20px); } 268 | to { opacity: 1; transform: translateY(0); } 269 | } 270 | 271 | /* Grid background */ 272 | .grid-bg { 273 | background-image: 274 | linear-gradient(hsl(var(--primary) / 0.1) 1px, transparent 1px), 275 | linear-gradient(90deg, hsl(var(--primary) / 0.1) 1px, transparent 1px); 276 | background-size: 40px 40px; 277 | } 278 | 279 | /* Terminal effects */ 280 | .terminal-flicker { 281 | animation: flicker 0.15s infinite; 282 | } 283 | 284 | @keyframes flicker { 285 | 0%, 100% { opacity: 1; } 286 | 50% { opacity: 0.98; } 287 | } 288 | 289 | .typing-cursor { 290 | animation: blink 1s step-end infinite; 291 | } 292 | 293 | @keyframes blink { 294 | 0%, 50% { opacity: 1; } 295 | 51%, 100% { opacity: 0; } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /supabase/functions/polymarket-up-down-15-markets-limit-order-bot/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Supabase Edge Function: polymarket-up-down-15-markets-limit-order-bot 3 | * 4 | * Automated limit order bot for Polymarket 15-minute up/down markets. 5 | * Places straddle orders on the closest upcoming market. 6 | */ 7 | 8 | import { PolymarketClient, createClientFromEnv } from "../_shared/polymarket/client.ts"; 9 | import { 10 | buildMarketSlug, 11 | formatTimeShort, 12 | createLogEntry, 13 | } from "../_shared/polymarket/utils.ts"; 14 | import type { SupportedAsset, BotLogEntry } from "../_shared/polymarket/types.ts"; 15 | import type { 16 | LimitOrderBotRequest, 17 | LimitOrderBotResponse, 18 | MarketOrderResult, 19 | } from "./types.ts"; 20 | 21 | // Trading configuration defaults 22 | const DEFAULT_ORDER_PRICE = 0.48; // 48% 23 | const DEFAULT_ORDER_SIZE_USD = 25; // $25 total 24 | 25 | const corsHeaders = { 26 | "Access-Control-Allow-Origin": "*", 27 | "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", 28 | "Access-Control-Allow-Methods": "POST, OPTIONS", 29 | }; 30 | 31 | /** 32 | * Validate that the asset is supported 33 | */ 34 | function isValidAsset(asset: string): asset is SupportedAsset { 35 | return ["BTC", "SOL", "ETH", "XRP"].includes(asset.toUpperCase()); 36 | } 37 | 38 | /** 39 | * Get current UTC timestamp in seconds 40 | */ 41 | function nowUtcSeconds(): number { 42 | return Math.floor(Date.now() / 1000); 43 | } 44 | 45 | /** 46 | * Get the closest upcoming 15-minute timestamp. 47 | * Rounds UP to the next 15-minute block. 48 | */ 49 | function getNext15MinTimestamp(): number { 50 | const now = nowUtcSeconds(); 51 | return Math.ceil(now / 900) * 900; 52 | } 53 | 54 | Deno.serve(async (req: Request) => { 55 | const logs: BotLogEntry[] = []; 56 | 57 | // Handle CORS preflight 58 | if (req.method === "OPTIONS") { 59 | return new Response(null, { headers: corsHeaders }); 60 | } 61 | 62 | try { 63 | // Validate request method 64 | if (req.method !== "POST") { 65 | logs.push(createLogEntry("ERROR", "Invalid request method", { method: req.method })); 66 | return new Response( 67 | JSON.stringify({ 68 | success: false, 69 | error: "Method not allowed. Use POST.", 70 | logs, 71 | } as LimitOrderBotResponse), 72 | { status: 405, headers: { ...corsHeaders, "Content-Type": "application/json" } } 73 | ); 74 | } 75 | 76 | // Parse request body 77 | let requestBody: LimitOrderBotRequest; 78 | try { 79 | requestBody = await req.json(); 80 | } catch { 81 | logs.push(createLogEntry("ERROR", "Invalid JSON in request body")); 82 | return new Response( 83 | JSON.stringify({ 84 | success: false, 85 | error: "Invalid JSON in request body", 86 | logs, 87 | } as LimitOrderBotResponse), 88 | { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } 89 | ); 90 | } 91 | 92 | const { asset, price, sizeUsd } = requestBody; 93 | 94 | // Validate asset 95 | if (!asset || !isValidAsset(asset)) { 96 | logs.push(createLogEntry("ERROR", "Invalid or missing asset", { asset })); 97 | return new Response( 98 | JSON.stringify({ 99 | success: false, 100 | error: "Invalid asset. Must be one of: BTC, SOL, ETH, XRP", 101 | logs, 102 | } as LimitOrderBotResponse), 103 | { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } 104 | ); 105 | } 106 | 107 | const normalizedAsset = asset.toUpperCase() as SupportedAsset; 108 | 109 | // Get order configuration from request 110 | const orderPrice = price ? price / 100 : DEFAULT_ORDER_PRICE; 111 | const orderSizeUsd = sizeUsd || DEFAULT_ORDER_SIZE_USD; 112 | 113 | // Get the closest upcoming 15-minute market timestamp 114 | const timestamp = getNext15MinTimestamp(); 115 | const marketSlug = buildMarketSlug(normalizedAsset, timestamp); 116 | 117 | // Initialize the Polymarket client 118 | let client: PolymarketClient; 119 | try { 120 | client = createClientFromEnv(); 121 | } catch (error) { 122 | const errorMsg = error instanceof Error ? error.message : String(error); 123 | logs.push(createLogEntry("ERROR", `Failed to initialize client: ${errorMsg}`)); 124 | return new Response( 125 | JSON.stringify({ 126 | success: false, 127 | error: `Client initialization failed: ${errorMsg}`, 128 | logs, 129 | } as LimitOrderBotResponse), 130 | { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } 131 | ); 132 | } 133 | 134 | // Process the market 135 | let marketResult: MarketOrderResult; 136 | 137 | try { 138 | // Fetch market data 139 | const market = await client.getMarketBySlug(marketSlug); 140 | logs.push(...client.getLogs()); 141 | client.clearLogs(); 142 | 143 | if (!market) { 144 | marketResult = { 145 | marketSlug, 146 | marketStartTime: formatTimeShort(timestamp), 147 | targetTimestamp: timestamp, 148 | error: "Market not found - may not be created yet", 149 | }; 150 | } else { 151 | // Extract token IDs 152 | let tokenIds; 153 | try { 154 | tokenIds = client.extractTokenIds(market); 155 | logs.push(...client.getLogs()); 156 | client.clearLogs(); 157 | } catch (error) { 158 | const errorMsg = error instanceof Error ? error.message : String(error); 159 | logs.push(createLogEntry("ERROR", `Failed to extract token IDs: ${errorMsg}`)); 160 | marketResult = { 161 | marketSlug, 162 | marketTitle: market.title, 163 | marketStartTime: formatTimeShort(timestamp), 164 | targetTimestamp: timestamp, 165 | error: `Token extraction failed: ${errorMsg}`, 166 | }; 167 | 168 | return new Response( 169 | JSON.stringify({ 170 | success: false, 171 | error: marketResult.error, 172 | data: { 173 | asset: normalizedAsset, 174 | pricePercent: orderPrice * 100, 175 | sizeUsd: orderSizeUsd, 176 | market: marketResult, 177 | }, 178 | logs, 179 | } as LimitOrderBotResponse), 180 | { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } } 181 | ); 182 | } 183 | 184 | // Place straddle orders 185 | const orderResults = await client.placeStraddleOrders(tokenIds, orderPrice, orderSizeUsd); 186 | logs.push(...client.getLogs()); 187 | client.clearLogs(); 188 | 189 | marketResult = { 190 | marketSlug, 191 | marketTitle: market.title, 192 | marketStartTime: formatTimeShort(timestamp), 193 | targetTimestamp: timestamp, 194 | ordersPlaced: orderResults, 195 | }; 196 | } 197 | 198 | } catch (error) { 199 | const errorMsg = error instanceof Error ? error.message : String(error); 200 | logs.push(createLogEntry("ERROR", `Error processing market ${marketSlug}: ${errorMsg}`)); 201 | marketResult = { 202 | marketSlug, 203 | marketStartTime: formatTimeShort(timestamp), 204 | targetTimestamp: timestamp, 205 | error: errorMsg, 206 | }; 207 | } 208 | 209 | const response: LimitOrderBotResponse = { 210 | success: !marketResult.error, 211 | data: { 212 | asset: normalizedAsset, 213 | pricePercent: orderPrice * 100, 214 | sizeUsd: orderSizeUsd, 215 | market: marketResult, 216 | }, 217 | logs, 218 | }; 219 | 220 | return new Response(JSON.stringify(response), { 221 | status: 200, 222 | headers: { ...corsHeaders, "Content-Type": "application/json" }, 223 | }); 224 | 225 | } catch (error) { 226 | const errorMsg = error instanceof Error ? error.message : String(error); 227 | logs.push(createLogEntry("ERROR", `Unhandled error: ${errorMsg}`)); 228 | 229 | return new Response( 230 | JSON.stringify({ 231 | success: false, 232 | error: errorMsg, 233 | logs, 234 | } as LimitOrderBotResponse), 235 | { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } 236 | ); 237 | } 238 | }); 239 | -------------------------------------------------------------------------------- /terminal/src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import Image from "next/image"; 5 | import Link from "next/link"; 6 | import { cn } from "@/lib/utils"; 7 | import { 8 | BarChart3, 9 | Copy, 10 | ChevronLeft, 11 | ChevronRight, 12 | Bot, 13 | Blocks, 14 | Globe, 15 | Swords, 16 | Wand2, 17 | Fish, 18 | ArrowLeftRight, 19 | Coins, 20 | TrendingUp 21 | } from "lucide-react"; 22 | 23 | interface SidebarProps { 24 | activeTab: string; 25 | } 26 | 27 | const navItems = [ 28 | { id: "analysis", label: "Market Analysis", icon: BarChart3, available: true, href: "/market-analysis" }, 29 | { id: "betting-bots", label: "Betting Bots", icon: Bot, available: true, href: "/betting-bots" }, 30 | { id: "agent-battles", label: "Agent Battles (x402)", icon: Swords, available: false }, 31 | { id: "no-code-builder", label: "No Code Builder", icon: Wand2, available: false }, 32 | { id: "whale-tracking", label: "Whale Tracking", icon: Fish, available: false }, 33 | { id: "copytrading", label: "Copytrading", icon: Copy, available: false }, 34 | { id: "arbitrage", label: "Arbitrage Opportunity", icon: ArrowLeftRight, available: false }, 35 | { id: "perps", label: "Perps Trading / Leverage", icon: TrendingUp, available: false }, 36 | { id: "staking", label: "$Predict Staking", icon: Coins, available: false }, 37 | { id: "sdk", label: "Predict Protocol SDK", icon: Blocks, available: false }, 38 | ]; 39 | 40 | export function Sidebar({ activeTab }: SidebarProps) { 41 | const [collapsed, setCollapsed] = useState(false); 42 | 43 | return ( 44 | 182 | ); 183 | } 184 | 185 | export default Sidebar; 186 | 187 | -------------------------------------------------------------------------------- /supabase/functions/_shared/polymarket/client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Polymarket CLOB Client for Deno/Supabase Edge Functions 3 | * 4 | * This client provides functionality to: 5 | * - Fetch market data from Gamma API 6 | * - Place orders on Polymarket CLOB using the official @polymarket/clob-client 7 | */ 8 | 9 | // @ts-ignore - Deno npm imports 10 | import { ClobClient, Side, OrderType } from "npm:@polymarket/clob-client@5.1.1"; 11 | // @ts-ignore - Deno npm imports 12 | import { Wallet } from "npm:ethers@5.7.2"; 13 | 14 | import type { 15 | PolymarketMarket, 16 | PolymarketClientConfig, 17 | OrderArgs, 18 | OrderResponse, 19 | TokenIds, 20 | BotLogEntry, 21 | } from "./types.ts"; 22 | import { parseTokenIds, createLogEntry } from "./utils.ts"; 23 | 24 | // API endpoints 25 | const GAMMA_API_URL = "https://gamma-api.polymarket.com"; 26 | const CLOB_HOST = "https://clob.polymarket.com"; 27 | const CHAIN_ID = 137; // Polygon 28 | 29 | // Default tick size for 15-min up/down markets 30 | const DEFAULT_TICK_SIZE = "0.01"; 31 | const DEFAULT_NEG_RISK = false; 32 | 33 | /** 34 | * Polymarket Client Class 35 | */ 36 | export class PolymarketClient { 37 | private config: PolymarketClientConfig; 38 | private logs: BotLogEntry[] = []; 39 | private clobClient: typeof ClobClient | null = null; 40 | 41 | constructor(config: PolymarketClientConfig) { 42 | this.config = { 43 | ...config, 44 | signatureType: config.signatureType ?? 1, // Default to Magic/Email login 45 | }; 46 | } 47 | 48 | /** 49 | * Get collected logs 50 | */ 51 | getLogs(): BotLogEntry[] { 52 | return [...this.logs]; 53 | } 54 | 55 | /** 56 | * Clear logs 57 | */ 58 | clearLogs(): void { 59 | this.logs = []; 60 | } 61 | 62 | /** 63 | * Add a log entry 64 | */ 65 | private log(level: BotLogEntry["level"], message: string, details?: Record): void { 66 | this.logs.push(createLogEntry(level, message, details)); 67 | console.log(`[${level}] ${message}`, details || ""); 68 | } 69 | 70 | /** 71 | * Initialize the CLOB client with API credentials 72 | */ 73 | private async initClobClient(): Promise { 74 | if (this.clobClient) { 75 | return this.clobClient; 76 | } 77 | 78 | const { privateKey, proxyAddress, signatureType } = this.config; 79 | 80 | this.log("INFO", "Initializing Polymarket CLOB client..."); 81 | 82 | try { 83 | // Create wallet signer from private key 84 | const signer = new Wallet(privateKey); 85 | 86 | // Create initial client to derive API credentials 87 | const tempClient = new ClobClient(CLOB_HOST, CHAIN_ID, signer); 88 | 89 | // Derive or create API credentials 90 | const creds = await tempClient.createOrDeriveApiKey(); 91 | this.log("INFO", "API credentials derived successfully"); 92 | 93 | // Create the full client with credentials and funder 94 | this.clobClient = new ClobClient( 95 | CLOB_HOST, 96 | CHAIN_ID, 97 | signer, 98 | creds, 99 | signatureType, 100 | proxyAddress 101 | ); 102 | 103 | this.log("SUCCESS", "CLOB client initialized", { 104 | funder: `${proxyAddress.slice(0, 10)}...${proxyAddress.slice(-8)}`, 105 | signatureType, 106 | }); 107 | 108 | return this.clobClient; 109 | } catch (error) { 110 | const errorMsg = error instanceof Error ? error.message : String(error); 111 | this.log("ERROR", `Failed to initialize CLOB client: ${errorMsg}`); 112 | throw error; 113 | } 114 | } 115 | 116 | /** 117 | * Fetch market data from Gamma API by slug 118 | */ 119 | async getMarketBySlug(slug: string): Promise { 120 | const url = `${GAMMA_API_URL}/markets/slug/${slug}`; 121 | this.log("INFO", `Fetching market data for slug: ${slug}`); 122 | 123 | try { 124 | const response = await fetch(url, { 125 | method: "GET", 126 | headers: { 127 | "Content-Type": "application/json", 128 | }, 129 | }); 130 | 131 | if (!response.ok) { 132 | if (response.status === 404) { 133 | this.log("WARN", `Market not found: ${slug}`); 134 | return null; 135 | } 136 | throw new Error(`Gamma API error: ${response.status} ${response.statusText}`); 137 | } 138 | 139 | const data = await response.json(); 140 | this.log("SUCCESS", `Found market: ${data.title || slug}`); 141 | return data as PolymarketMarket; 142 | } catch (error) { 143 | this.log("ERROR", `Failed to fetch market: ${error instanceof Error ? error.message : String(error)}`); 144 | throw error; 145 | } 146 | } 147 | 148 | /** 149 | * Extract Up and Down token IDs from market data 150 | */ 151 | extractTokenIds(market: PolymarketMarket): TokenIds { 152 | const clobTokenIdsStr = market.clobTokenIds; 153 | 154 | if (!clobTokenIdsStr) { 155 | throw new Error("No clobTokenIds found in market data"); 156 | } 157 | 158 | const [up, down] = parseTokenIds(clobTokenIdsStr); 159 | this.log("INFO", "Extracted token IDs", { 160 | up: `${up.slice(0, 16)}...${up.slice(-8)}`, 161 | down: `${down.slice(0, 16)}...${down.slice(-8)}` 162 | }); 163 | 164 | return { up, down }; 165 | } 166 | 167 | /** 168 | * Place a limit buy order on Polymarket CLOB 169 | */ 170 | async placeOrder(order: OrderArgs): Promise { 171 | const { privateKey, proxyAddress } = this.config; 172 | 173 | if (!privateKey) { 174 | this.log("ERROR", "Missing private key for order placement"); 175 | return { 176 | success: false, 177 | errorMsg: "Missing private key. Please set POLYMARKET_WALLET_PRIVATE_KEY.", 178 | }; 179 | } 180 | 181 | if (!proxyAddress) { 182 | this.log("ERROR", "Missing proxy address for order placement"); 183 | return { 184 | success: false, 185 | errorMsg: "Missing proxy address. Please set POLYMARKET_PROXY_WALLET_ADDRESS.", 186 | }; 187 | } 188 | 189 | this.log("INFO", `Placing ${order.side} order`, { 190 | tokenId: `${order.tokenId.slice(0, 16)}...`, 191 | price: order.price, 192 | size: Math.floor(order.size), 193 | }); 194 | 195 | try { 196 | // Initialize the CLOB client if not already done 197 | const client = await this.initClobClient(); 198 | 199 | // Create and post the order using the official client 200 | const orderResponse = await client.createAndPostOrder( 201 | { 202 | tokenID: order.tokenId, 203 | price: order.price, 204 | side: order.side === "BUY" ? Side.BUY : Side.SELL, 205 | size: Math.floor(order.size), 206 | feeRateBps: 0, 207 | }, 208 | { 209 | tickSize: DEFAULT_TICK_SIZE, 210 | negRisk: DEFAULT_NEG_RISK, 211 | }, 212 | OrderType.GTC // Good Till Cancelled 213 | ); 214 | 215 | this.log("SUCCESS", `Order placed successfully`, { 216 | orderId: orderResponse?.orderID || orderResponse?.id, 217 | status: orderResponse?.status, 218 | }); 219 | 220 | return { 221 | success: true, 222 | orderId: orderResponse?.orderID || orderResponse?.id, 223 | status: orderResponse?.status || "submitted", 224 | }; 225 | } catch (error) { 226 | const errorMsg = error instanceof Error ? error.message : String(error); 227 | this.log("ERROR", `Failed to place order: ${errorMsg}`); 228 | return { 229 | success: false, 230 | errorMsg, 231 | }; 232 | } 233 | } 234 | 235 | /** 236 | * Place straddle orders (buy both Up and Down) at a given price 237 | */ 238 | async placeStraddleOrders( 239 | tokenIds: TokenIds, 240 | price: number, 241 | sizeUsd: number 242 | ): Promise<{ up: OrderResponse; down: OrderResponse }> { 243 | const size = sizeUsd / price; 244 | 245 | this.log("INFO", `Placing straddle orders`, { 246 | price: `${(price * 100).toFixed(1)}%`, 247 | sizeUsd: `$${sizeUsd}`, 248 | shares: Math.floor(size), 249 | }); 250 | 251 | // Place Up order 252 | const upResult = await this.placeOrder({ 253 | tokenId: tokenIds.up, 254 | price, 255 | size, 256 | side: "BUY", 257 | }); 258 | 259 | // Place Down order 260 | const downResult = await this.placeOrder({ 261 | tokenId: tokenIds.down, 262 | price, 263 | size, 264 | side: "BUY", 265 | }); 266 | 267 | return { up: upResult, down: downResult }; 268 | } 269 | } 270 | 271 | /** 272 | * Create a Polymarket client from environment variables 273 | */ 274 | export function createClientFromEnv(): PolymarketClient { 275 | // @ts-ignore - Deno global 276 | const privateKey = Deno.env.get("POLYMARKET_WALLET_PRIVATE_KEY"); 277 | // @ts-ignore - Deno global 278 | const proxyAddress = Deno.env.get("POLYMARKET_PROXY_WALLET_ADDRESS"); 279 | // @ts-ignore - Deno global 280 | const signatureType = parseInt(Deno.env.get("POLYMARKET_SIGNATURE_TYPE") || "1", 10); 281 | 282 | if (!privateKey) { 283 | throw new Error("POLYMARKET_WALLET_PRIVATE_KEY environment variable is required"); 284 | } 285 | 286 | if (!proxyAddress) { 287 | throw new Error("POLYMARKET_PROXY_WALLET_ADDRESS environment variable is required"); 288 | } 289 | 290 | return new PolymarketClient({ 291 | privateKey, 292 | proxyAddress, 293 | signatureType, 294 | }); 295 | } 296 | -------------------------------------------------------------------------------- /terminal/src/components/TerminalInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, FormEvent, useRef } from "react"; 4 | import { Send, Link2, ChevronDown } from "lucide-react"; 5 | 6 | // Grok Models (xAI) 7 | export type GrokModel = 8 | | "grok-4-1-fast-reasoning" 9 | | "grok-4-1-fast-non-reasoning" 10 | | "grok-4-fast-reasoning" 11 | | "grok-4-fast-non-reasoning"; 12 | 13 | // OpenAI Models 14 | export type OpenAIModel = 15 | | "gpt-5.2" 16 | | "gpt-5.1" 17 | | "gpt-5-nano" 18 | | "gpt-4.1" 19 | | "gpt-4.1-mini"; 20 | 21 | // Combined AI Model type 22 | export type AIModel = GrokModel | OpenAIModel; 23 | 24 | // Model provider type 25 | export type AIProvider = "grok" | "openai"; 26 | 27 | interface ModelOption { 28 | value: AIModel; 29 | label: string; 30 | provider: AIProvider; 31 | } 32 | 33 | export const GROK_MODELS: ModelOption[] = [ 34 | { value: "grok-4-1-fast-reasoning", label: "Grok 4.1 Fast (Reasoning)", provider: "grok" }, 35 | { value: "grok-4-1-fast-non-reasoning", label: "Grok 4.1 Fast (Non-Reasoning)", provider: "grok" }, 36 | { value: "grok-4-fast-reasoning", label: "Grok 4 Fast (Reasoning)", provider: "grok" }, 37 | { value: "grok-4-fast-non-reasoning", label: "Grok 4 Fast (Non-Reasoning)", provider: "grok" }, 38 | ]; 39 | 40 | export const OPENAI_MODELS: ModelOption[] = [ 41 | { value: "gpt-5.2", label: "GPT-5.2", provider: "openai" }, 42 | { value: "gpt-5.1", label: "GPT-5.1", provider: "openai" }, 43 | { value: "gpt-5-nano", label: "GPT-5 Nano", provider: "openai" }, 44 | { value: "gpt-4.1", label: "GPT-4.1", provider: "openai" }, 45 | { value: "gpt-4.1-mini", label: "GPT-4.1 Mini", provider: "openai" }, 46 | ]; 47 | 48 | export const ALL_MODELS: ModelOption[] = [...GROK_MODELS, ...OPENAI_MODELS]; 49 | 50 | interface TerminalInputProps { 51 | onSubmit: (url: string, model: AIModel) => void; 52 | isLoading: boolean; 53 | shouldClear?: boolean; 54 | } 55 | 56 | const loadingMessages = [ 57 | "Fetching market data", 58 | "Analyzing probabilities", 59 | "Detecting alpha opportunities", 60 | "Consulting AI agents", 61 | "Calculating edge", 62 | "Generating recommendation", 63 | ]; 64 | 65 | const TerminalInput = ({ onSubmit, isLoading, shouldClear }: TerminalInputProps) => { 66 | const [input, setInput] = useState(""); 67 | const [dots, setDots] = useState(""); 68 | const [messageIndex, setMessageIndex] = useState(0); 69 | const [selectedModel, setSelectedModel] = useState("grok-4-1-fast-reasoning"); 70 | const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false); 71 | const prevShouldClear = useRef(shouldClear); 72 | const dropdownRef = useRef(null); 73 | 74 | // Close dropdown when clicking outside 75 | useEffect(() => { 76 | const handleClickOutside = (event: MouseEvent) => { 77 | if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { 78 | setIsModelDropdownOpen(false); 79 | } 80 | }; 81 | 82 | document.addEventListener("mousedown", handleClickOutside); 83 | return () => document.removeEventListener("mousedown", handleClickOutside); 84 | }, []); 85 | 86 | // Clear input when shouldClear changes to true 87 | useEffect(() => { 88 | if (shouldClear && !prevShouldClear.current) { 89 | setInput(""); 90 | } 91 | prevShouldClear.current = shouldClear; 92 | }, [shouldClear]); 93 | 94 | // Animate the dots 95 | useEffect(() => { 96 | if (!isLoading) { 97 | setDots(""); 98 | setMessageIndex(0); 99 | return; 100 | } 101 | 102 | const dotsInterval = setInterval(() => { 103 | setDots((prev) => { 104 | if (prev === "...") return ""; 105 | return prev + "."; 106 | }); 107 | }, 400); 108 | 109 | return () => clearInterval(dotsInterval); 110 | }, [isLoading]); 111 | 112 | // Cycle through loading messages 113 | useEffect(() => { 114 | if (!isLoading) return; 115 | 116 | const messageInterval = setInterval(() => { 117 | setMessageIndex((prev) => (prev + 1) % loadingMessages.length); 118 | }, 3000); 119 | 120 | return () => clearInterval(messageInterval); 121 | }, [isLoading]); 122 | 123 | const handleSubmit = (e: FormEvent) => { 124 | e.preventDefault(); 125 | if (input.trim() && !isLoading) { 126 | onSubmit(input.trim(), selectedModel); 127 | } 128 | }; 129 | 130 | const getModelLabel = (model: AIModel) => { 131 | return ALL_MODELS.find(m => m.value === model)?.label || model; 132 | }; 133 | 134 | const getProviderBadge = (model: AIModel) => { 135 | const modelOption = ALL_MODELS.find(m => m.value === model); 136 | return modelOption?.provider === "openai" ? "OpenAI" : "Grok"; 137 | }; 138 | 139 | return ( 140 |
141 |
142 |
143 | 144 | 145 | MARKET ANALYSIS INPUT 146 | 147 |
148 | 149 | {/* Model Dropdown in Header */} 150 |
151 | 168 | 169 | {isModelDropdownOpen && ( 170 |
171 | {/* Grok Section */} 172 |
173 |
174 | 175 | xAI 176 | 177 | Grok Models 178 |
179 |
180 |
181 | {GROK_MODELS.map((model) => ( 182 | 198 | ))} 199 |
200 | 201 | {/* OpenAI Section */} 202 |
203 |
204 | 205 | OpenAI 206 | 207 | GPT Models 208 |
209 |
210 |
211 | {OPENAI_MODELS.map((model) => ( 212 | 228 | ))} 229 |
230 |
231 | )} 232 |
233 |
234 | 235 |
236 | {">"} 237 | setInput(e.target.value)} 241 | placeholder="Paste Kalshi or Polymarket URL ..." 242 | disabled={isLoading} 243 | className="flex-1 bg-transparent border-none outline-none text-foreground placeholder:text-muted-foreground/50 font-mono" 244 | /> 245 | 252 |
253 | 254 | {isLoading && ( 255 |
256 |
257 |
258 | 259 | {loadingMessages[messageIndex]}{dots} 260 | 261 |
262 |
263 | )} 264 |
265 | ); 266 | }; 267 | 268 | export default TerminalInput; 269 | 270 | -------------------------------------------------------------------------------- /terminal/public/dome-icon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | PredictOS Banner 3 |

4 | 5 |

PredictOS

6 | 7 |

The leading all-in-one open-source framework for deploying custom AI agents and trading bots purpose-built for prediction markets - bring your own data, models, and strategies to dominate prediction forecasting

8 | 9 |

Built by PredictionXBT, the team behind Predict — The Layer 1 for Social Prediction Market

10 | 11 | 19 | 20 |
21 | 22 | ## ✨ What is PredictOS? 23 | 24 | Prediction markets are having their moment. With platforms like **Kalshi** and **Polymarket** opening up their APIs to the public, there's now unprecedented access to real-time market data, order books, and trading capabilities. But raw API access is just the beginning — what's been missing is a unified framework that lets anyone tap into this new financial primitive. 25 | 26 | **PredictOS is that framework.** 27 | 28 | ### 🔓 Why Open Source? 29 | 30 | Sure, there are hosted tools out there. But here's the problem: 31 | 32 | - **Your data isn't yours.** Every query, every strategy signal, every trade you analyze — it all flows through their servers. Your alpha becomes their alpha. Your edge gets commoditized the moment you share it with a third party. 33 | 34 | - **Your strategy isn't private.** Want to build a custom trading bot with proprietary signals? Maybe you've got insider domain knowledge, a unique data source, or a thesis nobody else is running. The moment you plug that into a hosted platform, you've handed over your playbook. 35 | 36 | - **You can't customize what you don't own.** Need a specific feature? Want to integrate your own AI model? Good luck submitting a feature request and waiting 6 months. 37 | 38 | With PredictOS, **you own everything**. Run it on your own infrastructure. Fork it. Modify it. Build your secret sauce without anyone watching. Your strategies stay yours. Your data never leaves your servers. And when you find an edge, you keep it. 39 | 40 | --- 41 | 42 | PredictOS is an open-source, AI-powered operating system for prediction markets. It provides a unified interface to analyze markets across platforms, delivering real-time AI insights to help you find alpha opportunities and make informed trading decisions. 43 | 44 | Whether you're a casual trader looking for quick market analysis or a power user building automated betting strategies with proprietary data, PredictOS gives you the tools to navigate prediction markets — on your own terms. 45 | 46 | **What's next?** We're building towards a complete prediction market toolkit: automated betting bots, whale tracking, copytrading, cross-platform arbitrage, and more. See the [Coming Soon](#-coming-soon) section for the full roadmap. 47 | 48 |
49 | Dome API 50 |
51 | 52 | ## 💎 The $PREDICT Token 53 | 54 | **$PREDICT** serves as the foundational pillar of the open-source PredictOS framework, powering a decentralized, community-driven Layer 1 ecosystem for social prediction markets, trading, and participation. 55 | 56 | As the primary utility token, $PREDICT is deeply integrated across the platform: 57 | 58 | - **Launchpad Liquidity** — The launchpad will be seeded with $PREDICT liquidity to ensure depth, stability, and fair access for new project discoveries and token launches 59 | - **No-Code Builder Access** — Essential for accessing upcoming no-code builder tools that allow anyone to effortlessly create custom prediction markets, agents, or interfaces with premium features unlocked through holding or using $PREDICT 60 | - **Ecosystem Engagement** — Required for full participation in the broader Predict ecosystem, including creating markets, accessing advanced analytics, AI-driven signals, and governance 61 | 62 | ### 🔥 Staking & Rewards 63 | 64 | A key feature driving adoption is the ability to **stake $PREDICT for attractive APY rewards**, delivering passive yields while empowering holders with enhanced capabilities: 65 | 66 | - **Unlocked Trading Abilities** — Enhanced access to trading features and boosted capabilities 67 | - **Prediction Market Access** — Boosted access to the native prediction market for betting on events, outcomes, or price movements 68 | - **Long-Term Value** — Staking and liquidity provision promotes long-term holding, strengthens network security, and redistributes value directly to the community 69 | 70 | > 💡 **$PREDICT is more than a token** — it's the core fuel powering adoption, liquidity, and innovation in the live PredictOS framework, establishing it as a leader in decentralized social prediction markets. 71 | 72 | ## 🎯 Current Features (v1.0.1) 73 | 74 | | Feature | Status | Description | Setup Guide | 75 | |---------|--------|-------------|-------------| 76 | | **AI Market Analysis** | ✅ Released | Paste a Kalshi or Polymarket URL and get instant AI-powered analysis with probability estimates, confidence scores, and trading recommendations | [📖 Setup Guide](docs/features/market-analysis.md) | 77 | | **Betting Bots** | ✅ Released | Polymarket 15 Minute Up/Down Arbitrage Bot (more bots coming) | [📖 Setup Guide](docs/features/betting-bots.md) | 78 | 79 | ## 🔮 Coming Soon 80 | 81 | | Feature | Description | 82 | |---------|-------------| 83 | | **Agent Battles (x402)** | Pit AI agents against each other to discover winning strategies | 84 | | **No Code Builder** | Build trading strategies without writing code | 85 | | **Whale Tracking** | Monitor and follow large traders across markets | 86 | | **Copytrading** | Automatically copy top-performing traders | 87 | | **Arbitrage Opportunity** | Detect and exploit cross-platform price differences | 88 | | **Perps Trading / Leverage** | Leveraged prediction market positions | 89 | | **$Predict Staking** | Stake for APY rewards, unlock enhanced trading abilities, and get boosted access to prediction markets | 90 | | **Predict Protocol SDK** | For trading Social markets built on Predict (currently Testnet on [predictionxbt.fun](https://predictionxbt.fun)) | 91 | 92 | ## 📦 Architecture 93 | 94 | ``` 95 | PredictOS/ 96 | ├── terminal/ # Frontend (Next.js 14) 97 | │ ├── src/ 98 | │ │ ├── app/ # Next.js App Router 99 | │ │ ├── components/ # React components 100 | │ │ └── types/ # TypeScript definitions 101 | │ └── public/ # Static assets 102 | │ 103 | └── supabase/ # Backend (Supabase) 104 | ├── migrations/ # DB migrations (future features) 105 | └── functions/ 106 | ├── _shared/ # Shared utilities 107 | │ ├── ai/ # AI integrations (xAI Grok & OpenAI) 108 | │ └── dome/ # Dome API client 109 | ├── analyze-event-markets/ # Market analysis endpoint 110 | └── / # Future edge functions 111 | ``` 112 | 113 | > 💡 **Extensibility:** New features are added as Edge Functions under `supabase/functions//` with shared logic in `_shared/`. Database schemas live in `supabase/migrations/`. 114 | 115 | ## 🏁 Getting Started 116 | 117 | ### Prerequisites 118 | 119 | - [Node.js](https://nodejs.org/) v18+ 120 | - [Supabase CLI](https://supabase.com/docs/guides/cli/getting-started) v1.0+ 121 | - [Docker](https://www.docker.com/) (for local Supabase) 122 | 123 | ### 1. Clone the Repository 124 | 125 | ```bash 126 | git clone https://github.com/PredictionXBT/PredictOS.git 127 | cd PredictOS 128 | ``` 129 | 130 | ### 2. Start the Backend (Supabase) 131 | 132 | ```bash 133 | # Navigate to supabase directory 134 | cd supabase 135 | 136 | # Copy environment template and add your API keys 137 | cp .env.example .env.local 138 | ``` 139 | 140 | Edit `.env.local` with the credentials required for the features you want to use: 141 | 142 | > 📖 **Feature-specific setup guides:** 143 | > - **Market Analysis:** [docs/features/market-analysis.md](docs/features/market-analysis.md) — requires `DOME_API_KEY` + AI provider key (`XAI_API_KEY` or `OPENAI_API_KEY`) 144 | > - **Betting Bots:** [docs/features/betting-bots.md](docs/features/betting-bots.md) — requires `POLYMARKET_WALLET_PRIVATE_KEY` + `POLYMARKET_PROXY_WALLET_ADDRESS` 145 | 146 | Example for Market Analysis: 147 | 148 | ```env 149 | DOME_API_KEY=your_dome_api_key # Get from https://dashboard.domeapi.io 150 | 151 | # AI Provider (only one is required) 152 | XAI_API_KEY=your_xai_api_key # Get from https://x.ai 153 | OPENAI_API_KEY=your_openai_api_key # Get from https://platform.openai.com 154 | ``` 155 | 156 | Example for Betting Bots: 157 | 158 | ```env 159 | POLYMARKET_WALLET_PRIVATE_KEY=0x... # Your wallet private key 160 | POLYMARKET_PROXY_WALLET_ADDRESS=0x... # Your Polymarket proxy wallet 161 | ``` 162 | 163 | > 💡 **Note:** See the setup guides linked above for detailed instructions on obtaining each API key and configuration. 164 | 165 | Start the Supabase services: 166 | 167 | ```bash 168 | supabase start 169 | ``` 170 | 171 | Once running, get your local credentials (you'll need these for the frontend): 172 | 173 | ```bash 174 | supabase status 175 | ``` 176 | 177 | This will display your `API URL` and `anon key` — save these for the next step. 178 | 179 | Now start the Edge Functions server (keep this running): 180 | 181 | ```bash 182 | supabase functions serve --env-file .env.local 183 | ``` 184 | 185 | ### 3. Start the Frontend (Terminal) 186 | 187 | Open a **new** terminal: 188 | 189 | ```bash 190 | # Navigate to terminal directory 191 | cd terminal 192 | 193 | # Install dependencies 194 | npm install 195 | 196 | # Copy environment template 197 | cp .env.example .env 198 | ``` 199 | 200 | Edit `.env` with credentials from `supabase status`: 201 | 202 | ```env 203 | SUPABASE_URL= 204 | SUPABASE_ANON_KEY= 205 | 206 | # Edge Function URLs (for local development) 207 | # Note that the base url might vary depending on `supabase status`: 208 | SUPABASE_EDGE_FUNCTION_ANALYZE_EVENT_MARKETS=http://127.0.0.1:54321/functions/v1/analyze-event-markets # Required for Market Analysis 209 | SUPABASE_EDGE_FUNCTION_BETTING_BOT=http://127.0.0.1:54321/functions/v1/polymarket-up-down-15-markets # Required for Betting Bots 210 | ``` 211 | 212 | Start the development server: 213 | 214 | ```bash 215 | npm run dev 216 | ``` 217 | 218 | Your PredictOS terminal will be running at [http://localhost:3000](http://localhost:3000) 219 | 220 | ## 🛠️ Tech Stack 221 | 222 | **Frontend:** 223 | - [Next.js 14](https://nextjs.org/) — React framework with App Router 224 | - [React 18](https://react.dev/) — UI library 225 | - [TailwindCSS](https://tailwindcss.com/) — Utility-first CSS 226 | - [Lucide React](https://lucide.dev/) — Icon library 227 | 228 | **Backend:** 229 | - [Supabase Edge Functions](https://supabase.com/docs/guides/functions) — Serverless Deno runtime 230 | - [Dome API](https://domeapi.io/) — Unified prediction market data 231 | - [xAI Grok](https://x.ai/) — xAI's reasoning models (Grok 4, Grok 4.1) 232 | - [OpenAI GPT](https://openai.com/) — OpenAI's language models (GPT-4.1, GPT-5) 233 | 234 | ## 🤝 Partners 235 | 236 | 237 | 238 | 243 | 249 | 250 | 251 | 256 | 262 | 263 |
239 | 240 | Dome API 241 | 242 | 244 |

Dome API

245 |

The unified API for prediction markets. Dome provides seamless access to Kalshi, Polymarket, and other prediction market platforms through a single, elegant interface.

246 |

🔗 PredictOS is proudly powered by Dome — they handle the complexity of multi-platform data aggregation so we can focus on building the best trading tools.

247 |

🌐 Website · 📊 Dashboard · 𝕏 Twitter

248 |
252 | 253 | OKBet 254 | 255 | 257 |

OKBet

258 |

The FIRST all-in-one prediction markets bot. Available on Telegram and soon on web, OKBet makes it easy to trade prediction markets from anywhere.

259 |

🔗 Our Predict_Agent provides direct OKBet links to place bets on Kalshi and Polymarket in Telegram.

260 |

🤖 Telegram · 🌐 Website · 📖 Docs · 𝕏 Twitter

261 |
264 | 265 | ## 💪 Contributing 266 | 267 | We welcome contributions from the community! Here's how you can help: 268 | 269 | 1. **Fork** the repository 270 | 2. **Create** a feature branch (`git checkout -b feature/amazing-feature`) 271 | 3. **Commit** your changes (`git commit -m 'Add amazing feature'`) 272 | 4. **Push** to the branch (`git push origin feature/amazing-feature`) 273 | 5. **Open** a Pull Request 274 | 275 | ## 📜 License 276 | 277 | This project is licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details. 278 | 279 | ## 🔗 Links 280 | 281 | - **Website:** [predictionxbt.fun](https://predictionxbt.fun) 282 | - **Twitter/X:** [@prediction_xbt](https://x.com/prediction_xbt) 283 | - **GitHub:** [PredictionXBT/PredictOS](https://github.com/PredictionXBT/PredictOS) 284 | 285 | --- 286 | 287 | ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=PredictionXBT/PredictOS&type=Date&theme=dark&v=1)](https://star-history.com/#PredictionXBT/PredictOS&Date) 288 | 289 | --- 290 | 291 |
292 |

Built with ❤️ by the PredictionXBT team

293 |

Powered by Dome

294 |
295 | 296 | -------------------------------------------------------------------------------- /supabase/functions/analyze-event-markets/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Supabase Edge Function: analyze-event-markets 3 | * 4 | * Analyzes all markets for a specific event to find alpha opportunities and predict outcomes. 5 | * Supports Kalshi and Polymarket prediction markets via Dome unified API. 6 | */ 7 | 8 | import { 9 | getKalshiMarketsByEvent, 10 | getPolymarketMarkets, 11 | buildKalshiMarketUrl, 12 | buildPolymarketUrl, 13 | } from "../_shared/dome/endpoints.ts"; 14 | import { analyzeEventMarketsPrompt } from "../_shared/ai/prompts/analyzeEventMarkets.ts"; 15 | import { callGrokResponses } from "../_shared/ai/callGrok.ts"; 16 | import { callOpenAIResponses } from "../_shared/ai/callOpenAI.ts"; 17 | import type { GrokMessage, GrokOutputText, OpenAIMessage, OpenAIOutputText } from "../_shared/ai/types.ts"; 18 | 19 | // OpenAI model identifiers 20 | const OPENAI_MODELS = ["gpt-5.2", "gpt-5.1", "gpt-5-nano", "gpt-4.1", "gpt-4.1-mini"]; 21 | 22 | /** 23 | * Determine if a model is an OpenAI model 24 | */ 25 | function isOpenAIModel(model: string): boolean { 26 | return OPENAI_MODELS.includes(model) || model.startsWith("gpt-"); 27 | } 28 | import type { 29 | AnalyzeMarketRequest, 30 | MarketAnalysis, 31 | AnalyzeMarketResponse, 32 | } from "./types.ts"; 33 | 34 | /** 35 | * Extracts event slug from a Polymarket URL 36 | * Handles URLs like: 37 | * - https://polymarket.com/event/fed-decision-in-december?tid=1765299517368 38 | * - https://polymarket.com/event/will-netflix-close-warner-brothers-acquisition-by-end-of-2026 39 | */ 40 | function extractPolymarketEventSlug(url: string): string | null { 41 | // Remove query parameters 42 | const urlWithoutParams = url.split('?')[0]; 43 | // Split by '/' and take the last element 44 | const parts = urlWithoutParams.split('/'); 45 | return parts[parts.length - 1] || null; 46 | } 47 | 48 | const corsHeaders = { 49 | "Access-Control-Allow-Origin": "*", 50 | "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", 51 | "Access-Control-Allow-Methods": "POST, OPTIONS", 52 | }; 53 | 54 | Deno.serve(async (req: Request) => { 55 | const startTime = Date.now(); 56 | 57 | // Handle CORS preflight 58 | if (req.method === "OPTIONS") { 59 | return new Response(null, { headers: corsHeaders }); 60 | } 61 | 62 | console.log("Received request:", req.method, req.url); 63 | 64 | try { 65 | // Validate request method 66 | if (req.method !== "POST") { 67 | console.log("Invalid method:", req.method); 68 | return new Response( 69 | JSON.stringify({ success: false, error: "Method not allowed. Use POST." }), 70 | { status: 405, headers: { ...corsHeaders, "Content-Type": "application/json" } } 71 | ); 72 | } 73 | 74 | // Parse request body 75 | let requestBody: AnalyzeMarketRequest; 76 | try { 77 | requestBody = await req.json(); 78 | console.log("Request body:", JSON.stringify(requestBody)); 79 | } catch { 80 | console.error("Failed to parse request body"); 81 | return new Response( 82 | JSON.stringify({ success: false, error: "Invalid JSON in request body" }), 83 | { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } 84 | ); 85 | } 86 | 87 | // Extract parameters 88 | const { url, question, pmType, model } = requestBody; 89 | 90 | // Use provided model or default to grok-4-1-fast-reasoning 91 | const selectedModel = model || "grok-4-1-fast-reasoning"; 92 | const useOpenAI = isOpenAIModel(selectedModel); 93 | 94 | // Validate required parameters 95 | if (!url) { 96 | console.log("Missing url parameter"); 97 | return new Response( 98 | JSON.stringify({ success: false, error: "Missing required parameter: 'url'" }), 99 | { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } 100 | ); 101 | } 102 | 103 | // Check if market type is supported 104 | if (pmType !== "Kalshi" && pmType !== "Polymarket") { 105 | console.log("Unsupported market type:", pmType); 106 | return new Response( 107 | JSON.stringify({ success: false, error: "Market type not supported. Use 'Kalshi' or 'Polymarket'" }), 108 | { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } 109 | ); 110 | } 111 | 112 | if (!question) { 113 | console.log("Missing question parameter"); 114 | return new Response( 115 | JSON.stringify({ success: false, error: "Missing required parameter: 'question'" }), 116 | { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } 117 | ); 118 | } 119 | 120 | let eventIdentifier: string; 121 | let markets: unknown[]; 122 | 123 | if (pmType === "Kalshi") { 124 | // Extract event ticker from Kalshi URL (last segment, capitalized) 125 | // e.g., https://kalshi.com/markets/kxcabout/next-cabinet-memeber-out/kxcabout-29 -> KXCABOUT-29 126 | const urlParts = url.split('/'); 127 | const eventTicker = urlParts[urlParts.length - 1]?.toUpperCase(); 128 | 129 | if (!eventTicker) { 130 | console.log("Could not extract event ticker from URL:", url); 131 | return new Response( 132 | JSON.stringify({ success: false, error: "Could not extract event ticker from 'url'" }), 133 | { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } 134 | ); 135 | } 136 | 137 | eventIdentifier = eventTicker; 138 | console.log("Starting Kalshi analysis via Dome API:", { eventTicker, question }); 139 | 140 | // Fetch all markets for the event via Dome API 141 | try { 142 | markets = await getKalshiMarketsByEvent(eventTicker); 143 | console.log(`Found ${markets.length} markets for Kalshi event:`, eventTicker); 144 | } catch (error) { 145 | console.error("Failed to fetch Kalshi markets:", error); 146 | const errorMessage = error instanceof Error ? error.message : "Unknown error"; 147 | const isNotFound = errorMessage.includes("404") || errorMessage.toLowerCase().includes("not found"); 148 | return new Response( 149 | JSON.stringify({ 150 | success: false, 151 | error: isNotFound 152 | ? `Event '${eventTicker}' not found on Kalshi. Please verify the URL is correct.` 153 | : `Failed to fetch markets from Kalshi for event '${eventTicker}': ${errorMessage}`, 154 | metadata: { 155 | requestId: crypto.randomUUID(), 156 | timestamp: new Date().toISOString(), 157 | eventTicker, 158 | marketsCount: 0, 159 | question, 160 | processingTimeMs: Date.now() - startTime, 161 | platform: "Kalshi", 162 | }, 163 | }), 164 | { status: isNotFound ? 404 : 502, headers: { ...corsHeaders, "Content-Type": "application/json" } } 165 | ); 166 | } 167 | } else { 168 | // Polymarket via Dome API 169 | // Extract event slug from URL (remove query params, take last segment) 170 | // e.g., https://polymarket.com/event/fed-decision-in-december?tid=1765299517368 -> fed-decision-in-december 171 | const eventSlug = extractPolymarketEventSlug(url); 172 | 173 | if (!eventSlug) { 174 | console.log("Could not extract event slug from URL:", url); 175 | return new Response( 176 | JSON.stringify({ success: false, error: "Could not extract event slug from 'url'" }), 177 | { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } 178 | ); 179 | } 180 | 181 | eventIdentifier = eventSlug; 182 | console.log("Starting Polymarket analysis via Dome API:", { eventSlug, question }); 183 | 184 | // Fetch markets via Dome API 185 | try { 186 | const response = await getPolymarketMarkets({ slug: eventSlug }); 187 | markets = response.markets; 188 | console.log(`Found ${markets.length} markets for Polymarket event:`, eventSlug); 189 | } catch (error) { 190 | console.error("Failed to fetch Polymarket markets:", error); 191 | const errorMessage = error instanceof Error ? error.message : "Unknown error"; 192 | const isNotFound = errorMessage.includes("404") || errorMessage.toLowerCase().includes("not found"); 193 | return new Response( 194 | JSON.stringify({ 195 | success: false, 196 | error: isNotFound 197 | ? `Event '${eventSlug}' not found on Polymarket. Please verify the URL is correct.` 198 | : `Failed to fetch markets from Polymarket for event '${eventSlug}': ${errorMessage}`, 199 | metadata: { 200 | requestId: crypto.randomUUID(), 201 | timestamp: new Date().toISOString(), 202 | eventTicker: eventSlug, 203 | marketsCount: 0, 204 | question, 205 | processingTimeMs: Date.now() - startTime, 206 | platform: "Polymarket", 207 | }, 208 | }), 209 | { status: isNotFound ? 404 : 502, headers: { ...corsHeaders, "Content-Type": "application/json" } } 210 | ); 211 | } 212 | } 213 | 214 | // Check if any markets were found 215 | if (markets.length === 0) { 216 | const platformName = pmType === "Kalshi" ? "Kalshi" : "Polymarket"; 217 | const identifierType = pmType === "Kalshi" ? "event ticker" : "event slug"; 218 | return new Response( 219 | JSON.stringify({ 220 | success: false, 221 | error: `No markets found for ${identifierType} '${eventIdentifier}' on ${platformName}. Please verify the URL is correct and the event exists.`, 222 | metadata: { 223 | requestId: crypto.randomUUID(), 224 | timestamp: new Date().toISOString(), 225 | eventTicker: eventIdentifier, 226 | marketsCount: 0, 227 | question, 228 | processingTimeMs: Date.now() - startTime, 229 | platform: platformName, 230 | }, 231 | }), 232 | { status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } } 233 | ); 234 | } 235 | 236 | // Build prompt and call AI 237 | const { systemPrompt, userPrompt } = analyzeEventMarketsPrompt(markets, eventIdentifier, question, pmType); 238 | 239 | let aiResponseModel: string; 240 | let aiTokensUsed: number | undefined; 241 | let text: string; 242 | 243 | if (useOpenAI) { 244 | console.log("Calling OpenAI with model:", selectedModel); 245 | const openaiResponse = await callOpenAIResponses( 246 | userPrompt, 247 | systemPrompt, 248 | "json_object", 249 | selectedModel, 250 | 3 251 | ); 252 | console.log("OpenAI response received, tokens:", openaiResponse.usage?.total_tokens); 253 | 254 | aiResponseModel = openaiResponse.model; 255 | aiTokensUsed = openaiResponse.usage?.total_tokens; 256 | 257 | // Parse OpenAI response 258 | const content: OpenAIOutputText[] = []; 259 | for (const item of openaiResponse.output) { 260 | if (item.type === "message") { 261 | const messageItem = item as OpenAIMessage; 262 | content.push(...messageItem.content); 263 | } 264 | } 265 | 266 | text = content 267 | .map((item) => item.text) 268 | .filter((t) => t !== undefined) 269 | .join("\n"); 270 | } else { 271 | console.log("Calling Grok AI with model:", selectedModel); 272 | const grokResponse = await callGrokResponses( 273 | userPrompt, 274 | systemPrompt, 275 | "json_object", 276 | selectedModel, 277 | 3 278 | ); 279 | console.log("Grok response received, tokens:", grokResponse.usage?.total_tokens); 280 | 281 | aiResponseModel = grokResponse.model; 282 | aiTokensUsed = grokResponse.usage?.total_tokens; 283 | 284 | // Parse Grok response 285 | const content: GrokOutputText[] = []; 286 | for (const item of grokResponse.output) { 287 | if (item.type === "message") { 288 | const messageItem = item as GrokMessage; 289 | content.push(...messageItem.content); 290 | } 291 | } 292 | 293 | text = content 294 | .map((item) => item.text) 295 | .filter((t) => t !== undefined) 296 | .join("\n"); 297 | } 298 | 299 | let analysisResult: MarketAnalysis; 300 | try { 301 | analysisResult = JSON.parse(text); 302 | console.log("Analysis result:", analysisResult.ticker, analysisResult.recommendedAction); 303 | } catch (parseError) { 304 | console.error("Failed to parse Grok response:", text.substring(0, 500)); 305 | return new Response( 306 | JSON.stringify({ 307 | success: false, 308 | error: `Failed to parse Grok response as JSON: ${text.substring(0, 200)}`, 309 | metadata: { 310 | requestId: crypto.randomUUID(), 311 | timestamp: new Date().toISOString(), 312 | eventTicker: eventIdentifier, 313 | marketsCount: markets.length, 314 | question, 315 | processingTimeMs: Date.now() - startTime, 316 | }, 317 | }), 318 | { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } 319 | ); 320 | } 321 | 322 | // Return success response 323 | const processingTimeMs = Date.now() - startTime; 324 | console.log("Request completed in", processingTimeMs, "ms"); 325 | 326 | // Build market URL based on platform 327 | let pmMarketUrl: string | undefined; 328 | if (analysisResult.ticker) { 329 | if (pmType === "Kalshi") { 330 | pmMarketUrl = `Market on @Kalshi: ${buildKalshiMarketUrl(analysisResult.ticker)}`; 331 | } else { 332 | // For Polymarket, use the event slug 333 | pmMarketUrl = `Market on @Polymarket: ${buildPolymarketUrl(eventIdentifier)}`; 334 | } 335 | } 336 | 337 | const response: AnalyzeMarketResponse = { 338 | success: true, 339 | data: analysisResult, 340 | metadata: { 341 | requestId: crypto.randomUUID(), 342 | timestamp: new Date().toISOString(), 343 | eventTicker: eventIdentifier, 344 | marketsCount: markets.length, 345 | question, 346 | processingTimeMs, 347 | grokModel: aiResponseModel, 348 | grokTokensUsed: aiTokensUsed, 349 | }, 350 | "pm-market-url": pmMarketUrl, 351 | }; 352 | 353 | return new Response(JSON.stringify(response), { 354 | status: 200, 355 | headers: { ...corsHeaders, "Content-Type": "application/json" }, 356 | }); 357 | 358 | } catch (error) { 359 | console.error("Unhandled error:", error); 360 | return new Response( 361 | JSON.stringify({ 362 | success: false, 363 | error: error instanceof Error ? error.message : "An unexpected error occurred", 364 | metadata: { processingTimeMs: Date.now() - startTime }, 365 | }), 366 | { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } 367 | ); 368 | } 369 | }); 370 | 371 | -------------------------------------------------------------------------------- /supabase/config.toml: -------------------------------------------------------------------------------- 1 | # For detailed configuration reference documentation, visit: 2 | # https://supabase.com/docs/guides/local-development/cli/config 3 | # A string used to distinguish different Supabase projects on the same host. Defaults to the 4 | # working directory name when running `supabase init`. 5 | project_id = "PredictOS" 6 | 7 | [api] 8 | enabled = true 9 | # Port to use for the API URL. 10 | port = 54321 11 | # Schemas to expose in your API. Tables, views and stored procedures in this schema will get API 12 | # endpoints. `public` and `graphql_public` schemas are included by default. 13 | schemas = ["public", "graphql_public"] 14 | # Extra schemas to add to the search_path of every request. 15 | extra_search_path = ["public", "extensions"] 16 | # The maximum number of rows returns from a view, table, or stored procedure. Limits payload size 17 | # for accidental or malicious requests. 18 | max_rows = 1000 19 | 20 | [api.tls] 21 | # Enable HTTPS endpoints locally using a self-signed certificate. 22 | enabled = false 23 | # Paths to self-signed certificate pair. 24 | # cert_path = "../certs/my-cert.pem" 25 | # key_path = "../certs/my-key.pem" 26 | 27 | [db] 28 | # Port to use for the local database URL. 29 | port = 54322 30 | # Port used by db diff command to initialize the shadow database. 31 | shadow_port = 54320 32 | # The database major version to use. This has to be the same as your remote database's. Run `SHOW 33 | # server_version;` on the remote database to check. 34 | major_version = 17 35 | 36 | [db.pooler] 37 | enabled = false 38 | # Port to use for the local connection pooler. 39 | port = 54329 40 | # Specifies when a server connection can be reused by other clients. 41 | # Configure one of the supported pooler modes: `transaction`, `session`. 42 | pool_mode = "transaction" 43 | # How many server connections to allow per user/database pair. 44 | default_pool_size = 20 45 | # Maximum number of client connections allowed. 46 | max_client_conn = 100 47 | 48 | # [db.vault] 49 | # secret_key = "env(SECRET_VALUE)" 50 | 51 | [db.migrations] 52 | # If disabled, migrations will be skipped during a db push or reset. 53 | enabled = true 54 | # Specifies an ordered list of schema files that describe your database. 55 | # Supports glob patterns relative to supabase directory: "./schemas/*.sql" 56 | schema_paths = [] 57 | 58 | [db.seed] 59 | # If enabled, seeds the database after migrations during a db reset. 60 | enabled = true 61 | # Specifies an ordered list of seed files to load during db reset. 62 | # Supports glob patterns relative to supabase directory: "./seeds/*.sql" 63 | sql_paths = ["./seed.sql"] 64 | 65 | [db.network_restrictions] 66 | # Enable management of network restrictions. 67 | enabled = false 68 | # List of IPv4 CIDR blocks allowed to connect to the database. 69 | # Defaults to allow all IPv4 connections. Set empty array to block all IPs. 70 | allowed_cidrs = ["0.0.0.0/0"] 71 | # List of IPv6 CIDR blocks allowed to connect to the database. 72 | # Defaults to allow all IPv6 connections. Set empty array to block all IPs. 73 | allowed_cidrs_v6 = ["::/0"] 74 | 75 | [realtime] 76 | enabled = true 77 | # Bind realtime via either IPv4 or IPv6. (default: IPv4) 78 | # ip_version = "IPv6" 79 | # The maximum length in bytes of HTTP request headers. (default: 4096) 80 | # max_header_length = 4096 81 | 82 | [studio] 83 | enabled = true 84 | # Port to use for Supabase Studio. 85 | port = 54323 86 | # External URL of the API server that frontend connects to. 87 | api_url = "http://127.0.0.1" 88 | # OpenAI API Key to use for Supabase AI in the Supabase Studio. 89 | openai_api_key = "env(OPENAI_API_KEY)" 90 | 91 | # Email testing server. Emails sent with the local dev setup are not actually sent - rather, they 92 | # are monitored, and you can view the emails that would have been sent from the web interface. 93 | [inbucket] 94 | enabled = true 95 | # Port to use for the email testing server web interface. 96 | port = 54324 97 | # Uncomment to expose additional ports for testing user applications that send emails. 98 | # smtp_port = 54325 99 | # pop3_port = 54326 100 | # admin_email = "admin@email.com" 101 | # sender_name = "Admin" 102 | 103 | [storage] 104 | enabled = true 105 | # The maximum file size allowed (e.g. "5MB", "500KB"). 106 | file_size_limit = "50MiB" 107 | 108 | # Uncomment to configure local storage buckets 109 | # [storage.buckets.images] 110 | # public = false 111 | # file_size_limit = "50MiB" 112 | # allowed_mime_types = ["image/png", "image/jpeg"] 113 | # objects_path = "./images" 114 | 115 | # Uncomment to allow connections via S3 compatible clients 116 | # [storage.s3_protocol] 117 | # enabled = true 118 | 119 | # Image transformation API is available to Supabase Pro plan. 120 | # [storage.image_transformation] 121 | # enabled = true 122 | 123 | # Store analytical data in S3 for running ETL jobs over Iceberg Catalog 124 | # This feature is only available on the hosted platform. 125 | [storage.analytics] 126 | enabled = false 127 | max_namespaces = 5 128 | max_tables = 10 129 | max_catalogs = 2 130 | 131 | # Analytics Buckets is available to Supabase Pro plan. 132 | # [storage.analytics.buckets.my-warehouse] 133 | 134 | # Store vector embeddings in S3 for large and durable datasets 135 | # This feature is only available on the hosted platform. 136 | [storage.vector] 137 | enabled = false 138 | max_buckets = 10 139 | max_indexes = 5 140 | 141 | # Vector Buckets is available to Supabase Pro plan. 142 | # [storage.vector.buckets.documents-openai] 143 | 144 | [auth] 145 | enabled = true 146 | # The base URL of your website. Used as an allow-list for redirects and for constructing URLs used 147 | # in emails. 148 | site_url = "http://127.0.0.1:3000" 149 | # A list of *exact* URLs that auth providers are permitted to redirect to post authentication. 150 | additional_redirect_urls = ["https://127.0.0.1:3000"] 151 | # How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week). 152 | jwt_expiry = 3600 153 | # JWT issuer URL. If not set, defaults to the local API URL (http://127.0.0.1:/auth/v1). 154 | # jwt_issuer = "" 155 | # Path to JWT signing key. DO NOT commit your signing keys file to git. 156 | # signing_keys_path = "./signing_keys.json" 157 | # If disabled, the refresh token will never expire. 158 | enable_refresh_token_rotation = true 159 | # Allows refresh tokens to be reused after expiry, up to the specified interval in seconds. 160 | # Requires enable_refresh_token_rotation = true. 161 | refresh_token_reuse_interval = 10 162 | # Allow/disallow new user signups to your project. 163 | enable_signup = true 164 | # Allow/disallow anonymous sign-ins to your project. 165 | enable_anonymous_sign_ins = false 166 | # Allow/disallow testing manual linking of accounts 167 | enable_manual_linking = false 168 | # Passwords shorter than this value will be rejected as weak. Minimum 6, recommended 8 or more. 169 | minimum_password_length = 6 170 | # Passwords that do not meet the following requirements will be rejected as weak. Supported values 171 | # are: `letters_digits`, `lower_upper_letters_digits`, `lower_upper_letters_digits_symbols` 172 | password_requirements = "" 173 | 174 | [auth.rate_limit] 175 | # Number of emails that can be sent per hour. Requires auth.email.smtp to be enabled. 176 | email_sent = 2 177 | # Number of SMS messages that can be sent per hour. Requires auth.sms to be enabled. 178 | sms_sent = 30 179 | # Number of anonymous sign-ins that can be made per hour per IP address. Requires enable_anonymous_sign_ins = true. 180 | anonymous_users = 30 181 | # Number of sessions that can be refreshed in a 5 minute interval per IP address. 182 | token_refresh = 150 183 | # Number of sign up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users). 184 | sign_in_sign_ups = 30 185 | # Number of OTP / Magic link verifications that can be made in a 5 minute interval per IP address. 186 | token_verifications = 30 187 | # Number of Web3 logins that can be made in a 5 minute interval per IP address. 188 | web3 = 30 189 | 190 | # Configure one of the supported captcha providers: `hcaptcha`, `turnstile`. 191 | # [auth.captcha] 192 | # enabled = true 193 | # provider = "hcaptcha" 194 | # secret = "" 195 | 196 | [auth.email] 197 | # Allow/disallow new user signups via email to your project. 198 | enable_signup = true 199 | # If enabled, a user will be required to confirm any email change on both the old, and new email 200 | # addresses. If disabled, only the new email is required to confirm. 201 | double_confirm_changes = true 202 | # If enabled, users need to confirm their email address before signing in. 203 | enable_confirmations = false 204 | # If enabled, users will need to reauthenticate or have logged in recently to change their password. 205 | secure_password_change = false 206 | # Controls the minimum amount of time that must pass before sending another signup confirmation or password reset email. 207 | max_frequency = "1s" 208 | # Number of characters used in the email OTP. 209 | otp_length = 6 210 | # Number of seconds before the email OTP expires (defaults to 1 hour). 211 | otp_expiry = 3600 212 | 213 | # Use a production-ready SMTP server 214 | # [auth.email.smtp] 215 | # enabled = true 216 | # host = "smtp.sendgrid.net" 217 | # port = 587 218 | # user = "apikey" 219 | # pass = "env(SENDGRID_API_KEY)" 220 | # admin_email = "admin@email.com" 221 | # sender_name = "Admin" 222 | 223 | # Uncomment to customize email template 224 | # [auth.email.template.invite] 225 | # subject = "You have been invited" 226 | # content_path = "./supabase/templates/invite.html" 227 | 228 | # Uncomment to customize notification email template 229 | # [auth.email.notification.password_changed] 230 | # enabled = true 231 | # subject = "Your password has been changed" 232 | # content_path = "./templates/password_changed_notification.html" 233 | 234 | [auth.sms] 235 | # Allow/disallow new user signups via SMS to your project. 236 | enable_signup = false 237 | # If enabled, users need to confirm their phone number before signing in. 238 | enable_confirmations = false 239 | # Template for sending OTP to users 240 | template = "Your code is {{ .Code }}" 241 | # Controls the minimum amount of time that must pass before sending another sms otp. 242 | max_frequency = "5s" 243 | 244 | # Use pre-defined map of phone number to OTP for testing. 245 | # [auth.sms.test_otp] 246 | # 4152127777 = "123456" 247 | 248 | # Configure logged in session timeouts. 249 | # [auth.sessions] 250 | # Force log out after the specified duration. 251 | # timebox = "24h" 252 | # Force log out if the user has been inactive longer than the specified duration. 253 | # inactivity_timeout = "8h" 254 | 255 | # This hook runs before a new user is created and allows developers to reject the request based on the incoming user object. 256 | # [auth.hook.before_user_created] 257 | # enabled = true 258 | # uri = "pg-functions://postgres/auth/before-user-created-hook" 259 | 260 | # This hook runs before a token is issued and allows you to add additional claims based on the authentication method used. 261 | # [auth.hook.custom_access_token] 262 | # enabled = true 263 | # uri = "pg-functions:////" 264 | 265 | # Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`. 266 | [auth.sms.twilio] 267 | enabled = false 268 | account_sid = "" 269 | message_service_sid = "" 270 | # DO NOT commit your Twilio auth token to git. Use environment variable substitution instead: 271 | auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)" 272 | 273 | # Multi-factor-authentication is available to Supabase Pro plan. 274 | [auth.mfa] 275 | # Control how many MFA factors can be enrolled at once per user. 276 | max_enrolled_factors = 10 277 | 278 | # Control MFA via App Authenticator (TOTP) 279 | [auth.mfa.totp] 280 | enroll_enabled = false 281 | verify_enabled = false 282 | 283 | # Configure MFA via Phone Messaging 284 | [auth.mfa.phone] 285 | enroll_enabled = false 286 | verify_enabled = false 287 | otp_length = 6 288 | template = "Your code is {{ .Code }}" 289 | max_frequency = "5s" 290 | 291 | # Configure MFA via WebAuthn 292 | # [auth.mfa.web_authn] 293 | # enroll_enabled = true 294 | # verify_enabled = true 295 | 296 | # Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`, 297 | # `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`, 298 | # `twitter`, `slack`, `spotify`, `workos`, `zoom`. 299 | [auth.external.apple] 300 | enabled = false 301 | client_id = "" 302 | # DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead: 303 | secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)" 304 | # Overrides the default auth redirectUrl. 305 | redirect_uri = "" 306 | # Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure, 307 | # or any other third-party OIDC providers. 308 | url = "" 309 | # If enabled, the nonce check will be skipped. Required for local sign in with Google auth. 310 | skip_nonce_check = false 311 | # If enabled, it will allow the user to successfully authenticate when the provider does not return an email address. 312 | email_optional = false 313 | 314 | # Allow Solana wallet holders to sign in to your project via the Sign in with Solana (SIWS, EIP-4361) standard. 315 | # You can configure "web3" rate limit in the [auth.rate_limit] section and set up [auth.captcha] if self-hosting. 316 | [auth.web3.solana] 317 | enabled = false 318 | 319 | # Use Firebase Auth as a third-party provider alongside Supabase Auth. 320 | [auth.third_party.firebase] 321 | enabled = false 322 | # project_id = "my-firebase-project" 323 | 324 | # Use Auth0 as a third-party provider alongside Supabase Auth. 325 | [auth.third_party.auth0] 326 | enabled = false 327 | # tenant = "my-auth0-tenant" 328 | # tenant_region = "us" 329 | 330 | # Use AWS Cognito (Amplify) as a third-party provider alongside Supabase Auth. 331 | [auth.third_party.aws_cognito] 332 | enabled = false 333 | # user_pool_id = "my-user-pool-id" 334 | # user_pool_region = "us-east-1" 335 | 336 | # Use Clerk as a third-party provider alongside Supabase Auth. 337 | [auth.third_party.clerk] 338 | enabled = false 339 | # Obtain from https://clerk.com/setup/supabase 340 | # domain = "example.clerk.accounts.dev" 341 | 342 | # OAuth server configuration 343 | [auth.oauth_server] 344 | # Enable OAuth server functionality 345 | enabled = false 346 | # Path for OAuth consent flow UI 347 | authorization_url_path = "/oauth/consent" 348 | # Allow dynamic client registration 349 | allow_dynamic_registration = false 350 | 351 | [edge_runtime] 352 | enabled = true 353 | # Supported request policies: `oneshot`, `per_worker`. 354 | # `per_worker` (default) — enables hot reload during local development. 355 | # `oneshot` — fallback mode if hot reload causes issues (e.g. in large repos or with symlinks). 356 | policy = "per_worker" 357 | # Port to attach the Chrome inspector for debugging edge functions. 358 | inspector_port = 8083 359 | # The Deno major version to use. 360 | deno_version = 2 361 | 362 | # [edge_runtime.secrets] 363 | # secret_key = "env(SECRET_VALUE)" 364 | 365 | [analytics] 366 | enabled = true 367 | port = 54327 368 | # Configure one of the supported backends: `postgres`, `bigquery`. 369 | backend = "postgres" 370 | 371 | # Experimental features may be deprecated any time 372 | [experimental] 373 | # Configures Postgres storage engine to use OrioleDB (S3) 374 | orioledb_version = "" 375 | # Configures S3 bucket URL, eg. .s3-.amazonaws.com 376 | s3_host = "env(S3_HOST)" 377 | # Configures S3 bucket region, eg. us-east-1 378 | s3_region = "env(S3_REGION)" 379 | # Configures AWS_ACCESS_KEY_ID for S3 bucket 380 | s3_access_key = "env(S3_ACCESS_KEY)" 381 | # Configures AWS_SECRET_ACCESS_KEY for S3 bucket 382 | s3_secret_key = "env(S3_SECRET_KEY)" 383 | -------------------------------------------------------------------------------- /terminal/src/components/BettingBotTerminal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect, useRef, useCallback } from "react"; 4 | import { Play, Square, ChevronDown, Bot, Percent, DollarSign, AlertTriangle, Loader2 } from "lucide-react"; 5 | import type { SupportedAsset, BotLogEntry, LimitOrderBotResponse } from "@/types/betting-bot"; 6 | 7 | const ASSETS: { value: SupportedAsset; label: string; icon: string }[] = [ 8 | { value: "BTC", label: "Bitcoin (BTC)", icon: "₿" }, 9 | { value: "ETH", label: "Ethereum (ETH)", icon: "Ξ" }, 10 | { value: "SOL", label: "Solana (SOL)", icon: "◎" }, 11 | { value: "XRP", label: "Ripple (XRP)", icon: "✕" }, 12 | ]; 13 | 14 | const PRICE_OPTIONS = [ 15 | { value: 45, label: "45%" }, 16 | { value: 46, label: "46%" }, 17 | { value: 47, label: "47%" }, 18 | { value: 48, label: "48% (Recommended)" }, 19 | { value: 49, label: "49%" }, 20 | ]; 21 | 22 | const POLL_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes 23 | 24 | /** 25 | * Get the next 15-minute market timestamp (rounds up to the next 15-min block) 26 | */ 27 | function getNext15MinTimestamp(): Date { 28 | const now = new Date(); 29 | const minutes = now.getMinutes(); 30 | const nextQuarter = Math.ceil(minutes / 15) * 15; 31 | const next = new Date(now); 32 | next.setMinutes(nextQuarter, 0, 0); 33 | if (nextQuarter >= 60) { 34 | next.setHours(next.getHours() + 1); 35 | next.setMinutes(0); 36 | } 37 | return next; 38 | } 39 | 40 | /** 41 | * Format a date to a human-readable string 42 | */ 43 | function formatNextMarketTime(date: Date): string { 44 | return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: true }); 45 | } 46 | 47 | const BettingBotTerminal = () => { 48 | const [selectedAsset, setSelectedAsset] = useState("BTC"); 49 | const [selectedPrice, setSelectedPrice] = useState(48); 50 | const [orderSize, setOrderSize] = useState(25); 51 | const [isDropdownOpen, setIsDropdownOpen] = useState(false); 52 | const [isPriceDropdownOpen, setIsPriceDropdownOpen] = useState(false); 53 | const [isBotRunning, setIsBotRunning] = useState(false); 54 | const [isSubmitting, setIsSubmitting] = useState(false); 55 | const [logs, setLogs] = useState([]); 56 | const [error, setError] = useState(null); 57 | const dropdownRef = useRef(null); 58 | const priceDropdownRef = useRef(null); 59 | const logsEndRef = useRef(null); 60 | const pollIntervalRef = useRef(null); 61 | 62 | // Close dropdowns when clicking outside 63 | useEffect(() => { 64 | const handleClickOutside = (event: MouseEvent) => { 65 | if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { 66 | setIsDropdownOpen(false); 67 | } 68 | if (priceDropdownRef.current && !priceDropdownRef.current.contains(event.target as Node)) { 69 | setIsPriceDropdownOpen(false); 70 | } 71 | }; 72 | document.addEventListener("mousedown", handleClickOutside); 73 | return () => document.removeEventListener("mousedown", handleClickOutside); 74 | }, []); 75 | 76 | // Auto-scroll logs to bottom 77 | useEffect(() => { 78 | logsEndRef.current?.scrollIntoView({ behavior: "smooth" }); 79 | }, [logs]); 80 | 81 | // Cleanup interval on unmount 82 | useEffect(() => { 83 | return () => { 84 | if (pollIntervalRef.current) { 85 | clearInterval(pollIntervalRef.current); 86 | } 87 | }; 88 | }, []); 89 | 90 | // Add local log entry 91 | const addLog = useCallback((level: BotLogEntry["level"], message: string, details?: Record) => { 92 | setLogs(prev => [...prev, { 93 | timestamp: new Date().toISOString(), 94 | level, 95 | message, 96 | details, 97 | }]); 98 | }, []); 99 | 100 | // Submit a single order to the limit order bot 101 | const submitOrder = useCallback(async () => { 102 | setIsSubmitting(true); 103 | setError(null); 104 | 105 | try { 106 | const response = await fetch("/api/limit-order-bot", { 107 | method: "POST", 108 | headers: { "Content-Type": "application/json" }, 109 | body: JSON.stringify({ 110 | asset: selectedAsset, 111 | price: selectedPrice, 112 | sizeUsd: orderSize, 113 | }), 114 | }); 115 | 116 | const data: LimitOrderBotResponse = await response.json(); 117 | 118 | if (!data.success) { 119 | setError(data.error || "Order submission failed"); 120 | addLog("ERROR", data.error || "Order submission failed"); 121 | // Still show next market time after error (15 minutes after current) 122 | const nextMarketStart = new Date(getNext15MinTimestamp().getTime() + 15 * 60 * 1000); 123 | const nextMarketEnd = new Date(nextMarketStart.getTime() + 15 * 60 * 1000); 124 | addLog("INFO", `Next Market Up: ${selectedAsset} Market ${formatNextMarketTime(nextMarketStart)} -- ${formatNextMarketTime(nextMarketEnd)}`); 125 | return; 126 | } 127 | 128 | // Log market result with Polymarket URL and order status 129 | if (data.data?.market) { 130 | const market = data.data.market; 131 | const asset = data.data.asset; 132 | const sizeUsd = data.data.sizeUsd; 133 | const polymarketUrl = `https://polymarket.com/event/${market.marketSlug}`; 134 | 135 | // Calculate market start and end times from the timestamp 136 | const marketStartTime = new Date(market.targetTimestamp * 1000); 137 | const marketEndTime = new Date(marketStartTime.getTime() + 15 * 60 * 1000); 138 | const startTimeStr = formatNextMarketTime(marketStartTime); 139 | const endTimeStr = formatNextMarketTime(marketEndTime); 140 | 141 | if (market.error) { 142 | addLog("ERROR", `${asset} Market ${startTimeStr} -- ${endTimeStr}: ${polymarketUrl} — Failed: ${market.error}`); 143 | } else { 144 | const upStatus = market.ordersPlaced?.up?.success ? "✓" : "✗"; 145 | const downStatus = market.ordersPlaced?.down?.success ? "✓" : "✗"; 146 | addLog("SUCCESS", `${asset} Market ${startTimeStr} -- ${endTimeStr}: ${polymarketUrl} Up: ${upStatus} Down: ${downStatus} $${sizeUsd}`); 147 | } 148 | 149 | // Log the next market time (15 minutes after the one we just placed orders for) 150 | const nextMarketStart = new Date(marketStartTime.getTime() + 15 * 60 * 1000); 151 | const nextMarketEnd = new Date(nextMarketStart.getTime() + 15 * 60 * 1000); 152 | addLog("INFO", `Next Market Up: ${asset} Market ${formatNextMarketTime(nextMarketStart)} -- ${formatNextMarketTime(nextMarketEnd)}`); 153 | } 154 | 155 | } catch (err) { 156 | const errorMsg = err instanceof Error ? err.message : "Network error"; 157 | setError(errorMsg); 158 | addLog("ERROR", `Submission failed: ${errorMsg}`); 159 | } finally { 160 | setIsSubmitting(false); 161 | } 162 | }, [selectedAsset, selectedPrice, orderSize, addLog]); 163 | 164 | // Start the bot with polling 165 | const startBot = useCallback(() => { 166 | setIsBotRunning(true); 167 | setError(null); 168 | addLog("INFO", `Bot started — ${selectedAsset} at ${selectedPrice}% with $${orderSize} total`); 169 | 170 | // Submit immediately 171 | submitOrder(); 172 | 173 | // Set up 15-minute polling 174 | pollIntervalRef.current = setInterval(() => { 175 | submitOrder(); 176 | }, POLL_INTERVAL_MS); 177 | }, [selectedAsset, selectedPrice, orderSize, addLog, submitOrder]); 178 | 179 | // Stop the bot 180 | const stopBot = useCallback(() => { 181 | if (pollIntervalRef.current) { 182 | clearInterval(pollIntervalRef.current); 183 | pollIntervalRef.current = null; 184 | } 185 | setIsBotRunning(false); 186 | addLog("INFO", "Bot stopped"); 187 | }, [addLog]); 188 | 189 | // Get log level styling 190 | const getLogLevelStyle = (level: BotLogEntry["level"]) => { 191 | switch (level) { 192 | case "SUCCESS": 193 | return "text-success"; 194 | case "ERROR": 195 | return "text-destructive"; 196 | case "WARN": 197 | return "text-warning"; 198 | default: 199 | return "text-muted-foreground"; 200 | } 201 | }; 202 | 203 | const getLogLevelIcon = (level: BotLogEntry["level"]) => { 204 | switch (level) { 205 | case "SUCCESS": 206 | return "✓"; 207 | case "ERROR": 208 | return "✗"; 209 | case "WARN": 210 | return "⚠"; 211 | default: 212 | return "›"; 213 | } 214 | }; 215 | 216 | const selectedAssetData = ASSETS.find(a => a.value === selectedAsset); 217 | 218 | return ( 219 |
220 |
221 |
222 | {/* Header */} 223 |
224 |
225 |

226 | Polymarket 15 Minute Up/Down Arbitrage Bot 227 |

228 |

(more bots coming soon)

229 |

230 | Automatically place straddle limit orders on Polymarket 15-minute Up/Down markets every 15 minutes. 231 |

232 |
233 |
234 | 235 | {/* How It Works Section */} 236 |
237 |

238 | How It Works 239 |

240 |
    241 |
  • 242 | 1. 243 | Select your preferred market type (BTC, ETH, SOL, or XRP) for 15-minute Up/Down markets 244 |
  • 245 |
  • 246 | 2. 247 | Set your order price (48% recommended for straddle) and total USD amount per side -- note that total shares cannot be below 5 on Polymarket, hence the minimum order size is $3. 248 |
  • 249 |
  • 250 | 3. 251 | Click "Start Bot" to begin — the bot will place limit orders on the next market immediately 252 |
  • 253 |
  • 254 | 4. 255 | The bot automatically runs every 15 minutes to place orders on each new market 256 |
  • 257 |
  • 258 | 5. 259 | Click "Stop Bot" to pause the bot at any time 260 |
  • 261 |
262 |
263 | 264 | {/* Why It Works Section */} 265 |
266 |
267 |

268 | Why It Works: 269 |

270 | 276 | x.com/hanakoxbt/status/1999149407955308699 277 | 278 |
279 |
280 |

281 | This strategy exploits a simple arbitrage opportunity in binary prediction markets: 282 |

283 |
284 |
285 | 286 | Find a 15m crypto market with high liquidity 287 |
288 |
289 | 290 | Place limit orders: buy "Yes" at $0.48 and "No" at $0.48 291 |
292 |
293 | 294 | Wait until both orders are filled 295 |
296 |
297 | 298 | Total cost: $0.96 for shares on both sides 299 |
300 |
301 |

302 | Regardless of the outcome, one side always pays out $1.00 — guaranteeing a ~4% profit per trade when both orders fill. 303 |

304 |
305 |
306 | 307 | {/* Controls Card */} 308 |
309 |
310 |
311 | 312 | 313 | BOT CONFIGURATION 314 | 315 |
316 | {isBotRunning && ( 317 |
318 |
319 | RUNNING 320 |
321 | )} 322 |
323 | 324 |
325 | {/* Asset Selection Row */} 326 |
327 | 330 | 331 | {/* Asset Dropdown */} 332 |
333 | 345 | 346 | {isDropdownOpen && ( 347 |
348 | {ASSETS.map((asset) => ( 349 | 365 | ))} 366 |
367 | )} 368 |
369 |
370 | 371 | {/* Price Selection Row */} 372 |
373 | 376 | 377 | {/* Price Dropdown */} 378 |
379 | 391 | 392 | {isPriceDropdownOpen && ( 393 |
394 | {PRICE_OPTIONS.map((option) => ( 395 | 410 | ))} 411 |
412 | )} 413 |
414 |
415 | 416 | {/* Order Size Row */} 417 |
418 | 421 | 422 | {/* Order Size Input */} 423 |
424 |
425 | 426 | setOrderSize(Math.max(3, parseInt(e.target.value) || 3))} 430 | disabled={isBotRunning} 431 | min="3" 432 | max="1000" 433 | className="bg-transparent border-none outline-none font-mono w-20 disabled:opacity-50 disabled:cursor-not-allowed" 434 | /> 435 | USD per side 436 |
437 |
438 |
439 | 440 | {/* Start/Stop Bot Button Row */} 441 |
442 | {!isBotRunning ? ( 443 | 452 | ) : ( 453 | 465 | )} 466 |
467 |
468 |
469 | 470 | {/* Error Display */} 471 | {error && ( 472 |
473 |
474 | 475 |

{error}

476 |
477 |
478 | )} 479 | 480 | {/* Logs Output */} 481 |
482 |
483 |
484 |
485 | 486 | BOT LOGS 487 | 488 |
489 | 496 |
497 | 498 |
499 | {logs.length === 0 ? ( 500 |
501 | No logs yet. Start the bot to begin. 502 |
503 | ) : ( 504 |
505 | {logs.map((log, index) => ( 506 |
507 | 508 | {new Date(log.timestamp).toLocaleTimeString()} 509 | 510 | 511 | {getLogLevelIcon(log.level)} 512 | 513 | 514 | {log.message} 515 | 516 | {log.details && ( 517 | 518 | {JSON.stringify(log.details)} 519 | 520 | )} 521 |
522 | ))} 523 |
524 |
525 | )} 526 |
527 |
528 |
529 |
530 |
531 | ); 532 | }; 533 | 534 | export default BettingBotTerminal; 535 | --------------------------------------------------------------------------------