Conventions that apply everywhere

  • Hashes and addresses are complete. Never truncated, anywhere. You should
    be able to match them by eye against a log.
  • Null means unknown. Never zero, never “none”. A null price means the
    oracle family isn’t replicated; a null health factor means no debt.
  • Bars are stored unscaled; decimals apply at read.
  • Values described as USD are in oracle base units (1e8 = $1) inside the
    warehouse and converted at the edge.

The catalogue

Every dataset on the Data Downloads form is a section
below, under its warehouse table, with every column the export form offers.
The catalogue keys are raw_logs, price_bars, aave_liquidations,
aave_health, compound_absorbs, morpho_liquidations, maker_vaults and
events; the same keys name a dataset on the API (GET /datasets,
POST /datasets/{key}/requests) and in a derived citation. A test checks that
every key in the catalogue has its section here, and that every column the
form offers is documented.

Every warehouse table carries chain (eth_mainnet today) and, where more
than one deployment writes to it, protocol.

Event logs: eth_logs_wide

The raw table: every log the chain emitted, one row per log, with the
transaction it came from. This is what the derived tables are computed from,
and the dataset to export when no derived table asks your question. Filter by
event signature (topic0), by an address in any role (sender, recipient,
emitter) and by block or date range; an unbounded export is refused by the
row cap rather than attempted.

Column Meaning
block_number the block the log is in
block_timestamp the block’s timestamp, UTC
tx_hash the transaction, complete
transaction_index the transaction’s position in the block
from_address / to_address the transaction’s sender and recipient (the called contract, or the recipient of a plain transfer)
value ETH sent with the transaction, wei, unscaled
log_emitter the contract that emitted the log
topic0 the event signature hash; decoded events says how it is read
topic1 / topic2 / topic3 the indexed arguments, as 32-byte hex; empty when the event has fewer
data the non-indexed arguments, ABI-encoded hex
log_index the log’s position in the block

Addresses are stored as hex without 0x, uppercase, as ClickHouse’s hex()
writes them; the export form accepts either casing.

Refresh: continuous. The bulk ingester on the warehouse host appends blocks
as they finalise, 64 confirmations (about 13 minutes) behind the head, and
never revisits a block; the last few minutes live in the head table the
explorer reads, not in this export. Ingest lag is on
the status page.

Price bars: dfrds_price_bars

One row per pool-hour, for every pool the chain has, not only tracked
markets. The catalogue entry is checked against this table’s CREATE TABLE
on every test run, so a column named here is a column you can export.

Column Meaning
chain / architecture eth_mainnet; cpmm (Uniswap V2-style, priced from Sync reserves), clamm (V3-style, from sqrtPriceX96), stableswap (Curve plain pools: the replayed get_dy for one unit), weighted (Balancer V2: the spot price from replayed balances and registered weights), batch_auction (CoW Protocol: realised clearing prices per pair) or consolidated (one row per pair-hour: the live venues’ closes weighted by volume; see the methodology)
pool pool address, uppercase hex with no 0x, as ClickHouse’s hex() writes it. The export form accepts either casing. For consolidated rows it is the pair key PAIR_<token0>_<token1>, addresses in address order; for batch_auction rows it is <SETTLEMENT>_<token0>_<token1>, the settlement contract and the pair
hour bucket start, UTC
open / high / low / close price within the bucket, token0 per token1, unscaled. token0 and token1 are the pool’s first two tokens (stableswap coins 0 and 1, the Vault’s first two registered tokens for weighted), or the pair in address order for batch_auction
volume0 summed trade amounts in the bucket, token0 units, unscaled
trades events folded into the bar: Syncs for cpmm, Swaps for clamm and weighted, TokenExchanges for stableswap, Trades for batch_auction. A stableswap or weighted bar also carries the quotes after liquidity events, which are not trades
max_block the highest block the bar has seen; the refresh’s watermark, and the version a recompute replaces

Scaling: price × 10^(decimals1 − decimals0), volume ÷ 10^decimals0, which is
what the platform’s own charts do.

Refresh: every 15 minutes (RefreshPriceBarsJob, block-watermark increments,
new pools get their full history ten per run), with the consolidated rows
recomputed five minutes behind each refresh.

Aave health panel: dfrds_aave_health

