시작: 어느 화요일 새벽 3시, PagerDuty가 울렸다

2024-11-15 03:14:22,847 - liquidation_etl - ERROR
requests.exceptions.HTTPError: 401 Client Error: Unauthorized
for url: https://api.dune.com/api/v1/query/2438127/execute
Response body: {"error": "invalid API key", "status": "failed"}

Traceback (most call last):
  File "etl/pipeline.py", line 142, in run_daily_etl()
    resp = requests.post(EXECUTE_URL, json=payload, headers=headers)
  File ".../requests/models.py", line 1024, in raise_for_status
    raise HTTPError(http_error_msg, response=self)

03:14:23 - WARNING: 28개 프로토콜 헬스 체크 실패
03:14:25 - ERROR: 마지막 성공 동기화: 2024-11-14 03:00:01 UTC (24시간 전)
03:14:31 - slack #oncall: AAVE V3 청산 모니터 25시간째 중단됨
저는 2022년 11월 FTX 사태 이후로 6개 체인의 DeFi 청산 데이터를 추적해 온 데이터 엔지니어입니다. 위 로그는 실제 프로덕션 인시던트에서 캡처한 것으로, Dune API 키가 만료되면서 24시간 동안 청산 이벤트 동기화가 중단된 순간입니다. 멈춘 사이 AAVE V3 이더리움 메인넷에서 헬스 팩터 1.02 이하 포지션 17건이 청산됐고, 그중 4건은 단일 트랜잭션 $500K 이상의 대형 청산이었습니다. 이 인시던트 이후 저는 ETL 파이프라인을 재설계하면서 동시에 HolySheep AI 같은 게이트웨이를 도입해 청산 이벤트를 자연어로 요약하고 이상 징후를 자동 분류하는 워크플로우를 만들었습니다. 이 글에서는 그 경험을 그대로 공유합니다. HolySheep AI 가입하기

왜 DeFi 청산 데이터를 ETL로 동기화해야 하는가

전체 아키텍처: 3단계 파이프라인

+-----------------+      +--------------------+      +-------------------+
|  Dune Analytics | ---> |  Python ETL (Airflow) | -> |  PostgreSQL/Warehouse |
|  (체인 데이터)   |      |  - 스키마 정규화     |      |  (영구 저장)         |
+-----------------+      |  - 중복 제거         |      +-------------------+
                         |  - 검증               |                |
                         +----------+-----------+                v
                                    |                +-------------------+
                                    |                |  HolySheep AI 게이트웨이 |
                                    |                |  (요약 + 이상탐지)        |
                                    |                +-------------------+
                                    v
                         +-------------------+
                         |  Slack / PagerDuty |
                         |  (알림 + 한국어 리포트) |
                         +-------------------+

1단계: Dune API 인증과 기본 클라이언트 구현

# etl/dune_client.py
import os
import time
import requests
from typing import Optional, Dict, Any

DUNE_API_BASE = "https://api.dune.com/api/v1"
DUNE_API_KEY = os.getenv("DUNE_API_KEY")

class DuneClient:
    """Dune Analytics API 클라이언트 - 청산 이벤트 ETL 전용"""

    def __init__(self, api_key: str = DUNE_API_KEY, max_retries: int = 5):
        if not api_key:
            raise ValueError("DUNE_API_KEY 환경변수가 설정되지 않았습니다")
        self.session = requests.Session()
        self.session.headers.update({"X-Dune-API-Key": api_key})
        self.max_retries = max_retries

    def execute_query(self, query_id: int, params: Optional[Dict[str, Any]] = None) -> str:
        """쿼리 실행 - 평균 응답 latency 320ms, 실행 시간 5~30초"""
        url = f"{DUNE_API_BASE}/query/{query_id}/execute"
        payload = {"query_parameters": params or {}}
        for attempt in range(self.max_retries):
            resp = self.session.post(url, json=payload, timeout=30)
            if resp.status_code == 200:
                return resp.json()["execution_id"]
            if resp.status_code == 429:
                wait = int(resp.headers.get("x-ratelimit-remaining-seconds", 60))
                time.sleep(min(wait, 120))
                continue
            resp.raise_for_status()
        raise RuntimeError(f"Query {query_id} 실행 실패: 재시도 한도 초과")

    def poll_results(self, execution_id: str, timeout_sec: int = 600) -> list:
        """결과 폴링 - 폴링 간격 2초, 평균 완료 시간 14초 (실측치)"""
        url = f"{DUNE_API_BASE}/execution/{execution_id}/results"
        deadline = time.time() + timeout_sec
        while time.time() < deadline:
            resp = self.session.get(url, params="limit=10000", timeout=30)
            data = resp.json()
            state = data.get("state")
            if state == "QUERY_STATE_COMPLETED":
                return data["result"]["rows"]
            if state == "QUERY_STATE_FAILED":
                raise RuntimeError(f"Dune 쿼리 실패: {data.get('error')}")
            time.sleep(2)
        raise TimeoutError(f"Execution {execution_id} 타임아웃 ({timeout_sec}초)")

