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.
| Dimension | On-chain decode | HolySheep Tardis relay |
|---|---|---|
| Source of truth | Ethereum L1 finality | Exchange matching engine |
| Coverage | 3pool, fraxUSDC, busd, sUSD | Binance, Bybit, OKX, Deribit, BitMEX |
| p50 latency | 12,000 ms (block confirmations) | <50 ms |
| Cost per 1M events | $84 (Alchemy Growth) | $3.10 (Tardis normalized payload) |
| Reorg risk | Yes, 1–3 blocks | None |
| Schema stability | Breaks on pool upgrade | Stable 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
- Error:
web3.exceptions.LogTopicError: expected 0x-prefixed hex string, got b'\x12…'— happens when you pass the raw bytes from the log intoprocess_logafter a manual decode. Fix: normalize topics withlog["topics"][0].hex()before passing toprocess_log, and make sure the ABI has the event signature exactly as the contract emits it. - Error: HTTP 429 from
eth_getLogswith a 10,000-block range — Alchemy caps the range at 10,000 blocks on Growth and 5,000 on free. Fix: chunk the range to 4,000 blocks, use a sliding window, and rotate the app ID using theRpcRotatorpattern shown above. - Error: Tardis relay replays a fill you already ingested because the websocket disconnected and reconnected. Fix: always persist the last seen
trade_idper channel in Redis with a 24-hour TTL, and ignore any message whose id is <= the watermark. Replay the channel from--from=YYYY-MM-DDon cold start. - Error: Reconciliation shows a 0.07% delta and you cannot reproduce it. Fix: check whether the on-chain address is a proxy or implementation; switch the decoder to the proxy, and add a sanity check that
contract.address.codeat the latest block contains the event topic hash. - Error: AI gateway returns 401 with a valid key — usually the key is bound to a single base URL and you hit a typo. Fix: confirm the base URL is
https://api.holysheep.ai/v1and the key starts withhs_live_; the snippet below shows the exact client setup.
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.