Column Meaning
block the snapshot block
user account address
total_collateral / total_debt oracle base units (1e8 = $1)
ltv_bps / liquidation_threshold_bps value-weighted, in basis points
health_factor wad (1e18); uint256 max means no debt
collateral_assets / debt_assets how many reserves the position touches
skip_reason why the account was not computed; empty when it was
cadence monthly (the longitudinal panel, one cross-section per ~30 days of blocks) or daily (one compact row per account at the last block of a UTC day). Filter on it: a query that groups by block across both would put points a day apart next to points a month apart
day the calendar day the cross-section stands for, and the table’s partition key on a table created since P3.6. 1970-01-01 on rows written before the column existed, which are all monthly

Refresh: the monthly cross-section is checked weekly (Aave Sundays, Spark
Saturdays, 03:00 UTC) and built once the chain is ~30 days of blocks past the
last one; the daily cross-section is built every morning at 07:00 UTC for
every deployment. clickhouse:aave_health:setup creates the table, or adds
the two columns to one that predates them (its partitioning cannot be changed
in place). Never a materialised view on the ingest table. See
health factors.

Both Aave-model tables carry chain and protocol. protocol is the
deployment key (aave_v3, spark), and every query must filter on it:
the same asset is a reserve in several deployments with different token
addresses.

Aave liquidations: dfrds_aave_liquidations

Column Meaning
block / block_timestamp the block of the LiquidationCall event and its timestamp, UTC
tx_hash / log_index the transaction, complete, and the log’s position in the block
borrower / liquidator the account liquidated and the account that repaid
collateral_asset / collateral_symbol the reserve seized, by address and by the symbol on record
debt_asset / debt_symbol the reserve repaid, by address and by the symbol on record
debt_repaid / collateral_seized scaled by token decimals
debt_price / collateral_price oracle price at that block, 1e8
debt_value_usd / collateral_value_usd USD; null when unpriced
realized_bonus_pct seized value over repaid value, minus one
configured_bonus_pct governance’s bonus for that reserve at that block
receive_atoken liquidator took aTokens rather than the underlying
price_note why a price is missing

Refresh: nightly (Aave V3 03:15 UTC, Spark 03:35), resuming from the newest
stored block and stopping 64 blocks behind the head. Rows replace on
(chain, protocol, block, tx_hash, log_index), so an overlapping re-run is
harmless. Last runs are on the admin ClickHouse page.

Compound v3 absorptions: dfrds_compound_absorbs

One row per absorption event, and Comet emits two kinds: the debt it took on
and each collateral it seized. kind tells them apart, so a query that
doesn’t filter on kind double-counts.

Column Meaning
block / block_timestamp the block of the event and its timestamp, UTC
tx_hash / log_index the transaction, complete, and the log’s position in the block
kind debt (the account’s debt, absorbed by the protocol) or collateral (one asset seized)
market the Comet proxy address; base_symbol is its borrowable base asset
absorber who triggered the absorption and took the reward: not a liquidator repaying debt
borrower the account absorbed
asset the base token on a debt row, the seized token on a collateral row
amount_raw unscaled, in asset’s own decimals
usd_value Compound’s own figure from its price feeds, not ours

usd_value is the protocol’s accounting, published in the event. That is
different provenance from the Aave tables above, where the oracle is replicated
and validated: see Compound v3.

Refresh: nightly at 04:15 UTC, resuming from the newest stored block and
stopping 64 blocks behind the head.

Maker vaults: dfrds_maker_vaults

One dated cross-section per build, like the Aave health panel, so the table
is a longitudinal panel rather than a snapshot that overwrites itself.
Protocol-native units: this is the Vat’s own arithmetic, not a
translation of it into health factors.

Column Meaning
block the snapshot block
ilk / urn collateral type (ETH-A) and the vault’s address
ink locked collateral, wad. 0 when the ilk’s ink is bookkeeping rather than a balance (the PSMs), with the reason in skip_reason
art normalised debt, wad; the debt is art × rate / RAY
rate the ilk’s accumulated rate, ray
spot collateral price with the liquidation ratio already folded in, ray
collateral_usd / debt_usd ink × spot and art × rate, converted; 0 when unknown, and skip_reason says so
safety ink × spot over art × rate: 1.0 at the liquidation boundary, not the collateralisation ratio people quote. Null when it cannot be computed
skip_reason not_a_balance, sentinel_ink, no_rate or no_spot; empty when the vault was computed