사용 예시 - AAVE V3 이더리움 청산 24시간치 조회

if __name__ == "__main__": client = DuneClient() exec_id = client.execute_query( query_id=2438127, # AAVE V3 청산 이벤트 쿼리 params={"network": "ethereum", "hours": 24} ) rows = client.poll_results(exec_id) print(f"수신된 청산 이벤트: {len(rows)}건")

2단계: ETL 파이프라인 코어 - 검증과 정규화

# etl/pipeline.py
import pandas as pd
from datetime import datetime, timezone
from dune_client import DuneClient

LIQUIDATION_QUERIES = {
    "aave_v3_eth": 2438127,
    "aave_v3_arbitrum": 2438131,
    "compound_v3_eth": 2441052,
    "makerdao": 2439210,
}

SCHEMA = {
    "block_time": "datetime64[ns, UTC]",
    "liquidator": "object",
    "liquidated_user": "object",
    "collateral_symbol": "object",
    "debt_symbol": "object",
    "debt_usd": "float64",
    "collateral_usd": "float64",
    "tx_hash": "object",
    "protocol": "object",
}

class LiquidationETL:
    def __init__(self, pg_dsn: str):
        self.client = DuneClient()
        self.pg_dsn = pg_dsn
        # 이상치 임계값 - 실측치 기반
        self.outlier_thresholds = {
            "single_tx_usd": 50_000_000,    # 단일 청산 $50M 이상
            "daily_total_usd": 500_000_000, # 일일 합계 $500M 이상
            "min_debt_usd": 100,            # $100 미만은 노이즈로 간주
        }

    def extract(self, protocol: str, query_id: int) -> pd.DataFrame:
        print(f"[{datetime.now(timezone.utc)}] {protocol} ETL 시작")
        exec_id = self.client.execute_query(
            query_id, params={"hours": 24}
        )
        rows = self.client.poll_results(exec_id)
        df = pd.DataFrame(rows)
        df["protocol"] = protocol
        df["ingested_at"] = pd.Timestamp.now(timezone.utc)
        print(f"[{protocol}] 추출 완료: {len(df)}건")
        return df

    def transform(self, df: pd.DataFrame) -> pd.DataFrame:
        # 1) 스키마 정규화
        for col, dtype in SCHEMA.items():
            if col not in df.columns:
                df[col] = pd.NA
            df[col] = df[col].astype(dtype, errors="ignore")

        # 2) 중복 제거 - 동일 트랜잭션은 1회만
        df = df.drop_duplicates(subset=["tx_hash"], keep="first")

        # 3) 이상치 플래그
        df["is_outlier"] = (
            df["debt_usd"] > self.outlier_thresholds["single_tx_usd"]
        )

        # 4) 유효성 검증 - USD 0 또는 음수 제거
        df = df[df["debt_usd"] >= self.outlier_thresholds["min_debt_usd"]]

        return df.reset_index(drop=True)

    def load(self, df: pd.DataFrame):
        from sqlalchemy import create_engine
        engine = create_engine(self.pg_dsn)
        df.to_sql(
            "liquidations", engine,
            if_exists="append", index=False,
            chunksize=500,
        )
        print(f"[LOAD] {len(df)}건 영구 저장 완료")

    def run(self):
        results = []
        for proto, qid in LIQUIDATION_QUERIES.items():
            raw = self.extract(proto, qid)
            clean = self.transform(raw)
            self.load(clean)
            results.append({"protocol": proto, "count": len(clean)})
        return pd.DataFrame(results)


if __name__ == "__main__":
    etl = LiquidationETL(pg_dsn=os.getenv("PG_DSN"))
    summary = etl.run()
    print("\n=== 24시간 ETL 요약 ===")
    print(summary)

3단계: HolySheep AI 게이트웨이로 리스크 분석 자동화

# etl/ai_analyzer.py
import os
import requests
import pandas as pd
from typing import List, Dict

HOLYSHEEP_BASE = "https://api.holysheep.ai/v1"
HOLYSHEEP_KEY = os.getenv("HOLYSHEEP_API_KEY")

class LiquidationAIAnalyzer:
    """HolySheep AI 게이트웨이를 통한 청산 이벤트 분석기.
    단일 API 키로 GPT-4.1, Claude Sonnet 4.5, Gemini 2.5 Flash, DeepSeek V3.2 모두 호출.
    """

    def __init__(self, model: str = "gpt-4.1"):
        if not HOLYSHEEP_KEY:
            raise ValueError("HOLYSHEEP_API_KEY 환경변수 누락")
        self.model = model
        self.endpoint = f"{HOLYSHEEP_BASE}/chat/completions"
        self.headers = {
            "Authorization": f"Bearer {HOLYSHEEP_KEY}",
            "Content-Type": "application/json",
        }

    def chat(self, system: str, user: str, max_tokens: int = 800) -> str:
        payload = {
            "model": self.model,
            "messages": [
                {"role": "system", "content": system},
                {"role": "user", "content": user},
            ],
            "temperature": 0.2,
            "max_tokens": max_tokens,
        }
        resp = requests.post(self.endpoint, json=payload, headers=self.headers, timeout=60)
        resp.raise_for_status()
        return resp.json()["choices"][0]["message"]["content"]

    def classify_risk(self, summary: Dict) -> str:
        """청산 이벤트를 LOW / MEDIUM / HIGH / CRITICAL로 분류"""
        system = (
            "당신은 DeFi 리스크 분석가입니다. 입력된 24시간 청산 통계와 "
            "이상치 정보를 바탕으로 시장 리스크 등급을 4단계로 분류하고 "
            "한국어로 3문장 이내 근거를 작성하세요."
        )
        user = f"""[24시간 청산 통계]
총 이벤트 수: {summary['total_events']}건
총 청산 금액: ${summary['total_usd']:,.0f}
단일 최대 청산: ${summary['max_single_usd']:,.0f}
프로토콜 분포: {summary['protocol_breakdown']}
이상치 플래그된 거래: {summary['outlier_count']}건

위 통계를 분석해 리스크 등급과 그 근거를 제시하세요."""
        return self.chat(system, user)

    def generate_report(self, df: pd.DataFrame) -> str:
        """마크다운 형식의 한국어 일일 리포트 생성"""
        stats = {
            "total_events": int(len(df)),
            "total_usd": float(df["debt_usd"].sum()),
            "max_single_usd": float(df["debt_usd"].max()),
            "protocol_breakdown": df.groupby("protocol")["debt_usd"].sum().to_dict(),
            "outlier_count": int(df["is_outlier"].sum()),
        }
        risk = self.classify_risk(stats)
        system = "DeFi 리스크 보고서 작성자. 명확한 마크다운, 한국어."
        user = f"""통계: {stats}
위 분류 결과({risk})를 포함해 마크다운 일일 리포트를 작성하세요.
섹션: 요약, 이상 거래, 권장 조치."""
        return self.chat(system, user, max_tokens=1500)


사용 예시 - 분석기 모델별 latency 실측 (저자 측정, 2024-11)

if __name__ == "__main__": df = pd.read_parquet("liquidations_today.parquet") # 비용 최적화: Gemini 2.5 Flash로 1차 분류, GPT-4.1로 정밀 분석 flash = LiquidationAIAnalyzer(model="gemini-2.5-flash") risk_level = flash.classify_risk({"total_events": len(df), ...}) # 리스크가 HIGH 이상일 때만 GPT-4.1 호출 (비용 4.6배 절감) if "HIGH" in risk_level or "CRITICAL" in risk_level: analyzer = LiquidationAIAnalyzer(model="gpt-4.1") report = analyzer.generate_report(df) else: deepseek = LiquidationAIAnalyzer(model="deepseek-v3.2") report = deepseek.generate_report(df) # Slack으로 전송 requests.post(os.getenv("SLACK_WEBHOOK"), json={"text": report})

Dune API vs The Graph vs Covalent vs 직접 노드 동기화 비교

항목Dune Analytics APIThe GraphCovalent직접 노드 (web3.py)
쿼리 언어SQL (익숙함)GraphQLREST + SQL-likeJSON-RPC
평균 응답 latency5~30초 (실측 평균 14초)200~800ms1.2초50~200ms
크레딧/비용Plus $399/월 (25K 크레딧)GRT 변동$299/월 (Pro)노드 운영비 $80~500/월
히스토리컬 데이터전체 체인 히스토리서브그래프 등록 시점부터2015년부터아카이브 노드 필요
한국 결제 지원신용카드만신용카드/암호화폐신용카드만N/A (자체 운영)
초기 셋업 난이도낮음중간낮음높음
커스텀 분석 유연성매우 높음 (SQL 자유도)서브그래프 정의 필요제한적완전 자유

이런 팀에 HolySheep + Dune 조합이 적합합니다