Financial reporting
Financial reporting components — cost, EBITDA, energy balance, hash balance, subsidy fee, and revenue chart views
@tetherto/mdk-react-devkit/foundation
Financial reporting components cover cost summary, EBITDA, energy balance, hash balance, subsidy fee, and revenue views. Most are self-contained reporting page composites — supply your API data and a <TimeframeControls> period selector; the component handles layout, charts, and metric tiles. RevenueChart is a standalone stacked-bar chart you can drop into a multi-site financial view.
For operational reporting see Operational. For shared controls see the Reporting tool overview.
Prerequisites
- Complete the @tetherto/mdk-react-devkit installation and add the dependency
<MdkProvider>from@tetherto/mdk-react-adapter
Components
| Component | Description |
|---|---|
Cost | Cost summary reporting page with chart and metric tiles |
Ebitda | EBITDA reporting view with charts and metrics |
EnergyBalance | Energy balance reporting view with cost and revenue tabs |
HashBalance | Hash balance reporting view |
SubsidyFee | Subsidy fee bar chart with single-stat metric |
RevenueChart | Monthly revenue per site as a stacked bar chart |
TimeframeControls
Year/month preset selector with custom date range override, shared across financial and operational reporting surfaces.
The shared period selector for the reporting composites on this page — pass it into each composite's controls slot (controls, datePicker, or timeframeControls depending on the component). It drives year, month, and week granularity and is also used by the operational surfaces.
Import
import { TimeframeControls } from '@tetherto/mdk-react-devkit/foundation'
import type { TimeframeControlsDateRange, TimeframeControlsOnRangeChange } from '@tetherto/mdk-react-devkit/foundation'Props
All props are optional.
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
dateRange | Optional | TimeframeControlsDateRange | — | Currently selected date range driving the connected selects |
timeframeType | Optional | TimeframeTypeValue | null | null | Active granularity (year / month / week) |
onRangeChange | Optional | TimeframeControlsOnRangeChange | — | Fires when the selected date range changes |
onTimeframeTypeChange | Optional | (type: TimeframeTypeValue) => void | — | Fires when the granularity changes |
isMonthSelectVisible | Optional | boolean | — | Toggles visibility of the month select |
isWeekSelectVisible | Optional | boolean | — | Toggles visibility of the week select |
layout | Optional | 'horizontal' | 'stacked' | — | Arrangement of the connected selects |
showResetButton | Optional | boolean | — | Renders a reset button |
onReset | Optional | VoidFunction | — | Fires when the reset button is clicked |
hint | Optional | string | — | Helper text shown alongside the selector |
Basic usage
Pair it with the useTimeframeControls state machine, then pass it into a composite's controls slot:
import { Cost, TimeframeControls } from '@tetherto/mdk-react-devkit/foundation'
import { useTimeframeControls } from '@tetherto/mdk-react-devkit/foundation'
const { dateRange, timeframeType, onRangeChange, onTimeframeTypeChange } = useTimeframeControls({ /* … */ })
<Cost
controls={
<TimeframeControls
dateRange={dateRange}
timeframeType={timeframeType}
onRangeChange={onRangeChange}
onTimeframeTypeChange={onTimeframeTypeChange}
/>
}
/>Behaviour
Renders connected year / month / week selects. Hide the month or week select via isMonthSelectVisible / isWeekSelectVisible, switch between inline and stacked arrangement with layout, and optionally surface a reset button (showResetButton + onReset). The selection state itself is owned by useTimeframeControls.
Cost
Single-site cost summary composite: page header, period selector slot, and a 2×2 grid of cost charts and metric tiles.
Import
import { Cost, buildCostSummaryViewModel } from '@tetherto/mdk-react-devkit/foundation'
import type { CostProps, CostSummaryResponse, FinancialDateRange } from '@tetherto/mdk-react-devkit/foundation'Props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
metrics | Required | CostSummaryDisplayMetrics | null | none | Derived metric tiles from buildCostSummaryViewModel |
costLog | Required | ReadonlyArray<CostTimeSeriesEntry> | none | Monthly cost time-series for the bar charts |
btcPriceLog | Required | ReadonlyArray<BtcPriceTimeSeriesEntry> | none | BTC price overlay series |
totals | Required | CostSummaryMonetaryTotals | null | none | Aggregate totals for the summary tile |
dateRange | Required | FinancialDateRange | null | none | Active period — drives chart x-axis |
avgAllInCostData | Optional | ReadonlyArray<AvgAllInCostDataPoint> | none | Revenue vs cost series for the Avg All-in Cost panel |
controls | Required | ReactElement | none | Period selector slot — pass <TimeframeControls> |
setCostAction | Optional | ReactElement | none | Optional header action slot (e.g. a "Set Monthly Cost" link) |
isLoading | Optional | boolean | false | Show a loading state |
error | Optional | unknown | none | Show an error state when defined |
Data preparation
Use buildCostSummaryViewModel to transform a raw API response into the props the component expects:
import { buildCostSummaryViewModel } from '@tetherto/mdk-react-devkit/foundation'
import type { CostSummaryResponse } from '@tetherto/mdk-react-devkit/foundation'
const viewModel = buildCostSummaryViewModel({ data: apiResponse })
// → { metrics, costLog, btcPriceLog, totals }FinancialDateRange type
type FinancialDateRange = {
start: number // epoch ms
end: number // epoch ms
period?: 'daily' | 'weekly' | 'monthly' | 'yearly'
}Basic usage
import { useState, useMemo } from 'react'
import {
Cost,
buildCostSummaryViewModel,
TimeframeControls,
PERIOD,
} from '@tetherto/mdk-react-devkit/foundation'
import type { FinancialDateRange } from '@tetherto/mdk-react-devkit/foundation'
import { startOfMonth, endOfMonth } from 'date-fns'
export const CostPage = ({ apiResponse }: { apiResponse: CostSummaryResponse }) => {
const [dateRange, setDateRange] = useState<FinancialDateRange>({
start: startOfMonth(new Date()).getTime(),
end: endOfMonth(new Date()).getTime(),
period: PERIOD.MONTHLY,
})
const viewModel = useMemo(
() => buildCostSummaryViewModel({ data: apiResponse }),
[apiResponse],
)
return (
<Cost
{...viewModel}
dateRange={dateRange}
controls={
<TimeframeControls
isMonthSelectVisible
isWeekSelectVisible={false}
dateRange={{ start: dateRange.start, end: dateRange.end }}
onRangeChange={(range, opts) =>
setDateRange({ start: range[0].getTime(), end: range[1].getTime(), period: opts.period })
}
/>
}
/>
)
}Behaviour
Renders a "Cost Summary" page header, the controls slot (period selector), and the CostContent 2×2 grid of charts and metric tiles (production cost chart, operations energy chart, avg all-in cost chart, and cost metric cards).
Styling
.mdk-cost: Root element.mdk-cost__header: Title and optional action row.mdk-cost__page-title: "Cost Summary" heading.mdk-cost__controls: Controls slot wrapper
Ebitda
Renders EBITDA charts and metric tiles from a display-metrics view model. Accepts bar chart data for revenue, cost, and EBITDA series.
Import
import { Ebitda } from '@tetherto/mdk-react-devkit/foundation'
import type { EbitdaProps } from '@tetherto/mdk-react-devkit/foundation'Props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
metrics | Required | CostSummaryDisplayMetrics | null | none | Derived metric tiles from buildCostSummaryViewModel |
ebitdaChartInput | Required | ToBarChartDataInput | null | none | EBITDA bar chart series |
btcProducedChartInput | Required | ToBarChartDataInput | null | none | BTC produced bar chart series |
hasBtcProducedAllZeros | Required | boolean | none | Hides the BTC produced series when all values are zero |
showEbitdaBarChart | Required | boolean | none | Toggle the EBITDA bar chart panel |
currentBTCPrice | Required | number | none | Live BTC price used in metric calculations |
datePicker | Required | ReactElement | none | Period selector slot |
hasDateSelection | Required | boolean | none | When false shows a "select a period" hint instead of empty data |
isLoading | Optional | boolean | false | Show a loading state |
errors | Optional | string[] | [] | Error messages displayed as an alert |
setCostHref | Optional | string | none | When provided, shows a "Set Monthly Cost" gear link in the header |
Basic usage
import { Ebitda } from '@tetherto/mdk-react-devkit/foundation'
<Ebitda
metrics={ebitdaMetrics}
ebitdaChartInput={ebitdaChartData}
btcProducedChartInput={btcProducedData}
hasBtcProducedAllZeros={false}
showEbitdaBarChart={true}
currentBTCPrice={65000}
datePicker={<TimeframeControls {...timeframeProps} />}
hasDateSelection={true}
/>Behaviour
Renders an "EBITDA" page header, period selector slot, and (when hasDateSelection is true and data is available) the EbitdaMetrics card grid and EbitdaCharts panels. Shows a "please select a period" hint when hasDateSelection is false. A full-page spinner appears while isLoading is true.
Styling
.mdk-ebitda: Root element.mdk-ebitda__header: Title and optional action row.mdk-ebitda__page-title: "EBITDA" heading.mdk-ebitda__header-actions: "Set Monthly Cost" link wrapper.mdk-ebitda__controls: Date picker slot.mdk-ebitda__loading: Spinner overlay.mdk-ebitda__error: Error / empty state container
EnergyBalance
Tabbed reporting surface showing energy cost and energy revenue balance with bar charts and period-level metric tiles.
Import
import { EnergyBalance } from '@tetherto/mdk-react-devkit/foundation'
import type { EnergyBalanceProps, EnergyBalanceViewModel } from '@tetherto/mdk-react-devkit/foundation'Props
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
viewModel | Required | EnergyBalanceViewModel | none | Full view-model from useEnergyBalance hook |
onTabChange | Required | function | none | Called when the Revenue/Cost tab changes |
onRevenueDisplayModeChange | Required | function | none | Called when the revenue chart display mode toggles |
onCostDisplayModeChange | Required | function | none | Called when the cost chart display mode toggles |
timeframeControls | Optional | ReactNode | none | Period selector slot rendered above the tabs |
setCostHref | Optional | string | none | When provided, shows a "Set Monthly Cost" gear link in the header |
isDemoMode | Optional | boolean | false | Suppresses error alerts (for demo/storybook use) |
EnergyBalanceTab type
type EnergyBalanceTab = 'revenue' | 'cost'Basic usage
import { EnergyBalance, TimeframeControls } from '@tetherto/mdk-react-devkit/foundation'
import { useEnergyBalance } from '@tetherto/mdk-react-devkit/foundation'
export const EnergyBalancePage = () => {
const viewModel = useEnergyBalance({ data: apiData, dateRange })
return (
<EnergyBalance
viewModel={viewModel}
onTabChange={(tab) => setActiveTab(tab)}
onRevenueDisplayModeChange={(mode) => setRevenueMode(mode)}
onCostDisplayModeChange={(mode) => setCostMode(mode)}
timeframeControls={<TimeframeControls {...timeframeProps} />}
/>
)
}Behaviour
Renders an "Energy Balance" page header, optional timeframe controls, and a two-tab layout:
- Energy Revenue — revenue metrics cards, revenue bar chart, downtime chart, power chart
- Energy Cost — cost metrics cards, cost bar chart, power chart
Empty and no-selection states are handled internally. A loading spinner overlays the view while data fetches.
Styling
.mdk-energy-balance: Root element.mdk-energy-balance__header: Title and optional action row.mdk-energy-balance__controls: Timeframe controls slot.mdk-energy-balance__tabs: Tab container.mdk-energy-balance__tabs-list: Tab switcher row.mdk-energy-balance__tabs-trigger: Individual tab button.mdk-energy-balance__tabs-content: Tab panel content area.mdk-energy-balance__loading: Spinner overlay.mdk-energy-balance__error: Error / empty state
HashBalance
Reporting composite for hash-rate balance across the pool, showing contribution, theoretical, and accepted hash metrics.
Import
import { HashBalance } from '@tetherto/mdk-react-devkit/foundation'
import type { HashBalanceProps, HashRevenueResponse } from '@tetherto/mdk-react-devkit/foundation'Props
All props are optional (the component renders an empty/loading state when omitted).
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
data | Optional | HashRevenueResponse | null | — | Hash revenue API response |
isLoading | Optional | boolean | false | Show a loading state |
isError | Optional | boolean | false | Show an error alert |
errorMessage | Optional | string | 'Error loading hash balance data…' | Override error message text |
initialDateRange | Optional | FinancialDateRange | current month | Starting period for the integrated timeframe controls |
onDateRangeChange | Optional | function | — | Fires when the user changes the period |
className | Optional | string | — | Root class override |
tabsClassName | Optional | string | — | Class on the tab container |
tabsListClassName | Optional | string | — | Class on the tab list |
Basic usage
import { HashBalance } from '@tetherto/mdk-react-devkit/foundation'
<HashBalance
data={hashRevenueData}
isLoading={isLoading}
onDateRangeChange={(range, query) => fetchHashBalance(query)}
/>Behaviour
Renders integrated TimeframeControls (year/month/week) with a reset button, and a two-tab layout:
- Hash Revenue — site hash revenue, network hashrate, and network hashprice charts with metric cards; includes a currency toggle (USD / BTC)
- Hash Cost — hash cost vs revenue comparison charts
Date range is managed internally unless initialDateRange is supplied; onDateRangeChange fires on every period change.
Styling
.mdk-hash-balance: Root element.mdk-hash-balance__loading: Spinner overlay.mdk-hash-balance__error: Error alert.mdk-hash-balance__tabs: Tab container.mdk-hash-balance__tabs-list: Tab row.mdk-hash-balance__tabs-trigger: Individual tab button.mdk-hash-balance__tabs-content: Tab panel
SubsidyFee
Bar chart of subsidy fees over a configurable time range alongside an average-fee metric tile.
Import
import { SubsidyFee } from '@tetherto/mdk-react-devkit/foundation'
import type { SubsidyFeesResponse } from '@tetherto/mdk-react-devkit/foundation'Props
All props are optional.
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
data | Optional | HashRevenueResponse | null | — | Hash revenue API response |
log | Optional | SubsidyFeesLogEntry[] | — | Per-block log entries (alternative to data) |
isLoading | Optional | boolean | false | Show a loading state |
isError | Optional | boolean | false | Show an error alert |
errorMessage | Optional | string | 'Error loading hash balance data…' | Override error message text |
showSummaryCards | Optional | boolean | false | Show Total Subsidy, Total Fees, and Average Fees stat cards above the charts |
onDateRangeChange | Optional | function | — | Fires when the user changes the period |
Basic usage
import { SubsidyFee } from '@tetherto/mdk-react-devkit/foundation'
<SubsidyFee
data={subsidyFeesData}
isLoading={isLoading}
showSummaryCards={true}
onDateRangeChange={(range, query) => fetchSubsidyFees(query)}
/>Behaviour
Renders integrated TimeframeControls (year/month/week) and two chart panels side by side:
- Subsidy/Fees — stacked bar chart of mining reward breakdown (subsidy vs transaction fees) with a % fee ratio overlay on the right y-axis
- Average Fees — average fees in sats/vbyte bar chart
When showSummaryCards is true, three SingleStatCard tiles (Total Subsidy, Total Fees, Average Fees) appear above the charts.
Styling
.mdk-subsidy-fee__panel: Each chart panel wrapper.mdk-subsidy-fee__panel--primary: Primary variant
RevenueChart
Stacked bar chart of monthly revenue per site. Automatically switches between BTC and Sats display based on value scale. Receives pre-fetched data as props; no internal data fetching.
Stacked bar chart showing monthly Bitcoin revenue across multiple mining sites. Values display in BTC when the per-label average exceeds 1 BTC, otherwise they are converted to Sats.
Import
import { RevenueChart } from '@tetherto/mdk-react-devkit/foundation'
import type { RevenueDataItem, SiteItem } from '@tetherto/mdk-react-devkit/foundation'Props
All props are optional (the component renders an empty/loading state when omitted).
| Prop | Status | Type | Default | Description |
|---|---|---|---|---|
data | Optional | RevenueDataItem[] | [] | Raw API response — each entry is one time period with site IDs as dynamic keys |
isLoading | Optional | boolean | false | Shows a loading spinner while data is being fetched |
siteList | Optional | (string | SiteItem)[] | [] | Resolves site IDs to display names in the legend |
legendPosition | Optional | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | Chart legend position |
legendAlign | Optional | 'start' | 'center' | 'end' | 'start' | Chart legend alignment |
Data shape
Each array element is one time bucket; site IDs are dynamic keys holding that site's revenue in BTC:
{
timeKey: 'Jan 2024', // X-axis label
period: 'monthly',
timestamp: 1704067200000,
'site-a': 0.0042, // Revenue in BTC for site-a
'site-b': 0.0031, // Revenue in BTC for site-b
}Basic usage
import { RevenueChart } from '@tetherto/mdk-react-devkit/foundation'
const siteList = [
{ id: 'site-a', name: 'Paraguay' },
{ id: 'site-b', name: 'Uruguay' },
]
<RevenueChart data={revenueData} siteList={siteList} isLoading={isLoadingRevenue} />Behaviour
Renders a stacked column chart with one series per site. Currency is auto-detected: if any per-label daily average is greater than 1 BTC the chart displays BTC (₿); otherwise values are multiplied by 1,000,000 and shown in Sats. While isLoading is true a spinner replaces the chart.
Related hooks
| Hook | Supplies |
|---|---|
useFinancialDateRange | Resolves the active financial date range consumed by reporting-section queries |
useEbitda | Transforms raw EBITDA API response into the view-model shape for <Ebitda /> |
useEnergyBalanceViewModel | Computes the full energy-balance view model (tab and display-mode state) for <EnergyBalance /> |
useHashBalance | Derives the hash-balance view model for <HashBalance /> |
useSubsidyFees | Derives subsidy-fee series for <SubsidyFee /> |