Debt and collateral fail separately: art × rate is real for every ilk,
including the ones whose ink is bookkeeping, so a vault can have a debt
figure and no collateral figure. protocol is maker.

Refresh: checked weekly (Mondays 03:00 UTC), building a new cross-section
once the chain is ~30 days of blocks past the last one.

Liquidation health: aave_liquidation_healths

Column Meaning
health_factor_before the position replayed at the block before the liquidation
health_factor_after derived from the event’s own amounts; null when the debt was fully repaid
collateral_threshold_bps the seized asset’s liquidation threshold, not the position average
aave_flash_loan an Aave flash loan funded the repayment; false means “no Aave flash loan”, not “no leverage”
swaps_in_tx V2 + V3 swaps in the liquidating transaction: the collateral dump
status / reason computed, or why the position could not be replayed

Gas and priority fee are absent by design: the warehouse ingests logs, not
receipts.

Event registry: chain_events

Column Meaning
id the event’s id, stable across imports: cite it, not the title
title / category what happened, in a line, and its kind: exploit, depeg, regulation, upgrade, adoption or other
protocol / classification / technique the protocol involved, the source’s class of incident and the technique used, as the source records them; blank when it does not
occurred_on / ended_on date; ended_on set only for episodes
amount_usd / returned_usd as the source publishes them; null if unpublished
chains chain names as the source records them
tx_hashes / addresses on-chain artifacts, added by curation. Complete or refused — a truncated hash is rejected at save, because it cannot be matched against anything and looks as though it can
anchors_verified_at when those hashes were last confirmed to exist in the warehouse. Null means nobody checked, which is not the same as having nothing to check
severity 1–5; see ChainEvent::SEVERITY for the written scale
severity_basis derived (computed from loss, recovery and duration) or judged (a curator’s call). Both are scores; they are not the same claim
scored_by_id / scored_at who applied the severity, and when
reference_prices labelled levels for a chart: pre, trough, recovery, each {label, value, on}
source_name / source_url / source_ref provenance; an event cannot exist without one. DeFiLlama (weekly hacks import, ref name-date), DFRDS depeg detector (daily, ref STABLE/QUOTE@block of the first off-peg bar), Governor (on-chain) (weekly, ref 0xgovernor:id), or a curator’s name with a URL
curated true once staff have edited the event by hand; an importer refreshes uncurated fields only

Exported as the events dataset, through the app rather than the warehouse:
the columns are ChainEvent::CSV_HEADERS, hashes complete and pipe-joined.

Refresh: the hacks import weekly (Mondays 06:00 UTC), the depeg detector
daily (06:00 UTC, from the hourly bars), governance events weekly (Mondays
07:00 UTC); a curated event changes when staff change it.

Dataset runs: dataset_runs

One row per scheduled pass over a dataset, read by the admin ClickHouse page
so the newest row per dataset says when a table was last extended and
whether it worked. Postgres, so it renders during a warehouse outage.

Column Meaning
dataset_key aave_v3_liquidations, spark_liquidations, morpho_liquidations, compound_v3_absorbs, aave_reference_data, timeline_anchors
status running, succeeded, failed or skipped (the warehouse lease was held, the table is missing, or the sandbox left the work to production: nothing went wrong, nothing was done)
from_block / to_block the range this run covered; to_block < from_block means the table was already at the head
rows_written rows the extractor wrote, updated chunk by chunk so a run killed mid-way still says how far it got
error the exception, or the reason for a skip

Coverage statements: protocol_coverages

One row per run of the job that produced a tracking dataset (a panel
cross-section, a nightly extraction), written by that job from what it just
wrote. The tracking pages, the CSV header comments and the guides read the
newest row per (protocol, dataset); nothing recounts the warehouse to state
coverage.

Column Meaning
protocol the tracking slug: aave, spark, makerdao, compound-v3, morpho
dataset health (the Aave-style panel), vaults (Maker’s panel), liquidations, absorptions
unit what is counted: accounts, vaults, liquidations, absorptions
block / cadence / day the cross-section the statement describes, for a panel run; null for an extraction, which describes the whole table
total / computed units the run saw, and units it computed a number for
skipped { reason => count } in the pipeline’s own vocabulary (emode, gho_debt, unsupported_price, not_a_balance, debt:no_price, decimals_unknown, unvalued); the pages label them (unsupported_price reads adapter oracle)
computed_at when the run finished

