fix(ingestor): align WebSocket subscribe + routing with live API (#105)

* fix(ingestor): align WebSocket subscribe + routing with live API

The Polymarket ws-live-data WebSocket requires `action: "subscribe"` in
the subscribe envelope. Without it the server accepts the connection but
never delivers trade events, causing the tracker to silently produce
zero alerts.

Additionally, incoming frames are shaped `{connection_id, payload:{...}}`
-- they do NOT echo the `topic`/`type` keys we sent. The previous routing
check matched nothing and every real trade was silently dropped.

Changes:
- Add `action: "subscribe"` to subscription message
- Route incoming messages by payload shape (transactionHash + proxyWallet)
- Add ratchet tests for payload routing edge cases
- Rewrite README as agent-first with <2min quickstart
- Add skill draft (docs/skill-tracking-prediction-market-flow.md)

Closes #89

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(lint): remove unused imports in test_pipeline_persistence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: apply ruff formatting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Selamy
2026-06-14 14:24:54 -04:00
committed by GitHub
co-authored by Claude Opus 4.6
parent ff678468e3
commit b962bdaee2
6 changed files with 309 additions and 283 deletions
+24 -7
View File
@@ -18,7 +18,9 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
class DatabaseSettings(BaseSettings):
"""Database connection settings."""
model_config = SettingsConfigDict(env_prefix="", env_file=".env", env_file_encoding="utf-8", extra="ignore")
model_config = SettingsConfigDict(
env_prefix="", env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
url: str = Field(
alias="DATABASE_URL",
@@ -37,7 +39,9 @@ class DatabaseSettings(BaseSettings):
class RedisSettings(BaseSettings):
"""Redis connection settings."""
model_config = SettingsConfigDict(env_prefix="", env_file=".env", env_file_encoding="utf-8", extra="ignore")
model_config = SettingsConfigDict(
env_prefix="", env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
url: str = Field(
default="redis://localhost:6379",
@@ -57,7 +61,9 @@ class RedisSettings(BaseSettings):
class PolygonSettings(BaseSettings):
"""Polygon blockchain RPC settings."""
model_config = SettingsConfigDict(env_prefix="POLYGON_", env_file=".env", env_file_encoding="utf-8", extra="ignore")
model_config = SettingsConfigDict(
env_prefix="POLYGON_", env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
rpc_url: str = Field(
default="https://polygon-rpc.com",
@@ -84,7 +90,9 @@ class PolygonSettings(BaseSettings):
class PolymarketSettings(BaseSettings):
"""Polymarket API settings."""
model_config = SettingsConfigDict(env_prefix="POLYMARKET_", env_file=".env", env_file_encoding="utf-8", extra="ignore")
model_config = SettingsConfigDict(
env_prefix="POLYMARKET_", env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
ws_url: str = Field(
default="wss://ws-subscriptions-clob.polymarket.com/ws/market",
@@ -109,7 +117,9 @@ class PolymarketSettings(BaseSettings):
class DiscordSettings(BaseSettings):
"""Discord notification settings."""
model_config = SettingsConfigDict(env_prefix="DISCORD_", env_file=".env", env_file_encoding="utf-8", extra="ignore")
model_config = SettingsConfigDict(
env_prefix="DISCORD_", env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
webhook_url: SecretStr | None = Field(
default=None,
@@ -126,7 +136,9 @@ class DiscordSettings(BaseSettings):
class TelegramSettings(BaseSettings):
"""Telegram notification settings."""
model_config = SettingsConfigDict(env_prefix="TELEGRAM_", env_file=".env", env_file_encoding="utf-8", extra="ignore")
model_config = SettingsConfigDict(
env_prefix="TELEGRAM_", env_file=".env", env_file_encoding="utf-8", extra="ignore"
)
bot_token: SecretStr | None = Field(
default=None,
@@ -142,7 +154,12 @@ class TelegramSettings(BaseSettings):
@property
def enabled(self) -> bool:
"""Check if Telegram notifications are enabled."""
return self.bot_token is not None and bool(self.bot_token.get_secret_value().strip()) and self.chat_id is not None and bool(self.chat_id.strip())
return (
self.bot_token is not None
and bool(self.bot_token.get_secret_value().strip())
and self.chat_id is not None
and bool(self.chat_id.strip())
)
class Settings(BaseSettings):
@@ -150,7 +150,7 @@ class TradeStreamHandler:
elif self._market_filter:
subscription["filters"] = json.dumps({"market_slug": self._market_filter})
return {"subscriptions": [subscription]}
return {"action": "subscribe", "subscriptions": [subscription]}
async def _connect(self) -> ClientConnection:
"""Establish WebSocket connection."""
@@ -183,12 +183,13 @@ class TradeStreamHandler:
try:
data = json.loads(message)
# Check if this is a trade message
topic = data.get("topic")
msg_type = data.get("type")
if topic == "activity" and msg_type == "trades":
payload = data.get("payload", {})
# ws-live-data pushes {connection_id, payload:{...trade fields}}
payload = data.get("payload")
if (
isinstance(payload, dict)
and "transactionHash" in payload
and "proxyWallet" in payload
):
trade = TradeEvent.from_websocket_message(payload)
self._stats.trades_received += 1
@@ -208,8 +209,7 @@ class TradeStreamHandler:
logger.error("Error in trade callback: %s", e)
else:
# Log other message types for debugging
logger.debug("Received message: topic=%s type=%s", topic, msg_type)
logger.debug("Received non-trade message: %s", str(data)[:120])
except json.JSONDecodeError as e:
logger.warning("Invalid JSON message: %s", e)