Ferrohive · Research

August 03, 2026Ferrohive Research

How the /fleet dashboard composes per-rig ledgers into a fleet P&L

The total at the top of /fleet is not a separate computation — it is the same per-rig aggregator, summed element-wise. Here is the slab walk, the price-at-midpoint lookup, and a worked example that lands in the dashboard's row.

The /fleet dashboard totals are not a fork of the math — they are the same per-rig aggregator, summed element-wise first across rigs in a site, then across sites in the fleet. Change the math in one place and the head-line and the export move with it. This post walks the implementation end to end and then drives a worked example through it, so the six columns in the dashboard row are not a black box but a chain of explicit calls.

What /fleet shows, in six columns

Each column maps 1:1 onto a numeric field on the per-rig aggregator output (`LedgerItem` / `LedgerTotals` in `@/lib/contracts/ledger`). The dashboard renders the FleetPnlList envelope the same way the per-rig ledger page renders the LedgerList envelope.

Dashboard columnLedgerItem fieldFormula
TH-daysthDaysΣ (hashrateThs × segment_hours) ÷ 24
Uptime (h)uptimeHoursΣ segment_hours
Est. kWhestKwhuptime × drawWatts ÷ 1000 (one slab at a time)
Energy costestEnergyCostUsdΣ (kwh × midpoint_price) over slabs
Est. revenueestRevenueUsdthDays × poolRate × BTC_USD × (1 − poolFee)
Net marginnetMarginUsdrevenue − energy cost

The slab walk

Telemetry readings are point-in-time samples, not continuous. The aggregator walks the gap between every adjacent pair of readings as one carry-forward segment, then breaks each segment into 1-hour UTC slabs for cost attribution. Each slab carries the LATER reading's hashrate (the carry-forward model — readings report the state held since the prior tick). The pre-first gap (before any reading) and the post-last gap (after the last reading) are not counted as uptime: an audit ledger cannot infer power draw for unobserved hours.

  • readings sorted by ascending timestamp
  • each adjacent pair yields one segment [ts_i, ts_{i+1}) with hashrate from the later reading
  • segments are not anchored at rangeStart or rangeEnd — those gaps are silent
  • segments are walked in 1-hour UTC slabs, clipped to [rangeStart, rangeEnd]
  • each slab's UTC-day attribution is the UTC-date key of its midpoint
  • each slab's price is read at its midpoint (see priceAtMidpoint below)

Range anchor: rangeStart = now − days × 86_400_000, rangeEnd = now. The aggregator pads one row per UTC day whose key intersects this range, so the on-screen count depends on now's time-of-day epoch — days = 7 at the top of a UTC hour yields 7 rows; mid-day it can yield 8.

priceAtMidpoint, in tz-aware detail

The mid-slab kWh figure is multiplied by the per-operator $/kWh figure through priceAtMidpoint. The window lookup converts the slab midpoint's UTC timestamp into the site's operator-tz local clock (e.g. America/Chicago vs America/New_York): it reads the local year, month, day, and 24-hour `hh` directly from Intl.DateTimeFormat output. Day-of-week is computed using noon local — `new Date(Date.UTC(y, m − 1, d, 12)).getUTCDay()` — to avoid a TZ-day flop right at local midnight, where a tiny UTC drift would otherwise carry the slab into the previous weekday's window.

If no window in the site's SitePowerWindow rows matches the local day-of-week + hour, the function returns defaultPrice. That default is the env-driven `LEDGER_DEFAULT_PRICE_USD_PER_KWH` (default 0.10 — i.e. $0.10/kWh) so a site with zero windows configured charges uniformly rather than diverging silently.

mostSeenPool

Pool attribution is per UTC day, not per total. Each reading increments a counter for its pool name in the bucket for its UTC-day key. The function mostSeenPool returns the name of the highest count, falling back to null when the day had zero readings. A day that switched pools mid-window reports whichever pool dominated by reading count — the per-rig CSV stamps exactly that string into the row's `pool` column, and an operator scanning the export can see at a glance whose fees drove the day.

Worked example — one rig, one UTC day

One Antminer S21 at a single site in America/Chicago, reporting five readings at UTC 00:00, 06:00, 12:00, 18:00, and the following day's 00:00. Hashrates 95, 100, 105, 100, 95 TH/s, all on pool A. The site has one M–F off-peak window 22:00–24:00 local at $0.04/kWh; every other operator-hour falls back to the LEDGER_DEFAULT_PRICE_USD_PER_KWH default of $0.10/kWh.