The sentence the pages print is "#{computed} of #{total} #{unit} computed; skipped: …", and the same sentence opens every tracking CSV as a # comment
line, followed by a # run: line naming the time and the block.

Maker ilk registry: maker_ilks

Maker’s on-chain IlkRegistry, persisted: one row per ilk the registry has ever
listed since the first sync. Read by the collateral composition (which ilks
and which adapters), the vault panel (which classes hold a balance) and the
Maker page. Refreshed by the nightly reference-data job and by
rake maker:ilks:sync; rake maker:validate:ilks syncs and then fails on
drift between the chain and the fallback constants.

Column Meaning
ilk the collateral type, e.g. ETH-A; unique
name / symbol / decimals / gem the collateral token as the registry describes it
klass the registry’s class (1 standard, 2 legacy, 3 RWA, 4 D3M, 5 allocator, 6 Lite PSM, 7 staking engine); see Maker
join_adapter the adapter the composition reads flows from; null for a class that holds none
pip the price feed, for reference
registered false once the registry stops listing the ilk: it is kept with its last-known facts and reported as retired
synced_block / synced_at the head block the registry was read at, and when

Protocol coverage registry: protocol_candidates

One row per (protocol, chain), because coverage is a grid and not a list:
Aave on Ethereum and Aave on BSC are different deployments, and “what becomes
real when we ingest BSC” has to include the second one.

Column Meaning
slug / chain unique together; the slug is DeFiLlama’s, so re-import is idempotent
category Lending and CDP are the roadmap; other categories are recorded so the registry can say what is not covered, without implying it is planned
model pool, cdp, comet or isolated — the four shapes the platform can already replicate. Null means unclassified, never guessed: a wrong model makes an expensive protocol look cheap
tvl_usd that chain’s share, not the protocol total repeated on every chain
tracked_as the TrackedProtocol slug when this is already covered — how Sky reconciles to makerdao
source / fetched_at which source gave these facts and when. No chain assignment rests on memory

Populated by bin/rails protocols:discover. It is deliberately not
TrackedProtocol: that is the seven live entries, edited by deploy, driving a
public roadmap that must not drift from what is built. This is a research list
of hundreds whose TVL changes weekly and which nobody has promised.

Morpho Blue liquidations: dfrds_morpho_liquidations

One row per Liquidate event. Every amount is in that market’s own tokens:
there is no USD column and no cross-market total, because each market names
its own oracle. See Morpho Blue.

Column Meaning
block / block_timestamp the block of the Liquidate event and its timestamp, UTC
tx_hash / log_index the transaction, complete, and the log’s position in the block
liquidator / borrower who repaid and who was liquidated
market_id the market’s id; loan_token/collateral_token are its ERC-20s, loan_symbol/collateral_symbol their symbols on record
lltv liquidation LTV, 1e18 = 100%
repaid_raw / seized_raw / bad_debt_raw unscaled, in the token’s own decimals
repaid / seized / bad_debt scaled: 0 when the token’s decimals are unknown, never guessed
bad_debt the shortfall written off against this market’s suppliers, in the loan token

bad_debt_raw > 0 is the filter for “this liquidation cost the suppliers
money”. Shares (repaidShares, badDebtShares) are decoded but not stored:
they are internal accounting, not amounts.

Refresh: nightly at 03:55 UTC, resuming from the newest stored block and
stopping 64 blocks behind the head.

Second factor: on users

Column Meaning
otp_secret the TOTP seed, encrypted at rest; null when 2FA is off
pending_otp_secret a replacement authenticator’s seed while the live one still works, encrypted at rest; promoted to otp_secret only when a code from it verifies, and null otherwise
otp_required_for_login whether sign-in asks for a code: set only after a code has verified
consumed_timestep the last accepted 30-second window, so a code can’t be replayed
otp_backup_codes bcrypt digests of the recovery codes, never the codes
otp_enabled_at when it was switched on

A recovery code is removed from the array as it is consumed, so
otp_backup_codes.size is how many remain. See two-factor
authentication
.

Research pages: over the API

