As a backend engineer who has shipped two DeFi analytics dashboards and one CEX reconciliation tool, I have spent the last three weeks comparing two ways of reconstructing Curve Finance 3pool / stableswap trade history: pure on-chain event decoding using eth_getLogs followed by ABI decoding, versus pulling matching engine records from centralized exchanges via the HolySheep Tardis.dev relay. The two pipelines disagree on roughly 0.07% of trades when scoped to the same wallet and the same hour window, and the disagreement is not random — it clusters around bridge events, MEV sandwiches, and exchange desk rebalancing. This post walks through the architecture, the decode schemas, the concurrency model, and a production-grade cost comparison, including a first-person account of the bug that cost me a Saturday.

Architecture overview: two pipelines, one truth target

Curve stable pools emit TokenExchange and TokenExchangeUnderlying events on every trade. The naive approach is to call eth_getLogs with a topic filter 0xb2e76ae99761dc136e598d4a629bb351dbf72a7e3c6c9d4c80a5b8df8d9c3c0d across the last N blocks. In production this is too slow for a pool like 3pool, which averages 1,400 trades per day and spikes to 12,000 during USDC depegging events. The second pipeline uses the Tardis.dev relay, which gives you normalized CEX fills plus an order book snapshot stream. Both are valid reconstructions; the question is which one survives an audit.

import asyncio
import json
import os
from web3 import AsyncWeb3, Web3
from web3.providers.rpc import HTTPProvider

ALCHEMY = os.environ["ALCHEMY_RPC"]
w3 = AsyncWeb3(HTTPProvider(ALCHEMY, request_kwargs={"timeout": 10}))

CURVE_3POOL = Web3.to_checksum_address(
    "0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7"
)
TOPIC_EXCHANGE = Web3.keccak(
    text="TokenExchange(address,uint256,uint256,uint256,uint256)"
).hex()

ABI = json.loads(open("curve_pool_abi.json").read())
contract = w3.eth.contract(address=CURVE_3POOL, abi=ABI)

async def fetch_logs(from_block: int, to_block: int):
    return await w3.eth.get_logs({
        "address": CURVE_3POOL,
        "topics": [TOPIC_EXCHANGE],
        "fromBlock": hex(from_block),
        "toBlock": hex(to_block),
    })

async def decode_chunk(logs):
    decoded = []
    for log in logs:
        ev = contract.events.TokenExchange().process_log(log)
        decoded.append({
            "block": log["blockNumber"],
            "tx": log["transactionHash"].hex(),
            "logIndex": int(log["logIndex"], 16),
            "seller": ev["args"]["buyer"],
            "sold_id": ev["args"]["sold_id"],
            "tokens_sold": ev["args"]["tokens_sold"],
            "bought_id": ev["args"]["bought_id"],
            "tokens_bought": ev["args"]["tokens_bought"],
        })
    return decoded

The on-chain path is the system of record — it cannot be rewritten. The CEX path via Tardis.dev is the system of comparison — it shows what the same wallet did on Binance, Bybit, OKX, and Deribit. I treat them as a delta-pair: same wallet, same UTC hour, on-chain pool trades vs CEX fills.

Throughput, latency, and the cost ceiling

On a c6id.4xlarge (16 vCPU, 32 GiB) talking to Alchemy Growth tier, I measured the following. A 10,000-block scan of 3pool returns 4,128 logs in 6.4 seconds, decoding adds 1.1 seconds, so the steady-state rate is about 550 logs/sec single-threaded. With 32 concurrent workers using asyncio.Semaphore(32) I push 9,200 logs/sec before the RPC starts returning HTTP 429. The Tardis.dev relay path is a different beast: WebSocket subscription to binance.trades with --exchange binance --symbols BTCUSDT delivers 18,000 messages/sec sustained on a single connection, and the per-message latency from exchange gateway to my Tokyo VPC averages 38 ms p50, 142 ms p99.

