Chain intelligence
Two tables link on-chain activity to exchange order flow. chain_entities groups addresses into anonymous entities; chain_flows records transfers, attributes them to venues and links them to trades. Together with book_l4 they make up L4+ data.
Chain intelligence is included in Enterprise and is an add-on for Desk; keys need the chain scope (see Authentication). The method is explained on Chain intelligence. This page is the column reference for schema 0.9 (draft).
Concepts
- Address: one address on one chain. The same string on two chains is two addresses.
- Entity: addresses that on-chain evidence shows share an owner. Entity ids such as
E-4417are random and encode nothing about their addresses. Only institutional wallets (exchanges, protocols, bridges) carry a name, inlabel. - Flow: one transfer between two addresses, attributed to a venue when either side is a known venue wallet.
- Link: a match between a flow and a trade on a venue, with the method in
link_typeand a calibratedconfidence.
Chains
| Chain | History from | Flows published after |
|---|---|---|
bitcoin | January 2019 | 3 blocks |
ethereum | January 2019 | 12 blocks |
arbitrum | Mainnet launch, August 2021 | batch posted to Ethereum |
solana | Mainnet beta, March 2020 | finalized commitment |
hyperliquid | Mainnet launch, 2023 | 1 block |
These five chains make up Phase 1. tron is planned for Phase 2. History goes back to January 2019, or to the chain's launch if later. Clustering and flows are computed over the full history shown. Deposits and withdrawals are attributed for all twelve venues on our plan from Phase 1, including venues whose market data is planned for later phases.
chain_entities
One row per address per version. Clusters are versioned hourly: when new evidence merges two entities or moves an address, the old row is closed (valid_to set) and a new one opened. Querying with a point-in-time filter returns the clustering exactly as it stood then.
| Column | Type | Description |
|---|---|---|
entity_id | string | Anonymous id, for example E-4417. Stable across addresses and venues. |
chain | enum | bitcoin, ethereum, arbitrum, solana or hyperliquid. |
address | string | One address per row, in the chain's canonical form. EVM and Hyperliquid addresses are lowercase hex with 0x. |
entity_type | enum | exchange, protocol, bridge or unlabeled. |
label | string | Institutional label such as binance:hot. Always null for unlabeled. |
cluster_method | enum | Evidence that put the address in the entity. See Cluster methods. |
confidence | float64 | Calibrated probability that the address belongs to the entity, 0.50 to 1. |
first_seen, last_seen | int64 | First and last on-chain activity of the address, nanoseconds. |
valid_from, valid_to | int64 | Version window, nanoseconds. valid_to is null for the current version. |
merged_into | string | The surviving entity_id when this entity was merged into another. |
model_version | string | Clustering and calibration version, for example 2026.09. |
For a backtest at time T, always filter valid_from <= T AND (valid_to IS NULL OR valid_to > T). The current version includes evidence that did not exist at T.
Cluster methods
cluster_method | Chains | Evidence |
|---|---|---|
common_input | bitcoin | Inputs spent together in one transaction share an owner. |
deposit_reuse | all | Addresses that send to the same exchange deposit address belong to that deposit's owner. |
gas_funding | ethereum, arbitrum | A fresh address whose first gas comes from another address, followed by matching activity. |
venue_native | hyperliquid | The trading account is the on-chain address, so bridge deposits and trades link directly. |
chain_flows
One row per transfer. Market-data tables use venue as the partition key; chain tables use chain, and carry the block time in ts_block next to our ts_capture.
| Column | Type | Description |
|---|---|---|
flow_id | string | Stable id for the flow. |
entity_id | string | Entity the flow belongs to: the non-venue side of a deposit or withdrawal, or the sender of a transfer. |
chain, tx_hash | enum, string | Chain and transaction. |
ts_block | int64 | Block timestamp, nanoseconds. |
ts_capture | int64 | When our node received the block, on the same clock as market data. |
direction | enum | deposit, withdrawal, transfer or bridge. |
from_address, to_address | string | Sender and receiver. |
from_entity, to_entity | string | Entity ids of both sides at the time of the flow, where resolved. |
asset | string | Asset moved, for example USDC. |
amount | decimal128(38,18) | Amount in asset units. |
amount_usd | decimal128(38,18) | USD value at ts_block. |
venue | enum | Attributed exchange, or null when neither side is a known venue wallet. |
link_venue, link_symbol, link_trade_id | string | The trade this flow is linked to, where one was matched. link_trade_id joins to trades.trade_id. |
link_type | enum | address_native or timing_size. See below. |
confidence | float64 | Calibrated probability that the venue attribution and, where present, the trade link are correct. |
model_version | string | Attribution and calibration version. |
Link types
address_native: the venue publishes participant ids and the participant is the on-chain address (Hyperliquid, dYdX). The link is exact; confidence reflects only the clustering behind the entity.timing_size: a flow is matched to trades by timing and size. Each candidate is scored against every competing explanation on the public tape in the same window.
| Venue | Phase | Trade links |
|---|---|---|
| Hyperliquid | 1 | address_native |
| Binance, Bybit, OKX | 2 | timing_size |
| Coinbase, Deribit, Kraken | 3 | L4 ids on the venue, timing_size to chain |
| Bitget | 3 | timing_size |
| KuCoin | 4 | timing_size |
| Bitfinex, Bitstamp | 4 | L4 ids on the venue, timing_size to chain |
| dYdX | 4 | address_native |
Confidence and calibration
Every link and every address assignment carries a confidence between 0 and 1, and the scores are calibrated, so they can be read as probabilities: among links scored near 0.90, about 90% are correct. Links below 0.50 are not published.
The raw model output is mapped to a probability with isotonic regression, fitted on a labeled set of links whose truth is known and checked on a held-out part of that set. The labeled set has three sources: Hyperliquid links where the deposit, the account and the trades are all public; transfers and trades we make ourselves between our own accounts on covered venues; and exchange addresses that venues publish themselves, such as proof-of-reserves lists. Calibration is refit monthly. Each row records model_version, so a result can be reproduced after a refit.
| Predicted confidence | Links | Observed share correct |
|---|---|---|
| 0.50 to 0.60 | 1,842 | 0.56 |
| 0.60 to 0.70 | 2,317 | 0.64 |
| 0.70 to 0.80 | 2,905 | 0.76 |
| 0.80 to 0.90 | 3,688 | 0.85 |
| 0.90 to 0.95 | 2,410 | 0.93 |
| 0.95 to 1.00 | 6,972 | 0.98 |
As a working rule: use links at 0.95 and above for direct joins, 0.80 and above for aggregates over many flows with a sensitivity check, and lower scores only weighted by their confidence.
Update schedule
| Delivery | chain_flows | chain_entities |
|---|---|---|
WebSocket chain_flows channel | Each flow once its block reaches the depth in the chains table | Not streamed |
| REST | Queryable within 60 s of publication | New version every hour |
| S3 | Daily file per chain, before 02:00 UTC the next day | Daily file per chain with the day's hourly versions |
A trade link can be added after a flow is first published, once the matching trade is seen. Links added later appear as a new row version with a later ts_capture, so as-of queries never see a link before it existed.
Joining to order flow
With the add-on, book_l4 rows also carry entity_id on venues with L4 (see Order book depth). On Hyperliquid the participant id is the on-chain address, so the join is exact.
From an order to an entity's flows
import polars as pl
from fdc import Client
fdc = Client(api_key="fdc_live_...")
window = dict(start="2027-02-03T13:00:00Z", end="2027-02-03T14:00:00Z")
# largest attributed participant on Hyperliquid BTC-PERP fills in the hour
l4 = fdc.book(venue="hyperliquid", symbol="BTC-PERP", depth="L4", **window).to_polars()
top = (l4.filter(pl.col("event") == "fill")
.group_by("entity_id").agg(pl.col("fill_size").sum())
.sort("fill_size", descending=True)[0, "entity_id"])
# every flow of that entity, high-confidence links only
flows = fdc.chain_flows(entity_id=top, min_confidence=0.8, **window).to_polars()curl -G https://api.financedatacloud.com/v1/chain_flows \
-H "Authorization: Bearer $FDC_API_KEY" \
-d entity_id=E-4417 -d min_confidence=0.8 \
-d start=2027-02-03T13:00:00Z -d end=2027-02-03T14:00:00ZPoint-in-time join in SQL
The same join from S3 files, with the clustering as it stood at each fill. participant_id is the maker of a fill and counterparty_id the taker.
WITH fills AS (
SELECT ts_capture, counterparty_id AS taker, side, price, fill_size
FROM read_parquet('s3://fdc-archive/book_l4/venue=hyperliquid/date=2026-09-15/BTC-PERP.parquet')
WHERE event = 'fill'
),
ent AS (
SELECT address, entity_id, confidence, valid_from, valid_to
FROM read_parquet('s3://fdc-archive/chain_entities/chain=hyperliquid/date=*/entities.parquet')
)
SELECT e.entity_id,
count(*) AS fills,
sum(f.price * f.fill_size) AS taker_notional
FROM fills f
JOIN ent e
ON e.address = f.taker
AND e.valid_from <= f.ts_capture
AND (e.valid_to IS NULL OR e.valid_to > f.ts_capture)
WHERE e.confidence >= 0.95
GROUP BY e.entity_id
ORDER BY taker_notional DESC
LIMIT 20;Flows to trades on another venue
Where a flow has a matched trade, link_venue, link_symbol and link_trade_id join it to trades. Use ts_capture on both sides; it is the same clock for chain and market data, so the join reflects what was knowable at the time.
linked = flows.filter(pl.col("link_trade_id").is_not_null())
trades = fdc.trades(venue="binance", symbol="BTC-PERP", **window).to_polars()
matched = linked.join(trades, left_on="link_trade_id", right_on="trade_id", how="inner",
suffix="_trade")Binance market data is planned for Phase 2, subject to a data agreement with the exchange; until then the same code returns flows attributed to Binance with no trade links.
REST and bulk access
Parameters: entity_id, chain, venue, direction, asset, link_type, min_confidence (default 0.5), min_amount_usd, start, end. Time filters apply to ts_capture.
Parameters: address, entity_id, chain, entity_type, as_of (default now), history=true for every version, min_confidence.
Both follow the pagination, format and error conventions of the REST API. In S3, files sit under chain_flows/chain={chain}/date={YYYY-MM-DD}/flows.parquet and chain_entities/chain={chain}/date={YYYY-MM-DD}/entities.parquet; see Bulk S3.
Limitations
- Movements inside a centralized venue are invisible. A deposit shows that funds arrived; which account received them stays unknown.
timing_sizelinks are statistical. In busy markets several trades can fit a deposit; the confidence score reflects that competition, and many deposits get no link at all.- Custodial and omnibus wallets hold funds for many owners. They are labeled where known and their users are not clustered.
- Mixers and privacy pools end the trail at the pool contract.
- Bridges other than the Hyperliquid bridge are linked only where the bridge emits matching events on both chains.
- Entities, never identities. We use public blockchain and market data only, collect no names or contact details, and do not attempt to link an entity to a person. The data license prohibits using chain intelligence to identify or target individuals. Questions go to chris@financedatacloud.com.