Home / Solutions / Trading Bots & Automation
Trading Bots & Automation

Trading bot data: decoded swaps, executed prices, and backtest depth

Every DEX trade settles as a Swap event, and the executed price is in its amounts, fee and slippage included. SQD serves those events decoded, from genesis to chain head, across 225+ networks, with the same query shape for a backtest and a live signal. Confirmed blocks only: order signing and submission stay on your bot's own RPC path.

225+ Networks
Genesis Backtest depth
NDJSON Live streaming
Confirmed Blocks only
The problem

A backtest is only as honest as its fills, and the fills are Swap events.

The price a backtest should fill at is the price real trades actually got, and that price is in each fill's amounts. Rebuilt off an RPC node, replaying a pool's history means paginating eth_getLogs against per-call block-range and result-count caps, standing up archive access, and reading prices back off resampled candles that average the real fills away. The fills are onchain already; the cost is in how you read them back.

How it works

Decode the fill and its executed price falls out of the amounts.

Every trade a Uniswap V3 pool executes settles as a Swap event, and the price the trade actually got is in its amounts, fee and slippage included. Take the USDC/WETH 0.05% pool (0x88e6…5640): its PoolCreated log at block 12,376,729 fixes token0 as USDC and token1 as WETH, so every fill since decodes the same way.

The first fill in block 21,000,006 put 65,003.274773 USDC into the pool and took 24.641506187 WETH out. Divide the two and the executed price of that fill is 2,637.96 USDC per WETH:

FieldDecodedMeaning
  • amount0+65,003.274773 USDCinto the pool (trader sold USDC)
  • amount1-24.641506187054117683 WETHout of the pool (trader bought WETH)
  • executed price2,637.96 USDC/WETH65,003.274773 / 24.641506187
  • sqrtPriceX962,636.96 USDC/WETHpool marginal price after the fill
Decoded from the log data below. USDC has 6 decimals, WETH 18; the last two data words are in-range liquidity and the post-fill tick (197546).

The query is one filter on the pool address and the Swap topic0, pinned here to a single block so you can run it and get exactly this:

your terminal
curl -s -X POST https://portal.sqd.dev/datasets/ethereum-mainnet/stream \
-H 'content-type: application/json' \
-d '{
"type": "evm",
"fromBlock": 21000006, "toBlock": 21000006,
"logs": [{
"address": ["0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640"],
"topic0": ["0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"]
}],
"fields": {
"block": { "number": true, "timestamp": true },
"log": { "data": true, "transactionHash": true }
}
}'
first returned fill · block 21,000,006
{
"header": { "number": 21000006, "timestamp": 1729345619 },
"logs": [{
"transactionHash": "0xde878d5bb0de7a115a1140dd23aaf5c797011948271cf13e0989db0af1b7e964",
"data": "0x0000000000000000000000000000000000000000000000000000000f227f4215fffffffffffffffffffffffffffffffffffffffffffffffeaa07c8adde0c8ccd0000000000000000000000000000000000004c11afeeb66259776aae8645d5c90000000000000000000000000000000000000000000000009056af32d6badf2300000000000000000000000000000000000000000000000000000000000303aa"
}]
}

The block's line carries a second fill after this one (0xde87…e964 is the first). Two decode gotchas: amount0 and amount1 are signed int256 in two's complement, so the 0xff... word above is minus 24.64 WETH, and an unsigned parse reads it as a 78-digit number. And sqrtPriceX96 is the pool's marginal price after the fill (2,636.96 here), not the price the trade got; a backtest that fills orders at it misprices every trade.

The second fill in the same block went the other way: 1.6335 WETH sold into the pool for 4,305.286906 USDC, an executed price of 2,635.62. Two fills, one block, a 2.34 USDC gap between the buy and the sell that a single candle collapses to one number.

Backfill and resume

One filter returns the day. The same shape returns the history.

Widen the same query to 7,200 blocks (about 24 hours from 2024-10-19 13:45 UTC) and it returns every fill the pool executed in that window. Only blocks holding matching fills come back, plus a few chunk-boundary headers, not one line per scanned block:

MeasureCount
  • blocks scanned (21,000,000 to 21,007,199)7,200
  • blocks containing fills2,008
  • Swap events returned2,499
  • sequential responses to cover the range5
USDC/WETH 0.05% pool, blocks 21,000,000-21,007,199, counted from the streamed responses

Against an RPC node the same backfill is a pagination problem: eth_getLogs is capped per call, by block range or by result count, and historical ranges need archive access. At the 50-block cap some public endpoints enforce, this one day is 144 calls; the pool's full history since 2021 is the same problem repeated at scale. The caps are documented, verbatim, in the eth_getLogs limits guide.

One gotcha: a range request can end before toBlock, at HTTP 200, when the stream reaches an internal chunk boundary. The first response for this window stopped at block 21,001,566; the client reads the last line's header number and re-issues from the next block. Five sequential requests covered the full 7,200-block range, so treat the resume loop as part of the client.

The same filter pointed at chain head streams new fills as blocks confirm, so a backtest and its live signal read identical data. There is no mempool or pending-transaction view; order signing and submission stay on the bot's own RPC path.

