From 2bc869b09a908f46840e3be73c085ea8faa3d77e Mon Sep 17 00:00:00 2001 From: vikions Date: Mon, 9 Feb 2026 23:27:39 +0700 Subject: [PATCH] feat: TGE Discord monitoring agent with auto-trade --- examples/telegram-bot-integration/README.md | 107 ++++ .../telegram-bot-integration/bot_example.py | 206 +++++++ examples/telegram-bot-integration/client.py | 147 +++++ .../functions/_shared/discord/keywords.ts | 50 ++ supabase/functions/_shared/discord/monitor.ts | 46 ++ supabase/functions/_shared/discord/types.ts | 25 + supabase/functions/tge-discord-agent/index.ts | 491 +++++++++++++++ .../src/app/api/tge-discord-agent/route.ts | 47 ++ terminal/src/app/tge-monitor/page.tsx | 19 + terminal/src/components/Sidebar.tsx | 4 +- .../src/components/TgeMonitorTerminal.tsx | 579 ++++++++++++++++++ 11 files changed, 1720 insertions(+), 1 deletion(-) create mode 100644 examples/telegram-bot-integration/README.md create mode 100644 examples/telegram-bot-integration/bot_example.py create mode 100644 examples/telegram-bot-integration/client.py create mode 100644 supabase/functions/_shared/discord/keywords.ts create mode 100644 supabase/functions/_shared/discord/monitor.ts create mode 100644 supabase/functions/_shared/discord/types.ts create mode 100644 supabase/functions/tge-discord-agent/index.ts create mode 100644 terminal/src/app/api/tge-discord-agent/route.ts create mode 100644 terminal/src/app/tge-monitor/page.tsx create mode 100644 terminal/src/components/TgeMonitorTerminal.tsx diff --git a/examples/telegram-bot-integration/README.md b/examples/telegram-bot-integration/README.md new file mode 100644 index 0000000..474078a --- /dev/null +++ b/examples/telegram-bot-integration/README.md @@ -0,0 +1,107 @@ +# Telegram Bot Integration — TGE Discord Agent + +Autonomous TGE monitoring agent that watches Discord, discovers markets via x402, and trades on Polymarket. + +## Architecture + +``` +Discord Channel + → TGE Discord Agent (Supabase Edge Function) + → Step 1: Monitor Discord messages for TGE keywords + → Step 2: x402 tool discovery (PayAI bazaar) + → Step 3: Fetch market data via x402 → Dome API + → Step 4: Execute trade via Polymarket CLOB + → JSON Response + → Telegram notification + trade confirmation +``` + +## Setup + +1. Deploy PredictOS locally + +``` +cd supabase +supabase start +supabase functions serve --env-file .env.local +``` + +2. Required environment variables + +```bash +# Edge function +DISCORD_TOKEN="your_discord_bot_token" +DOME_API_KEY="your_dome_api_key" +X402_DISCOVERY_URL="https://bazaar.payai.network/api/v1/resources" +POLYMARKET_WALLET_PRIVATE_KEY="your_eoa_private_key" +POLYMARKET_PROXY_WALLET_ADDRESS="your_safe_wallet_address" + +# Python bot +PREDICTOS_URL="http://127.0.0.1:54321/functions/v1" +PREDICTOS_KEY="your_supabase_anon_key" +TELEGRAM_TOKEN="your_telegram_bot_token" +``` + +3. Install Python dependencies + +``` +pip install requests python-telegram-bot +``` + +4. Run the bot + +``` +python bot_example.py +``` + +## Usage + +### One-time check (no trade) + +```python +from client import PredictOSClient + +client = PredictOSClient() +result = client.check_tge(channel_id="1072952844161916938") +``` + +### Auto-trade on detection + +```python +result = client.check_tge( + channel_id="1072952844161916938", + market_slug="will-x-launch-tge-2025", + auto_trade={ + "side": "YES", + "budget_usdc": 10, + "min_confidence": 0.6, + }, +) + +if result.get("trade", {}).get("success"): + print(f"Bought {result['trade']['size']} shares!") +``` + +### Telegram bot commands + +- **Check TGE** — one-time check on a selected project +- **Start Monitor** — polls every 60s with auto-trade enabled +- **Stop Monitor** — stops the polling loop + +## Production + +``` +supabase functions deploy tge-discord-agent +``` + +Update `PREDICTOS_URL` to your production Supabase URL. + +## Integration Points + +- **Privy**: Connect wallet on PredictOS frontend for browser-based trading +- **Custom bots**: Use `client.py` as HTTP wrapper from any Python app +- **Direct HTTP**: POST to `/functions/v1/tge-discord-agent` from any language + +## Support + +- Repo: https://github.com/PredictionXBT/PredictOS +- Twitter: @prediction_xbt diff --git a/examples/telegram-bot-integration/bot_example.py b/examples/telegram-bot-integration/bot_example.py new file mode 100644 index 0000000..e933b5b --- /dev/null +++ b/examples/telegram-bot-integration/bot_example.py @@ -0,0 +1,206 @@ +""" +Telegram Bot Example - TGE Discord Monitoring via PredictOS + +Shows how a bot integrates with the TGE Discord Agent: + - Agent handles detection (Discord + Dome + x402) + - Bot handles trading (user's own wallet from DB) +""" + +import os +import asyncio +from typing import Dict, Any + +from telegram import Update, ReplyKeyboardMarkup, KeyboardButton +from telegram.ext import Application, CommandHandler, MessageHandler, ContextTypes, filters + +from client import PredictOSClient + + +predictos = PredictOSClient() + +# Project configs: Discord channel + Polymarket market +PROJECTS: Dict[str, Dict[str, Any]] = { + "Base": { + "channel_id": "1072952844161916938", + "market_slug": "will-base-launch-tge", + "side": "YES", + }, + "Opinion": { + "channel_id": "your_opinion_channel_id", + "market_slug": "will-opinion-launch-token", + "side": "YES", + }, +} + +_monitors: Dict[int, bool] = {} + + +def format_signal(signal: Dict[str, Any], project: str) -> str: + if not signal.get("detected"): + return f"[{project}] No TGE detected." + + lines = [ + f"[{project}] TGE DETECTED", + f"Confidence: {signal.get('confidence', 0):.0%}", + f"Keywords: {', '.join(signal.get('keywords', []))}", + f"Action: {signal.get('recommendation', 'N/A')}", + ] + + msg = signal.get("message") + if msg: + lines.append(f"\nSignal: {msg.get('content', '')[:200]}") + + # Dome market data + for m in signal.get("dome_markets", []): + prices = m.get("outcome_prices", []) + yes_price = f"{prices[0]*100:.0f}%" if prices else "?" + lines.append(f"\nMarket: {m['question']}") + lines.append(f" YES: {yes_price} | Vol: ${m.get('volume', 0):,.0f}") + + # x402 info + tool = signal.get("tool_discovery") + if tool: + lines.append(f"\nx402: {tool['name']}") + + return "\n".join(lines) + + +async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message: + return + keyboard = [ + [KeyboardButton("Check TGE"), KeyboardButton("Start Monitor")], + [KeyboardButton("Stop Monitor")], + ] + await update.message.reply_text( + "TGE Discord Agent (powered by PredictOS)\n\n" + "Check TGE - one-time signal check\n" + "Start Monitor - auto-poll every 60s\n" + "Stop Monitor - stop polling", + reply_markup=ReplyKeyboardMarkup(keyboard, resize_keyboard=True), + ) + + +async def check_tge(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message: + return + keyboard = [[KeyboardButton(p) for p in PROJECTS], [KeyboardButton("Back")]] + await update.message.reply_text( + "Select project:", + reply_markup=ReplyKeyboardMarkup(keyboard, resize_keyboard=True), + ) + context.user_data["mode"] = "check" + + +async def start_monitor(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message: + return + keyboard = [[KeyboardButton(p) for p in PROJECTS], [KeyboardButton("Back")]] + await update.message.reply_text( + "Select project to monitor:", + reply_markup=ReplyKeyboardMarkup(keyboard, resize_keyboard=True), + ) + context.user_data["mode"] = "monitor" + + +async def handle_selection(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message: + return + + project = update.message.text + mode = context.user_data.pop("mode", None) + + if project not in PROJECTS: + await update.message.reply_text("Unknown project.") + return + + config = PROJECTS[project] + + if mode == "monitor": + chat_id = update.message.chat_id + if _monitors.get(chat_id): + await update.message.reply_text("Already monitoring. Stop first.") + return + + _monitors[chat_id] = True + await update.message.reply_text(f"Monitoring {project} every 60s...") + + async def poll() -> None: + while _monitors.get(chat_id): + try: + signal = await asyncio.to_thread( + predictos.detect, + channel_id=config["channel_id"], + market_slug=config.get("market_slug"), + ) + if signal.get("detected"): + msg = format_signal(signal, project) + confidence = signal.get("confidence", 0) + + if confidence >= 0.6: + # ── YOUR BOT TRADES HERE ── + # user_wallet = get_wallet_from_db(chat_id) + # clob = create_clob_client(user_wallet) + # clob.place_order(tokenId=..., price=..., side="BUY") + msg += "\n\n>> Your bot would execute trade here <<" + + await context.bot.send_message(chat_id=chat_id, text=msg) + except Exception as exc: + await context.bot.send_message(chat_id=chat_id, text=f"Error: {exc}") + await asyncio.sleep(60) + + asyncio.create_task(poll()) + return + + # One-time check + loading = await update.message.reply_text(f"Checking {project}...") + signal = await asyncio.to_thread( + predictos.detect, + channel_id=config["channel_id"], + market_slug=config.get("market_slug"), + ) + await loading.delete() + await update.message.reply_text(format_signal(signal, project)) + + +async def stop_monitor(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message: + return + chat_id = update.message.chat_id + if _monitors.pop(chat_id, None): + await update.message.reply_text("Stopped.") + else: + await update.message.reply_text("No monitor running.") + + +async def router(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None: + if not update.message: + return + text = update.message.text + if context.user_data.get("mode"): + await handle_selection(update, context) + elif text == "Check TGE": + await check_tge(update, context) + elif text == "Start Monitor": + await start_monitor(update, context) + elif text == "Stop Monitor": + await stop_monitor(update, context) + elif text == "Back": + await start(update, context) + + +def main() -> None: + token = os.getenv("TELEGRAM_TOKEN") + if not token: + print("TELEGRAM_TOKEN not set") + return + + app = Application.builder().token(token).build() + app.add_handler(CommandHandler("start", start)) + app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, router)) + print("Bot started") + app.run_polling() + + +if __name__ == "__main__": + main() diff --git a/examples/telegram-bot-integration/client.py b/examples/telegram-bot-integration/client.py new file mode 100644 index 0000000..2c1cee6 --- /dev/null +++ b/examples/telegram-bot-integration/client.py @@ -0,0 +1,147 @@ +""" +PredictOS TGE Discord Agent - Python Client + +Two modes: + 1. Detection only — returns signal, your bot trades locally + 2. Auto-trade — agent trades with server wallet (for demo/standalone use) +""" + +import os +from typing import Optional, Dict, Any + +import requests + + +class PredictOSClient: + """ + Client for PredictOS TGE Discord Agent. + + Detection mode (recommended for bots with their own wallets): + client = PredictOSClient() + signal = client.detect(channel_id="123", market_slug="will-x-tge") + if signal["detected"] and signal["confidence"] >= 0.6: + # Your bot trades with user's wallet from your DB + your_clob_client.place_order(...) + + Auto-trade mode (for demo / standalone): + result = client.detect_and_trade( + channel_id="123", + market_slug="will-x-tge", + side="YES", + budget_usdc=10, + ) + """ + + def __init__( + self, + base_url: Optional[str] = None, + api_key: Optional[str] = None, + ) -> None: + self.base_url = base_url or os.getenv("PREDICTOS_URL") + self.api_key = api_key or os.getenv("PREDICTOS_KEY") + + if not self.base_url: + raise ValueError("PREDICTOS_URL not set") + + self.headers: Dict[str, str] = {"Content-Type": "application/json"} + if self.api_key: + self.headers["Authorization"] = f"Bearer {self.api_key}" + + def _call(self, payload: Dict[str, Any]) -> Dict[str, Any]: + try: + resp = requests.post( + f"{self.base_url}/tge-discord-agent", + headers=self.headers, + json=payload, + timeout=30, + ) + resp.raise_for_status() + return resp.json() + except requests.exceptions.RequestException as exc: + return {"detected": False, "error": str(exc)} + + # ── Mode 1: Detection only (recommended for bot integration) ───────── + + def detect( + self, + channel_id: str, + market_slug: Optional[str] = None, + limit: int = 10, + search_query: Optional[str] = None, + ) -> Dict[str, Any]: + """ + Check Discord channel for TGE signals. No trading. + Your bot decides what to do with the signal. + + Returns: + { + "detected": bool, + "confidence": float, # 0-1 + "project": str | None, + "keywords": ["TGE", ...], + "recommendation": "BUY YES" | "MONITOR", + "dome_markets": [...], # Dome API market data + "tool_discovery": {...}, # x402 discovery info + } + """ + payload: Dict[str, Any] = { + "channelId": channel_id, + "checkLatest": False, + "limit": limit, + } + if market_slug: + payload["marketSlug"] = market_slug + if search_query: + payload["searchQuery"] = search_query + + return self._call(payload) + + # ── Mode 2: Detection + trade (demo / standalone) ──────────────────── + + def detect_and_trade( + self, + channel_id: str, + market_slug: str, + side: str = "YES", + budget_usdc: float = 5, + min_confidence: float = 0.6, + ) -> Dict[str, Any]: + """ + Check Discord + auto-trade via server wallet if TGE detected. + Uses POLYMARKET_WALLET_PRIVATE_KEY from edge function env. + """ + return self._call({ + "channelId": channel_id, + "checkLatest": False, + "limit": 10, + "marketSlug": market_slug, + "autoTrade": { + "enabled": True, + "side": side, + "budgetUsdc": budget_usdc, + "minConfidence": min_confidence, + }, + }) + + +if __name__ == "__main__": + client = PredictOSClient() + + # Detection only — no trade + signal = client.detect(channel_id="1072952844161916938") + + if signal.get("detected"): + print(f"TGE: {signal.get('project')} | {signal.get('confidence'):.0%}") + print(f"Keywords: {', '.join(signal.get('keywords', []))}") + print(f"Action: {signal.get('recommendation')}") + + # Dome markets found + for m in signal.get("dome_markets", []): + print(f" Market: {m['question']} | {m['slug']}") + + # x402 tool + tool = signal.get("tool_discovery") + if tool: + print(f" x402: {tool['name']}") + else: + print("No TGE detected") diff --git a/supabase/functions/_shared/discord/keywords.ts b/supabase/functions/_shared/discord/keywords.ts new file mode 100644 index 0000000..a51f9e0 --- /dev/null +++ b/supabase/functions/_shared/discord/keywords.ts @@ -0,0 +1,50 @@ +export const TGE_KEYWORDS = [ + "tge", + "token generation", + "token launch", + "token generation event", + "airdrop", + "claim", + "snapshot", + "listing", + "trading starts", + "dex listing", + "cex listing", + "tokenomics", + "token distribution", +]; + +export function hasTGEKeywords(text: string): boolean { + const lowercased = text.toLowerCase(); + return TGE_KEYWORDS.some((keyword) => lowercased.includes(keyword)); +} + +export function extractMatchedKeywords(text: string): string[] { + const lowercased = text.toLowerCase(); + return TGE_KEYWORDS.filter((keyword) => lowercased.includes(keyword)); +} + +export function extractProjectName(text: string): string | null { + // Simple heuristic: look for capitalized words before TGE keywords + const patterns = [ + /(\w+)\s+(?:will\s+)?(?:launch|announce|tge|token)/i, + /(\w+)\s+token\s+(?:launch|generation)/i, + ]; + + for (const pattern of patterns) { + const match = text.match(pattern); + if (match && match[1]) { + return match[1]; + } + } + + return null; +} + +export function calculateConfidence(text: string, keywords: string[]): number { + // More matched keywords = higher confidence + const maxKeywords = 5; + const matchCount = Math.min(keywords.length, maxKeywords); + return matchCount / maxKeywords; +} + diff --git a/supabase/functions/_shared/discord/monitor.ts b/supabase/functions/_shared/discord/monitor.ts new file mode 100644 index 0000000..d23d2a7 --- /dev/null +++ b/supabase/functions/_shared/discord/monitor.ts @@ -0,0 +1,46 @@ +import type { DiscordMessage } from "./types.ts"; + +export class DiscordMonitor { + private token: string; + private baseUrl = "https://discord.com/api/v10"; + + constructor(token: string) { + this.token = token; + } + + async getMessages(channelId: string, limit: number = 10): Promise { + const response = await fetch( + `${this.baseUrl}/channels/${channelId}/messages?limit=${limit}`, + { + headers: { + Authorization: `Bot ${this.token}`, + "Content-Type": "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error(`Discord API error: ${response.status}`); + } + + const messages = await response.json(); + + return (messages || []).map((msg: Record) => ({ + id: String(msg.id || ""), + content: String(msg.content || ""), + author: { + id: String((msg.author as { id?: string })?.id || ""), + username: String((msg.author as { username?: string })?.username || ""), + global_name: (msg.author as { global_name?: string })?.global_name, + }, + timestamp: String(msg.timestamp || ""), + channel_id: String(msg.channel_id || channelId), + })); + } + + async getLatestMessage(channelId: string): Promise { + const messages = await this.getMessages(channelId, 1); + return messages.length > 0 ? messages[0] : null; + } +} + diff --git a/supabase/functions/_shared/discord/types.ts b/supabase/functions/_shared/discord/types.ts new file mode 100644 index 0000000..86ef51b --- /dev/null +++ b/supabase/functions/_shared/discord/types.ts @@ -0,0 +1,25 @@ +export interface DiscordMessage { + id: string; + content: string; + author: { + id: string; + username: string; + global_name?: string; + }; + timestamp: string; + channel_id: string; +} + +export interface DiscordConfig { + token: string; + channelId: string; +} + +export interface TGEDetectionResult { + detected: boolean; + project?: string; + keywords?: string[]; + message?: DiscordMessage; + confidence?: number; +} + diff --git a/supabase/functions/tge-discord-agent/index.ts b/supabase/functions/tge-discord-agent/index.ts new file mode 100644 index 0000000..d0eff34 --- /dev/null +++ b/supabase/functions/tge-discord-agent/index.ts @@ -0,0 +1,491 @@ +/** + * Supabase Edge Function: tge-discord-agent + * + * Autonomous TGE monitoring agent: + * 1. Monitors Discord channels for TGE announcements + * 2. Discovers market data tools via x402 protocol + * 3. Fetches Polymarket data through x402 → Dome API + * 4. Executes trades automatically via CLOB client + */ + +import { DiscordMonitor } from "../_shared/discord/monitor.ts"; +import { + hasTGEKeywords, + extractMatchedKeywords, + extractProjectName, + calculateConfidence, +} from "../_shared/discord/keywords.ts"; +import type { DiscordMessage } from "../_shared/discord/types.ts"; +import { getPolymarketMarkets } from "../_shared/dome/endpoints.ts"; +import type { PolymarketMarket as DomeMarket } from "../_shared/dome/types.ts"; +import { PolymarketClient, createClientFromEnv } from "../_shared/polymarket/client.ts"; +import { listBazaarSellers, callX402Seller } from "../_shared/x402/client.ts"; +import type { X402SellerInfo } from "../_shared/x402/types.ts"; + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": + "authorization, x-client-info, apikey, content-type", + "Access-Control-Allow-Methods": "POST, OPTIONS", +}; + +// ─── Request / Response types ─────────────────────────────────────────────── + +interface TgeDiscordRequest { + channelId: string; + checkLatest?: boolean; + limit?: number; + /** Polymarket market slug — if omitted, agent tries x402→Dome discovery */ + marketSlug?: string; + /** Free-text query for x402 market discovery */ + searchQuery?: string; + /** Auto-trade configuration */ + autoTrade?: AutoTradeConfig; +} + +interface AutoTradeConfig { + enabled: boolean; + side: "YES" | "NO"; + budgetUsdc: number; + minConfidence?: number; +} + +interface DetectionResult { + message: DiscordMessage; + detected: boolean; + keywords: string[]; + project: string | null; + confidence: number; +} + +interface TradeExecution { + success: boolean; + orderId?: string; + status?: string; + side: "YES" | "NO"; + price: number; + size: number; + costUsd: number; + marketSlug: string; + marketTitle: string; + error?: string; +} + +interface X402Discovery { + name: string; + resourceUrl: string; + priceUsdc: string; + networks: string[]; + marketDataFromX402?: boolean; +} + +// ─── Dome API helpers ──────────────────────────────────────────────────────── + +interface DomeMarketInfo { + question: string; + slug: string; + outcome_prices: number[]; + volume: number; + liquidity: number; + active: boolean; +} + +/** + * Search Polymarket markets via Dome API (direct call). + * This is the primary data path — required for the Dome API prize track. + */ +async function searchMarketsViaDome(query: string): Promise { + try { + console.log("[tge-discord-agent] Dome API: searching markets for:", query); + const response = await getPolymarketMarkets({ + slug: query, + active: true, + limit: 5, + }); + + if (response.markets && response.markets.length > 0) { + console.log("[tge-discord-agent] Dome API: found", response.markets.length, "markets"); + return response.markets.map((m: DomeMarket) => ({ + question: m.question, + slug: m.slug, + outcome_prices: m.outcome_prices, + volume: m.volume, + liquidity: m.liquidity, + active: m.active, + })); + } + + // Fallback: try market_slug parameter + const response2 = await getPolymarketMarkets({ + market_slug: query, + active: true, + limit: 5, + }); + + if (response2.markets && response2.markets.length > 0) { + console.log("[tge-discord-agent] Dome API (market_slug): found", response2.markets.length, "markets"); + return response2.markets.map((m: DomeMarket) => ({ + question: m.question, + slug: m.slug, + outcome_prices: m.outcome_prices, + volume: m.volume, + liquidity: m.liquidity, + active: m.active, + })); + } + + return []; + } catch (error) { + console.warn("[tge-discord-agent] Dome API search failed:", error); + return []; + } +} + +// ─── x402 helpers ─────────────────────────────────────────────────────────── + +function scoreSeller(seller: X402SellerInfo, tokens: string[]): number { + const haystack = [ + seller.name, + seller.description || "", + seller.resourceUrl, + seller.inputDescription || "", + ] + .join(" ") + .toLowerCase(); + + let score = 0; + for (const token of tokens) { + if (haystack.includes(token)) score += 1; + } + return score; +} + +async function discoverX402Tool(query: string): Promise { + const discoveryUrl = Deno.env.get("X402_DISCOVERY_URL"); + if (!discoveryUrl) return null; + + const tokens = query.toLowerCase().split(/\s+/).filter(Boolean); + if (tokens.length === 0) return null; + + try { + const sellers = await listBazaarSellers({ limit: 50, offset: 0 }); + let best: { seller: X402SellerInfo; score: number } | null = null; + + for (const seller of sellers) { + const score = scoreSeller(seller, tokens); + if (score > 0 && (!best || score > best.score)) { + best = { seller, score }; + } + } + return best ? best.seller : null; + } catch (error) { + console.warn("[tge-discord-agent] x402 discovery failed:", error); + return null; + } +} + +async function fetchMarketViaX402( + searchQuery: string +): Promise<{ markets: Array>; usedX402: boolean }> { + try { + // Try explicit seller URL first, then discover + let sellerUrl = Deno.env.get("X402_DOME_SELLER_URL"); + + if (!sellerUrl) { + const seller = await discoverX402Tool("polymarket dome market data"); + if (!seller) return { markets: [], usedX402: false }; + sellerUrl = seller.resourceUrl; + console.log("[tge-discord-agent] Discovered x402 Dome seller:", sellerUrl); + } + + const result = await callX402Seller(sellerUrl, searchQuery); + if (result.success && result.data) { + const data = result.data as Record; + const markets = Array.isArray(data) + ? data + : Array.isArray(data.markets) + ? (data.markets as Array>) + : []; + return { markets, usedX402: true }; + } + return { markets: [], usedX402: true }; + } catch (error) { + console.warn("[tge-discord-agent] x402 market fetch failed:", error); + return { markets: [], usedX402: false }; + } +} + +// ─── Trade execution ──────────────────────────────────────────────────────── + +async function executeTrade( + marketSlug: string, + side: "YES" | "NO", + budgetUsdc: number +): Promise { + console.log("[tge-discord-agent] Executing trade:", { marketSlug, side, budgetUsdc }); + + try { + const client = createClientFromEnv(); + const market = await client.getMarketBySlug(marketSlug); + + if (!market) { + return { success: false, side, price: 0, size: 0, costUsd: 0, marketSlug, marketTitle: "", error: `Market not found: ${marketSlug}` }; + } + if (!market.acceptingOrders) { + return { success: false, side, price: 0, size: 0, costUsd: 0, marketSlug, marketTitle: market.title, error: "Market is not accepting orders" }; + } + if (market.closed) { + return { success: false, side, price: 0, size: 0, costUsd: 0, marketSlug, marketTitle: market.title, error: "Market is closed" }; + } + + const tokenIds = client.extractTokenIds(market); + const outcomes: string[] = JSON.parse(market.outcomes || '["Yes", "No"]'); + const prices: string[] = JSON.parse(market.outcomePrices || '["0.5", "0.5"]'); + + const yesIdx = outcomes.findIndex((o) => o.toLowerCase() === "yes" || o.toLowerCase() === "up"); + const noIdx = outcomes.findIndex((o) => o.toLowerCase() === "no" || o.toLowerCase() === "down"); + + let tokenId: string; + let currentPrice: number; + + if (side === "YES") { + tokenId = yesIdx === 0 ? tokenIds.up : tokenIds.down; + currentPrice = parseFloat(prices[yesIdx >= 0 ? yesIdx : 0]); + } else { + tokenId = noIdx === 0 ? tokenIds.up : tokenIds.down; + currentPrice = parseFloat(prices[noIdx >= 0 ? noIdx : 1]); + } + + const size = budgetUsdc / currentPrice; + + if (Math.floor(size) < 5) { + return { + success: false, side, price: currentPrice, size: Math.floor(size), + costUsd: budgetUsdc, marketSlug, marketTitle: market.title, + error: `Budget too small. At ${(currentPrice * 100).toFixed(1)}% need min $${(5 * currentPrice).toFixed(2)}`, + }; + } + + const orderResponse = await client.placeOrder({ + tokenId, + price: currentPrice, + size, + side: "BUY", + }); + + return { + success: orderResponse.success, + orderId: orderResponse.orderId, + status: orderResponse.status, + side, + price: currentPrice, + size: Math.floor(size), + costUsd: Math.round(Math.floor(size) * currentPrice * 100) / 100, + marketSlug, + marketTitle: market.title, + error: orderResponse.errorMsg, + }; + } catch (error) { + console.error("[tge-discord-agent] Trade error:", error); + return { + success: false, side, price: 0, size: 0, costUsd: 0, marketSlug, + marketTitle: "", error: error instanceof Error ? error.message : "Unknown trade error", + }; + } +} + +// ─── Utility ──────────────────────────────────────────────────────────────── + +function isDiscordMessage(m: DiscordMessage | null): m is DiscordMessage { + return m !== null; +} + +// ─── Main handler ─────────────────────────────────────────────────────────── + +Deno.serve(async (req: Request) => { + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); + } + + if (req.method !== "POST") { + return new Response( + JSON.stringify({ error: "Method not allowed. Use POST.", detected: false }), + { status: 405, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + const startTime = Date.now(); + + try { + const body = (await req.json()) as TgeDiscordRequest; + const channelId = body.channelId ? String(body.channelId).trim() : ""; + + if (!channelId) { + return new Response( + JSON.stringify({ error: "channelId is required", detected: false }), + { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + const discordToken = Deno.env.get("DISCORD_TOKEN"); + if (!discordToken) throw new Error("DISCORD_TOKEN not configured"); + + const checkLatest = body.checkLatest !== false; + const limit = typeof body.limit === "number" && body.limit > 0 + ? Math.min(body.limit, 20) + : 5; + + // ── Step 1: Monitor Discord ────────────────────────────────────────── + + const monitor = new DiscordMonitor(discordToken); + const messages = checkLatest + ? [await monitor.getLatestMessage(channelId)] + : await monitor.getMessages(channelId, limit); + + const validMessages = messages.filter(isDiscordMessage); + + if (validMessages.length === 0) { + return new Response( + JSON.stringify({ detected: false, status_message: "No messages found in channel", checked_messages: 0 }), + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + // ── Step 2: Detect TGE keywords ────────────────────────────────────── + + const results: DetectionResult[] = validMessages.map((message) => { + const detected = hasTGEKeywords(message.content); + const keywords = detected ? extractMatchedKeywords(message.content) : []; + const project = detected ? extractProjectName(message.content) : null; + const confidence = detected ? calculateConfidence(message.content, keywords) : 0; + return { message, detected, keywords, project, confidence }; + }); + + const bestMatch = results + .filter((r) => r.detected) + .sort((a, b) => b.confidence - a.confidence)[0]; + + // ── Step 3: x402 tool discovery ────────────────────────────────────── + + const toolDiscovery = await discoverX402Tool("polymarket market data prediction"); + + const x402Info: X402Discovery | null = toolDiscovery + ? { + name: toolDiscovery.name, + resourceUrl: toolDiscovery.resourceUrl, + priceUsdc: toolDiscovery.priceUsdc, + networks: toolDiscovery.networks, + } + : null; + + // ── No TGE detected → return early ─────────────────────────────────── + + if (!bestMatch) { + return new Response( + JSON.stringify({ + detected: false, + checked_messages: validMessages.length, + status_message: "No TGE announcements detected", + tool_discovery: x402Info, + processingTimeMs: Date.now() - startTime, + }), + { headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } + + // ── Step 4a: Market data via Dome API (direct) ───────────────────────── + + const searchQuery = body.searchQuery || body.marketSlug || bestMatch.project || bestMatch.keywords.join(" "); + + let domeMarkets: DomeMarketInfo[] = []; + if (searchQuery) { + domeMarkets = await searchMarketsViaDome(searchQuery); + } + + // ── Step 4b: Market discovery via x402 (enrichment) ────────────────── + + let x402MarketData: Array> = []; + let usedX402ForMarket = false; + + if (searchQuery) { + const x402Result = await fetchMarketViaX402(searchQuery); + x402MarketData = x402Result.markets; + usedX402ForMarket = x402Result.usedX402; + } + + if (x402Info) { + x402Info.marketDataFromX402 = usedX402ForMarket; + } + + // ── Step 5: Trade execution ────────────────────────────────────────── + + let trade: TradeExecution | null = null; + + if (body.autoTrade?.enabled && body.marketSlug) { + const minConfidence = typeof body.autoTrade.minConfidence === "number" + ? Math.min(Math.max(body.autoTrade.minConfidence, 0), 1) + : 0.6; + + if (bestMatch.confidence >= minConfidence) { + trade = await executeTrade( + body.marketSlug, + body.autoTrade.side || "YES", + body.autoTrade.budgetUsdc || 5 + ); + } else { + trade = { + success: false, + side: body.autoTrade.side || "YES", + price: 0, size: 0, costUsd: 0, + marketSlug: body.marketSlug, marketTitle: "", + error: `Confidence ${(bestMatch.confidence * 100).toFixed(0)}% below threshold ${(minConfidence * 100).toFixed(0)}%`, + }; + } + } + + // ── Step 6: Response ───────────────────────────────────────────────── + + const response = { + detected: true, + project: bestMatch.project || undefined, + keywords: bestMatch.keywords, + confidence: bestMatch.confidence, + message: { + id: bestMatch.message.id, + content: bestMatch.message.content.substring(0, 500), + author: bestMatch.message.author.username, + timestamp: bestMatch.message.timestamp, + }, + recommendation: bestMatch.confidence > 0.6 + ? `BUY ${body.autoTrade?.side || "YES"}` + : "MONITOR", + reasoning: `Detected ${bestMatch.keywords.length} TGE keyword(s) with ${Math.round(bestMatch.confidence * 100)}% confidence`, + checked_messages: validMessages.length, + + // Dome API results (direct call) + dome_markets: domeMarkets.length > 0 ? domeMarkets : undefined, + + // x402 protocol results + tool_discovery: x402Info, + x402_market_results: x402MarketData.length > 0 ? x402MarketData.slice(0, 5) : undefined, + + // Trade execution + trade: trade || undefined, + processingTimeMs: Date.now() - startTime, + }; + + return new Response(JSON.stringify(response), { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("[tge-discord-agent] Error:", error); + return new Response( + JSON.stringify({ + error: error instanceof Error ? error.message : "Unknown error", + detected: false, + processingTimeMs: Date.now() - startTime, + }), + { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } } + ); + } +}); diff --git a/terminal/src/app/api/tge-discord-agent/route.ts b/terminal/src/app/api/tge-discord-agent/route.ts new file mode 100644 index 0000000..a36b56b --- /dev/null +++ b/terminal/src/app/api/tge-discord-agent/route.ts @@ -0,0 +1,47 @@ +import { NextRequest, NextResponse } from "next/server"; + +export async function POST(request: NextRequest) { + try { + const supabaseUrl = process.env.SUPABASE_URL; + const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; + + if (!supabaseUrl || !supabaseAnonKey) { + return NextResponse.json( + { error: "Server configuration error: Missing Supabase credentials" }, + { status: 500 } + ); + } + + const body = await request.json(); + + if (!body.channelId) { + return NextResponse.json( + { error: "Missing required field: channelId" }, + { status: 400 } + ); + } + + const edgeFunctionUrl = process.env.SUPABASE_EDGE_FUNCTION_TGE_DISCORD_AGENT + || `${supabaseUrl}/functions/v1/tge-discord-agent`; + + const response = await fetch(edgeFunctionUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${supabaseAnonKey}`, + apikey: supabaseAnonKey, + }, + body: JSON.stringify(body), + }); + + const data = await response.json(); + return NextResponse.json(data, { status: response.status }); + } catch (error) { + console.error("Error in tge-discord-agent API route:", error); + return NextResponse.json( + { error: error instanceof Error ? error.message : "An unexpected error occurred" }, + { status: 500 } + ); + } +} + diff --git a/terminal/src/app/tge-monitor/page.tsx b/terminal/src/app/tge-monitor/page.tsx new file mode 100644 index 0000000..0c0c716 --- /dev/null +++ b/terminal/src/app/tge-monitor/page.tsx @@ -0,0 +1,19 @@ +"use client"; + +import TgeMonitorTerminal from "@/components/TgeMonitorTerminal"; +import Sidebar from "@/components/Sidebar"; + +export default function TgeMonitorPage() { + return ( +
+
+ +
+ +
+ +
+
+ ); +} + diff --git a/terminal/src/components/Sidebar.tsx b/terminal/src/components/Sidebar.tsx index 6d20972..e212ee7 100644 --- a/terminal/src/components/Sidebar.tsx +++ b/terminal/src/components/Sidebar.tsx @@ -17,7 +17,8 @@ import { ArrowLeftRight, Coins, TrendingUp, - Eye + Eye, + Bell } from "lucide-react"; interface SidebarProps { @@ -27,6 +28,7 @@ interface SidebarProps { const navItems = [ { id: "analysis", label: "Predict Super Intelligence", icon: BarChart3, available: true, href: "/market-analysis" }, { id: "arbitrage", label: "Arbitrage Intelligence", icon: ArrowLeftRight, available: true, href: "/arbitrage" }, + { id: "tge-monitor", label: "TGE Monitor", icon: Bell, available: true, href: "/tge-monitor" }, { id: "betting-bots", label: "Betting Bots", icon: Bot, available: true, href: "/betting-bots" }, { id: "wallet-tracking", label: "Wallet Tracking", icon: Eye, available: true, href: "/wallet-tracking" }, { id: "no-code-builder", label: "No Code Builder", icon: Wand2, available: false }, diff --git a/terminal/src/components/TgeMonitorTerminal.tsx b/terminal/src/components/TgeMonitorTerminal.tsx new file mode 100644 index 0000000..2d8109e --- /dev/null +++ b/terminal/src/components/TgeMonitorTerminal.tsx @@ -0,0 +1,579 @@ +"use client"; + +import { useState, useRef, useCallback, useEffect } from "react"; +import { + Bell, + Hash, + Search, + Loader2, + CheckCircle2, + XCircle, + Activity, + ExternalLink, + Play, + Square, + Zap, + TrendingUp, +} from "lucide-react"; + +// ─── Types ────────────────────────────────────────────────────────────────── + +interface TgeMessage { + id: string; + content: string; + author: string; + timestamp: string; +} + +interface TradeResult { + success: boolean; + orderId?: string; + status?: string; + side: string; + price: number; + size: number; + costUsd: number; + marketSlug: string; + marketTitle: string; + error?: string; +} + +interface ToolDiscovery { + name: string; + resourceUrl: string; + priceUsdc: string; + networks: string[]; + marketDataFromX402?: boolean; +} + +interface TgeResponse { + detected: boolean; + project?: string; + keywords?: string[]; + confidence?: number; + message?: TgeMessage; + status_message?: string; + recommendation?: string; + reasoning?: string; + checked_messages?: number; + tool_discovery?: ToolDiscovery | null; + x402_market_results?: Array>; + trade?: TradeResult; + processingTimeMs?: number; + error?: string; +} + +// ─── Constants ────────────────────────────────────────────────────────────── + +const DEFAULT_CHANNEL_ID = "1072952844161916938"; +const POLL_INTERVALS = [ + { label: "10s", value: 10_000 }, + { label: "30s", value: 30_000 }, + { label: "1m", value: 60_000 }, + { label: "5m", value: 300_000 }, +]; + +function formatTimestamp(timestamp?: string): string { + if (!timestamp) return "Unknown"; + const date = new Date(timestamp); + return Number.isNaN(date.getTime()) ? timestamp : date.toLocaleString(); +} + +// ─── Component ────────────────────────────────────────────────────────────── + +const TgeMonitorTerminal = () => { + // Inputs + const [channelId, setChannelId] = useState(DEFAULT_CHANNEL_ID); + const [marketSlug, setMarketSlug] = useState(""); + const [tradeSide, setTradeSide] = useState<"YES" | "NO">("YES"); + const [budgetUsdc, setBudgetUsdc] = useState("5"); + const [autoTradeEnabled, setAutoTradeEnabled] = useState(false); + const [minConfidence, setMinConfidence] = useState("0.6"); + + // Polling + const [monitoring, setMonitoring] = useState(false); + const [pollInterval, setPollInterval] = useState(30_000); + const pollRef = useRef | null>(null); + + // State + const [loading, setLoading] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [history, setHistory] = useState>([]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (pollRef.current) clearInterval(pollRef.current); + }; + }, []); + + const runCheck = useCallback(async () => { + if (!channelId) return; + setLoading(true); + setError(null); + + try { + const payload: Record = { + channelId, + checkLatest: false, + limit: 10, + }; + + if (marketSlug) payload.marketSlug = marketSlug; + + if (autoTradeEnabled && marketSlug) { + payload.autoTrade = { + enabled: true, + side: tradeSide, + budgetUsdc: parseFloat(budgetUsdc) || 5, + minConfidence: parseFloat(minConfidence) || 0.6, + }; + } + + const response = await fetch("/api/tge-discord-agent", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + + const data: TgeResponse = await response.json(); + if (!response.ok) setError(data.error || "Request failed"); + setResult(data); + + // Add to history + setHistory((prev) => [ + { + time: new Date().toLocaleTimeString(), + detected: data.detected, + project: data.project, + trade: data.trade, + }, + ...prev.slice(0, 19), + ]); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to check channel"); + } finally { + setLoading(false); + } + }, [channelId, marketSlug, autoTradeEnabled, tradeSide, budgetUsdc, minConfidence]); + + const toggleMonitoring = useCallback(() => { + if (monitoring) { + if (pollRef.current) clearInterval(pollRef.current); + pollRef.current = null; + setMonitoring(false); + } else { + setMonitoring(true); + runCheck(); // Immediate first check + pollRef.current = setInterval(runCheck, pollInterval); + } + }, [monitoring, pollInterval, runCheck]); + + return ( +
+ {/* Header */} +
+
+
+
+
+ +
+
+

+ TGE Discord Agent +

+

+ Autonomous TGE detection → x402 discovery → Polymarket trading +

+
+
+ {monitoring && ( + + + MONITORING + + )} +
+
+
+ +
+ {/* ─── Config Panel ────────────────────────────────────────────── */} +
+ {/* Row 1: Channel + Market */} +
+
+ +
+ + setChannelId(e.target.value)} + placeholder={DEFAULT_CHANNEL_ID} + className="w-full h-[46px] pl-10 pr-4 bg-secondary rounded-lg border border-border focus:border-primary focus:ring-1 focus:ring-primary/50 transition-all text-foreground placeholder:text-muted-foreground/50" + /> +
+
+
+ +
+ + setMarketSlug(e.target.value)} + placeholder="e.g. will-x-launch-tge-2025" + className="w-full h-[46px] pl-10 pr-4 bg-secondary rounded-lg border border-border focus:border-primary focus:ring-1 focus:ring-primary/50 transition-all text-foreground placeholder:text-muted-foreground/50" + /> +
+
+
+ + {/* Row 2: Auto-trade config */} +
+
+ + + Auto-Trade + +
+ + {autoTradeEnabled && ( + <> +
+ +
+ {(["YES", "NO"] as const).map((s) => ( + + ))} +
+
+
+ + setBudgetUsdc(e.target.value)} + className="mt-2 w-24 h-[46px] px-3 bg-secondary rounded-lg border border-border focus:border-primary text-foreground text-center" + /> +
+
+ + setMinConfidence(e.target.value)} + className="mt-2 w-20 h-[46px] px-3 bg-secondary rounded-lg border border-border focus:border-primary text-foreground text-center" + /> +
+ + )} +
+ + {/* Row 3: Actions */} +
+ + +
+ + +
+
+
+ + {/* ─── Error ───────────────────────────────────────────────────── */} + {error && ( +
+
+ +
+

Error

+

{error}

+
+
+
+ )} + + {/* ─── Results ─────────────────────────────────────────────────── */} + {result && ( +
+ {/* Detection header */} +
+
+ {result.detected ? ( + + ) : ( + + )} +
+

+ {result.detected ? "TGE Detected" : "No TGE Detected"} +

+

+ Checked {result.checked_messages ?? 0} message(s) + {result.processingTimeMs != null && ` in ${result.processingTimeMs}ms`} +

+
+
+ {result.confidence != null && ( + + {Math.round(result.confidence * 100)}% + + )} +
+ + {result.detected && ( +
+ {/* Project + Keywords */} + {result.project && ( +
+ Project + + {result.project} + +
+ )} + + {result.keywords && result.keywords.length > 0 && ( +
+ {result.keywords.map((kw) => ( + + {kw} + + ))} +
+ )} + + {/* Discord message */} + {result.message && ( +
+
Signal
+

+ {result.message.content} +

+

+ {result.message.author} — {formatTimestamp(result.message.timestamp)} +

+
+ )} + + {/* Recommendation */} + {result.recommendation && ( +
+
+ + {result.recommendation} +
+ {result.reasoning && ( +

{result.reasoning}

+ )} +
+ )} + + {/* ─── Trade Execution Result ───────────────────────────── */} + {result.trade && ( +
+
+ + + {result.trade.success ? "Trade Executed" : "Trade Failed"} + +
+ {result.trade.success ? ( +
+
+
Side
+
{result.trade.side}
+
+
+
Price
+
{(result.trade.price * 100).toFixed(1)}%
+
+
+
Shares
+
{result.trade.size}
+
+
+
Cost
+
${result.trade.costUsd.toFixed(2)}
+
+ {result.trade.orderId && ( +
+
Order ID
+
{result.trade.orderId}
+
+ )} + {result.trade.marketTitle && ( +
+
Market
+
{result.trade.marketTitle}
+
+ )} +
+ ) : ( +

{result.trade.error}

+ )} +
+ )} +
+ )} + + {!result.detected && ( +

+ {result.status_message || "No TGE announcements found."} +

+ )} + + {/* ─── x402 Tool Discovery ─────────────────────────────────── */} + {result.tool_discovery && ( +
+
x402 Tool Discovery
+
{result.tool_discovery.name}
+
{result.tool_discovery.resourceUrl}
+
+ Price: {result.tool_discovery.priceUsdc} + Networks: {result.tool_discovery.networks.join(", ")} + {result.tool_discovery.marketDataFromX402 && ( + Market data fetched via x402 + )} +
+
+ )} +
+ )} + + {/* ─── History log ─────────────────────────────────────────────── */} + {history.length > 0 && ( +
+

+ Monitoring Log +

+
+ {history.map((entry, i) => ( +
+ {entry.time} + + {entry.detected ? "TGE" : "---"} + + {entry.project && {entry.project}} + {entry.trade && ( + + {entry.trade.success + ? `TRADED ${entry.trade.side} $${entry.trade.costUsd}` + : `TRADE FAILED`} + + )} +
+ ))} +
+
+ )} + + {/* ─── Empty state ─────────────────────────────────────────────── */} + {!loading && !result && history.length === 0 && ( +
+

Configure a channel and market slug, then start monitoring.

+

+ Enable Auto-Trade to execute orders when TGE is detected. +

+
+ )} +
+
+ ); +}; + +export default TgeMonitorTerminal;