Home / Solutions / Risk & Monitoring
Risk & Monitoring

Position, collateral, and liquidation data for risk engines

A lending protocol writes the record a risk engine runs on: every supply, borrow, repay, withdrawal, and liquidation, block by block. SQD serves that record as decoded logs, with traces and state diffs alongside, from genesis to head across 225+ networks. You compute the health factors and the scores; SQD provides the inputs.

225+ Networks
Genesis Full history
State diffs Storage writes
NDJSON Streaming
The problem

A risk engine reads its inputs straight off the chain and computes its own scores

Every position a lending protocol tracks is written to its log stream: each supply, borrow, repay, and withdrawal, and the liquidation that closes a failing one, block by block. Feeding a risk model the naive way means paginating that history over eth_getLogs against per-provider block-range and result-size caps, standing up an archive node for blocks old enough to backtest against, then decoding each event's data words and per-token decimals before a single health factor is computed. It is all onchain already; the cost is not in the model but in reading the record back cleanly, per block.

Anatomy of a liquidation

A liquidation is a log line. A risk engine needs every field of it.

A risk engine does not consume a vendor's risk score; it consumes those raw fields, per block, and computes its own.

Here is a real one, an Aave v3 LiquidationCall from Ethereum block 25,488,542 (2026-07-08): a keeper repaid 774.53 USDT of a failing position's debt and seized 0.4697 WETH of its collateral. The three indexed topics identify the assets and the borrower; the four data words carry the amounts, the keeper, and the payout mode:

FieldWhereDecoded
  • collateralAssettopics[1]WETH
  • debtAssettopics[2]USDT
  • user (the borrower)topics[3]0xe1a5…058f
  • debtToCoverdata, word 0774528437 = 774.528437 USDT
  • liquidatedCollateralAmountdata, word 1469677438827459135 = 0.469677438827459135 WETH
  • liquidatordata, word 20xd340…4c55
  • receiveATokendata, word 3false

Real, from transaction 0x6241…a8ea at block 25,488,542 (2026-07-08 14:47:35 UTC), emitted by the Aave v3 Pool 0x8787…A4E2. Field layout from the Aave v3 IPool interface.

One gotcha: the liquidator and both amounts ride in data, not in topics. You can filter by collateral asset, debt asset, or borrower, because those are indexed, but finding every liquidation executed by a given keeper means decoding data on your side. And each amount is a raw integer in its own token's decimals: here the debt is 6-decimal USDT and the collateral is 18-decimal WETH, a 10^12 gap that a single-decimals assumption misreads completely.

The topic0 is keccak256 of the event signature, LiquidationCall(address,address,address,uint256,uint256,address,bool). The position events ride the same stream under their own signatures: Supply, Borrow, Repay, Withdraw, and ReserveDataUpdated with the pool's updated rates and indices. Another Aave v3 deployment is the same query with the dataset name and pool address swapped.

The subscription is two filter lines: the pool's address and the event's topic0. Pinned to one block, it returns exactly the row below; pointed at the chain head, it is the live feed your alerting runs on:

your terminal
curl -s -X POST https://portal.sqd.dev/datasets/ethereum-mainnet/stream \
-H 'content-type: application/json' \
-d '{
"type": "evm",
"fromBlock": 25488542, "toBlock": 25488542,
"logs": [{
"address": ["0x87870bca3f3fd6335c3f4ce8392d69350b4fa4e2"],
"topic0": ["0xe413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286"]
}],
"fields": { "block": { "number": true, "timestamp": true }, "log": { "topics": true, "data": true, "transactionHash": true, "logIndex": true } }
}'
the returned row · verbatim
{
"header": { "number": 25488542, "timestamp": 1783522055 },
"logs": [{
"logIndex": 34,
"transactionHash": "0x62416631c26baf463a5acd676b9e6c5ef2b5ad50afc8ccc73bc4071c5ac9a8ea",
"data": "0x000000000000000000000000000000000000000000000000000000002e2a5db50000000000000000000000000000000000000000000000000684a1246453723f000000000000000000000000d340538f4f7fc5c13b810644cbdf4ca9b7734c550000000000000000000000000000000000000000000000000000000000000000",
"topics": [
"0xe413a321e8681d831f4dbccbca790d2952b56f977908e45be37335533e005286",
"0x000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
"0x000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7",
"0x000000000000000000000000e1a51470845b1ed50d4a14758ddc778fa1ce058f"
]
}]
}
Backtest depth

Replay a real stress window, not a sample of it

History runs to genesis through the same query as live data, so a risk model can be tested against every past liquidation rather than a curated sample. Pin a stress window: the 48 hours from 2025-10-10 00:00 UTC, resolved by timestamp to Ethereum blocks 23,543,616 through 23,557,920. The query above, with only the block range changed, returns 902 LiquidationCall events from the Aave v3 Pool:

Collateral assetLiquidations
  • WETH268
  • LINK145
  • UNI102
  • AAVE96
  • WBTC67
  • 22 other collateral assets224