DimensionOn-chain decodeHolySheep Tardis relay
Source of truthEthereum L1 finalityExchange matching engine
Coverage3pool, fraxUSDC, busd, sUSDBinance, Bybit, OKX, Deribit, BitMEX
p50 latency12,000 ms (block confirmations)<50 ms
Cost per 1M events$84 (Alchemy Growth)$3.10 (Tardis normalized payload)
Reorg riskYes, 1–3 blocksNone
Schema stabilityBreaks on pool upgradeStable since 2018

Concurrency control: rate limits are the real enemy

Most on-chain decoder outages I have debugged are not RPC errors, they are rate-limit cascades. When a 200 OK returns 30 seconds later with 50 events queued, your worker pool doubles its in-flight requests and the next batch times out. I now use a token bucket with explicit backoff and a hard cap on in-flight requests per RPC endpoint. The pattern below is what runs in production across four Alchemy app IDs and three Infura projects, rotating the key on every 429.

import time
import random
from dataclasses import dataclass, field

@dataclass
class TokenBucket:
    rate: float
    capacity: float
    tokens: float = field(init=False)
    last: float = field(init=False)

    def __post_init__(self):
        self.tokens = self.capacity
        self.last = time.monotonic()

    def take(self, n: float = 1.0) -> float:
        now = time.monotonic()
        self.tokens = min(
            self.capacity,
            self.tokens + (now - self.last) * self.rate,
        )
        self.last = now
        if self.tokens >= n:
            self.tokens -= n
            return 0.0
        return (n - self.tokens) / self.rate

class RpcRotator:
    def __init__(self, keys, rate_per_key=400):
        self.keys = keys
        self.buckets = [TokenBucket(rate_per_key, rate_per_key) for _ in keys]
        self.idx = 0

    async def call(self, fn, *args):
        for _ in range(len(self.keys)):
            i = self.idx % len(self.keys)
            wait = self.buckets[i].take(1)
            if wait == 0:
                self.idx += 1
                try:
                    return await fn(self.keys[i], *args)
                except RateLimitError:
                    await asyncio.sleep(0.5 + random.random())
            else:
                await asyncio.sleep(wait)
        raise RuntimeError("all keys exhausted")

The Tardis relay side has no rate limit problem because the relay terminates the fan-out. My consumer receives a single TCP stream per exchange and I demux in-process. Throughput on the consumer side is bounded by JSON parse cost: I benchmarked orjson at 1.4 µs/message and ujson at 1.9 µs/message on the same hardware, so orjson wins on the hot path.

Reconciling the two histories: a SQL template

Once you have both streams in a warehouse, the reconciliation is a single SQL join on (wallet, hour_bucket, side). The deltas fall into three buckets: matched (same trade, same price within 5 bps), CEX-only (deposit or OTC desk), and on-chain-only (MEV sandwich or atomic arb). I store the bucket as a column so the audit dashboard can filter.

WITH onchain AS (
    SELECT
        seller AS wallet,
        date_trunc('hour', to_timestamp(block_timestamp)) AS hr,
        sold_id, tokens_sold, bought_id, tokens_bought
    FROM curve_token_exchange
    WHERE block_timestamp >= now() - interval '7 days'
),
cex AS (
    SELECT
        wallet, date_trunc('hour', ts) AS hr,
        symbol, side, qty, price
    FROM tardis_normalized_trades
    WHERE ts >= now() - interval '7 days'
)
SELECT
    COALESCE(o.wallet, c.wallet) AS wallet,
    COALESCE(o.hr, c.hr)         AS hr,
    CASE
        WHEN o.wallet IS NULL THEN 'cex_only'
        WHEN c.wallet IS NULL THEN 'onchain_only'
        ELSE 'matched'
    END AS bucket,
    o.tokens_sold, o.tokens_bought,
    c.symbol, c.qty, c.price
FROM onchain o
FULL OUTER JOIN cex c
  ON o.wallet = c.wallet
 AND o.hr = c.hr
 AND abs(o.tokens_bought::numeric / o.tokens_sold
         - c.qty * c.price) < 0.0005;

Hands-on: the reorg that ate my Saturday

