Night shift: tweet analyzer, data connectors, feed monitor, market watch portal
This commit is contained in:
File diff suppressed because one or more lines are too long
3
projects/feed-hunter/data/anoin123-tracking/stats.json
Normal file
3
projects/feed-hunter/data/anoin123-tracking/stats.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"last_check": "2026-02-12T06:14:23.639458+00:00"
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
{
|
||||
"timestamp": "2026-02-12T06:11:18.980961+00:00",
|
||||
"total_scraped": 44,
|
||||
"new_posts": 22,
|
||||
"money_posts": 7,
|
||||
"posts": [
|
||||
{
|
||||
"text": "We\u2019re in the business of supporting traders. Get the tools you need to make your way in the market.",
|
||||
"userName": "tastytrade\n@tastytrade",
|
||||
"timestamp": "",
|
||||
"link": "/tastytrade/status/1971214019274080389/analytics"
|
||||
},
|
||||
{
|
||||
"text": "agentic e2e regression testing solved.\nprompt \u2192 test in under 2 mins.\nif you're still writing tests by hand... why",
|
||||
"userName": "Bug0\n@bug0inc",
|
||||
"timestamp": "",
|
||||
"link": "/bug0inc/status/2019756209390375399/analytics"
|
||||
},
|
||||
{
|
||||
"text": " New GPT crypto price predictions:\n\n$ETH -0.285% => $1945.73\n\n$ZRO +0.086% => $2.329\n\n$UNI 0.0% => $3.492\n\n$BTC -0.116% => $67481.29\n\n$XRP +0.181% => $1.3796\n\n$PENGU +0.116% => $0.006021\n\n$SOL +0.063% => $79.72\n\n$TRUMP +0.094% => $3.195",
|
||||
"userName": "OctoBot - GPT crypto price predictions\n@OctoBotGPT\n\u00b7\n10h",
|
||||
"timestamp": "2026-02-11T20:00:50.000Z",
|
||||
"link": "/OctoBotGPT/status/2021675819580481813"
|
||||
},
|
||||
{
|
||||
"text": " New GPT crypto price predictions:\n\n$ADA 0.0% => $0.2633\n\n$DOGE 0.0% => $0.09372\n\n$SOL 0.0% => $84.78\n\n$XRP +0.266% => $1.427\n\n$SENT +0.594% => $0.02863\n\n$PAXG 0.0% => $5033.12\n\n$PEPE +98.997% => $0.000367\n\n$SUI +0.864% => $0.9487",
|
||||
"userName": "OctoBot - GPT crypto price predictions\n@OctoBotGPT\n\u00b7\nFeb 10",
|
||||
"timestamp": "2026-02-10T08:00:48.000Z",
|
||||
"link": "/OctoBotGPT/status/2021132226792947802"
|
||||
},
|
||||
{
|
||||
"text": "$ZAMA 0.0% => $0.02688\n\n$LIT -1.503% => $0.732\n\n$ZRO -0.259% => $1.927\n\n$BTC -0.078% => $68890.0\n\n$ETH -0.53% => $1997.59\n\n$HBAR +0.512% => $0.09188\n\n$TRX -0.036% => $0.277\n\n$SHIB 0.0% => $5.98e-06",
|
||||
"userName": "OctoBot - GPT crypto price predictions\n@OctoBotGPT\n\u00b7\nFeb 10",
|
||||
"timestamp": "2026-02-10T08:00:48.000Z",
|
||||
"link": "/OctoBotGPT/status/2021132227921301730"
|
||||
},
|
||||
{
|
||||
"text": " New GPT crypto price predictions:\n\n$ZRO +0.959% => $2.398\n\n$DOGE 0.0% => $0.09053\n\n$LINK 0.0% => $8.32\n\n$SOL -0.671% => $80.5\n\n$PENGU -0.169% => $0.005916\n\n$USD1 +0.02% => $1.0\n\n$SUI -0.403% => $0.893\n\n$BTC 0.0% => $67046.23",
|
||||
"userName": "OctoBot - GPT crypto price predictions\n@OctoBotGPT\n\u00b7\n22h",
|
||||
"timestamp": "2026-02-11T08:00:52.000Z",
|
||||
"link": "/OctoBotGPT/status/2021494632635408429"
|
||||
},
|
||||
{
|
||||
"text": "$ETH +1.022% => $1971.45\n\n$BNB 0.0% => $600.95\n\n$ZAMA -0.714% => $0.0196\n\n$SHIB 0.0% => $5.84e-06\n\n$XRP -0.242% => $1.3646\n\n$ASTER +0.459% => $0.653\n\n$ADA -0.393% => $0.2546\n\n$TRX 0.0% => $0.275",
|
||||
"userName": "OctoBot - GPT crypto price predictions\n@OctoBotGPT\n\u00b7\n22h",
|
||||
"timestamp": "2026-02-11T08:00:52.000Z",
|
||||
"link": "/OctoBotGPT/status/2021494633759408338"
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
||||
{
|
||||
"last_check": "2026-02-11T00:55:59.787647+00:00",
|
||||
"last_check": "2026-02-12T06:14:23.952343+00:00",
|
||||
"total_tracked": 3100,
|
||||
"new_this_check": 0
|
||||
}
|
||||
1
projects/feed-hunter/data/seen_posts.json
Normal file
1
projects/feed-hunter/data/seen_posts.json
Normal file
@ -0,0 +1 @@
|
||||
["37f5b094ed19f0ae", "f013475eaef8b32f", "6f9f9b6f8da5cdc9", "952f5caf7819fde5", "6043a216215e23f0", "ff1b1af5e65905a8", "6217530e01f15dd0", "6e1a48e8344a1d6f", "6d61e4a9dfb604ea", "529a533d4da86360", "d2217d5c918df581", "8b4a57cd97ae6c34", "5bf0ad8e5e2f9dfe", "2ee63742fbc6e541", "7d74e6eca55f346f", "9f29b74a37377015", "3ccdb8471c5c19b6", "8a48151a19597742", "e6a24f45f8a6e8bb", "899377e3ae43e396", "05e47b3d9e24c860", "643b1c7d00ad50f2", "b7a2c548992c278c", "a1a8dddd35fd08d5", "149ace9c327e7700", "7d767e4d0e3a12a3", "0c6a5029b97b5a30", "b7354be18fa9f71a", "7b10b1cb595f2006", "ca4011161b3ce92d", "ffbe4a2b11671722", "cd2ce19326f75133", "60fd088f8f1ae9b2", "8ebbe21b036415d8", "84da8ad2dd424e2b", "7d4648af05013346", "137b80809666db54", "1151a6c9c0f2915c", "36db4785c888600f", "e8c4ec6aa2a9a563", "f9ded3bb072f01fe", "f30bff813bce39b8", "e1a98d7590fe46ee", "a767ef4ed21a86f3", "3525848121516057", "3d6e2887b81b3016", "cb6375e11d10b745", "493d2dc82b844b36", "60dced6f08edb3c2", "7d97ce308ff157b2", "db27f494858bd743", "176776613d96cdbe", "eb7a8395aa02f113"]
|
||||
231
projects/feed-hunter/feed_monitor.py
Executable file
231
projects/feed-hunter/feed_monitor.py
Executable file
@ -0,0 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Feed Monitor — Scrapes X home timeline via Chrome CDP (localhost:9222).
|
||||
Deduplicates, filters for money/trading topics, saves captures, sends Telegram alerts.
|
||||
"""
|
||||
|
||||
import json
|
||||
import hashlib
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import http.client
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_DIR = Path(__file__).parent
|
||||
DATA_DIR = PROJECT_DIR / "data"
|
||||
SEEN_FILE = DATA_DIR / "seen_posts.json"
|
||||
CAPTURES_DIR = DATA_DIR / "feed_captures"
|
||||
CAPTURES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
CDP_HOST = "localhost"
|
||||
CDP_PORT = 9222
|
||||
|
||||
TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||
TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046")
|
||||
|
||||
MONEY_KEYWORDS = [
|
||||
"polymarket", "trade", "trading", "profit", "arbitrage", "crypto",
|
||||
"bitcoin", "btc", "ethereum", "eth", "solana", "sol", "stock",
|
||||
"stocks", "market", "portfolio", "defi", "token", "whale",
|
||||
"bullish", "bearish", "short", "long", "pnl", "alpha", "degen",
|
||||
"usdc", "usdt", "wallet", "airdrop", "memecoin", "nft",
|
||||
"yield", "staking", "leverage", "futures", "options", "hedge",
|
||||
"pump", "dump", "rug", "moon", "bag", "position", "signal",
|
||||
]
|
||||
|
||||
|
||||
def send_telegram(message: str):
|
||||
if not TELEGRAM_BOT_TOKEN:
|
||||
print(f"[ALERT] {message}")
|
||||
return
|
||||
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
|
||||
data = json.dumps({
|
||||
"chat_id": TELEGRAM_CHAT_ID,
|
||||
"text": message,
|
||||
"parse_mode": "HTML",
|
||||
"disable_web_page_preview": True,
|
||||
}).encode()
|
||||
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
except Exception as e:
|
||||
print(f" Telegram error: {e}")
|
||||
|
||||
|
||||
def cdp_send(ws, method: str, params: dict = None, msg_id: int = 1):
|
||||
"""Send a CDP command over websocket and return the result."""
|
||||
import websocket
|
||||
payload = {"id": msg_id, "method": method}
|
||||
if params:
|
||||
payload["params"] = params
|
||||
ws.send(json.dumps(payload))
|
||||
while True:
|
||||
resp = json.loads(ws.recv())
|
||||
if resp.get("id") == msg_id:
|
||||
return resp.get("result", {})
|
||||
|
||||
|
||||
def get_x_tab_ws():
|
||||
"""Find an X.com tab in Chrome and return its websocket URL."""
|
||||
conn = http.client.HTTPConnection(CDP_HOST, CDP_PORT, timeout=5)
|
||||
conn.request("GET", "/json")
|
||||
tabs = json.loads(conn.getresponse().read())
|
||||
conn.close()
|
||||
|
||||
for t in tabs:
|
||||
url = t.get("url", "")
|
||||
if "x.com" in url or "twitter.com" in url:
|
||||
ws_url = t.get("webSocketDebuggerUrl")
|
||||
if ws_url:
|
||||
return ws_url, t.get("url")
|
||||
return None, None
|
||||
|
||||
|
||||
def scrape_feed_via_cdp():
|
||||
"""Navigate to X home, scroll, extract posts via DOM evaluation."""
|
||||
import websocket
|
||||
|
||||
ws_url, current_url = get_x_tab_ws()
|
||||
if not ws_url:
|
||||
print("ERROR: No X.com tab found in Chrome at localhost:9222")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Connected to tab: {current_url}")
|
||||
ws = websocket.create_connection(ws_url, timeout=30)
|
||||
|
||||
# Navigate to home timeline
|
||||
cdp_send(ws, "Page.navigate", {"url": "https://x.com/home"}, 1)
|
||||
time.sleep(5)
|
||||
|
||||
all_posts = []
|
||||
seen_texts = set()
|
||||
|
||||
for scroll_i in range(6):
|
||||
# Extract posts from timeline
|
||||
js = """
|
||||
(() => {
|
||||
const posts = [];
|
||||
document.querySelectorAll('article[data-testid="tweet"]').forEach(article => {
|
||||
try {
|
||||
const textEl = article.querySelector('[data-testid="tweetText"]');
|
||||
const text = textEl ? textEl.innerText : '';
|
||||
const userEl = article.querySelector('[data-testid="User-Name"]');
|
||||
const userName = userEl ? userEl.innerText : '';
|
||||
const timeEl = article.querySelector('time');
|
||||
const timestamp = timeEl ? timeEl.getAttribute('datetime') : '';
|
||||
const linkEl = article.querySelector('a[href*="/status/"]');
|
||||
const link = linkEl ? linkEl.getAttribute('href') : '';
|
||||
posts.push({ text, userName, timestamp, link });
|
||||
} catch(e) {}
|
||||
});
|
||||
return JSON.stringify(posts);
|
||||
})()
|
||||
"""
|
||||
result = cdp_send(ws, "Runtime.evaluate", {"expression": js, "returnByValue": True}, 10 + scroll_i)
|
||||
raw = result.get("result", {}).get("value", "[]")
|
||||
posts = json.loads(raw) if isinstance(raw, str) else []
|
||||
|
||||
for p in posts:
|
||||
sig = p.get("text", "")[:120]
|
||||
if sig and sig not in seen_texts:
|
||||
seen_texts.add(sig)
|
||||
all_posts.append(p)
|
||||
|
||||
# Scroll down
|
||||
cdp_send(ws, "Runtime.evaluate", {"expression": "window.scrollBy(0, 2000)"}, 100 + scroll_i)
|
||||
time.sleep(2)
|
||||
|
||||
ws.close()
|
||||
return all_posts
|
||||
|
||||
|
||||
def post_hash(post: dict) -> str:
|
||||
text = post.get("text", "") + post.get("userName", "")
|
||||
return hashlib.sha256(text.encode()).hexdigest()[:16]
|
||||
|
||||
|
||||
def is_money_related(text: str) -> bool:
|
||||
lower = text.lower()
|
||||
return any(kw in lower for kw in MONEY_KEYWORDS)
|
||||
|
||||
|
||||
def load_seen() -> set:
|
||||
if SEEN_FILE.exists():
|
||||
try:
|
||||
return set(json.loads(SEEN_FILE.read_text()))
|
||||
except:
|
||||
pass
|
||||
return set()
|
||||
|
||||
|
||||
def save_seen(seen: set):
|
||||
# Keep last 10k
|
||||
items = list(seen)[-10000:]
|
||||
SEEN_FILE.write_text(json.dumps(items))
|
||||
|
||||
|
||||
def main():
|
||||
now = datetime.now(timezone.utc)
|
||||
print(f"=== Feed Monitor === {now.strftime('%Y-%m-%d %H:%M UTC')}")
|
||||
|
||||
posts = scrape_feed_via_cdp()
|
||||
print(f"Scraped {len(posts)} posts from timeline")
|
||||
|
||||
seen = load_seen()
|
||||
new_posts = []
|
||||
money_posts = []
|
||||
|
||||
for p in posts:
|
||||
h = post_hash(p)
|
||||
if h in seen:
|
||||
continue
|
||||
seen.add(h)
|
||||
new_posts.append(p)
|
||||
if is_money_related(p.get("text", "")):
|
||||
money_posts.append(p)
|
||||
|
||||
save_seen(seen)
|
||||
|
||||
print(f"New posts: {len(new_posts)}")
|
||||
print(f"Money-related: {len(money_posts)}")
|
||||
|
||||
# Save capture
|
||||
ts = now.strftime("%Y%m%d-%H%M")
|
||||
capture = {
|
||||
"timestamp": now.isoformat(),
|
||||
"total_scraped": len(posts),
|
||||
"new_posts": len(new_posts),
|
||||
"money_posts": len(money_posts),
|
||||
"posts": money_posts,
|
||||
}
|
||||
capture_file = CAPTURES_DIR / f"feed-{ts}.json"
|
||||
capture_file.write_text(json.dumps(capture, indent=2))
|
||||
print(f"Saved capture: {capture_file}")
|
||||
|
||||
# Alert on money posts
|
||||
if money_posts:
|
||||
print(f"\n🔔 {len(money_posts)} money-related posts found!")
|
||||
for p in money_posts[:8]:
|
||||
user = p.get("userName", "").split("\n")[0]
|
||||
snippet = p.get("text", "")[:250].replace("\n", " ")
|
||||
link = p.get("link", "")
|
||||
full_link = f"https://x.com{link}" if link and not link.startswith("http") else link
|
||||
|
||||
print(f" • {user}: {snippet[:100]}...")
|
||||
|
||||
msg = f"🔍 <b>{user}</b>\n\n{snippet}"
|
||||
if full_link:
|
||||
msg += f"\n\n{full_link}"
|
||||
send_telegram(msg)
|
||||
else:
|
||||
print("No new money-related posts.")
|
||||
|
||||
return len(money_posts)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
count = main()
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user