Field Meaning
tags kind:name labels, shared within the page’s space
blocks[].kind text, heading, code, callout, divider
blocks[].settings kind-specific: heading level, code language, callout tone
citations[].verification_state verified, mismatch, unresolved, unverified
public_url present only when the page is published; null otherwise, never a broken link

A mismatch means the value no longer matches what the chain says. Treat it as
a finding, not a formatting detail.

Bundle sales: bundle_sales

What a partner sold, and the split it produced. Every column is a snapshot
taken at the moment of sale
and is never recomputed, so repricing a bundle or
renegotiating a cap cannot move a historical record.

Column Meaning
bundle_name the bundle’s name at sale time, not today’s
seats seats sold; may be fewer than the bundle’s own count, priced pro rata
list_price_cents the bundle’s full price when it was sold
charged_cents what the institution was actually charged
per_seat_cents the implied rate, which a later top-up is priced against
discount_percent the partner’s margin. Never exposed to the institution
partner_share_cents what the partner earned. Never exposed to the institution
dfrds_share_cents what dfrds recognised. partner_share + dfrds_share == charged
sold_at when the sale was recorded
recognized_at when payment arrived. Null means earned-but-unpaid; earnings count recognised sales only
stripe_invoice_id the invoice the payment reconciles to, when there is one

Bundle authorisations: partner_bundles

Which bundles a partner may sell, and their cap on each.

Column Meaning
max_discount_percent the cap on this bundle
— the effective cap is the lower of this and partners.max_discount_percent, so raising one alone can never widen what a partner may do

Plans: plans

Column Meaning
price_cents the price per billing period; for a per-seat plan, the price of one seat. Null means undecided: the row is listed, not sold, until staff set it
per_seat sold with a quantity; the organization’s allowance follows the subscription item’s quantity
seat_minimum the smallest quantity checkout accepts on a per-seat plan
seat_limit the allowance of a fixed plan, and the fallback for a per-seat plan with no quantity recorded
stripe_lookup_key how the Stripe price is found; dfrds_data_monthly and dfrds_data_annual are live and fixed

organizations.purchased_seats is the capacity bought (per-seat quantity, or
bundle seats plus top-ups) and purchased_seat_cents the rate it was bought
at; both are null for an organization that bought none.

Payment state: on organizations

Column Meaning
payment_state ok, past_due (a renewal failed, inside the 14-day grace period) or grace_expired (the grace ran out: data downloads and the API are closed until a payment lands)
payment_failed_at when the first failed invoice of the episode arrived; the grace period is 14 days from here, and a retry does not move it
payment_failed_invoice_url Stripe’s hosted invoice, so every notice can link it
payment_failed_amount_cents what was due

Driven by invoice.payment_failed and invoice.paid; the notices are
billing_events rows of type local.dunning, one per step and episode.

Plan limits: mcp_calls_per_day

MCP tool calls allowed per API key per day, counted apart from
api_requests_per_hour.

   
Default 2000 (Plan::DEFAULT_LIMITS)
Window Calendar day, UTC; resets at midnight
Scope Per API key, not per organization
Refusal HTTP 429, X-RateLimit-Limit / X-RateLimit-Remaining

Deliberately generous: an agent answering one research question may make a
dozen tool calls, so a limit set by REST intuition would be far too tight. The
number should be revisited once there is a week of real usage behind it.

api_keys.mcp_call_count and api_keys.mcp_last_used_at are lifetime
counters on the key, shown on the organization page. They are counters rather
than a per-period record because MCP is not a billing dimension today; making
it one would need retained per-period rows instead.

Alerts: alerts

Non-price alerts. price_alerts remains a separate table for price
thresholds, which predate this one and were deliberately not migrated.

Column Meaning
kind liquidation or chain_event
condition jsonb, per kind. Liquidation: token, optional protocols[]. Event: optional category, chain, protocol — all absent means every event
space_id Alerts belong to a space; firing emails every member of it
status active or triggered. One-shot: it will not fire again until re-armed
triggered_payload jsonb snapshot of what actually matched, kept so the email and the record agree

Firing is idempotent: Alert#fire! returns false on an already-triggered
alert. This matters because the liquidation extractor and the event importer
both re-run over overlapping ranges by design, so “nothing fires twice” has to
be a property of the model rather than of every caller.

Derived citations: derived_citations

An aggregate over a bounded query, cited like a value.