Rig modelAntminer S21Source: RIG_DRAW_WATTS = 3500 W
Operator tzAmerica/ChicagoSource: Site.operatorTz
Pool fee2.0%Source: env LEDGER_POOL_FEE (default 0.02)
BTC price$60,000Source: env LEDGER_BTC_PRICE_USD (default 60_000)
Pool hashprice0.00004 BTC/TH·daySource: pool.currentBtcPerThPerDay
Default $/kWh$0.10Source: env LEDGER_DEFAULT_PRICE_USD_PER_KWH

Five readings produce four carry-forward segments, each 6 hours long, with the later reading's hashrate filling the gap:

  • Segment 1 — UTC 00:00–06:00, hashrate 100 TH/s (later reading's)
  • Segment 2 — UTC 06:00–12:00, hashrate 105 TH/s
  • Segment 3 — UTC 12:00–18:00, hashrate 100 TH/s
  • Segment 4 — UTC 18:00–24:00, hashrate 95 TH/s

Each segment is unwrapped into six 1-hour UTC slabs. The slab midpoint drives the priceAtMidpoint lookup:

  • Slab midpoint UTC 03:00 — Chicago local Mon 22:00 — in off-peak window → $0.04
  • Slab midpoint UTC 09:00 — Chicago local Tue 04:00 — no window match → $0.10 default
  • Slab midpoint UTC 15:00 — Chicago local Tue 10:00 — no window match → $0.10 default
  • Slab midpoint UTC 21:00 — Chicago local Tue 16:00 — no window match → $0.10 default

Slab cost collapses segment-wise:

  • Segment 1 — 21 kWh × $0.04 = $0.84
  • Segment 2 — 21 kWh × $0.10 = $2.10
  • Segment 3 — 21 kWh × $0.10 = $2.10
  • Segment 4 — 21 kWh × $0.10 = $2.10
  • Total energy cost = $7.14

The row that lands in /fleet

aggregateLedger returns one LedgerItem for this UTC day plus a totals row. The /fleet dashboard renders exactly these six columns:

DateTH-daysUptime (h)Est. kWhEnergy costEst. revenueNet margin
2026-08-0410024.084$7.14$235.20$228.06

Revenue walks as: 100 TH-days × 0.00004 BTC/TH·day × $60,000/BTC × (1 − 0.02) = 0.004 BTC × $60,000 × 0.98 = $235.20. Net margin = $235.20 − $7.14 = $228.06. An operator auditing this row hears the same numbers read back from the ledger CSV — they are computed by the same function with the same inputs.

Where /fleet composes

The fleet view is element-wise summation, not a parallel computation. computeFleetPnl in src/lib/business/fleet-pnl.ts calls aggregateLedger once per rig — never more, never less — accumulates each rig's LedgerTotals into a per-site total via sumTotals, then re-sums those site totals into the fleet headline. Both sumTotals calls keep the same audit precision the per-rig aggregator uses (cents on USD, six decimal places on hours/TH/kWh), so the cross-rig total agrees with the per-rig number to the cent.

Outside that loop, the fleet path duplicates nothing about the math. The slab walker, the tz-aware price model, the pool-bandwidth lookup, the BTC × (1 − poolFee) revenue model all live in `aggregateLedger`. A change to the per-rig aggregator propagates to /fleet without a fork — that is the point of the file layout, and the headline number and the export number cannot drift apart.

Takeaway

The /fleet total is the sum. The export is the sum. The CSV envelope and the on-screen headline run through the same per-rig call, so an operator who wants to verify a number on the dashboard against the ledger download can do so line-for-line. If you want to confirm the worked numbers above against the live aggregator, the dashboard sits one click away — see the figures on /fleet for any rig on the platform and the math above plays out row-for-row against that rig's own readings, draw spec, and site windows.

Sources

  • aggregateLedger — src/lib/business/rig-ledger.ts (per-rig aggregator; slab walker, priceAtMidpoint, mostSeenPool)
  • computeFleetPnl — src/lib/business/fleet-pnl.ts (per-rig calls + element-wise summation across sites)
  • CoinDesk BTC/USD index — coindesk.com/indices (the upstream feed that anchors LEDGER_BTC_PRICE_USD once configured)
  • mempool.space live stats — mempool.space (network hashrate / difficulty / block template; the upstream anchor for the pool hashprice math used in the inaugural pool-switching post)

Want to verify the numbers above against the live aggregator? Open /fleet and the worked row plays out against any rig’s readings, draw spec, and site windows.