Collateral seized across 902 LiquidationCall events, Aave v3 Pool, Ethereum blocks 23,543,616-23,557,920 (2025-10-10 00:00 to 2025-10-12 00:00 UTC). USDC or USDT was the debt asset in 719 of the 902.

One gotcha: a single POST does not return the whole window. The stream ends at a dataset chunk boundary (on the order of a thousand blocks here), not at your toBlock; the 14,305-block window above came back in 13 sequential responses, and the client resumes each one from the last block header received plus one. By default the stream also returns only blocks with matching data, plus sparse boundary headers, so the quiet stretches between liquidations cost almost nothing to read. The same resume loop, pointed at the chain head, is the alerting feed.

Running the same backfill against an RPC node means eth_getLogs, and eth_getLogs is capped: public endpoints enforce block-range and result-size limits that turn a year of history into a pagination loop over provider-specific windows, and blocks this old need an archive node behind the endpoint. The caps are measured, verbatim, in the eth_getLogs guide. On the Portal the filter is the whole integration: a dataset name, a pool address, a topic0, on any of 225+ networks.

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
}
},
"transactions": [
{
"to": [
"0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2"
]
}
]
}'
Ethereumethereum-mainnet/stream
Transactions supplying assets to Aave V3
Capabilities

Built for Risk & Monitoring at scale

Liquidation event streams

Filter any lending pool by contract address and event signature and stream every LiquidationCall with borrower, collateral asset, debt asset, and both amounts, decoded against the protocol ABI. The same filter shape reads Aave v3, Compound, Morpho, and any EVM lending fork.

Position events, block by block

Supply, Borrow, Repay, and Withdraw events carry every position change; ReserveDataUpdated carries the pool's updated rates and indices with each interaction. Fold them into per-borrower positions in your own store: the Portal serves the events, not a reconstructed balance.

Traces and state diffs, beyond logs

A liquidation is one transaction with a tree of internal calls underneath: the repayment, the collateral seizure, the flash loan a keeper wrapped around both. Traces expose that call tree; state diffs expose the storage writes that emit no event at all.

Backfill and alerting, one query shape

The same filter that replays years of liquidation history for a backtest streams new blocks at the head as NDJSON over plain HTTP. There is no separate historical and real-time integration; the block range is the only thing that changes.

Why SQD

What you would build yourself

The liquidations, position events, traces, and state diffs are all onchain already; the work a provider saves is in reading them back cleanly, per block, across every protocol and chain.

  • Historical backfill
    Yourself eth_getLogs is capped: block-range and result-size limits turn a year of history into a pagination loop over provider-specific windows, and blocks that old need an archive node behind the endpoint.
    With SQD Full history runs from genesis to head through one query shape, with only the block range changed.
  • Backfill and alerting
    Yourself A separate historical pipeline and a separate real-time feed, each integrated and maintained on its own.
    With SQD The same filter that replays years of liquidations resumes at the chain head as the alerting feed; only the block range changes.
  • Protocol and chain coverage
    Yourself Each lending protocol and each chain is its own indexer and ABI wiring.
    With SQD The same address-plus-topic0 filter reads Aave v3, Compound, Morpho, and any EVM lending fork, with the dataset name and pool address swapped across 225+ networks.
  • Beyond logs
    Yourself Logs alone leave the internal calls and the storage writes that emit no event invisible.
    With SQD Traces expose a liquidation's internal call tree and state diffs expose the storage writes that emit no event at all.

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

  • Liquidation alerting from decoded event streams
  • Health-factor engines fed by per-block position events
  • Risk model backtests against full liquidation history
  • Collateral and exposure dashboards across lending markets
  • Liquidation keeper bots on event and trace streams
  • Protocol risk reporting with block-level evidence

Frequently asked questions

How do I get liquidation events from a lending protocol?
Filter the log stream by the pool contract address and the LiquidationCall topic0, then decode against the protocol ABI. On Aave v3 the collateral asset, debt asset, and borrower are indexed topics; the liquidator and both amounts are ABI-encoded in the data field. The same address-plus-topic0 filter shape works for any EVM lending protocol on any of the 225+ supported networks; only the address and the ABI change.
Does SQD compute health factors or liquidation risk scores?
No. SQD serves the raw inputs: decoded events, transactions, traces, and state diffs. It does not return reconstructed contract state, so there is no health-factor field and no balance-at-block endpoint. Your engine folds position events into balances, applies prices and liquidation thresholds, and owns the scoring. That separation is deliberate: risk models differ, the inputs do not.
How much liquidation history is available for backtesting?
Full history from each chain's genesis, through the same query as live data. A pinned 48-hour window from October 2025 deterministically returns 902 LiquidationCall events from the Aave v3 Pool on Ethereum, which is what makes a backtest reproducible. See the eth_getLogs guide for why the same pull is painful against a raw RPC endpoint.
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.