From f949ed9b879d575ba1f6e15e5f0e497ffbf8bf11 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Mon, 3 Aug 2026 20:23:05 -0500 Subject: [PATCH 1/2] feat(dashboard): estimated XvB net band from measured realization priors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tier calculator's unmeasured state showed XvB's face value labeled as an upper bound — honest, but unanswerable for the question the panel exists for ("is enabling worth it?"). Everything except one factor IS computable on any box: cost (linear P2Pool model) and draw odds (winners feed) are pure math; per-win collection is not derivable from outside XvB, but it is BOUNDED by measurement. Ship that bound: - XVB_REALIZATION_PRIOR = (0.24, 0.42), from baseline-subtracted 6h post-win payout-stream measurements on a production wallet across two regimes: 24% with the credited average riding the tier threshold (terminated rounds), 42% with comfortable margin. - Unmeasured tiers emit assumed_reward_year_range; the panel's Net becomes a range labeled (estimated) — red only when even the optimistic end loses, green only when even the pessimistic end profits. Measured supersedes it. - Accuracy fixes the stream measurement demanded: the local-measurement attribution window widens 2h -> 6h (the stream is front-loaded but runs ~6h), and the box's own linear P2Pool baseline over the windowed hours is subtracted so ordinary payouts can't inflate the factor. Co-Authored-By: Claude Fable 5 --- .../web/static/components.mjs | 59 +++++++++++++++---- .../mining_dashboard/web/static/logic.mjs | 13 +++- build/dashboard/mining_dashboard/web/views.py | 49 ++++++++++++--- .../tests/frontend/components.test.mjs | 25 ++++++++ build/dashboard/tests/frontend/logic.test.mjs | 14 +++++ build/dashboard/tests/web/test_views.py | 24 ++++++++ docs/dashboard.md | 2 +- 7 files changed, 166 insertions(+), 20 deletions(-) diff --git a/build/dashboard/mining_dashboard/web/static/components.mjs b/build/dashboard/mining_dashboard/web/static/components.mjs index 78ce1ee1..ad973f6e 100644 --- a/build/dashboard/mining_dashboard/web/static/components.mjs +++ b/build/dashboard/mining_dashboard/web/static/components.mjs @@ -516,9 +516,26 @@ class XvbComparison extends Component { tiers.find((t) => t.name === calc.target_tier) || tiers[0]; const cmp = xvbTierComparison(sel, coeffDay); - // The actionable net: measured realization when the wallet has one, face value otherwise — - // the label always says which (#872: the face-value net once had the wrong SIGN). - const netShown = cmp.realizedNet !== null ? cmp.realizedNet : cmp.net; + // The actionable net, best first (#872): measured realization when the wallet has one; the + // measured-prior band otherwise; raw face value only when even the band can't be computed. + // The label always says which — the face-value net once had the wrong SIGN. + const netShown = + cmp.realizedNet !== null ? cmp.realizedNet : cmp.assumedNetRange ? null : cmp.net; + const netLabel = + cmp.realizedNet !== null + ? "Net / yr (measured)" + : cmp.assumedNetRange + ? "Net / yr (estimated)" + : "Net / yr (face value)"; + // A range is red only when even its optimistic end loses, green only when even its + // pessimistic end profits — a zero-spanning band stays neutral. + const rangeCls = cmp.assumedNetRange + ? cmp.assumedNetRange[1] < 0 + ? "status-bad" + : cmp.assumedNetRange[0] > 0 + ? "status-ok" + : "" + : ""; // The same sustains rule the tier block states: donating the threshold must fit inside the // donateable share of the what-if hashrate. An unsustainable tier's Net is "—" — showing, // say, Mega's +56 XMR/yr to a 269 kH/s fleet would imply an unreachable payout. @@ -538,9 +555,27 @@ class XvbComparison extends Component { title="XvB's own published expected reward for this tier per year (their reward_calc figures, fetched over Tor). This is the raffle expectation across all qualifiers — donating above the tier threshold does NOT raise it." /> <${StatCard} label="Cost / yr" value=${cmp.cost !== null ? formatXmr(cmp.cost) : "—"} title="P2Pool earnings foregone by donating the tier threshold for a year (threshold × the P2Pool daily rate × 365)." /> - <${StatCard} label=${cmp.realizedNet !== null ? "Net / yr (measured)" : "Net / yr (face value)"} - value=${sustainable && netShown !== null ? formatXmr(netShown) : "—"} - cls=${sustainable && netShown !== null ? netCls(netShown) : ""} + <${StatCard} label=${netLabel} + value=${ + !sustainable + ? "—" + : cmp.realizedNet !== null + ? formatXmr(cmp.realizedNet) + : cmp.assumedNetRange + ? `${formatXmr(cmp.assumedNetRange[0])} … ${formatXmr(cmp.assumedNetRange[1])}` + : cmp.net !== null + ? formatXmr(cmp.net) + : "—" +} + cls=${ + !sustainable + ? "" + : cmp.realizedNet !== null || cmp.assumedNetRange === null + ? netShown !== null + ? netCls(netShown) + : "" + : rangeCls +} title=${ !sustainable ? "Not shown — this tier isn't sustainable at your hashrate, so its payout isn't reachable." @@ -548,10 +583,14 @@ class XvbComparison extends Component { ? `XvB's published reward scaled to what this wallet's wins actually paid — ` + `${calc.realization_pct}% of face value over the last ${calc.realization_wins} wins — ` + `minus the P2Pool earnings given up.` - : "XvB's published FACE-VALUE reward minus the P2Pool earnings given up. The face " + - "value prices every bonus hash at full block reward and assumes every won round " + - "runs to completion — wallets collect less; this net is an upper bound. Once " + - "enough wins land, this figure switches to your measured payout realization." + : cmp.assumedNetRange + ? "XvB's published reward scaled by the measured realization band from live " + + "deployments — wallets collected 24% of face value (tight margin over the " + + "tier threshold) to 42% (comfortable margin) — minus the P2Pool earnings " + + "given up. Your own measurement replaces this band once enough wins land." + : "XvB's published FACE-VALUE reward minus the P2Pool earnings given up — an " + + "upper bound: it prices every bonus hash at full block reward and assumes " + + "every won round runs to completion." } /> ${ diff --git a/build/dashboard/mining_dashboard/web/static/logic.mjs b/build/dashboard/mining_dashboard/web/static/logic.mjs index 83ffc830..51654ff8 100644 --- a/build/dashboard/mining_dashboard/web/static/logic.mjs +++ b/build/dashboard/mining_dashboard/web/static/logic.mjs @@ -323,9 +323,20 @@ export function xvbTierComparison(tier, coeffDay) { tier && Number.isFinite(tier.expected_reward_year) ? tier.expected_reward_year : null; const realized = tier && Number.isFinite(tier.realized_reward_year) ? tier.realized_reward_year : null; + // The measured-prior band for unmeasured boxes (#872): published × [low, high] realization. + // Server-emitted only while no local measurement exists, so realizedNet and assumedNetRange + // are mutually exclusive by construction. + const assumed = + tier && + Array.isArray(tier.assumed_reward_year_range) && + tier.assumed_reward_year_range.every(Number.isFinite) + ? tier.assumed_reward_year_range + : null; const net = expected !== null && cost !== null ? expected - cost : null; const realizedNet = realized !== null && cost !== null ? realized - cost : null; - return { expected, cost, net, realized, realizedNet }; + const assumedNetRange = + assumed !== null && cost !== null ? [assumed[0] - cost, assumed[1] - cost] : null; + return { expected, cost, net, realized, realizedNet, assumedNetRange }; } // Decimal places for a coin amount: more for small amounts (a day's earnings can be a tiny diff --git a/build/dashboard/mining_dashboard/web/views.py b/build/dashboard/mining_dashboard/web/views.py index 6145814a..e360d777 100644 --- a/build/dashboard/mining_dashboard/web/views.py +++ b/build/dashboard/mining_dashboard/web/views.py @@ -1510,16 +1510,28 @@ def xvb_current_tier_key(metrics, tiers): # Measuring what a raffle win actually pays (#866/#872). A win's bonus round mines for up to an -# hour and lands as ordinary small P2Pool payouts shortly after; production measurement put the -# attributable payout mass inside 2h of the win timestamp. A win younger than the settle window -# may still have payouts in flight, so it is left out of the sample rather than dragging the -# factor down. Below the minimum sample the factor is noise — callers fall back to the published -# figure and the UI labels it face value. -_XVB_WIN_PAYOUT_WINDOW_S = 2 * 3600 +# hour and its shares then ride the PPLNS window — a baseline-subtracted stream measurement on a +# production wallet (17 wins, two eras) put ~90% of the attributable excess inside 6h of the win +# timestamp, front-loaded in the first two hours. A win younger than the settle window may still +# have payouts in flight, so it is left out of the sample rather than dragging the factor down. +# Below the minimum sample the factor is noise — callers fall back to the published figure and +# the UI labels it face value. +_XVB_WIN_PAYOUT_WINDOW_S = 6 * 3600 _XVB_WIN_SETTLE_S = 12 * 3600 _XVB_REALIZATION_MIN_WINS = 5 _XVB_REALIZATION_WINDOW_S = 45 * SECONDS_PER_DAY +# Measured realization PRIOR for boxes with no local measurement (#872): the band a wallet's +# collected-vs-published fraction actually landed in on live deployments (Jun–Aug 2026, one +# wallet, two regimes; baseline-subtracted 6h post-win payout streams). 0.24 = Whale with the +# credited average riding the 100k round minimum (terminated rounds); 0.42 = VIP with a +# comfortable margin above its threshold. The mechanism behind the sub-1.0 ceiling even at +# comfortable margin is not visible from outside XvB, so this is an empirical bound, not a +# model — a local measurement (xvb_realization) supersedes it. +# ponytail: two hard-coded endpoints from one wallet's history — recalibrate if more deployments +# report measurements outside the band. +XVB_REALIZATION_PRIOR = (0.24, 0.42) + def xvb_forecast_tier_key(metrics, tiers): """The tier the expected-wins forecast should speak to: held, else targeted (#866). @@ -1558,7 +1570,7 @@ def xvb_expected_wins_day(round_stats_state, tier_key, tiers): return total if total > 0 else None -def xvb_realization(payouts, raffle_wins, xvb_day, expected_wins_day, now=None): +def xvb_realization(payouts, raffle_wins, xvb_day, expected_wins_day, now=None, p2pool_day=None): """Measured fraction of XvB's published expectation this wallet actually collects (#866/#872). Numerator: mean confirmed XMR landing within the attribution window after each settled win in @@ -1590,6 +1602,11 @@ def xvb_realization(payouts, raffle_wins, xvb_day, expected_wins_day, now=None): ) / ATOMIC_PER_XMR ) + # Ordinary P2Pool payouts also land inside the attribution windows; left in, they inflate the + # factor (the failure mode this whole measurement exists to prevent). Subtract the expected + # baseline: the box's own linear P2Pool rate over the windowed hours. + if p2pool_day and p2pool_day > 0: + realized -= p2pool_day * (_XVB_WIN_PAYOUT_WINDOW_S / SECONDS_PER_DAY) * len(stamps) frac = (realized / len(stamps)) / face_per_win return (max(0.0, min(1.0, frac)), len(stamps)) @@ -1837,6 +1854,18 @@ def _odds_day(key): if estimates_available and key in estimates and realization else None ), + # Unmeasured boxes still get a calculable band (#872): the published figure + # scaled by the measured realization PRIOR below. None once a local + # measurement exists (realized_reward_year supersedes it) or estimates are + # stale — the two never show together. + "assumed_reward_year_range": ( + [ + float(estimates[key]) * XVB_REALIZATION_PRIOR[0], + float(estimates[key]) * XVB_REALIZATION_PRIOR[1], + ] + if estimates_available and key in estimates and not realization + else None + ), "win_odds_day": _odds_day(key), "players_avg": (round_types.get(key) or {}).get("players_avg"), } @@ -2003,7 +2032,11 @@ def build_state(data, state_mgr, range_arg, window=None, avg_window=DEFAULT_HASH state_mgr.get_xvb_round_stats(), xvb_forecast_tier_key(metrics, xvb_tiers), xvb_tiers ) xvb_realized = xvb_realization( - monero_payouts, raffle_wins, earnings["xvb_day"], xvb_wins_day + monero_payouts, + raffle_wins, + earnings["xvb_day"], + xvb_wins_day, + p2pool_day=earnings["coeff_day"] * metrics.p2pool_30d, ) egress = egress_posture_from_config() # per-component egress route + privacy roll-up (#170) diff --git a/build/dashboard/tests/frontend/components.test.mjs b/build/dashboard/tests/frontend/components.test.mjs index 10f40250..6fd72f40 100644 --- a/build/dashboard/tests/frontend/components.test.mjs +++ b/build/dashboard/tests/frontend/components.test.mjs @@ -707,6 +707,31 @@ test('XvB tier comparison prefers the measured net and shows the draw behind it assert.doesNotMatch(fv, /id="xvb-draw-line"/); }); +test('XvB tier comparison shows the estimated band on an unmeasured box (#872)', () => { + const base = clone(); + base.earnings.available = true; + base.earnings.coeff_day = 1e-7; + base.earnings.p2pool_hr = 200000; + base.xvb_calc = { + enabled: true, estimates_available: true, estimates_stale: false, max_fraction: 0.85, + current_tier: 'Whale (100.00 kH/s+)', target_tier: 'Whale (100.00 kH/s+)', + target_threshold: 100000, sustainable: true, note: 'raffle status', mode_note: null, + realization_pct: null, realization_wins: null, + tiers: [ + { name: 'Whale (100.00 kH/s+)', threshold: 100000, expected_reward_year: 6.17, + realized_reward_year: null, assumed_reward_year_range: [6.17 * 0.19, 6.17 * 0.55], + win_odds_day: 0.84, players_avg: 8.2 }, + ], + }; + const out = renderApp({ state: base }); + // Both endpoints of published × [0.19, 0.55] − 3.65 cost render, labeled estimated; the + // face-value net (+2.52) must not appear as the acted-on figure. + assert.match(out, /Net \/ yr \(estimated\)/); + assert.match(out, /-2\.477\d* XMR … -0\.256\d* XMR/); + assert.doesNotMatch(out, /2\.5200 XMR/); + assert.match(out, /24% of face value \(tight margin/); +}); + test('CadenceCard shows the — placeholders on a cold stack, real figures when available (#84)', () => { // The base fixture has no pool difficulty → cadence.available === false → server-sent dashes. const cold = renderApp(); diff --git a/build/dashboard/tests/frontend/logic.test.mjs b/build/dashboard/tests/frontend/logic.test.mjs index 5faeced4..89a025d0 100644 --- a/build/dashboard/tests/frontend/logic.test.mjs +++ b/build/dashboard/tests/frontend/logic.test.mjs @@ -386,6 +386,20 @@ test('xvbTierComparison: measured realization yields realizedNet — the sign th assert.ok(c.realizedNet < 0); }); +test('xvbTierComparison: the prior band yields assumedNetRange, exclusive with measured (#872)', () => { + const tier = { + name: 'Whale', threshold: 100_000, + expected_reward_year: 6.17, realized_reward_year: null, + assumed_reward_year_range: [6.17 * 0.19, 6.17 * 0.55], + }; + const c = xvbTierComparison(tier, 1e-7); // cost 3.65 + assert.ok(Math.abs(c.assumedNetRange[0] - (6.17 * 0.19 - 3.65)) < 1e-9); + assert.ok(Math.abs(c.assumedNetRange[1] - (6.17 * 0.55 - 3.65)) < 1e-9); + // No cost (network stats down) -> no range either; absent field -> null (old server). + assert.equal(xvbTierComparison(tier, 0).assumedNetRange, null); + assert.equal(xvbTierComparison({ name: 'W', threshold: 1, expected_reward_year: 1 }, 1e-7).assumedNetRange, null); +}); + test('xvbTierComparison: unmeasured realization stays null — never fabricated (#872)', () => { const tier = { name: 'Whale', threshold: 100_000, expected_reward_year: 6.17, realized_reward_year: null }; const c = xvbTierComparison(tier, 1e-7); diff --git a/build/dashboard/tests/web/test_views.py b/build/dashboard/tests/web/test_views.py index cf2249e8..88700e64 100644 --- a/build/dashboard/tests/web/test_views.py +++ b/build/dashboard/tests/web/test_views.py @@ -1971,6 +1971,16 @@ def test_missing_inputs_yield_none(self): # A hostile/corrupt negative published figure gives a negative face value — no factor. assert xvb_realization(self._payouts(1), self.WINS, -0.016, 1.0, now=self.NOW) is None + def test_baseline_subtraction_removes_ordinary_p2pool_leak(self): + # Ordinary P2Pool payouts land inside win windows too; the box's linear rate over the + # windowed hours is subtracted so the factor measures only the wins' excess. Here the + # 4.8 mXMR gross per win contains 1.6 mXMR of baseline (6.4 mXMR/day × 6h): excess + # 3.2 mXMR against a 16 mXMR face => 0.2, not the inflated 0.3. + out = xvb_realization( + self._payouts(4_800_000_000), self.WINS, 0.016, 1.0, now=self.NOW, p2pool_day=0.0064 + ) + assert out == (pytest.approx(0.2), 6) + class TestEarningsVsActualTempering: NOW = 1_760_000_000 @@ -2307,6 +2317,20 @@ def test_realization_scales_published_rewards_into_realized(self): assert out["realization_pct"] == 19 assert out["realization_wins"] == 15 + def test_unmeasured_boxes_get_the_prior_band_measured_boxes_do_not(self): + # #872: no local measurement -> published × the measured prior band, so "should I enable + # this" is answerable everywhere. A measured factor supersedes it (never both), and stale + # estimates null both — a band cannot resurrect a stale face value. + out = build_xvb_calc(_metrics(), self._sm()) + whale = next(t for t in out["tiers"] if t["threshold"] == 100_000) + lo, hi = views.XVB_REALIZATION_PRIOR + assert whale["assumed_reward_year_range"] == pytest.approx([6.17 * lo, 6.17 * hi]) + out = build_xvb_calc(_metrics(), self._sm(), realization=(0.19, 15)) + assert all(t["assumed_reward_year_range"] is None for t in out["tiers"]) + stale = self._sm(last_update=time.time() - XVB_STATS_STALE_AFTER_S - 1) + out = build_xvb_calc(_metrics(), stale) + assert all(t["assumed_reward_year_range"] is None for t in out["tiers"]) + def test_no_realization_leaves_realized_none(self): # Unmeasured (too few wins / payout confirmation off): realized stays None so the client # falls back to face value AND says so — never a fabricated factor. diff --git a/docs/dashboard.md b/docs/dashboard.md index f36b4703..01741d6c 100644 --- a/docs/dashboard.md +++ b/docs/dashboard.md @@ -644,7 +644,7 @@ Below the tier figures, a **per-tier payout comparison** dropdown weighs each do |---|---| | **Expected (XvB)** | XvB's own published expected reward for the tier, in XMR per year. This is XvB's pre-computed `reward_calc` figure for the tier's donor round, fetched over Tor from `reward_estimate_pub.txt` — the dashboard does not re-derive it. It is the raffle expectation across all qualifiers, so donating **above** the tier threshold does not raise it. It is also **face value**: it prices every bonus hash at full block reward and assumes every won round runs to completion — wallets collect less. `estimate unavailable` when the fetch is stale or failed — never a stale figure implied fresh. | | **Cost / yr** | The P2Pool earnings given up by donating the tier threshold for a year: `threshold × the P2Pool daily rate × 365`, using the same rate the Monero tab shows. | -| **Net / yr** | The reward minus the P2Pool earnings given up — the number to act on. Labeled **(measured)** when enough of your wins have confirmed payouts to measure what they actually paid: the published reward is scaled to that measured fraction first, because the face-value net can carry the wrong **sign** (a production Whale box showed +2.97 XMR/yr while the measured net was about −1.8). Labeled **(face value)** until then — read it as an upper bound. | +| **Net / yr** | The reward minus the P2Pool earnings given up — the number to act on. Labeled **(measured)** when enough of your wins have confirmed payouts to measure what they actually paid: the published reward is scaled to that measured fraction first, because the face-value net can carry the wrong **sign** (a production Whale box showed +2.97 XMR/yr while the measured net was about −1.8). Before your own measurement exists it is labeled **(estimated)** and shows a **range**: the published reward scaled by the realization band measured on live deployments — wallets collected 24% of face value (donation riding the tier threshold, terminated rounds) to 42% (comfortable margin) — so "is this worth enabling" is answerable on a fresh box. Red only when even the optimistic end loses; green only when even the pessimistic end profits. Falls back to a labeled **(face value)** upper bound only when no band can be computed. | Below the figures, a **draw line** shows the selected tier's raffle odds from the winners file: about how many wins per 30 days its rounds pay out and among how many qualifiers the draw runs. A From 40abb7804ca3f4f0f56823d58c4a5ac3c2bb5eb7 Mon Sep 17 00:00:00 2001 From: Vijit Singh Date: Mon, 3 Aug 2026 20:25:20 -0500 Subject: [PATCH 2/2] refactor: decide the Net card's mode once (ponytail-review) Co-Authored-By: Claude Fable 5 --- .../web/static/components.mjs | 94 +++++++++---------- 1 file changed, 43 insertions(+), 51 deletions(-) diff --git a/build/dashboard/mining_dashboard/web/static/components.mjs b/build/dashboard/mining_dashboard/web/static/components.mjs index ad973f6e..25275402 100644 --- a/build/dashboard/mining_dashboard/web/static/components.mjs +++ b/build/dashboard/mining_dashboard/web/static/components.mjs @@ -518,24 +518,45 @@ class XvbComparison extends Component { const cmp = xvbTierComparison(sel, coeffDay); // The actionable net, best first (#872): measured realization when the wallet has one; the // measured-prior band otherwise; raw face value only when even the band can't be computed. - // The label always says which — the face-value net once had the wrong SIGN. - const netShown = - cmp.realizedNet !== null ? cmp.realizedNet : cmp.assumedNetRange ? null : cmp.net; - const netLabel = + // Decided ONCE here — label, value, colour and tooltip below all follow `net`. The + // face-value net once had the wrong SIGN, so the label always says which figure this is. + // Range colour: red only when even the optimistic end loses, green only when even the + // pessimistic end profits — a zero-spanning band stays neutral. + const [lo, hi] = cmp.assumedNetRange || [null, null]; + const net = cmp.realizedNet !== null - ? "Net / yr (measured)" + ? { + label: "Net / yr (measured)", + value: formatXmr(cmp.realizedNet), + cls: netCls(cmp.realizedNet), + title: + `XvB's published reward scaled to what this wallet's wins actually paid — ` + + `${calc.realization_pct}% of face value over the last ${calc.realization_wins} wins — ` + + `minus the P2Pool earnings given up.`, + } : cmp.assumedNetRange - ? "Net / yr (estimated)" - : "Net / yr (face value)"; - // A range is red only when even its optimistic end loses, green only when even its - // pessimistic end profits — a zero-spanning band stays neutral. - const rangeCls = cmp.assumedNetRange - ? cmp.assumedNetRange[1] < 0 - ? "status-bad" - : cmp.assumedNetRange[0] > 0 - ? "status-ok" - : "" - : ""; + ? { + label: "Net / yr (estimated)", + value: `${formatXmr(lo)} … ${formatXmr(hi)}`, + cls: hi < 0 ? "status-bad" : lo > 0 ? "status-ok" : "", + title: + "XvB's published reward scaled by the measured realization band from live " + + "deployments — wallets collected 24% of face value (tight margin over the " + + "tier threshold) to 42% (comfortable margin) — minus the P2Pool earnings " + + "given up. Your own measurement replaces this band once enough wins land.", + } + : { + label: "Net / yr (face value)", + value: cmp.net !== null ? formatXmr(cmp.net) : "—", + cls: cmp.net !== null ? netCls(cmp.net) : "", + title: + "XvB's published FACE-VALUE reward minus the P2Pool earnings given up — an " + + "upper bound: it prices every bonus hash at full block reward and assumes " + + "every won round runs to completion.", + }; + // Fiat mirror wants one number; a range has none, so its fiat net shows "—". + const netShown = + cmp.realizedNet !== null ? cmp.realizedNet : cmp.assumedNetRange ? null : cmp.net; // The same sustains rule the tier block states: donating the threshold must fit inside the // donateable share of the what-if hashrate. An unsustainable tier's Net is "—" — showing, // say, Mega's +56 XMR/yr to a 269 kH/s fleet would imply an unreachable payout. @@ -555,42 +576,13 @@ class XvbComparison extends Component { title="XvB's own published expected reward for this tier per year (their reward_calc figures, fetched over Tor). This is the raffle expectation across all qualifiers — donating above the tier threshold does NOT raise it." /> <${StatCard} label="Cost / yr" value=${cmp.cost !== null ? formatXmr(cmp.cost) : "—"} title="P2Pool earnings foregone by donating the tier threshold for a year (threshold × the P2Pool daily rate × 365)." /> - <${StatCard} label=${netLabel} - value=${ - !sustainable - ? "—" - : cmp.realizedNet !== null - ? formatXmr(cmp.realizedNet) - : cmp.assumedNetRange - ? `${formatXmr(cmp.assumedNetRange[0])} … ${formatXmr(cmp.assumedNetRange[1])}` - : cmp.net !== null - ? formatXmr(cmp.net) - : "—" -} - cls=${ - !sustainable - ? "" - : cmp.realizedNet !== null || cmp.assumedNetRange === null - ? netShown !== null - ? netCls(netShown) - : "" - : rangeCls -} + <${StatCard} label=${net.label} + value=${sustainable ? net.value : "—"} + cls=${sustainable ? net.cls : ""} title=${ - !sustainable - ? "Not shown — this tier isn't sustainable at your hashrate, so its payout isn't reachable." - : cmp.realizedNet !== null - ? `XvB's published reward scaled to what this wallet's wins actually paid — ` + - `${calc.realization_pct}% of face value over the last ${calc.realization_wins} wins — ` + - `minus the P2Pool earnings given up.` - : cmp.assumedNetRange - ? "XvB's published reward scaled by the measured realization band from live " + - "deployments — wallets collected 24% of face value (tight margin over the " + - "tier threshold) to 42% (comfortable margin) — minus the P2Pool earnings " + - "given up. Your own measurement replaces this band once enough wins land." - : "XvB's published FACE-VALUE reward minus the P2Pool earnings given up — an " + - "upper bound: it prices every bonus hash at full block reward and assumes " + - "every won round runs to completion." + sustainable + ? net.title + : "Not shown — this tier isn't sustainable at your hashrate, so its payout isn't reachable." } /> ${