FlareWatch assigns each P-Chain validator a 0–100 composite score across 9 dimensions. The math is deterministic, the inputs are public chain data [Flare Explorer] [FSE] [Flaremetrics], and the same algorithm applies to every validator on the network — including FlareWatch's own validator node, which is scored by this exact function with no special treatment. This page documents every dimension and threshold so operators and stakers can see exactly how the score is computed and why each value was chosen. Every claim here links back to its primary on-chain or upstream source — see Sources & references at the bottom.
staking mode on the validators page (delegating FLR to a P-Chain validator for VRM + MIRROR rewards). The FTSO provider score shown in delegation mode (delegating WFLR to FTSO data providers) uses a separate 13-dimension algorithm focused on data-provider performance — accuracy, V2 protocol participation, etc. They're distinct on-chain roles with distinct rewards, scored separately. See FTSO Provider Score Methodology for the delegation side.The Net APR on the staking table is the all-in rate a delegator receives — one number, no mental math. It is measured from Flare's reward-scripts (actual payouts, not a formula), net of the validator's fee, and it moves each epoch with real rewards.
Comparing to Flare Systems Explorer? FSE and other explorers show the delegation rate only — they don't add MIRROR — so our Total APR reads higher on any MIRROR-active validator (the gap is exactly the MIRROR line above). Both figures are ~8-epoch trailing averages from the same reward-scripts data, so a validator's mid-window fee change lags a current-fee snapshot on either site until it ages through the window.
Two other figures appear in the APR tooltip and are not the delegator's rate: the theoretical baseline (network gross APR × (1 − fee), staking only — used as a fallback before enough measured history exists), and the operator's self-bond yield (the validator's own stake return, amplified by fee capture — an operator metric, not what you earn).
For scoring: the Net Yield dimension scores the VRM delegation rate only, and MIRROR is scored in its own dimension — so MIRROR is never double-counted, even though it's included in the displayed Total APR.
| 90+ | Top tier — top ~10–20% of operators. Typical profile: full-stack validator + FTSO + FDC, low-end fee, MIRROR-active, consistent FIP-10 reliability, healthy delegator base, meaningful self-bond. No single dimension required — operators reach Top tier by stacking strength across most categories. |
| 80–89 | Strong — meets most key benchmarks; one or two dimensions short of top tier. |
| 70–79 | Good — meets all baseline criteria; no major gaps. |
| 60–69 | Acceptable — usable but not differentiated. |
| <60 | Below median — significant gaps in one or more dimensions. Mathematical fact, not a quality judgment. |
if (uptime >= 99.5) raw = 17 + (uptime - 99.5) * 6 else if (uptime >= 99) raw = 13 + (uptime - 99) * 8 else if (uptime >= 95) raw = 4 + (uptime - 95) * 2.25 // v3.9: was (u - 95) * 3.25 starting at 0 else raw = max(0, uptime - 90) * 0.8 // 90 → 0, 95 → 4 (continuity) raw = clamp(raw, 0, 20) reliability = epochsUptimeEligible / epochsObserved // last 8 epochs score = raw * clamp(reliability, 0, 1) // dimension max 20
scoreAPR = delegationAPY > 0 ? delegationAPY : baseAPR // VRM, net of fee medianAPR = median(scoreAPR across all validators) cappedAPR = min(scoreAPR, 25) // APY_DISPLAY_CAP if (medianAPR > 0): ratio = cappedAPR / medianAPR score = clamp(((ratio - 0.6) / 0.6) * 18, 0, 18) else: score = min(18, (cappedAPR / 8) * 18) // fallback: BASE_APY = 8
// anchor = max(observed minimum active fee, 20% protocol floor) d = fee - anchor // distance above market best if (d <= 0) score = 7 // at/below best available else if (d <= 5) score = 7 - d * 0.2 // 0 → 5 over: 7 → 6 else if (d <= 10) score = 6 - (d - 5) * 0.3 // 5 → 10 over: 6 → 4.5 else if (d <= 15) score = 4.5 - (d - 10) * 0.4 // 10 → 15 over: 4.5 → 2.5 else if (d <= 20) score = 2.5 - (d - 15) * 0.5 // 15 → 20 over: 2.5 → 0 else score = 0 // fees > anchor+20 saturate at 0/7 — the v4.5 extreme-fee // penalty below takes over from there
// Verification baseline (v3.10 hybrid) isCurated = (in KNOWN_VALIDATORS) OR ( daysObserved >= 90 AND operatorDelegatorCount >= 25 AND retention30d >= -15% AND selfBondFLR >= 1_000_000 ) if (isCurated) verification = 7 else if (name auto-discovered) verification = 3 else verification = 0 // FTSO-derived (only when ftsoOperatorScore is a number) clamped = clamp(ftsoOperatorScore, 50, 100) ftsoDerived = 4 + (clamped - 50) * (8/50) // FTSO 50 → 4, FTSO 100 → 12 // Final score score = max(verification, ftsoDerived)
passthrough = max(0, 1 - fee / 100) if (mirrorStatus == "active") score = 10 * passthrough else if (mirrorStatus == "paused") score = 5 * passthrough else if (mirrorStatus == "inactive") score = 0 else score = 5 * passthrough // no data yet
utilization = clamp(1 - freeSpaceFLR / maxDelegationFLR, 0, 1) if (utilization <= 0.70): score = 1.75 + (utilization / 0.70) * 5.25 // 1.75 → 7 ramp else: score = 7 - ((utilization - 0.70) / 0.30) * 1.75 // 7 → 5.25 ramp
// Count signal (max 6)
if (delegatorCount <= 5) countScore = 0
else if (delegatorCount >= 500) countScore = 6
else countScore = clamp(log(delegatorCount / 5) / log(100) * 6, 0, 6)
// Concentration adjustment (-1 to +1) — skip if <3 delegators
avgFLR = delegatedFLR / delegatorCount
if (avgFLR < 500_000) concentrationAdj = +1
else if (avgFLR < 5_000_000) concentrationAdj = +0.5
else if (avgFLR > 50_000_000) concentrationAdj = -1
else if (avgFLR > 20_000_000) concentrationAdj = -0.5
else concentrationAdj = 0
// Longevity bonus (0 to +1)
daysObserved = (now - firstObservedAtMs) / 86400000
if (daysObserved >= 90) longevityBonus = +1
else if (daysObserved >= 30) longevityBonus = +0.5
else longevityBonus = 0
// Self-bond alignment (-1 to +2)
selfBondRatio = selfBondFLR / (selfBondFLR + delegatedFLR)
if (selfBondFLR < 1_000_000) selfBondAdj = -1
else if (selfBondRatio >= 0.10) selfBondAdj = +2
else if (selfBondRatio >= 0.05) selfBondAdj = +1
else selfBondAdj = 0
// Retention (-0.5 to +0.5, v3.7) — skip if no 30-day baseline yet
delta = (delegatedFLR - delegatedFLR30dAgo) / delegatedFLR30dAgo
if (delta >= +0.10) retentionAdj = +0.5
else if (delta <= -0.15) retentionAdj = -0.5
else retentionAdj = 0
// Self-bond trajectory (-0.5 to +0.5, v3.7) — skip if no baseline
sbDelta = (selfBondFLR - selfBondFLR30dAgo) / selfBondFLR30dAgo
if (sbDelta >= +0.20) selfBondTrajectoryAdj = +0.5
else if (sbDelta <= -0.10) selfBondTrajectoryAdj = -0.5
else selfBondTrajectoryAdj = 0
score = clamp(countScore + concentrationAdj + longevityBonus + selfBondAdj
+ retentionAdj + selfBondTrajectoryAdj, 0, 11)// Time-weighted rate is computed upstream from per-epoch // reward-scripts data with decay = 0.85 per epoch back. r = deliveryRatio // capped at 1.0 if (r >= 1.00) base = 10 else if (r >= 0.97) base = 9 + (r - 0.97) * (1 / 0.03) // 0.97 → 9, 1.00 → 10 else if (r >= 0.95) base = 8 + (r - 0.95) * (1 / 0.02) // 0.95 → 8, 0.97 → 9 else if (r >= 0.90) base = 6 + (r - 0.90) * (2 / 0.05) // 0.90 → 6, 0.95 → 8 else if (r >= 0.85) base = 4 + (r - 0.85) * (2 / 0.05) // 0.85 → 4, 0.90 → 6 else if (r >= 0.80) base = 2 + (r - 0.80) * (2 / 0.05) // 0.80 → 2, 0.85 → 4 else base = max(0, r * 2.5) // 0 → 0, 0.80 → 2 // Variance penalty (CoV = std-dev / mean across per-epoch rates) variancePenalty = min(0.30, coefficientOfVariation * 0.5) score = base * (1 - variancePenalty) // Sample-size confidence dampener for < 3 epochs if (totalStakesCompleted < 3): confidence = totalStakesCompleted / 3 score = 5 + (score - 5) * confidence
daysLeft = (endTimeMs - now) / 86_400_000 if (daysLeft < 14) score = 0 else if (daysLeft < 30) score = 0 + (daysLeft - 14) * (2 / 16) // 14 → 0, 30 → 2 else if (daysLeft < 60) score = 2 + (daysLeft - 30) * (1 / 30) // 30 → 2, 60 → 3 else if (daysLeft < 120) score = 3 + (daysLeft - 60) * (2 / 60) // 60 → 3, 120 → 5 else score = 5
consecutiveMisses = 0 for entry in participation.recent (newest-first): if entry.eligible: break consecutiveMisses++ if consecutiveMisses < 2: penalty = 0 elif consecutiveMisses == 2: penalty = 3 elif consecutiveMisses == 3: penalty = 6 else: penalty = 10 // 4+ score = max(0, positiveDimensionsSum - penalty)
// v4.5 — fees beyond the Fee dimension's range (anchor+20)
if fee <= 50: penalty = 0
else: fraction = min(1, (fee - 50) / 50) * 0.75
penalty = positiveDimensionsSum * fraction
// fee 50% → no change · 75% → −37.5% of score · 100% → −75%
score = max(0, positiveDimensionsSum - outagePenalty - penalty)delegationFee, selfBond, delegatedStake, and FTSO score (if you're also a data provider).generated-files/reward-epoch-N/nodes-data.json. Count how many epochs your nodeID had uptimeEligible: true. That ratio drives your Uptime dimension multiplier.RewardClaimed events with claimType=3 referencing your nodeID. If there are none recent, you'll show as MIRROR-inactive.GET /api/validators/{nodeID}/score-breakdown to retrieve the persisted breakdown — every dimension's value, the algorithm version that produced it, and the recent score history — as JSON. The score-breakdown panel in the UI reads from the same source.// Step 1 — Raw composite (sum of dimensions, minus streak deduction)
positive = uptime + netYield + fee + operatorQuality
+ mirror + capacity + trust + delivery + timeRemaining
// v4.3 — active-outage streak penalty (see the dimension card above)
// 0-1 consecutive missed epochs → 0, 2 → 3, 3 → 6, 4+ → 10
raw = clamp(positive - streakPenalty, 0, 100)
// Step 2 — Smooth across recent cron snapshots (v3.5)
// Weights: current 0.5, prev1 0.3, prev2 0.15, prev3 0.05
smoothed = 0.5*raw + 0.3*prev1 + 0.15*prev2 + 0.05*prev3
// Step 3 — Apply sudden-change penalty if flagged this run
// Triggers: fee +50% or +5pt jump, self-bond -20% drop, uptime -5% crash
// Magnitude: -10 pts on detection, decays -7, -4, -1 over 3 runs
suddenPenalty = -10 if any flag triggered else (decaying remainder)
// Step 4 — Final score
score = clamp(smoothed + suddenPenalty, 0, 100)services/validators/scoring.ts in the FlareWatch codebase. Operators or researchers who want to inspect the implementation directly (rather than reading the prose + formulas above) — or who want to fork it for their own use — can email hello@flarewatch.io to request access. We'll publish the file as a standalone open-source package if there's real demand./api/cron/refresh-validators recomputes every active validator's score every 5 minutes. Inputs (P-Chain RPC reads, Flaremetrics, FSE, reward-scripts) are fetched fresh on every run.Flare does not slash validator stake. The entire penalty mechanism for validator misbehavior is reward forfeiture plus the FIP-10 passes system. There is no double-signing slash, no equivocation slash, no stake-destruction event we need to track. Validators who fail FIP-10 minimum conditions lose that epoch's rewards (full forfeit if at zero passes, otherwise lose one pass per protocol they failed); their staked principal is untouched.
That means the score has no "slashing history" dimension — there is no such history to track. What we DO track is the consequence of every minimum failure: the validator earned zero in that epoch, which is captured in epochsIncluded / epochsObserved. The v4.2 update on 2026-05-14 widened the score's reliability multiplier to use that ratio across the full FIP-10 minimums set (uptime, FSP signing, FTSO submission rate, FDC participation) so a validator failing any minimum is proportionally penalized in the Uptime dimension regardless of which axis failed.
FIP-10 minimum thresholds, sourced from dev.flare.network/network/fsp/rewarding: staking requires 80% uptime + 1M FLR active self-bond; FTSO anchor feeds require estimates within 0.5% of consensus median in 80% of rounds; FTSO block-latency feeds require submitting 80% of expected updates; FDC requires participating in 60% of voting rounds. Validators meeting the 80% uptime + 1M self-bond floor but below the 3M / 15M earn thresholds still receive rewards but can't accumulate passes — the gray zone surfaced via this card's passEligibility: "at-risk" classification.