├── 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 |
50 | {/* Logo */}
51 |
52 |
53 |
64 | {!collapsed && (
65 |
66 |
67 | PredictOS
68 |
69 |
70 | All-In-One Prediction Market Framework
71 |
72 |
73 | )}
74 |
75 | {/* Collapse Toggle */}
76 |
setCollapsed(!collapsed)}
78 | className="w-6 h-6 rounded-full bg-secondary border border-border flex items-center justify-center hover:bg-primary/20 hover:border-primary/50 transition-colors shrink-0"
79 | >
80 | {collapsed ? (
81 |
82 | ) : (
83 |
84 | )}
85 |
86 |
87 |
88 | {/* Navigation */}
89 |
90 | {navItems.map((item) => {
91 | const content = (
92 | <>
93 |
98 | {!collapsed && (
99 |
100 | {item.label}
101 | {!item.available && (
102 |
103 | Soon
104 |
105 | )}
106 |
107 | )}
108 | >
109 | );
110 |
111 | const className = cn(
112 | "w-full flex items-center gap-3 px-3 py-3 rounded-lg transition-all duration-200",
113 | item.available ? "hover:bg-secondary/50 cursor-pointer" : "cursor-not-allowed",
114 | activeTab === item.id && item.available
115 | ? "bg-primary/10 terminal-border-glow text-primary"
116 | : "text-muted-foreground hover:text-foreground"
117 | );
118 |
119 | if (item.available && item.href) {
120 | return (
121 |
122 | {content}
123 |
124 | );
125 | }
126 |
127 | return (
128 |
129 | {content}
130 |
131 | );
132 | })}
133 |
134 |
135 | {/* Social Links & Version */}
136 |
137 |
138 |
173 |
174 | {/* Version Tag */}
175 |
176 | v1.0.1
177 |
178 |
179 |
180 |
181 |
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 |
setIsModelDropdownOpen(!isModelDropdownOpen)}
154 | disabled={isLoading}
155 | className="flex items-center gap-1.5 px-2.5 py-1 rounded-md bg-secondary/50 border border-border text-[10px] text-muted-foreground hover:text-foreground hover:border-primary/50 transition-all disabled:opacity-50 disabled:cursor-not-allowed font-mono whitespace-nowrap"
156 | >
157 |
162 | {getProviderBadge(selectedModel)}
163 |
164 | {getModelLabel(selectedModel)}
165 | Model
166 |
167 |
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 | {
186 | setSelectedModel(model.value);
187 | setIsModelDropdownOpen(false);
188 | }}
189 | className={`w-full px-4 py-2.5 text-left text-sm font-mono transition-colors ${
190 | selectedModel === model.value
191 | ? 'bg-primary/20 text-primary'
192 | : 'text-muted-foreground hover:bg-secondary hover:text-foreground'
193 | }`}
194 | >
195 | {model.label}
196 | {model.value}
197 |
198 | ))}
199 |
200 |
201 | {/* OpenAI Section */}
202 |
203 |
204 |
205 | OpenAI
206 |
207 | GPT Models
208 |
209 |
210 |
211 | {OPENAI_MODELS.map((model) => (
212 | {
216 | setSelectedModel(model.value);
217 | setIsModelDropdownOpen(false);
218 | }}
219 | className={`w-full px-4 py-2.5 text-left text-sm font-mono transition-colors ${
220 | selectedModel === model.value
221 | ? 'bg-primary/20 text-primary'
222 | : 'text-muted-foreground hover:bg-secondary hover:text-foreground'
223 | }`}
224 | >
225 | {model.label}
226 | {model.value}
227 |
228 | ))}
229 |
230 |
231 | )}
232 |
233 |
234 |
235 |
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 |
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 |
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 |
239 |
240 |
241 |
242 |
243 |
244 |
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 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
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 |
262 |
263 |
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 [](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 |
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 |
328 | Market Type:
329 |
330 |
331 | {/* Asset Dropdown */}
332 |
333 |
!isBotRunning && setIsDropdownOpen(!isDropdownOpen)}
336 | disabled={isBotRunning}
337 | className="w-full flex items-center justify-between gap-2 px-4 py-3 rounded-lg bg-secondary/50 border border-border text-sm hover:border-primary/50 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
338 | >
339 |
340 | {selectedAssetData?.icon}
341 | {selectedAssetData?.label}
342 |
343 |
344 |
345 |
346 | {isDropdownOpen && (
347 |
348 | {ASSETS.map((asset) => (
349 | {
353 | setSelectedAsset(asset.value);
354 | setIsDropdownOpen(false);
355 | }}
356 | className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${
357 | selectedAsset === asset.value
358 | ? "bg-primary/20 text-primary"
359 | : "hover:bg-secondary text-foreground"
360 | }`}
361 | >
362 | {asset.icon}
363 | {asset.label}
364 |
365 | ))}
366 |
367 | )}
368 |
369 |
370 |
371 | {/* Price Selection Row */}
372 |
373 |
374 | Order Price:
375 |
376 |
377 | {/* Price Dropdown */}
378 |
379 |
!isBotRunning && setIsPriceDropdownOpen(!isPriceDropdownOpen)}
382 | disabled={isBotRunning}
383 | className="w-full flex items-center justify-between gap-2 px-4 py-3 rounded-lg bg-secondary/50 border border-border text-sm hover:border-primary/50 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
384 | >
385 |
386 |
387 |
{selectedPrice}%{selectedPrice === 48 && " (Recommended)"}
388 |
389 |
390 |
391 |
392 | {isPriceDropdownOpen && (
393 |
394 | {PRICE_OPTIONS.map((option) => (
395 | {
399 | setSelectedPrice(option.value);
400 | setIsPriceDropdownOpen(false);
401 | }}
402 | className={`w-full flex items-center gap-3 px-4 py-3 text-left transition-colors ${
403 | selectedPrice === option.value
404 | ? "bg-primary/20 text-primary"
405 | : "hover:bg-secondary text-foreground"
406 | }`}
407 | >
408 | {option.label}
409 |
410 | ))}
411 |
412 | )}
413 |
414 |
415 |
416 | {/* Order Size Row */}
417 |
418 |
419 | Order Size:
420 |
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 |
449 |
450 | Start Bot
451 |
452 | ) : (
453 |
458 | {isSubmitting ? (
459 |
460 | ) : (
461 |
462 | )}
463 | Stop Bot
464 |
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 |
setLogs([])}
492 | className="text-xs text-muted-foreground hover:text-foreground transition-colors"
493 | >
494 | Clear
495 |
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 |
--------------------------------------------------------------------------------