Playground

Try the Portal API

Live queries against real blockchain data. Pick a network, choose a query, hit run.

The public endpoint is free for development. See plans for dedicated portals and higher limits.

Network+200 more
Query
curl --compressed -X POST \
'https://portal.sqd.dev/datasets/ethereum-mainnet/stream' \
-H 'Content-Type: application/json' \
-d '{
"type": "evm",
"fromBlock": 21000000,
"toBlock": 21000050,
"fields": {
"block": {"number":true,"timestamp":true},
"transaction": {
"hash": true,
"from": true,
"to": true,
"value": true,
"sighash": true
}
},
"transactions": [
{
"to": [
"0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"
]
}
]
}'
Ethereumethereum-mainnet/stream
Transactions hitting the Uniswap router
Capabilities

Built for Trading Bots & Automation at scale

Executed prices from decoded swaps

A Swap event carries the signed amounts of a real fill, so price comes from amount0 and amount1, fee and slippage included, not from a candle or an oracle quote. The same decode reads Uniswap V3 on Ethereum and any fork of it on any EVM chain; Solana DEX fills come from the instruction stream instead.

Backtest history to genesis

Every historical swap of a pool streams from one filter on its address and topic0, from the pool's creation block to head. No per-call block-range caps or result-count limits to code around, and no archive node to run; wide ranges stream in resumable chunks.

Live signals over NDJSON

The same filter pointed at chain head streams new fills as blocks confirm, over plain HTTP. A backtest and its production signal read identical data, which removes one class of train-live skew. There is no mempool or pending-transaction data; the Portal serves confirmed blocks only.

Position tracking from transfer streams

Decoded ERC-20 Transfer events filtered by your trading addresses are the position ledger: entries and exits as they confirm. Balances are aggregations you fold from that history downstream; the Portal does not serve reconstructed balance snapshots.

Why SQD

What you would build yourself

Every fill is a public Swap event. What a provider saves a bot is the reading: backfill depth, an honest executed price, and one data path shared by the backtest and the live signal.

  • Backfilling history
    Yourself eth_getLogs is capped per call by block range or result count and needs archive access; at a 50-block cap, one day of this pool is 144 calls, repeated for the full history since 2021.
    With SQD One filter takes the whole range and streams back only the blocks holding fills; five sequential requests covered a 7,200-block day.
  • Executed price
    Yourself Read price off resampled candles that average the real fills away, or off sqrtPriceX96, the pool's marginal price after the fill, not the price the trade got.
    With SQD The executed price is read from each fill's signed amounts, fee and slippage included; candles are an aggregation you build downstream.
  • Backtest vs live
    Yourself The backtest and the live bot read from different sources, so the two can diverge, one class of train-live skew.
    With SQD The same filter serves the backfill and chain head, so a backtest and its live signal read identical data.
  • Cross-DEX coverage
    Yourself Integrate each DEX and each chain the bot trades on separately.
    With SQD The same Swap decode reads Uniswap V3 and any fork of it across EVM chains; Solana DEX fills come from the instruction stream.

See this on your own contracts

Tell us the chains and contracts you care about, and we will show you the query that returns them.

Use cases

What teams build with SQD

  • Strategy backtesting on complete pool swap history
  • Live signal generation from decoded fills
  • Executed-price series for the pools you trade
  • Position and PnL tracking from transfer history
  • Cross-DEX monitoring across 225+ networks
  • Perp analytics from the Hyperliquid fills dataset

Frequently asked questions

How do I backtest a trading strategy on onchain data?
Replay the pool's historical Swap events. Each one is a real executed fill: the signed amounts give you the price the trade actually got, fee and slippage included, which resampled candles average away. The Portal streams a pool's full history from its creation block through ranged requests (one filter returned 2,499 fills for a 24-hour window on the USDC/WETH 0.05% pool), so the backtest runs on the fills themselves. OHLC series, if you want them, are an aggregation you build downstream. See the eth_getLogs limits guide for why the same backfill paginates against an RPC node.
Can I use SQD as a DEX price feed?
As a trade-price feed, yes: the Portal serves decoded Swap events from the pools you pick, and the executed price is read from each fill's amounts as blocks confirm, streamed as NDJSON over plain HTTP. It is not an oracle: no cross-venue aggregation is done for you, and the Portal does not serve a current-price field or reconstructed pool state, so slot0-style contract reads stay on an RPC node. The DeFi & Trading page covers the surrounding swap, liquidation, and trace data.
Does SQD serve mempool data or execute trades?
No to both. The Portal serves confirmed blocks only: there is no mempool or pending-transaction view, and nothing on the order-submission path. A bot signs and submits through its own RPC connection; SQD covers everything around execution: backtesting on full swap history, live signals from confirmed fills, position tracking from transfer streams, and PnL accounting. RPC vs indexed data walks through where each side belongs.
Get started

Your blockchain data infrastructure, handled.

Private Portal. Dedicated. Validated. Managed. Tell us what you're building, we'll show you what it looks like on SQD.