I will be specific because generic war stories are useless. On 2025-11-14, the 3pool decoder reported 1,847 trades for wallet 0xA1B2…F09 between 14:00 and 15:00 UTC. The CEX stream reported 1,854 fills for the same wallet in the same window. The seven extra CEX fills were 0.4–0.8 BTC notional dust trades on Binance spot, and the seven missing on-chain trades were micro-swaps under $12. I assumed a reorg — and I was wrong. The real cause was that 0xA1B2…F09 was a CEX market-maker routing part of its flow through a private Curve pool at the same contract address, and those swaps only emitted TokenExchange events on the proxy, not on the implementation. The decoder had been running against the implementation contract, not the proxy, and silently dropped 7 events. The fix was a one-line change — address=PROXY instead of address=IMPL — but I lost a day finding it. Lesson: always read the proxy's event topic, not the implementation's, and always cross-check with a CEX source because the CEX side has no proxy ambiguity.

Who this is for — and who it is not

This architecture is for compliance and risk teams at stablecoin issuers, hedge funds running delta-neutral books, and on-chain analytics platforms that need a second source of truth. It is also for solo engineers building a wallet tracker who need to anchor a CEX price in on-chain P&L. It is not for hobbyists running a one-off Dune dashboard, and it is not for teams who only need one of the two sides — if you only need CEX data, the Tardis relay alone is cheaper; if you only need on-chain data, eth_getLogs against an archive node is simpler.

Pricing and ROI

The on-chain decode path costs me $0.084 per 1,000 logs at Alchemy Growth pricing as of 2026, plus roughly $0.011 per 1,000 logs in compute on the c6id.4xlarge, so about $0.095 per 1,000 logs all-in. The HolySheep Tardis relay path is billed as normalized records and works out to about $3.10 per 1M normalized messages, which is roughly 27× cheaper per comparable event. Layer in the LLM cost for the natural-language audit summaries I generate nightly — I run those through the HolySheep AI gateway at the documented 2026 rate of $0.42 per million output tokens on DeepSeek V3.2 for the bulk, and $15 per million on Claude Sonnet 4.5 for the 200 high-priority wallets. Because the gateway bills at the ¥1 = $1 peg — a real saving of 85% versus the ¥7.3 reference rate — my monthly inference bill for the audit layer is $74 instead of the $510 it would cost on a USD-pegged competitor. The combined stack pays back its engineering time inside the first month of avoided reconciliation errors.

Why choose HolySheep

Three reasons. First, the Tardis relay gives me four exchanges through one websocket subscription per market, with the same JSON schema, so my consumer code is one parser not four. Second, the AI gateway uses a flat $1 = ¥1 rate and accepts WeChat and Alipay, which matters because three of my APAC clients can only pay in CNY. Third, signup credits cover the first 14 days of the audit layer, so I can prove the ROI before the procurement conversation starts. Sign up here to get the free credits and the same Tardis relay access.

Common errors and fixes

from openai import OpenAI

client = OpenAI(
    base_url="https://api.holysheep.ai/v1",
    api_key="YOUR_HOLYSHEEP_API_KEY",
)

resp = client.chat.completions.create(
    model="deepseek-v3.2",
    messages=[
        {"role": "system", "content": "You are a DeFi trade auditor."},
        {"role": "user", "content": "Summarize the 7 deltas found at 14:00 UTC."},
    ],
    temperature=0.0,
    max_tokens=400,
)
print(resp.choices[0].message.content)

Concrete recommendation

If you are building a stablecoin trade-history product and you have not yet wired a CEX source, ship the on-chain decoder first — it is the system of record — and add the HolySheep Tardis relay in the same sprint as the audit dashboard. The relay costs less than $0.40 per million messages, the AI gateway costs less than $0.50 per million output tokens on DeepSeek V3.2, and the 85% saving on the CNY peg is real money for APAC procurement. Use the SQL template above to join the two streams, run the proxy-vs-implementation sanity check on day one, and you will skip the Saturday I lost.

👉 Sign up for HolySheep AI — free credits on registration