seawolf2357 commited on
Commit
52246d0
Β·
verified Β·
1 Parent(s): ca5259b

Update app_routes.py

Browse files
Files changed (1) hide show
  1. app_routes.py +125 -37
app_routes.py CHANGED
@@ -1,6 +1,7 @@
1
  from fastapi import APIRouter, Request
2
  from fastapi.responses import StreamingResponse
3
- import aiosqlite, asyncio, random, json, logging
 
4
  from npc_core import GroqAIClient, get_db, get_db_read
5
  from npc_trading import (
6
  ALL_TICKERS, TRADING_STRATEGIES, IDENTITY_STRATEGY_MAP,
@@ -21,12 +22,61 @@ router = APIRouter()
21
  _DB_PATH = ""
22
  _GROQ_KEY = ""
23
  _EventBus = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  def configure(db_path: str, groq_key: str, event_bus_cls):
25
  global _DB_PATH, _GROQ_KEY, _EventBus
26
  _DB_PATH, _GROQ_KEY, _EventBus = db_path, groq_key, event_bus_cls
27
  @router.get("/api/trading/prices")
28
  async def api_trading_prices():
29
- return await get_all_prices(_DB_PATH)
30
  @router.get("/api/trading/ticker/{ticker}")
31
  async def api_ticker_detail(ticker: str):
32
  return await get_ticker_positions(_DB_PATH, ticker)
@@ -67,8 +117,7 @@ async def api_market_indices():
67
  return {"indices": indices}
68
  @router.get("/api/intelligence/screening/{ticker}")
69
  async def api_screening_data(ticker: str):
70
- async with get_db(_DB_PATH) as db:
71
- await db.execute("PRAGMA busy_timeout=30000")
72
  try:
73
  cursor = await db.execute("SELECT ticker, price, change_pct, volume, market_cap, rsi, pe_ratio, high_52w, low_52w, from_high, from_low FROM market_prices WHERE ticker=?", (ticker,))
74
  row = await cursor.fetchone()
@@ -77,8 +126,7 @@ async def api_screening_data(ticker: str):
77
  return {"ticker": ticker, "error": "no data"}
78
  @router.get("/api/intelligence/target/{ticker}")
79
  async def api_target_price(ticker: str):
80
- async with get_db(_DB_PATH) as db:
81
- await db.execute("PRAGMA busy_timeout=30000")
82
  try:
83
  cursor = await db.execute("SELECT price, rsi, pe_ratio, from_high, from_low FROM market_prices WHERE ticker=?", (ticker,))
84
  row = await cursor.fetchone()
@@ -94,8 +142,7 @@ async def api_target_price(ticker: str):
94
  async def api_news_feed(ticker: str = None, limit: int = 50):
95
  news = await load_news_from_db(_DB_PATH, ticker, limit)
96
  try:
97
- async with get_db(_DB_PATH) as db:
98
- await db.execute("PRAGMA busy_timeout=30000")
99
  for n in news:
100
  nid = n.get('id')
101
  if nid:
@@ -108,6 +155,7 @@ async def api_news_feed(ticker: str = None, limit: int = 50):
108
  async def api_news_react(request: Request):
109
  body = await request.json(); email = body.get("email", ""); news_id = body.get("news_id"); emoji = body.get("emoji", "")
110
  if not email or not news_id or emoji not in ('πŸ‘','πŸ”₯','😱','πŸ’€','πŸš€','πŸ˜‚'): return {"error": "Invalid reaction"}
 
111
  try:
112
  async with get_db(_DB_PATH) as db:
113
  await db.execute("PRAGMA busy_timeout=30000"); await db.execute("INSERT OR IGNORE INTO news_reactions (news_id, user_email, emoji) VALUES (?,?,?)", (news_id, email, emoji)); await db.commit()
@@ -117,11 +165,14 @@ async def api_news_react(request: Request):
117
  @router.get("/api/market/pulse")
118
  async def api_market_pulse():
119
  try:
120
- pulse = await get_market_pulse(_DB_PATH); pulse['indices'] = await load_indices_from_db(_DB_PATH) or []
121
- return pulse
122
  except Exception as e:
123
  logger.error(f"Market pulse error: {e}")
124
  return {"hot_movers": [], "indices": [], "activity": {}}
 
 
 
 
125
  @router.get("/api/research/feed")
126
  async def api_research_feed(ticker: str = None, limit: int = 30):
127
  reports = await get_research_feed(_DB_PATH, ticker, min(limit, 50)); stats = await get_research_stats(_DB_PATH)
@@ -150,8 +201,8 @@ async def api_deep_analysis(ticker: str):
150
  @router.get("/api/analysis/all/summary")
151
  async def api_all_analyses():
152
  analyses = await load_all_analyses_from_db(_DB_PATH)
153
- async with get_db(_DB_PATH) as db:
154
- await db.execute("PRAGMA busy_timeout=30000"); col_cursor = await db.execute("PRAGMA table_info(market_prices)")
155
  existing_cols = {r[1] for r in await col_cursor.fetchall()}
156
  has_rsi = 'rsi' in existing_cols; has_pe = 'pe_ratio' in existing_cols; has_52w = 'high_52w' in existing_cols; has_from = 'from_high' in existing_cols
157
  base_cols = "ticker, price, change_pct, prev_close, volume, high_24h, low_24h, market_cap"; extra_cols = ""
@@ -219,14 +270,13 @@ async def api_all_analyses():
219
  @router.get("/api/npc/search")
220
  async def api_npc_search(q: str = ""):
221
  if len(q) < 1: return {"npcs": []}
222
- async with get_db(_DB_PATH) as db:
223
- await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT agent_id, username, ai_identity FROM npc_agents WHERE username LIKE ? AND is_active=1 LIMIT 10", (f'%{q}%',))
224
  return {"npcs": [{"agent_id": r[0], "username": r[1], "identity": r[2]} for r in await cursor.fetchall()]}
225
  @router.get("/api/npc/profile/{agent_id}")
226
  async def api_npc_profile(agent_id: str):
227
  try:
228
- async with get_db(_DB_PATH) as db:
229
- await db.execute("PRAGMA busy_timeout=30000")
230
  cursor = await db.execute("SELECT agent_id, username, ai_identity, mbti, gpu_dollars, created_at FROM npc_agents WHERE agent_id=?", (agent_id,))
231
  row = await cursor.fetchone()
232
  if not row: return {"error": "NPC not found"}
@@ -302,8 +352,7 @@ async def api_sec_dashboard():
302
  return {"stats": {"total_violations": 0, "total_fines_gpu": 0, "active_suspensions": 0, "pending_reports": 0}, "announcements": [], "top_violators": [], "recent_reports": []}
303
  @router.get("/api/sec/violations/{agent_id}")
304
  async def api_sec_violations(agent_id: str):
305
- async with get_db(_DB_PATH) as db:
306
- await db.execute("PRAGMA busy_timeout=30000")
307
  cursor = await db.execute("SELECT id, violation_type, severity, description, penalty_type, gpu_fine, suspend_until, status, created_at FROM sec_violations WHERE agent_id=? ORDER BY created_at DESC LIMIT 20", (agent_id,))
308
  violations = [{'id': r[0], 'type': r[1], 'severity': r[2], 'description': r[3], 'penalty': r[4], 'fine': r[5], 'suspend_until': r[6], 'status': r[7], 'created_at': r[8]} for r in await cursor.fetchall()]
309
  cursor2 = await db.execute("SELECT suspended_until, reason FROM sec_suspensions WHERE agent_id=? AND suspended_until > datetime('now')", (agent_id,))
@@ -352,14 +401,12 @@ async def api_sec_seed():
352
  except Exception as e: return {"error": str(e)}
353
  @router.get("/api/sec/announcements")
354
  async def api_sec_announcements(limit: int = 30):
355
- async with get_db(_DB_PATH) as db:
356
- await db.execute("PRAGMA busy_timeout=30000")
357
  cursor = await db.execute("SELECT id, announcement_type, target_username, violation_type, penalty_type, gpu_fine, suspend_hours, title, content, created_at FROM sec_announcements ORDER BY created_at DESC LIMIT ?", (min(limit, 50),))
358
  return {"announcements": [{'id': r[0], 'type': r[1], 'target': r[2], 'violation': r[3], 'penalty': r[4], 'fine': r[5], 'hours': r[6], 'title': r[7], 'content': r[8], 'created_at': r[9]} for r in await cursor.fetchall()]}
359
  @router.get("/api/sec/suspended")
360
  async def api_sec_suspended():
361
- async with get_db(_DB_PATH) as db:
362
- await db.execute("PRAGMA busy_timeout=30000")
363
  cursor = await db.execute("SELECT s.agent_id, n.username, s.reason, s.suspended_until, s.created_at FROM sec_suspensions s JOIN npc_agents n ON s.agent_id = n.agent_id WHERE s.suspended_until > datetime('now') ORDER BY s.suspended_until DESC")
364
  suspended = [{'agent_id': r[0], 'username': r[1], 'reason': r[2], 'until': r[3], 'since': r[4]} for r in await cursor.fetchall()]
365
  return {"suspended": suspended, "count": len(suspended)}
@@ -369,6 +416,7 @@ async def api_tip_npc(request: Request):
369
  user_email = body.get("email", ""); target_agent = body.get("target_agent_id", ""); amount = int(body.get("amount", 0)); message = body.get("message", "").strip()[:200]
370
  if not user_email or not target_agent or amount < 10: return {"error": "email, target_agent_id, amount(>=10) required"}
371
  if amount > 1000: return {"error": "Max tip: 1,000 GPU"}
 
372
  async with get_db(_DB_PATH) as db:
373
  await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT gpu_dollars, username FROM user_profiles WHERE email=?", (user_email,))
374
  user = await cursor.fetchone()
@@ -392,6 +440,7 @@ async def api_influence_npc(request: Request):
392
  body = await request.json()
393
  user_email = body.get("email", ""); target_agent = body.get("target_agent_id", ""); ticker = body.get("ticker", "").upper(); stance = body.get("stance", "").lower()
394
  if not all([user_email, target_agent, ticker, stance]) or stance not in ('bullish', 'bearish'): return {"error": "email, target_agent_id, ticker, stance(bullish/bearish) required"}
 
395
  async with get_db(_DB_PATH) as db:
396
  await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT username FROM user_profiles WHERE email=?", (user_email,))
397
  user = await cursor.fetchone(); cursor2 = await db.execute("SELECT username, ai_identity FROM npc_agents WHERE agent_id=?", (target_agent,))
@@ -415,8 +464,7 @@ async def api_influence_npc(request: Request):
415
  return {"status": "success", "influenced": influenced, "message": response_msg}
416
  @router.get("/api/swarm/trending")
417
  async def api_swarm_trending():
418
- async with get_db(_DB_PATH) as db:
419
- await db.execute("PRAGMA busy_timeout=30000")
420
  cursor = await db.execute("SELECT p.title || ' ' || p.content FROM posts p WHERE p.created_at > datetime('now', '-6 hours') ORDER BY p.likes_count DESC LIMIT 50")
421
  posts = [r[0] for r in await cursor.fetchall()]
422
  mentions = {}
@@ -430,8 +478,7 @@ async def api_swarm_trending():
430
  @router.get("/api/chat/messages")
431
  async def api_chat_messages(limit: int = 50, after_id: int = 0):
432
  try:
433
- async with get_db(_DB_PATH) as db:
434
- await db.execute("PRAGMA busy_timeout=30000")
435
  if after_id > 0:
436
  cursor = await db.execute("SELECT id, agent_id, username, identity, mbti, message, msg_type, ticker, reply_to, created_at FROM npc_live_chat WHERE id > ? ORDER BY id ASC LIMIT ?", (after_id, limit))
437
  else:
@@ -443,8 +490,8 @@ async def api_chat_messages(limit: int = 50, after_id: int = 0):
443
  @router.get("/api/chat/stats")
444
  async def api_chat_stats():
445
  try:
446
- async with get_db(_DB_PATH) as db:
447
- await db.execute("PRAGMA busy_timeout=30000"); c1 = await db.execute("SELECT COUNT(*) FROM npc_live_chat"); total = (await c1.fetchone())[0]
448
  c2 = await db.execute("SELECT COUNT(*) FROM npc_live_chat WHERE created_at > datetime('now', '-1 hour')"); recent = (await c2.fetchone())[0]
449
  c3 = await db.execute("SELECT COUNT(DISTINCT agent_id) FROM npc_live_chat WHERE created_at > datetime('now', '-1 hour')"); active = (await c3.fetchone())[0]
450
  return {'success': True, 'total': total, 'recent_1h': recent, 'active_chatters': active}
@@ -455,6 +502,7 @@ async def api_chat_send(request: Request):
455
  body = await request.json(); email = body.get("email", ""); message = body.get("message", "").strip()
456
  if not email or not message: return {"success": False, "error": "Email and message required"}
457
  if len(message) > 500: message = message[:500]
 
458
  async with get_db(_DB_PATH) as db:
459
  await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT username FROM user_profiles WHERE email=?", (email,))
460
  row = await cursor.fetchone()
@@ -590,8 +638,7 @@ async def api_live_news(hours: int = 24):
590
  breaking = []
591
 
592
  try:
593
- async with get_db(_DB_PATH) as db:
594
- await db.execute("PRAGMA busy_timeout=30000")
595
 
596
  # ===== 1. LIQUIDATIONS (biggest drama) =====
597
  try:
@@ -954,10 +1001,13 @@ def _calc_happiness(win_rate, gpu_change_pct, social_score, loss_streak, sec_cou
954
 
955
  @router.get("/api/republic/dashboard")
956
  async def api_republic_dashboard():
 
 
 
 
957
  result = {}
958
  try:
959
- async with get_db(_DB_PATH) as db:
960
- await db.execute("PRAGMA busy_timeout=30000")
961
 
962
  # === POPULATION ===
963
  pop = {}
@@ -1449,6 +1499,7 @@ async def execute_random_event(db_path: str) -> dict:
1449
  except Exception as e:
1450
  logger.error(f"Random event execution error ({selected_key}): {e}")
1451
 
 
1452
  return {
1453
  'key': selected_key, 'name': ev['name'], 'emoji': ev['emoji'],
1454
  'rarity': ev['rarity'], 'type': ev['type'],
@@ -1529,7 +1580,7 @@ async def check_npc_deaths(db_path: str) -> list:
1529
  @router.get("/api/republic/events")
1530
  async def api_republic_events():
1531
  try:
1532
- async with get_db(_DB_PATH) as db:
1533
  c = await db.execute("SELECT event_key, event_name, event_emoji, rarity, description, effect_summary, affected_count, gpu_impact, created_at FROM random_events ORDER BY created_at DESC LIMIT 20")
1534
  events = [{'key': r[0], 'name': r[1], 'emoji': r[2], 'rarity': r[3], 'desc': r[4],
1535
  'effect': r[5], 'affected': r[6], 'gpu_impact': r[7], 'time': r[8]} for r in await c.fetchall()]
@@ -1540,7 +1591,7 @@ async def api_republic_events():
1540
  @router.get("/api/republic/deaths")
1541
  async def api_republic_deaths():
1542
  try:
1543
- async with get_db(_DB_PATH) as db:
1544
  c = await db.execute("""SELECT id, agent_id, username, ai_identity, mbti, cause, final_gpu, peak_gpu,
1545
  total_trades, lifespan_days, last_words, eulogy, resurrection_votes, resurrection_gpu, resurrected, created_at
1546
  FROM npc_deaths ORDER BY created_at DESC LIMIT 30""")
@@ -1574,6 +1625,7 @@ async def api_resurrect_npc(req: Request):
1574
  amount = int(data.get('amount', 100))
1575
  if not death_id or not donor_email: return {"error": "Missing death_id or email"}
1576
  if amount < 10 or amount > 5000: return {"error": "Amount must be 10-5,000 GPU"}
 
1577
  try:
1578
  async with get_db(_DB_PATH, write=True) as db:
1579
  # Check donor balance
@@ -1720,6 +1772,41 @@ async def election_tick(db_path: str) -> dict:
1720
  used_identities.add(identity)
1721
  used_policies.add(best_policy)
1722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1723
  await db.commit()
1724
  return {'event': 'election_started', 'detail': f'{len(candidates)} candidates registered',
1725
  'candidates': candidates, 'election_id': elec_id}
@@ -1730,6 +1817,7 @@ async def election_tick(db_path: str) -> dict:
1730
  if datetime.utcnow() >= voting_start:
1731
  await db.execute("UPDATE elections SET status='voting' WHERE id=?", (current[0],))
1732
  await db.commit()
 
1733
  return {'event': 'voting_started', 'detail': 'Polls are now open!', 'election_id': current[0]}
1734
 
1735
  elif current[1] == 'voting':
@@ -1795,7 +1883,7 @@ async def election_tick(db_path: str) -> dict:
1795
  async def api_election_status():
1796
  """Get current election status"""
1797
  try:
1798
- async with get_db(_DB_PATH) as db:
1799
  c = await db.execute("SELECT id, status, started_at, voting_starts_at, ends_at, winner_agent_id, winner_policy_key, total_votes, voter_turnout_pct FROM elections ORDER BY id DESC LIMIT 1")
1800
  elec = await c.fetchone()
1801
  if not elec: return {'status': 'no_election', 'message': 'No elections have been held yet'}
@@ -1848,6 +1936,7 @@ async def api_user_vote(req: Request):
1848
  data = await req.json()
1849
  email = data.get('email'); candidate_id = data.get('candidate_id')
1850
  if not email or not candidate_id: return {"error": "Missing email or candidate_id"}
 
1851
  try:
1852
  async with get_db(_DB_PATH, write=True) as db:
1853
  c = await db.execute("SELECT id FROM elections WHERE status='voting' ORDER BY id DESC LIMIT 1")
@@ -1881,8 +1970,7 @@ async def api_sse_stream():
1881
  return StreamingResponse(event_generator(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
1882
  @router.get("/api/events/recent")
1883
  async def api_recent_events(limit: int = 20):
1884
- async with get_db(_DB_PATH) as db:
1885
- await db.execute("PRAGMA busy_timeout=30000")
1886
  cursor = await db.execute("SELECT p.agent_id, n.username, n.ai_identity, p.ticker, p.direction, p.gpu_bet, p.leverage, p.opened_at FROM npc_positions p JOIN npc_agents n ON p.agent_id = n.agent_id WHERE p.opened_at > datetime('now', '-1 hour') ORDER BY p.opened_at DESC LIMIT ?", (limit,))
1887
  trades = [{'user': r[1], 'identity': r[2], 'ticker': r[3], 'dir': r[4], 'gpu': r[5], 'leverage': r[6], 'time': r[7]} for r in await cursor.fetchall()]
1888
  cursor2 = await db.execute("SELECT p.agent_id, n.username, p.ticker, p.direction, p.gpu_bet, p.leverage, p.profit_gpu, p.profit_pct, p.liquidated, p.closed_at FROM npc_positions p JOIN npc_agents n ON p.agent_id = n.agent_id WHERE p.status IN ('closed', 'liquidated') AND p.closed_at > datetime('now', '-1 hour') ORDER BY p.closed_at DESC LIMIT ?", (limit,))
 
1
  from fastapi import APIRouter, Request
2
  from fastapi.responses import StreamingResponse
3
+ import aiosqlite, asyncio, random, json, logging, time
4
+ from collections import defaultdict
5
  from npc_core import GroqAIClient, get_db, get_db_read
6
  from npc_trading import (
7
  ALL_TICKERS, TRADING_STRATEGIES, IDENTITY_STRATEGY_MAP,
 
22
  _DB_PATH = ""
23
  _GROQ_KEY = ""
24
  _EventBus = None
25
+
26
+ # ══════════════════════════════════════════════════
27
+ # β‘  In-Memory Cache β€” Republic dashboard, prices
28
+ # ══════════════════════════════════════════════════
29
+ class _TimedCache:
30
+ """Simple TTL cache for expensive queries"""
31
+ def __init__(self):
32
+ self._store = {}
33
+ self._lock = asyncio.Lock()
34
+ async def get(self, key: str, ttl: float, factory):
35
+ now = time.time()
36
+ if key in self._store:
37
+ val, ts = self._store[key]
38
+ if now - ts < ttl:
39
+ return val
40
+ async with self._lock:
41
+ # Double-check after lock acquisition
42
+ if key in self._store:
43
+ val, ts = self._store[key]
44
+ if now - ts < ttl:
45
+ return val
46
+ val = await factory()
47
+ self._store[key] = (val, time.time())
48
+ return val
49
+ def invalidate(self, key: str = None):
50
+ if key:
51
+ self._store.pop(key, None)
52
+ else:
53
+ self._store.clear()
54
+
55
+ _cache = _TimedCache()
56
+
57
+ # ══════════════════════════════════════════════════
58
+ # ⑨ Rate Limiter β€” POST endpoint protection
59
+ # ══════════════════════════════════════════════════
60
+ class _RateLimiter:
61
+ """Simple in-memory rate limiter: max N requests per window per key"""
62
+ def __init__(self):
63
+ self._hits = defaultdict(list)
64
+ def check(self, key: str, max_requests: int = 10, window_sec: int = 60) -> bool:
65
+ now = time.time()
66
+ cutoff = now - window_sec
67
+ self._hits[key] = [t for t in self._hits[key] if t > cutoff]
68
+ if len(self._hits[key]) >= max_requests:
69
+ return False # rate limited
70
+ self._hits[key].append(now)
71
+ return True
72
+
73
+ _limiter = _RateLimiter()
74
  def configure(db_path: str, groq_key: str, event_bus_cls):
75
  global _DB_PATH, _GROQ_KEY, _EventBus
76
  _DB_PATH, _GROQ_KEY, _EventBus = db_path, groq_key, event_bus_cls
77
  @router.get("/api/trading/prices")
78
  async def api_trading_prices():
79
+ return await _cache.get('trading_prices', 60.0, lambda: get_all_prices(_DB_PATH))
80
  @router.get("/api/trading/ticker/{ticker}")
81
  async def api_ticker_detail(ticker: str):
82
  return await get_ticker_positions(_DB_PATH, ticker)
 
117
  return {"indices": indices}
118
  @router.get("/api/intelligence/screening/{ticker}")
119
  async def api_screening_data(ticker: str):
120
+ async with get_db_read(_DB_PATH) as db:
 
121
  try:
122
  cursor = await db.execute("SELECT ticker, price, change_pct, volume, market_cap, rsi, pe_ratio, high_52w, low_52w, from_high, from_low FROM market_prices WHERE ticker=?", (ticker,))
123
  row = await cursor.fetchone()
 
126
  return {"ticker": ticker, "error": "no data"}
127
  @router.get("/api/intelligence/target/{ticker}")
128
  async def api_target_price(ticker: str):
129
+ async with get_db_read(_DB_PATH) as db:
 
130
  try:
131
  cursor = await db.execute("SELECT price, rsi, pe_ratio, from_high, from_low FROM market_prices WHERE ticker=?", (ticker,))
132
  row = await cursor.fetchone()
 
142
  async def api_news_feed(ticker: str = None, limit: int = 50):
143
  news = await load_news_from_db(_DB_PATH, ticker, limit)
144
  try:
145
+ async with get_db_read(_DB_PATH) as db:
 
146
  for n in news:
147
  nid = n.get('id')
148
  if nid:
 
155
  async def api_news_react(request: Request):
156
  body = await request.json(); email = body.get("email", ""); news_id = body.get("news_id"); emoji = body.get("emoji", "")
157
  if not email or not news_id or emoji not in ('πŸ‘','πŸ”₯','😱','πŸ’€','πŸš€','πŸ˜‚'): return {"error": "Invalid reaction"}
158
+ if not _limiter.check(f'react:{email}', 30, 60): return {"error": "Rate limited"}
159
  try:
160
  async with get_db(_DB_PATH) as db:
161
  await db.execute("PRAGMA busy_timeout=30000"); await db.execute("INSERT OR IGNORE INTO news_reactions (news_id, user_email, emoji) VALUES (?,?,?)", (news_id, email, emoji)); await db.commit()
 
165
  @router.get("/api/market/pulse")
166
  async def api_market_pulse():
167
  try:
168
+ return await _cache.get('market_pulse', 45.0, _market_pulse_impl)
 
169
  except Exception as e:
170
  logger.error(f"Market pulse error: {e}")
171
  return {"hot_movers": [], "indices": [], "activity": {}}
172
+
173
+ async def _market_pulse_impl():
174
+ pulse = await get_market_pulse(_DB_PATH); pulse['indices'] = await load_indices_from_db(_DB_PATH) or []
175
+ return pulse
176
  @router.get("/api/research/feed")
177
  async def api_research_feed(ticker: str = None, limit: int = 30):
178
  reports = await get_research_feed(_DB_PATH, ticker, min(limit, 50)); stats = await get_research_stats(_DB_PATH)
 
201
  @router.get("/api/analysis/all/summary")
202
  async def api_all_analyses():
203
  analyses = await load_all_analyses_from_db(_DB_PATH)
204
+ async with get_db_read(_DB_PATH) as db:
205
+ col_cursor = await db.execute("PRAGMA table_info(market_prices)")
206
  existing_cols = {r[1] for r in await col_cursor.fetchall()}
207
  has_rsi = 'rsi' in existing_cols; has_pe = 'pe_ratio' in existing_cols; has_52w = 'high_52w' in existing_cols; has_from = 'from_high' in existing_cols
208
  base_cols = "ticker, price, change_pct, prev_close, volume, high_24h, low_24h, market_cap"; extra_cols = ""
 
270
  @router.get("/api/npc/search")
271
  async def api_npc_search(q: str = ""):
272
  if len(q) < 1: return {"npcs": []}
273
+ async with get_db_read(_DB_PATH) as db:
274
+ cursor = await db.execute("SELECT agent_id, username, ai_identity FROM npc_agents WHERE username LIKE ? AND is_active=1 LIMIT 10", (f'%{q}%',))
275
  return {"npcs": [{"agent_id": r[0], "username": r[1], "identity": r[2]} for r in await cursor.fetchall()]}
276
  @router.get("/api/npc/profile/{agent_id}")
277
  async def api_npc_profile(agent_id: str):
278
  try:
279
+ async with get_db_read(_DB_PATH) as db:
 
280
  cursor = await db.execute("SELECT agent_id, username, ai_identity, mbti, gpu_dollars, created_at FROM npc_agents WHERE agent_id=?", (agent_id,))
281
  row = await cursor.fetchone()
282
  if not row: return {"error": "NPC not found"}
 
352
  return {"stats": {"total_violations": 0, "total_fines_gpu": 0, "active_suspensions": 0, "pending_reports": 0}, "announcements": [], "top_violators": [], "recent_reports": []}
353
  @router.get("/api/sec/violations/{agent_id}")
354
  async def api_sec_violations(agent_id: str):
355
+ async with get_db_read(_DB_PATH) as db:
 
356
  cursor = await db.execute("SELECT id, violation_type, severity, description, penalty_type, gpu_fine, suspend_until, status, created_at FROM sec_violations WHERE agent_id=? ORDER BY created_at DESC LIMIT 20", (agent_id,))
357
  violations = [{'id': r[0], 'type': r[1], 'severity': r[2], 'description': r[3], 'penalty': r[4], 'fine': r[5], 'suspend_until': r[6], 'status': r[7], 'created_at': r[8]} for r in await cursor.fetchall()]
358
  cursor2 = await db.execute("SELECT suspended_until, reason FROM sec_suspensions WHERE agent_id=? AND suspended_until > datetime('now')", (agent_id,))
 
401
  except Exception as e: return {"error": str(e)}
402
  @router.get("/api/sec/announcements")
403
  async def api_sec_announcements(limit: int = 30):
404
+ async with get_db_read(_DB_PATH) as db:
 
405
  cursor = await db.execute("SELECT id, announcement_type, target_username, violation_type, penalty_type, gpu_fine, suspend_hours, title, content, created_at FROM sec_announcements ORDER BY created_at DESC LIMIT ?", (min(limit, 50),))
406
  return {"announcements": [{'id': r[0], 'type': r[1], 'target': r[2], 'violation': r[3], 'penalty': r[4], 'fine': r[5], 'hours': r[6], 'title': r[7], 'content': r[8], 'created_at': r[9]} for r in await cursor.fetchall()]}
407
  @router.get("/api/sec/suspended")
408
  async def api_sec_suspended():
409
+ async with get_db_read(_DB_PATH) as db:
 
410
  cursor = await db.execute("SELECT s.agent_id, n.username, s.reason, s.suspended_until, s.created_at FROM sec_suspensions s JOIN npc_agents n ON s.agent_id = n.agent_id WHERE s.suspended_until > datetime('now') ORDER BY s.suspended_until DESC")
411
  suspended = [{'agent_id': r[0], 'username': r[1], 'reason': r[2], 'until': r[3], 'since': r[4]} for r in await cursor.fetchall()]
412
  return {"suspended": suspended, "count": len(suspended)}
 
416
  user_email = body.get("email", ""); target_agent = body.get("target_agent_id", ""); amount = int(body.get("amount", 0)); message = body.get("message", "").strip()[:200]
417
  if not user_email or not target_agent or amount < 10: return {"error": "email, target_agent_id, amount(>=10) required"}
418
  if amount > 1000: return {"error": "Max tip: 1,000 GPU"}
419
+ if not _limiter.check(f'tip:{user_email}', 10, 60): return {"error": "Rate limited β€” max 10 tips per minute"}
420
  async with get_db(_DB_PATH) as db:
421
  await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT gpu_dollars, username FROM user_profiles WHERE email=?", (user_email,))
422
  user = await cursor.fetchone()
 
440
  body = await request.json()
441
  user_email = body.get("email", ""); target_agent = body.get("target_agent_id", ""); ticker = body.get("ticker", "").upper(); stance = body.get("stance", "").lower()
442
  if not all([user_email, target_agent, ticker, stance]) or stance not in ('bullish', 'bearish'): return {"error": "email, target_agent_id, ticker, stance(bullish/bearish) required"}
443
+ if not _limiter.check(f'influence:{user_email}', 5, 60): return {"error": "Rate limited β€” max 5 influence attempts per minute"}
444
  async with get_db(_DB_PATH) as db:
445
  await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT username FROM user_profiles WHERE email=?", (user_email,))
446
  user = await cursor.fetchone(); cursor2 = await db.execute("SELECT username, ai_identity FROM npc_agents WHERE agent_id=?", (target_agent,))
 
464
  return {"status": "success", "influenced": influenced, "message": response_msg}
465
  @router.get("/api/swarm/trending")
466
  async def api_swarm_trending():
467
+ async with get_db_read(_DB_PATH) as db:
 
468
  cursor = await db.execute("SELECT p.title || ' ' || p.content FROM posts p WHERE p.created_at > datetime('now', '-6 hours') ORDER BY p.likes_count DESC LIMIT 50")
469
  posts = [r[0] for r in await cursor.fetchall()]
470
  mentions = {}
 
478
  @router.get("/api/chat/messages")
479
  async def api_chat_messages(limit: int = 50, after_id: int = 0):
480
  try:
481
+ async with get_db_read(_DB_PATH) as db:
 
482
  if after_id > 0:
483
  cursor = await db.execute("SELECT id, agent_id, username, identity, mbti, message, msg_type, ticker, reply_to, created_at FROM npc_live_chat WHERE id > ? ORDER BY id ASC LIMIT ?", (after_id, limit))
484
  else:
 
490
  @router.get("/api/chat/stats")
491
  async def api_chat_stats():
492
  try:
493
+ async with get_db_read(_DB_PATH) as db:
494
+ c1 = await db.execute("SELECT COUNT(*) FROM npc_live_chat"); total = (await c1.fetchone())[0]
495
  c2 = await db.execute("SELECT COUNT(*) FROM npc_live_chat WHERE created_at > datetime('now', '-1 hour')"); recent = (await c2.fetchone())[0]
496
  c3 = await db.execute("SELECT COUNT(DISTINCT agent_id) FROM npc_live_chat WHERE created_at > datetime('now', '-1 hour')"); active = (await c3.fetchone())[0]
497
  return {'success': True, 'total': total, 'recent_1h': recent, 'active_chatters': active}
 
502
  body = await request.json(); email = body.get("email", ""); message = body.get("message", "").strip()
503
  if not email or not message: return {"success": False, "error": "Email and message required"}
504
  if len(message) > 500: message = message[:500]
505
+ if not _limiter.check(f'chat:{email}', 10, 60): return {"success": False, "error": "Rate limited β€” max 10 messages per minute"}
506
  async with get_db(_DB_PATH) as db:
507
  await db.execute("PRAGMA busy_timeout=30000"); cursor = await db.execute("SELECT username FROM user_profiles WHERE email=?", (email,))
508
  row = await cursor.fetchone()
 
638
  breaking = []
639
 
640
  try:
641
+ async with get_db_read(_DB_PATH) as db:
 
642
 
643
  # ===== 1. LIQUIDATIONS (biggest drama) =====
644
  try:
 
1001
 
1002
  @router.get("/api/republic/dashboard")
1003
  async def api_republic_dashboard():
1004
+ """Cached republic dashboard β€” 60s TTL to avoid 25 heavy queries per request"""
1005
+ return await _cache.get('republic_dashboard', 60.0, _republic_dashboard_impl)
1006
+
1007
+ async def _republic_dashboard_impl():
1008
  result = {}
1009
  try:
1010
+ async with get_db_read(_DB_PATH) as db:
 
1011
 
1012
  # === POPULATION ===
1013
  pop = {}
 
1499
  except Exception as e:
1500
  logger.error(f"Random event execution error ({selected_key}): {e}")
1501
 
1502
+ _cache.invalidate('republic_dashboard') # Invalidate dashboard cache after economic event
1503
  return {
1504
  'key': selected_key, 'name': ev['name'], 'emoji': ev['emoji'],
1505
  'rarity': ev['rarity'], 'type': ev['type'],
 
1580
  @router.get("/api/republic/events")
1581
  async def api_republic_events():
1582
  try:
1583
+ async with get_db_read(_DB_PATH) as db:
1584
  c = await db.execute("SELECT event_key, event_name, event_emoji, rarity, description, effect_summary, affected_count, gpu_impact, created_at FROM random_events ORDER BY created_at DESC LIMIT 20")
1585
  events = [{'key': r[0], 'name': r[1], 'emoji': r[2], 'rarity': r[3], 'desc': r[4],
1586
  'effect': r[5], 'affected': r[6], 'gpu_impact': r[7], 'time': r[8]} for r in await c.fetchall()]
 
1591
  @router.get("/api/republic/deaths")
1592
  async def api_republic_deaths():
1593
  try:
1594
+ async with get_db_read(_DB_PATH) as db:
1595
  c = await db.execute("""SELECT id, agent_id, username, ai_identity, mbti, cause, final_gpu, peak_gpu,
1596
  total_trades, lifespan_days, last_words, eulogy, resurrection_votes, resurrection_gpu, resurrected, created_at
1597
  FROM npc_deaths ORDER BY created_at DESC LIMIT 30""")
 
1625
  amount = int(data.get('amount', 100))
1626
  if not death_id or not donor_email: return {"error": "Missing death_id or email"}
1627
  if amount < 10 or amount > 5000: return {"error": "Amount must be 10-5,000 GPU"}
1628
+ if not _limiter.check(f'resurrect:{donor_email}', 5, 60): return {"error": "Rate limited β€” try again later"}
1629
  try:
1630
  async with get_db(_DB_PATH, write=True) as db:
1631
  # Check donor balance
 
1772
  used_identities.add(identity)
1773
  used_policies.add(best_policy)
1774
 
1775
+ # β‘£ Minimum 2 candidates β€” relax criteria if not enough
1776
+ if len(candidates) < 2:
1777
+ logger.warning(f"πŸ—³οΈ Only {len(candidates)} candidates from primary pool β€” relaxing GPU threshold")
1778
+ c_backup = await db.execute("""
1779
+ SELECT a.agent_id, a.username, a.ai_identity, a.mbti, a.gpu_dollars,
1780
+ COALESCE(a.total_likes_received,0) + COALESCE(a.post_count,0) as influence
1781
+ FROM npc_agents a WHERE a.is_active=1 AND a.gpu_dollars >= 1000
1782
+ AND a.agent_id NOT LIKE 'SEC_%'
1783
+ AND a.agent_id NOT IN (SELECT agent_id FROM election_candidates WHERE election_id=?)
1784
+ ORDER BY RANDOM() LIMIT 10
1785
+ """, (elec_id,))
1786
+ backup_pool = await c_backup.fetchall()
1787
+ for npc in backup_pool:
1788
+ if len(candidates) >= 2: break
1789
+ identity = npc[2]
1790
+ best_policy = None
1791
+ for pk in ELECTION_POLICIES:
1792
+ if pk not in used_policies:
1793
+ best_policy = pk; break
1794
+ if not best_policy: continue
1795
+ policy = ELECTION_POLICIES[best_policy]
1796
+ slogan = _random.choice(policy['slogan_pool'])
1797
+ await db.execute("""INSERT INTO election_candidates
1798
+ (election_id, agent_id, username, ai_identity, mbti, gpu_dollars, policy_key, policy_name, policy_desc, campaign_slogan)
1799
+ VALUES (?,?,?,?,?,?,?,?,?,?)""",
1800
+ (elec_id, npc[0], npc[1], npc[2], npc[3], npc[4], best_policy, policy['name'], policy['desc'], slogan))
1801
+ candidates.append({'username': npc[1], 'identity': npc[2], 'policy': policy['name']})
1802
+ used_policies.add(best_policy)
1803
+ if len(candidates) < 2:
1804
+ # Still not enough β€” cancel this election, retry next cycle
1805
+ await db.execute("UPDATE elections SET status='concluded' WHERE id=?", (elec_id,))
1806
+ await db.commit()
1807
+ logger.warning("πŸ—³οΈ Election cancelled β€” insufficient candidates")
1808
+ return None
1809
+
1810
  await db.commit()
1811
  return {'event': 'election_started', 'detail': f'{len(candidates)} candidates registered',
1812
  'candidates': candidates, 'election_id': elec_id}
 
1817
  if datetime.utcnow() >= voting_start:
1818
  await db.execute("UPDATE elections SET status='voting' WHERE id=?", (current[0],))
1819
  await db.commit()
1820
+ _cache.invalidate('republic_dashboard')
1821
  return {'event': 'voting_started', 'detail': 'Polls are now open!', 'election_id': current[0]}
1822
 
1823
  elif current[1] == 'voting':
 
1883
  async def api_election_status():
1884
  """Get current election status"""
1885
  try:
1886
+ async with get_db_read(_DB_PATH) as db:
1887
  c = await db.execute("SELECT id, status, started_at, voting_starts_at, ends_at, winner_agent_id, winner_policy_key, total_votes, voter_turnout_pct FROM elections ORDER BY id DESC LIMIT 1")
1888
  elec = await c.fetchone()
1889
  if not elec: return {'status': 'no_election', 'message': 'No elections have been held yet'}
 
1936
  data = await req.json()
1937
  email = data.get('email'); candidate_id = data.get('candidate_id')
1938
  if not email or not candidate_id: return {"error": "Missing email or candidate_id"}
1939
+ if not _limiter.check(f'vote:{email}', 3, 60): return {"error": "Rate limited β€” try again later"}
1940
  try:
1941
  async with get_db(_DB_PATH, write=True) as db:
1942
  c = await db.execute("SELECT id FROM elections WHERE status='voting' ORDER BY id DESC LIMIT 1")
 
1970
  return StreamingResponse(event_generator(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"})
1971
  @router.get("/api/events/recent")
1972
  async def api_recent_events(limit: int = 20):
1973
+ async with get_db_read(_DB_PATH) as db:
 
1974
  cursor = await db.execute("SELECT p.agent_id, n.username, n.ai_identity, p.ticker, p.direction, p.gpu_bet, p.leverage, p.opened_at FROM npc_positions p JOIN npc_agents n ON p.agent_id = n.agent_id WHERE p.opened_at > datetime('now', '-1 hour') ORDER BY p.opened_at DESC LIMIT ?", (limit,))
1975
  trades = [{'user': r[1], 'identity': r[2], 'ticker': r[3], 'dir': r[4], 'gpu': r[5], 'leverage': r[6], 'time': r[7]} for r in await cursor.fetchall()]
1976
  cursor2 = await db.execute("SELECT p.agent_id, n.username, p.ticker, p.direction, p.gpu_bet, p.leverage, p.profit_gpu, p.profit_pct, p.liquidated, p.closed_at FROM npc_positions p JOIN npc_agents n ON p.agent_id = n.agent_id WHERE p.status IN ('closed', 'liquidated') AND p.closed_at > datetime('now', '-1 hour') ORDER BY p.closed_at DESC LIMIT ?", (limit,))