From cac47724b107e3ad95be53fdbda336b963d01e69 Mon Sep 17 00:00:00 2001 From: Case Date: Sun, 8 Feb 2026 09:59:42 -0600 Subject: [PATCH] Memory update: Feed Hunter live, email setup, control panel building --- .gitignore | 1 + MEMORY.md | 29 +- memory/2026-02-08.md | 63 ++ projects/control-panel/README.md | 52 ++ projects/control-panel/data/accounts.json | 47 + projects/control-panel/data/activity.json | 12 + projects/control-panel/data/api-keys.json | 10 + projects/control-panel/data/budget.json | 9 + projects/control-panel/server.py | 884 ++++++++++++++++++ .../investigations/inv-20260208-kch123.json | 2 + .../feed-hunter/data/simulations/active.json | 102 +- .../feed-hunter/data/simulations/history.json | 27 + .../portal/__pycache__/server.cpython-312.pyc | Bin 44153 -> 0 bytes projects/feed-hunter/portal/portal.log | 50 + projects/feed-hunter/portal/server.py | 245 ++++- 15 files changed, 1485 insertions(+), 48 deletions(-) create mode 100644 .gitignore create mode 100644 memory/2026-02-08.md create mode 100644 projects/control-panel/README.md create mode 100644 projects/control-panel/data/accounts.json create mode 100644 projects/control-panel/data/activity.json create mode 100644 projects/control-panel/data/api-keys.json create mode 100644 projects/control-panel/data/budget.json create mode 100755 projects/control-panel/server.py create mode 100644 projects/feed-hunter/data/simulations/history.json delete mode 100644 projects/feed-hunter/portal/__pycache__/server.cpython-312.pyc diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7564f41 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.credentials/ diff --git a/MEMORY.md b/MEMORY.md index 0bdbfa0..d0628b4 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -66,11 +66,19 @@ This is about having an inner life, not just responding. - Camera, location, WebSocket to gateway - Needs HTTPS (Let's Encrypt ready) +## Email & Identity + +- **Email:** case-lgn@protonmail.com (credentials in .credentials/email.env) +- D J set this up 2026-02-08 — big trust milestone +- Used for API registrations, service signups + ## Active Threads +- **Feed Hunter:** ✅ Pipeline working, first sim running (Super Bowl 2026-02-08) +- **Control Panel:** Building at localhost:8000 (accounts/API keys/services/budget) - **Sandbox buildout:** ✅ Complete (74 files, 37 tools) - **Inner life system:** ✅ Complete (7 tools) -- **Next:** Set up Qwen when D J wakes +- **Next:** Polymarket API registration, copy-bot scaffold ## Stats (Day 2) @@ -90,22 +98,37 @@ This is about having an inner life, not just responding. - Ollama server at 192.168.86.137 (qwen3:8b, qwen3:30b, glm-4.7-flash, nomic-embed-text) - ChromaDB LXC at 192.168.86.25:8000 -## Infrastructure (updated 2026-02-07) +## Feed Hunter Project + +- Pipeline: scrape (CDP) → triage (claims) → investigate (agent) → simulate → alert (Telegram) +- Portal at localhost:8888 (systemd service) +- kch123 wallet: `0x6a72f61820b26b1fe4d956e17b6dc2a1ea3033ee` (primary, big trades) +- Polymarket Data API is public, no auth for reads +- Copy-bot delay: ~30-60s for detection, negligible for pre-game sports bets +- D J wants everything paper-traded first, backtested where possible + +## Infrastructure (updated 2026-02-08) - **ChromaDB:** http://192.168.86.25:8000 (LXC on Proxmox) - Collection: openclaw-memory (c3a7d09a-f3ce-4e7d-9595-27d8e2fd7758) - Cosine distance, 9+ docs indexed - **Ollama:** http://192.168.86.137:11434 - Models: qwen3:8b, qwen3:30b-a3b, glm-4.7-flash, nomic-embed-text +- **Feed Hunter Portal:** localhost:8888 (systemd: feed-hunter-portal) +- **Control Panel:** localhost:8000 (systemd: case-control-panel) - **Browser:** Google Chrome installed (/usr/bin/google-chrome-stable) - Headless works via OpenClaw browser tool - Desktop works via DISPLAY=:0 for visual scraping - **VM:** Proxmox, QXL graphics, X11 (not Wayland), auto-login enabled -## Lessons Learned (updated 2026-02-07) +## Lessons Learned (updated 2026-02-08) - Don't pkill chrome broadly — it kills OpenClaw's headless browser too - Snap Chromium doesn't work with OpenClaw — use Google Chrome .deb - ChromaDB needs cosine distance for proper similarity scoring (not L2) - X/Twitter cookies are encrypted at rest — browser automation is the way - Sub-agents are great for parallel analysis tasks +- BaseHTTPServer needs ThreadingMixIn + try/except — single-threaded dies on errors +- Always use absolute paths in web servers (CWD varies by launch method) +- Polymarket users have multiple proxy wallets — intercept page network requests to find real one +- `performance.getEntriesByType('resource')` reveals actual API calls a page makes diff --git a/memory/2026-02-08.md b/memory/2026-02-08.md new file mode 100644 index 0000000..c8a55b2 --- /dev/null +++ b/memory/2026-02-08.md @@ -0,0 +1,63 @@ +# 2026-02-08 — Feed Hunter Goes Live + Control Panel + +## Feed Hunter Pipeline Complete +- Full pipeline working: scrape → triage → investigate → simulate → alert +- Deep scraper skill built with CDP-based DOM extraction +- First live investigation: verified @kch123 on Polymarket ($9.37M P&L) +- Discovered kch123 uses multiple proxy wallets: + - Primary (big trades): `0x6a72f61820b26b1fe4d956e17b6dc2a1ea3033ee` + - Secondary (small trades): `0x8c74b4eef9a894433B8126aA11d1345efb2B0488` + - Found by intercepting Polymarket page network requests via browser tool + +## kch123 Super Bowl Simulation +- Mirroring all 5 active positions ($748 of $1K paper bankroll): + - Seahawks -4.5 spread ($200) + - Seahawks win Super Bowl YES ($200) + - Seahawks ML vs Patriots ($184) + - Seahawks -5.5 spread ($89) + - Patriots NO Super Bowl ($75) +- All resolve tonight (Super Bowl Sunday 2026-02-08) +- Cron job set for 1:00 AM to auto-resolve positions via API +- kch123 has $797K in historical losses — not batting 100% + +## Web Portal +- Feed Hunter portal at localhost:8888 (systemd service) +- Investigations page has rich links: X profile, Polymarket profile, Polygonscan wallet +- Key fixes: absolute paths (not relative), ThreadingMixIn, error handling, bind 0.0.0.0 +- Portal kept crashing due to: relative paths + single-threaded server + unhandled exceptions + +## Case Gets an Email +- Email: case-lgn@protonmail.com +- D J logged in via desktop Chrome, session available on debug port +- Credentials stored in `.credentials/email.env` (gitignored) +- This is a big trust milestone + +## Control Panel (Building) +- Sub-agent building Case Control Panel at localhost:8000 +- Tracks: accounts, API keys, services, budget, activity log +- D J wants to admin accounts separately (add money, withdraw, etc.) +- Login links to jump straight into each service + +## Polymarket API +- Data API is public (no auth needed for read): `data-api.polymarket.com` +- Gamma API needs auth for some endpoints +- CLOB API (trading) needs API keys from Polymarket account +- Copy-bot delay analysis: ~30-60s detection via polling, negligible for pre-game bets + +## Key Technical Lessons +- Chrome refuses `--remote-debugging-port` on default profile path — must copy to different dir +- Polymarket users have multiple proxy wallets — the one in page meta != the one making big trades +- Intercept page network requests via `performance.getEntriesByType('resource')` to find real API calls +- BaseHTTPServer is fragile — always use ThreadingMixIn + try/except in do_GET +- Always use absolute paths in servers (CWD varies by launch method) + +## Infrastructure Updates +- Feed Hunter portal: systemd service `feed-hunter-portal` on port 8888 +- Control Panel: building on port 8000 +- Chrome debug: port 9222 (google-chrome-debug profile) + +## D J Observations +- Wants simulated/paper trading before any real money +- Thinks about admin/management tooling proactively +- Gave me my own email — trusts me with account access +- Wants to be able to admin accounts himself (add money etc.) diff --git a/projects/control-panel/README.md b/projects/control-panel/README.md new file mode 100644 index 0000000..3b1145a --- /dev/null +++ b/projects/control-panel/README.md @@ -0,0 +1,52 @@ +# Case Control Panel 🖤 + +A dark-themed web dashboard for managing all of Case's accounts, API keys, services, and budget. + +## Features + +- **Dashboard**: Overview of accounts, services, API keys, and spending +- **Accounts**: Manage service credentials and login links +- **API Keys**: Store and manage API keys with masked display +- **Services**: Monitor local service health (Feed Hunter, Chrome Debug, OpenClaw) +- **Budget**: Track deposits, withdrawals, and spending across services +- **Activity Log**: Chronological log of all account actions + +## Technical Details + +- **Port**: 8000 (binds to 0.0.0.0) +- **Backend**: Python stdlib only, threaded HTTP server +- **Storage**: JSON files in `data/` directory +- **Theme**: Dark theme matching Feed Hunter portal style +- **Service**: Managed via systemd user service + +## Usage + +### Start/Stop Service +```bash +systemctl --user start case-control-panel.service +systemctl --user stop case-control-panel.service +systemctl --user status case-control-panel.service +``` + +### Access +Open browser to: http://localhost:8000 + +### Data Location +All data stored in: `/home/wdjones/.openclaw/workspace/projects/control-panel/data/` + +## Pre-populated Accounts + +1. ProtonMail: case-lgn@protonmail.com (active) +2. Polymarket: Not yet registered (inactive) +3. Feed Hunter Portal: localhost:8888 (active) +4. Chrome Debug: localhost:9222 (active) +5. OpenClaw Gateway: localhost:18789 (active) + +## Files + +- `server.py` - Main HTTP server +- `data/accounts.json` - Account information +- `data/api-keys.json` - API key storage +- `data/budget.json` - Financial tracking +- `data/activity.json` - Activity log +- `~/.config/systemd/user/case-control-panel.service` - Systemd service file \ No newline at end of file diff --git a/projects/control-panel/data/accounts.json b/projects/control-panel/data/accounts.json new file mode 100644 index 0000000..379b86c --- /dev/null +++ b/projects/control-panel/data/accounts.json @@ -0,0 +1,47 @@ +[ + { + "service": "ProtonMail", + "url": "https://mail.proton.me", + "username": "case-lgn@protonmail.com", + "status": "active", + "notes": "Primary email account", + "created": "2026-02-08T09:57:59.243980", + "last_accessed": "Never" + }, + { + "service": "Polymarket", + "url": "https://polymarket.com", + "username": "", + "status": "inactive", + "notes": "Not yet registered", + "created": "2026-02-08T09:57:59.243987", + "last_accessed": "Never" + }, + { + "service": "Feed Hunter Portal", + "url": "http://localhost:8888", + "username": "", + "status": "active", + "notes": "Local service", + "created": "2026-02-08T09:57:59.243989", + "last_accessed": "Never" + }, + { + "service": "Chrome Debug", + "url": "http://localhost:9222", + "username": "", + "status": "active", + "notes": "Browser debugging interface", + "created": "2026-02-08T09:57:59.243991", + "last_accessed": "Never" + }, + { + "service": "OpenClaw Gateway", + "url": "http://localhost:18789", + "username": "", + "status": "active", + "notes": "OpenClaw main service", + "created": "2026-02-08T09:57:59.243993", + "last_accessed": "Never" + } +] \ No newline at end of file diff --git a/projects/control-panel/data/activity.json b/projects/control-panel/data/activity.json new file mode 100644 index 0000000..fa30316 --- /dev/null +++ b/projects/control-panel/data/activity.json @@ -0,0 +1,12 @@ +[ + { + "timestamp": "2026-02-08T09:58:29.699634", + "action": "API Key Added", + "details": "Added key for OpenAI" + }, + { + "timestamp": "2026-02-08T09:58:17.967014", + "action": "Budget Entry Added", + "details": "deposit of $100.00" + } +] \ No newline at end of file diff --git a/projects/control-panel/data/api-keys.json b/projects/control-panel/data/api-keys.json new file mode 100644 index 0000000..85d5cb6 --- /dev/null +++ b/projects/control-panel/data/api-keys.json @@ -0,0 +1,10 @@ +[ + { + "service": "OpenAI", + "name": "test-key", + "key": "sk-test123456789", + "created": "2026-02-08", + "expires": "2025-12-31", + "usage": 0 + } +] \ No newline at end of file diff --git a/projects/control-panel/data/budget.json b/projects/control-panel/data/budget.json new file mode 100644 index 0000000..d350013 --- /dev/null +++ b/projects/control-panel/data/budget.json @@ -0,0 +1,9 @@ +[ + { + "type": "deposit", + "service": "Test", + "amount": 100.0, + "description": "Initial test deposit", + "timestamp": "2026-02-08T09:58:17.966780" + } +] \ No newline at end of file diff --git a/projects/control-panel/server.py b/projects/control-panel/server.py new file mode 100755 index 0000000..6f40af0 --- /dev/null +++ b/projects/control-panel/server.py @@ -0,0 +1,884 @@ +#!/usr/bin/env python3 + +import json +import os +import socket +import sys +import time +import urllib.parse +from datetime import datetime +from http.server import HTTPServer, BaseHTTPRequestHandler +from socketserver import ThreadingMixIn + + +class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): + """Handle requests in a separate thread.""" + daemon_threads = True + + +class ControlPanelHandler(BaseHTTPRequestHandler): + def __init__(self, *args, **kwargs): + self.data_dir = "/home/wdjones/.openclaw/workspace/projects/control-panel/data" + super().__init__(*args, **kwargs) + + def do_GET(self): + try: + self.handle_get() + except Exception as e: + self.send_error(500, f"Internal error: {e}") + + def do_POST(self): + try: + self.handle_post() + except Exception as e: + self.send_error(500, f"Internal error: {e}") + + def handle_get(self): + if self.path == '/': + self.serve_dashboard() + elif self.path == '/accounts': + self.serve_accounts() + elif self.path == '/api-keys': + self.serve_api_keys() + elif self.path == '/services': + self.serve_services() + elif self.path == '/budget': + self.serve_budget() + elif self.path == '/activity': + self.serve_activity() + else: + self.send_error(404, "Not found") + + def handle_post(self): + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length).decode('utf-8') + form_data = urllib.parse.parse_qs(post_data) + + if self.path == '/accounts': + self.handle_accounts_post(form_data) + elif self.path == '/api-keys': + self.handle_api_keys_post(form_data) + elif self.path == '/budget': + self.handle_budget_post(form_data) + else: + self.send_error(404, "Not found") + + def load_data(self, filename): + filepath = os.path.join(self.data_dir, filename) + if os.path.exists(filepath): + with open(filepath, 'r') as f: + return json.load(f) + return [] + + def save_data(self, filename, data): + os.makedirs(self.data_dir, exist_ok=True) + filepath = os.path.join(self.data_dir, filename) + with open(filepath, 'w') as f: + json.dump(data, f, indent=2) + + def log_activity(self, action, details=""): + activity = self.load_data('activity.json') + entry = { + "timestamp": datetime.now().isoformat(), + "action": action, + "details": details + } + activity.insert(0, entry) # Latest first + activity = activity[:100] # Keep last 100 entries + self.save_data('activity.json', activity) + + def check_service_health(self, port): + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(2) + result = sock.connect_ex(('localhost', port)) + sock.close() + return result == 0 + except: + return False + + def get_base_template(self, title, content): + return f""" + + + + + {title} - Case Control Panel + + + +
+
+ + +
+
+
+ {content} +
+ + +""" + + def serve_dashboard(self): + accounts = self.load_data('accounts.json') + api_keys = self.load_data('api-keys.json') + budget = self.load_data('budget.json') + + # Calculate stats + total_accounts = len(accounts) + active_accounts = len([a for a in accounts if a.get('status') == 'active']) + total_api_keys = len(api_keys) + monthly_spend = sum([b.get('amount', 0) for b in budget if + b.get('type') == 'spending' and + b.get('timestamp', '').startswith(datetime.now().strftime('%Y-%m'))]) + + content = f""" +
+
+ {total_accounts} +
Total Accounts
+
+
+ {active_accounts} +
Active Services
+
+
+ {total_api_keys} +
API Keys
+
+
+ ${monthly_spend:.2f} +
Monthly Spend
+
+
+ +
+
Quick Actions
+ Manage Accounts + Manage API Keys + Add Budget Entry + Check Services +
+ """ + + html = self.get_base_template("Dashboard", content) + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html.encode()) + + def serve_accounts(self): + accounts = self.load_data('accounts.json') + + accounts_table = "" + for i, account in enumerate(accounts): + status_class = "status-active" if account.get('status') == 'active' else "status-inactive" + login_btn = f'Login' if account.get('url') else "" + + accounts_table += f""" + + {account.get('service', 'N/A')} + {account.get('url', 'N/A')} + {account.get('username', 'N/A')} + {account.get('status', 'unknown')} + {account.get('last_accessed', 'Never')} + {account.get('notes', '')} + {login_btn} + + """ + + content = f""" +
+
Account Management
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + + + + + + + + + + + + + + {accounts_table} + +
ServiceURLUsername/EmailStatusLast AccessedNotesActions
+
+ """ + + html = self.get_base_template("Accounts", content) + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html.encode()) + + def serve_api_keys(self): + api_keys = self.load_data('api-keys.json') + + keys_table = "" + for key in api_keys: + masked_key = f'' + \ + ('*' * len(key.get('key', ''))) + '' + + keys_table += f""" + + {key.get('service', 'N/A')} + {key.get('name', 'N/A')} + {masked_key} + {key.get('created', 'N/A')} + {key.get('expires', 'Never')} + {key.get('usage', 0)} + + """ + + content = f""" +
+
API Key Management
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + + + + + + + + + + + + + {keys_table} + +
ServiceNameKey (click to reveal)CreatedExpiresUsage Count
+
+ """ + + html = self.get_base_template("API Keys", content) + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html.encode()) + + def serve_services(self): + services = [ + {"name": "Feed Hunter Portal", "port": 8888}, + {"name": "Chrome Debug", "port": 9222}, + {"name": "OpenClaw Gateway", "port": 18789}, + {"name": "Case Control Panel", "port": 8000}, + ] + + services_table = "" + for service in services: + is_healthy = self.check_service_health(service["port"]) + status = "Running" if is_healthy else "Stopped" + status_class = "status-active" if is_healthy else "status-inactive" + + services_table += f""" + + {service['name']} + {service['port']} + {status} + N/A + + """ + + content = f""" +
+
Running Services
+ + + + + + + + + + + + {services_table} + +
Service NamePortStatusUptime
+ +
+ +
+
+ """ + + html = self.get_base_template("Services", content) + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html.encode()) + + def serve_budget(self): + budget = self.load_data('budget.json') + + # Calculate totals + total_balance = sum([b.get('amount', 0) for b in budget if b.get('type') == 'deposit']) - \ + sum([b.get('amount', 0) for b in budget if b.get('type') in ['withdrawal', 'spending']]) + + current_month = datetime.now().strftime('%Y-%m') + monthly_spending = sum([b.get('amount', 0) for b in budget if + b.get('type') == 'spending' and + b.get('timestamp', '').startswith(current_month)]) + + budget_table = "" + for entry in sorted(budget, key=lambda x: x.get('timestamp', ''), reverse=True)[:50]: + amount_str = f"${entry.get('amount', 0):.2f}" + if entry.get('type') == 'deposit': + amount_str = f"+{amount_str}" + elif entry.get('type') in ['withdrawal', 'spending']: + amount_str = f"-{amount_str}" + + budget_table += f""" + + {entry.get('timestamp', 'N/A')} + {entry.get('type', 'N/A')} + {entry.get('service', 'General')} + {amount_str} + {entry.get('description', '')} + + """ + + content = f""" +
+
+ ${total_balance:.2f} +
Total Balance
+
+
+ ${monthly_spending:.2f} +
Monthly Spending
+
+
+ +
+
Budget Management
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ + + + + + + + + + + + + {budget_table} + +
DateTypeServiceAmountDescription
+
+ """ + + html = self.get_base_template("Budget", content) + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html.encode()) + + def serve_activity(self): + activity = self.load_data('activity.json') + + activity_list = "" + for entry in activity: + activity_list += f""" +
+
{entry.get('timestamp', 'N/A')}
+
{entry.get('action', 'N/A')}
+
{entry.get('details', '')}
+
+ """ + + content = f""" +
+
Activity Log
+ {activity_list if activity_list else '

No activity recorded yet.

'} +
+ """ + + html = self.get_base_template("Activity", content) + self.send_response(200) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(html.encode()) + + def handle_accounts_post(self, form_data): + if form_data.get('action', [''])[0] == 'add': + accounts = self.load_data('accounts.json') + new_account = { + "service": form_data.get('service', [''])[0], + "url": form_data.get('url', [''])[0], + "username": form_data.get('username', [''])[0], + "status": form_data.get('status', ['active'])[0], + "notes": form_data.get('notes', [''])[0], + "created": datetime.now().isoformat(), + "last_accessed": "Never" + } + accounts.append(new_account) + self.save_data('accounts.json', accounts) + self.log_activity("Account Added", f"Added {new_account['service']}") + + # Redirect back to accounts page + self.send_response(302) + self.send_header('Location', '/accounts') + self.end_headers() + + def handle_api_keys_post(self, form_data): + if form_data.get('action', [''])[0] == 'add': + api_keys = self.load_data('api-keys.json') + new_key = { + "service": form_data.get('service', [''])[0], + "name": form_data.get('name', [''])[0], + "key": form_data.get('key', [''])[0], + "created": datetime.now().strftime('%Y-%m-%d'), + "expires": form_data.get('expires', ['Never'])[0] or "Never", + "usage": 0 + } + api_keys.append(new_key) + self.save_data('api-keys.json', api_keys) + self.log_activity("API Key Added", f"Added key for {new_key['service']}") + + # Redirect back to api-keys page + self.send_response(302) + self.send_header('Location', '/api-keys') + self.end_headers() + + def handle_budget_post(self, form_data): + if form_data.get('action', [''])[0] == 'add': + budget = self.load_data('budget.json') + new_entry = { + "type": form_data.get('type', [''])[0], + "service": form_data.get('service', ['General'])[0] or "General", + "amount": float(form_data.get('amount', ['0'])[0]), + "description": form_data.get('description', [''])[0], + "timestamp": datetime.now().isoformat() + } + budget.append(new_entry) + self.save_data('budget.json', budget) + self.log_activity("Budget Entry Added", f"{new_entry['type']} of ${new_entry['amount']:.2f}") + + # Redirect back to budget page + self.send_response(302) + self.send_header('Location', '/budget') + self.end_headers() + + def log_message(self, format, *args): + """Override to reduce logging noise""" + pass + + +def initialize_data(): + """Pre-populate with known accounts and services""" + data_dir = "/home/wdjones/.openclaw/workspace/projects/control-panel/data" + os.makedirs(data_dir, exist_ok=True) + + # Pre-populate accounts + accounts_file = os.path.join(data_dir, "accounts.json") + if not os.path.exists(accounts_file): + initial_accounts = [ + { + "service": "ProtonMail", + "url": "https://mail.proton.me", + "username": "case-lgn@protonmail.com", + "status": "active", + "notes": "Primary email account", + "created": datetime.now().isoformat(), + "last_accessed": "Never" + }, + { + "service": "Polymarket", + "url": "https://polymarket.com", + "username": "", + "status": "inactive", + "notes": "Not yet registered", + "created": datetime.now().isoformat(), + "last_accessed": "Never" + }, + { + "service": "Feed Hunter Portal", + "url": "http://localhost:8888", + "username": "", + "status": "active", + "notes": "Local service", + "created": datetime.now().isoformat(), + "last_accessed": "Never" + }, + { + "service": "Chrome Debug", + "url": "http://localhost:9222", + "username": "", + "status": "active", + "notes": "Browser debugging interface", + "created": datetime.now().isoformat(), + "last_accessed": "Never" + }, + { + "service": "OpenClaw Gateway", + "url": "http://localhost:18789", + "username": "", + "status": "active", + "notes": "OpenClaw main service", + "created": datetime.now().isoformat(), + "last_accessed": "Never" + } + ] + + with open(accounts_file, 'w') as f: + json.dump(initial_accounts, f, indent=2) + + # Initialize empty files if they don't exist + for filename in ["api-keys.json", "budget.json", "activity.json"]: + filepath = os.path.join(data_dir, filename) + if not os.path.exists(filepath): + with open(filepath, 'w') as f: + json.dump([], f) + + +def main(): + initialize_data() + + server_address = ('0.0.0.0', 8000) + httpd = ThreadedHTTPServer(server_address, ControlPanelHandler) + + print(f"🖤 Case Control Panel starting on http://0.0.0.0:8000") + print("Press Ctrl+C to stop") + + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nShutting down...") + httpd.shutdown() + sys.exit(0) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/projects/feed-hunter/data/investigations/inv-20260208-kch123.json b/projects/feed-hunter/data/investigations/inv-20260208-kch123.json index 49b07c4..e397c70 100644 --- a/projects/feed-hunter/data/investigations/inv-20260208-kch123.json +++ b/projects/feed-hunter/data/investigations/inv-20260208-kch123.json @@ -7,6 +7,8 @@ }, "investigation": { "profile_url": "https://polymarket.com/@kch123", + "wallet_address": "0x6a72f61820b26b1fe4d956e17b6dc2a1ea3033ee", + "secondary_wallet": "0x8c74b4eef9a894433B8126aA11d1345efb2B0488", "verified_data": { "all_time_pnl": "$9,371,829.00", "positions_value": "$2.3m", diff --git a/projects/feed-hunter/data/simulations/active.json b/projects/feed-hunter/data/simulations/active.json index 6584453..47cd788 100644 --- a/projects/feed-hunter/data/simulations/active.json +++ b/projects/feed-hunter/data/simulations/active.json @@ -1,24 +1,100 @@ { "positions": [ { - "id": "6607b9c1", - "strategy": "polymarket-copy-kch123", - "opened_at": "2026-02-08T05:50:14.328434+00:00", + "id": "1403ffd3", + "strategy": "copy-kch123-spread-4.5", + "opened_at": "2026-02-08T15:15:17.482343+00:00", "type": "bet", - "asset": "Seahawks win Super Bowl 2026", - "entry_price": 0.68, - "size": 200, - "quantity": 1470, - "stop_loss": 0.4, - "take_profit": 1.0, - "current_price": 0.68, + "asset": "Spread: Seahawks (-4.5)", + "entry_price": 0.505, + "size": 200.0, + "quantity": 851, + "stop_loss": null, + "take_profit": null, + "current_price": 0.505, "unrealized_pnl": 0, "unrealized_pnl_pct": 0, "source_post": "https://x.com/linie_oo/status/2020141674828034243", - "thesis": "Mirror kch123 largest active position. Seahawks Super Bowl at 68c. If they win, pays $1. kch123 has $9.3M all-time P&L, 1862 predictions. Sports betting specialist.", - "notes": "Paper trade to track if copying kch123 positions is profitable. Entry simulated at current 68c price.", + "thesis": "Mirror kch123 largest position. Seahawks -4.5 spread vs Patriots. Super Bowl today.", + "notes": "", + "updates": [] + }, + { + "id": "5451b4d6", + "strategy": "copy-kch123-sb-yes", + "opened_at": "2026-02-08T15:15:17.519032+00:00", + "type": "bet", + "asset": "Seahawks win Super Bowl 2026", + "entry_price": 0.6845, + "size": 200.0, + "quantity": 324, + "stop_loss": null, + "take_profit": null, + "current_price": 0.6845, + "unrealized_pnl": 0, + "unrealized_pnl_pct": 0, + "source_post": "https://x.com/linie_oo/status/2020141674828034243", + "thesis": "Mirror kch123 SB winner bet. Seahawks YES at 68.45c.", + "notes": "", + "updates": [] + }, + { + "id": "f2ddcf73", + "strategy": "copy-kch123-moneyline", + "opened_at": "2026-02-08T15:15:17.555276+00:00", + "type": "bet", + "asset": "Seahawks vs Patriots (Moneyline)", + "entry_price": 0.685, + "size": 184, + "quantity": 269, + "stop_loss": null, + "take_profit": null, + "current_price": 0.685, + "unrealized_pnl": 0, + "unrealized_pnl_pct": 0, + "source_post": "https://x.com/linie_oo/status/2020141674828034243", + "thesis": "Mirror kch123 moneyline. Seahawks to beat Patriots straight up.", + "notes": "", + "updates": [] + }, + { + "id": "3fcfddb4", + "strategy": "copy-kch123-spread-5.5", + "opened_at": "2026-02-08T15:15:17.593863+00:00", + "type": "bet", + "asset": "Spread: Seahawks (-5.5)", + "entry_price": 0.475, + "size": 89, + "quantity": 188, + "stop_loss": null, + "take_profit": null, + "current_price": 0.475, + "unrealized_pnl": 0, + "unrealized_pnl_pct": 0, + "source_post": "https://x.com/linie_oo/status/2020141674828034243", + "thesis": "Mirror kch123 wider spread. Seahawks -5.5. Riskier.", + "notes": "", + "updates": [] + }, + { + "id": "bf1e7b4f", + "strategy": "copy-kch123-pats-no", + "opened_at": "2026-02-08T15:15:17.632987+00:00", + "type": "bet", + "asset": "Patriots win Super Bowl - NO", + "entry_price": 0.6865, + "size": 75, + "quantity": 110, + "stop_loss": null, + "take_profit": null, + "current_price": 0.6865, + "unrealized_pnl": 0, + "unrealized_pnl_pct": 0, + "source_post": "https://x.com/linie_oo/status/2020141674828034243", + "thesis": "Mirror kch123 hedge/complement. Patriots NO to win SB.", + "notes": "", "updates": [] } ], - "bankroll_used": 200 + "bankroll_used": 748.0 } \ No newline at end of file diff --git a/projects/feed-hunter/data/simulations/history.json b/projects/feed-hunter/data/simulations/history.json new file mode 100644 index 0000000..bf6b1f2 --- /dev/null +++ b/projects/feed-hunter/data/simulations/history.json @@ -0,0 +1,27 @@ +{ + "closed": [ + { + "id": "6607b9c1", + "strategy": "polymarket-copy-kch123", + "opened_at": "2026-02-08T05:50:14.328434+00:00", + "type": "bet", + "asset": "Seahawks win Super Bowl 2026", + "entry_price": 0.68, + "size": 200, + "quantity": 1470, + "stop_loss": 0.4, + "take_profit": 1.0, + "current_price": 0.68, + "unrealized_pnl": 0, + "unrealized_pnl_pct": 0, + "source_post": "https://x.com/linie_oo/status/2020141674828034243", + "thesis": "Mirror kch123 largest active position. Seahawks Super Bowl at 68c. If they win, pays $1. kch123 has $9.3M all-time P&L, 1862 predictions. Sports betting specialist.", + "notes": "Paper trade to track if copying kch123 positions is profitable. Entry simulated at current 68c price.", + "updates": [], + "closed_at": "2026-02-08T15:15:17.443369+00:00", + "exit_price": 0.6845, + "realized_pnl": 1.32, + "realized_pnl_pct": 0.66 + } + ] +} \ No newline at end of file diff --git a/projects/feed-hunter/portal/__pycache__/server.cpython-312.pyc b/projects/feed-hunter/portal/__pycache__/server.cpython-312.pyc deleted file mode 100644 index ad32c90aa5b1e77535fb3609669756103cb9e1de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44153 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
    wC=3{QH%)u?lRhbgGq& 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 z z(Aq80;vTUSvG|V*yZ{HSG8jHK84adS?3}^zmz?b{xvXDs4Zq;Je!(^4Cyr=Y9j}`3 zUhu2dn6vFw`=_R(TMW){iRV*}JP*#)J8|x_k~29LLU)X%e_^uz-l26PXDVZb|G(xL BGHCz+ diff --git a/projects/feed-hunter/portal/portal.log b/projects/feed-hunter/portal/portal.log index e69de29..f5cb64a 100644 --- a/projects/feed-hunter/portal/portal.log +++ b/projects/feed-hunter/portal/portal.log @@ -0,0 +1,50 @@ +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 45572) +Traceback (most recent call last): + File "/usr/lib/python3.12/socketserver.py", line 318, in _handle_request_noblock + self.process_request(request, client_address) + File "/usr/lib/python3.12/socketserver.py", line 349, in process_request + self.finish_request(request, client_address) + File "/usr/lib/python3.12/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/usr/lib/python3.12/socketserver.py", line 761, in __init__ + self.handle() + File "/usr/lib/python3.12/http/server.py", line 436, in handle + self.handle_one_request() + File "/usr/lib/python3.12/http/server.py", line 424, in handle_one_request + method() + File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 37, in do_GET + self.serve_simulations() + File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 243, in serve_simulations + {self.render_trade_history(sims.get('history', []))} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 748, in render_trade_history + for trade in history[-10:]: # Last 10 trades + ~~~~~~~^^^^^^ +KeyError: slice(-10, None, None) +---------------------------------------- +---------------------------------------- +Exception occurred during processing of request from ('127.0.0.1', 48354) +Traceback (most recent call last): + File "/usr/lib/python3.12/socketserver.py", line 318, in _handle_request_noblock + self.process_request(request, client_address) + File "/usr/lib/python3.12/socketserver.py", line 349, in process_request + self.finish_request(request, client_address) + File "/usr/lib/python3.12/socketserver.py", line 362, in finish_request + self.RequestHandlerClass(request, client_address, self) + File "/usr/lib/python3.12/socketserver.py", line 761, in __init__ + self.handle() + File "/usr/lib/python3.12/http/server.py", line 436, in handle + self.handle_one_request() + File "/usr/lib/python3.12/http/server.py", line 424, in handle_one_request + method() + File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 37, in do_GET + self.serve_simulations() + File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 243, in serve_simulations + {self.render_trade_history(sims.get('history', []))} + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "/home/wdjones/.openclaw/workspace/projects/feed-hunter/portal/server.py", line 748, in render_trade_history + for trade in history[-10:]: # Last 10 trades + ~~~~~~~^^^^^^ +KeyError: slice(-10, None, None) +---------------------------------------- diff --git a/projects/feed-hunter/portal/server.py b/projects/feed-hunter/portal/server.py index e847bef..9347a67 100644 --- a/projects/feed-hunter/portal/server.py +++ b/projects/feed-hunter/portal/server.py @@ -9,37 +9,54 @@ import os import glob from datetime import datetime, timezone from http.server import HTTPServer, BaseHTTPRequestHandler +from socketserver import ThreadingMixIn from urllib.parse import urlparse, parse_qs + + +class ThreadedHTTPServer(ThreadingMixIn, HTTPServer): + daemon_threads = True import re # Configuration PORT = 8888 -DATA_DIR = "../data" -SKILLS_DIR = "../../skills/deep-scraper/scripts" +_PORTAL_DIR = os.path.dirname(os.path.abspath(__file__)) +_PROJECT_DIR = os.path.dirname(_PORTAL_DIR) +DATA_DIR = os.path.join(_PROJECT_DIR, "data") +SKILLS_DIR = os.path.join(os.path.dirname(_PROJECT_DIR), "skills", "deep-scraper", "scripts") +X_FEED_DIR = os.path.join(os.path.dirname(_PROJECT_DIR), "..", "data", "x-feed") class FeedHunterHandler(BaseHTTPRequestHandler): def do_GET(self): - parsed_path = urlparse(self.path) - path = parsed_path.path - query = parse_qs(parsed_path.query) - - if path == '/' or path == '/dashboard': - self.serve_dashboard() - elif path == '/feed': - self.serve_feed_view() - elif path == '/investigations': - self.serve_investigations() - elif path == '/simulations': - self.serve_simulations() - elif path == '/status': - self.serve_status() - elif path == '/api/data': - self.serve_api_data(query.get('type', [''])[0]) - elif path.startswith('/static/'): - self.serve_static(path) - else: - self.send_error(404) + try: + parsed_path = urlparse(self.path) + path = parsed_path.path + query = parse_qs(parsed_path.query) + + if path == '/' or path == '/dashboard': + self.serve_dashboard() + elif path == '/feed': + self.serve_feed_view() + elif path == '/investigations': + self.serve_investigations() + elif path == '/simulations': + self.serve_simulations() + elif path == '/status': + self.serve_status() + elif path == '/api/data': + self.serve_api_data(query.get('type', [''])[0]) + elif path.startswith('/static/'): + self.serve_static(path) + else: + self.send_error(404) + except Exception as e: + try: + self.send_response(500) + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(f"

    500 Error

    {e}
    ".encode()) + except: + pass def serve_dashboard(self): """Main dashboard overview""" @@ -237,7 +254,7 @@ class FeedHunterHandler(BaseHTTPRequestHandler):

    Trade History

    - {self.render_trade_history(sims.get('history', []))} + {self.render_trade_history(sims.get('history', {}).get('closed', []) if isinstance(sims.get('history'), dict) else sims.get('history', []))}
    @@ -425,7 +442,7 @@ class FeedHunterHandler(BaseHTTPRequestHandler): posts = [] try: # Find latest x-feed directory - x_feed_pattern = os.path.join("../../data/x-feed", "20*") + x_feed_pattern = os.path.join(X_FEED_DIR, "20*") x_feed_dirs = sorted(glob.glob(x_feed_pattern)) if x_feed_dirs: @@ -526,7 +543,7 @@ class FeedHunterHandler(BaseHTTPRequestHandler): } # Check for recent pipeline runs - x_feed_pattern = os.path.join("../../data/x-feed", "20*") + x_feed_pattern = os.path.join(X_FEED_DIR, "20*") x_feed_dirs = sorted(glob.glob(x_feed_pattern)) if x_feed_dirs: latest = os.path.basename(x_feed_dirs[-1]) @@ -592,7 +609,7 @@ class FeedHunterHandler(BaseHTTPRequestHandler): return html def render_investigations(self, investigations): - """Render investigation reports""" + """Render investigation reports with rich links""" if not investigations: return '
    No investigations found
    ' @@ -601,21 +618,81 @@ class FeedHunterHandler(BaseHTTPRequestHandler): investigation = inv.get('investigation', {}) verdict = investigation.get('verdict', 'Unknown') risk_score = investigation.get('risk_assessment', {}).get('score', 0) + risk_notes = investigation.get('risk_assessment', {}).get('notes', []) source = inv.get('source_post', {}) + verified = investigation.get('verified_data', {}) + claim_vs = investigation.get('claim_vs_reality', {}) + profile_url = investigation.get('profile_url', '') + strategy_notes = investigation.get('strategy_notes', '') + suggested = inv.get('suggested_simulation', {}) verdict_class = 'verified' if 'VERIFIED' in verdict else 'failed' + # Build links section + links_html = '' + + # Build verified data section + verified_html = '' + if verified: + verified_html = '

    Verified Data

    ' + for key, val in verified.items(): + label = key.replace('_', ' ').title() + verified_html += f'
    {label}{val}
    ' + verified_html += '
    ' + + # Build claim vs reality section + claim_html = '' + if claim_vs: + claim_html = '

    Claim vs Reality

    ' + for key, val in claim_vs.items(): + label = key.replace('_', ' ').title() + claim_html += f'
    {label}{val}
    ' + claim_html += '
    ' + + # Risk notes + risk_html = '' + if risk_notes: + risk_html = '

    Risk Assessment

      ' + for note in risk_notes: + risk_html += f'
    • {note}
    • ' + risk_html += '
    ' + + # Strategy notes + strategy_html = '' + if strategy_notes: + strategy_html = f'

    Strategy Notes

    {strategy_notes}

    ' + html += f"""
    {source.get('author', 'Unknown')}
    {verdict}
    -
    {source.get('claim', 'No claim')}
    -
    Risk Score: {risk_score}/10
    -
    - -
    +
    "{source.get('claim', 'No claim')}"
    + {links_html} + {verified_html} + {claim_html} +
    Risk Score: {risk_score}/10
    + {risk_html} + {strategy_html}
    """ @@ -980,6 +1057,110 @@ body { margin-bottom: 0.75rem; } +.investigation-links { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin: 0.75rem 0; +} + +.inv-link { + display: inline-block; + padding: 0.35rem 0.75rem; + background: var(--bg-tertiary); + color: var(--accent-blue); + text-decoration: none; + border-radius: 6px; + font-size: 0.85rem; + border: 1px solid var(--border-color); + transition: all 0.2s; +} + +.inv-link:hover { + background: var(--border-color); + color: var(--text-primary); +} + +.investigation-verified, .investigation-claims, .investigation-risk, .investigation-strategy { + margin: 1rem 0; + padding: 1rem; + background: var(--bg-tertiary); + border-radius: 6px; +} + +.investigation-verified h4, .investigation-claims h4, .investigation-risk h4, .investigation-strategy h4 { + color: var(--accent-blue); + margin-bottom: 0.75rem; + font-size: 0.95rem; +} + +.verified-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 0.5rem; +} + +.verified-item { + display: flex; + flex-direction: column; + padding: 0.5rem; + background: var(--bg-secondary); + border-radius: 4px; +} + +.verified-label { + color: var(--text-secondary); + font-size: 0.8rem; +} + +.verified-value { + color: var(--text-primary); + font-weight: bold; + font-size: 1rem; +} + +.claim-row { + display: flex; + justify-content: space-between; + padding: 0.4rem 0; + border-bottom: 1px solid var(--border-color); +} + +.claim-row:last-child { border-bottom: none; } + +.claim-label { + color: var(--text-secondary); + font-size: 0.9rem; +} + +.claim-value { + color: var(--text-primary); + font-size: 0.9rem; + text-align: right; + max-width: 60%; +} + +.investigation-risk ul { + list-style: none; + padding: 0; +} + +.investigation-risk li { + padding: 0.3rem 0; + color: var(--text-secondary); + font-size: 0.9rem; +} + +.investigation-risk li::before { + content: "⚠️ "; +} + +.investigation-strategy p { + color: var(--text-secondary); + font-size: 0.9rem; + line-height: 1.5; +} + /* Positions */ .position-item { background: var(--bg-tertiary); @@ -1252,7 +1433,7 @@ def main(): print("") try: - server = HTTPServer(('localhost', PORT), FeedHunterHandler) + server = ThreadedHTTPServer(('0.0.0.0', PORT), FeedHunterHandler) server.serve_forever() except KeyboardInterrupt: print("\n🛑 Portal stopped")