Column Meaning
dataset / filters The query, in the export path’s own vocabulary — the same fields a DataRequest holds, read by the same DatasetCatalogue and DatasetQuery
aggregate / column count, sum, avg, min or max; the column is intersected with the dataset’s own variables
display_value The answer as it was when cited. Text, so a re-derivation compares like with like rather than through float coercion
content_hash SHA-256 of the canonical spec plus the result. Resolves publicly at /citations/:hash
verification_state unverified, verified, mismatch, unresolved — the same four value_citations uses
verification_note On a mismatch, what it re-derived as. The cited value is never rewritten: both figures are kept, because a number that moved is itself a finding

The spec is canonicalised with sorted filter keys, so the same query hashes
the same way however the filters arrived — and citing the same claim twice
returns the same citation rather than a duplicate.

Dataset citations: dataset_citations

The hash of the canonical bytes of one dataset query, resolving publicly at
/citations/:hash (HTML, .json DataCite, .bib, .cff, and .csv for
subscribers while the bytes still match).

Column Meaning
kind price_series (one pool’s canonical series; params holds pool, pair, protocol, chain, period, from, to) or dataset (any catalogue query)
spec For dataset: dataset (a catalogue key), filters (as DataRequestFilters writes them, keys sorted, blanks dropped), columns (in the catalogue’s order) and block_range ([from, to], the blocks the bytes covered, pinned at creation and applied on re-derivation)
content_hash SHA-256 of the canonical CSV: catalogue column order, natural-key row order, one number spelling, empty nulls, LF, UTF-8
row_count / as_of The rows the bytes hold and the last date they reach
named_organization_id The citing user’s organization when they chose to be named as a creator; nullified if the organization goes
verification_state unverified, verified, mismatch, unresolved: the same four value_citations uses, set by the nightly re-derivation
verification_note On a mismatch, the digest the spec produces now. The cited hash is never rewritten

The version a citation reports is the dataset_releases row it falls in
(block containment, or the last date for datasets without blocks), or
unreleased. A price_series citation is matched against the price_bars
releases.

Dataset releases: dataset_releases

One row per dataset per month, cut by BuildDatasetReleasesJob on the first
at 06:00 UTC under the warehouse lease. Refresh: monthly. The raw log table
has no releases.

Column Meaning
dataset_key A catalogue key
version YYYY.MM, unique per dataset
block_from / block_to The table’s block extent at release, for datasets with a block column
as_of The last value of the date column at release, for the rest
row_count Rows in the release
content_hash SHA-256 of the full canonical export (every column, natural-key order), streamed through the digest; the same bytes CanonicalDataset writes for the whole dataset over that block range
released_at When it was cut

Contract ABIs: contract_abis

The explorer’s ABI cache, one row per (chain, address). See
decoded events for how rows get here.

Column Meaning
address / chain lowercase hex, no 0x; ethereum
abi jsonb, the ABI as fetched (the implementation’s for a proxy). Registry rows hold events only
source static (the built-in registry), sourcify, or etherscan
verified the ABI came from a verified source; false on a miss
implementation_address set when the address is an EIP-1967 proxy: the ABI describes this contract
fetched_at when the ABI was written
searched_at / found the search log, as in contract_labels and event_signatures: a hit is never looked up again, a miss is retried after seven days

Warehouse gaps: warehouse_gaps

What the daily completeness check (CheckWarehouseCompletenessJob, 00:30
UTC, over the previous UTC day) found short in the bulk table. Listed on
the status page while open. A row is a finding, never a repair.

Column Meaning
chain eth_mainnet
block_from / block_to the range, inclusive: a 1,000-block window clipped to the day (source head) or a single sampled block (source rpc)
expected blocks with at least one log in the range according to the source
found the same count from eth_logs_wide
source head: compared against the follower’s head table where it overlapped the day; rpc: a sampled block the node’s receipts say emitted logs, absent from the bulk table
detected_at the last run that found it short; a range seen again is refreshed, not duplicated
resolved_at set by the first later run over that day that finds the range complete

Export checksums: data_requests.checksum_sha256

The SHA-256 of the delivered gzip file, taken after it was written (the
warehouse’s file is streamed back through the digest; a registry export is
hashed as it is built). Shown on the download page, sent as
X-Checksum-SHA256 with the download, carried in the export’s citation
line and in the data_export.ready webhook and data.exported audit row.