FlareWatch assigns each FTSO data provider a 0–100 composite score across 13 dimensions. The math is deterministic, the inputs are public on-chain and Flare-ecosystem data[Flaremetrics] [FSE] [Flare Explorer], and the same algorithm applies to every provider — including FlareWatch's own FTSO provider, which is scored by this exact function with no special treatment. This page documents every dimension and threshold so delegators and operators can see exactly how the score is computed and why each value was chosen. Every claim here links back to its primary upstream source — see Sources & references at the bottom.
delegation mode on the validators page (delegating WFLR to FTSO data providers for FTSO inflation share). The validator score shown in staking mode (delegating FLR to a P-Chain validator for VRM + MIRROR rewards) uses a separate 9-dimension algorithm focused on validator operations — uptime, fee, reliability, etc. They're distinct on-chain roles with distinct rewards, scored separately. See Validator Score Methodology for the staking side.| 90+ | Top tier — top ~10–20% of FTSO providers. Typical profile: above-median reward rate, high accuracy, full V2 protocol participation (FTSO Scaling + Fast Updates + FDC), low/zero fee, large delegator base, MIRROR-paying validator nodes, named brand. No single dimension required — providers 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 (rate <= 0 || medianRate <= 0): score = 0 else: ratio = rate / medianRate score = min(25, round(ratio * 12.5 * 10) / 10) // 1 decimal place // Anomaly detection: providers > 3× the median are capped at // median × 3 for scoring purposes (prevents data outliers from // distorting the linear curve).
if (fseAccuracySecondary > 0): pct = fseAccuracySecondary / 100 if (pct >= 97) score = 25 elif (pct >= 95) score = 18 + (pct - 95) * 3.5 // 95 → 18, 97 → 25 elif (pct >= 93) score = 16 + (pct - 93) * 1 // 93 → 16, 95 → 18 elif (pct >= 90) score = 13 + (pct - 90) * 1 // 90 → 13, 93 → 16 elif (pct >= 85) score = 10 + (pct - 85) * 0.6 // 85 → 10, 90 → 13 elif (pct >= 80) score = 6 + (pct - 80) * 0.8 // 80 → 6, 85 → 10 else score = max(0, 2 + (pct - 70) * 0.4) elif (fseAccuracyPrimary > 0): // Primary fallback: similar piecewise linear curve. else: score = WEIGHT_ACCURACY / 2 // Neutral — no FSE data
recent = last 12 epochs where rewardRate > 0 if (recent.length < 3) return 10 // Neutral — insufficient recent history med = median(recent) cv = 1.4826 * median(|r - med|) / med score = max(0, round((1 - min(1, cv * 5)) * 20 * 10) / 10)
if (!isActive) score = 0 // see "Active" below
else:
score = 3 // active baseline
if (hasSubmitAddress AND hasSigningPolicyAddress AND voterRegistered):
score += 4 // V1 registered
if (fseFtsoScaling) score += 8/3 // ~2.67 each
if (fseFastUpdates) score += 8/3
if (fseFdc) score += 8/3
score = min(15, round(score * 10) / 10)anchor = max(lowest active fee observed, protocol fee floor) // FIP-16 sets a 20% minimum entity fee. All 98 providers charge // exactly 20%, so the anchor is 20% and nobody is docked for // charging the only fee the protocol permits. Same curve and // same anchor the validator page's Fee dimension uses. if (!isActive) score = 0 d = fee - anchor // distance ABOVE the best real offer if (d <= 0) score = 15 // at or below the anchor → full marks elif (d <= 5) score = 15 - d * 0.43 elif (d <= 10) score = 12.9 - (d - 5) * 0.64 elif (d <= 15) score = 9.6 - (d - 10) * 0.86 elif (d <= 20) score = 5.4 - (d - 15) * 1.07 else: score = 0 // extractive
if (no fseNodeIDs) score = 0 if (mirrorStatsMap empty) score = 12 / 2 = 6 // Neutral seed before data lands activeCount = nodes_with_status_active fraction = activeCount / fseNodeIDs.length base = 12 * fraction // Overperformance bonus per node, applied only when ALL gates pass: // - >= 30 days observed since first reading (firstObservedAtMs) // - >= 3 paid stake observations // - finite medianOverpaymentRatio // Bayesian shrinkage with prior k=5 toward 1.0 (neutral): // shrunken = (observedRatio * N + 1.0 * 5) / (N + 5) // Bucketed bonus from shrunken: // < 1.05 → 0 < 1.15 → 1 < 1.30 → 2 >= 1.30 → 3 avgBonus = sum_per_node(bonus) / fseNodeIDs.length score = round((base + avgBonus) * 10) / 10
if (count <= 5) score = 0 elif (count >= 500) score = 12 else: ratio = log(count / 5) / log(100) // maps [5, 500] → [0, 1] score = round(min(12, max(0, ratio * 12)) * 10) / 10
if (fseActive) score = 10 elif (rewardRate > 0) score = 7 else score = 0
change = abs(votePowerDailyChangePct * 100) if (change < 1) score = 10 elif (change < 3) score = 10 - (change - 1) * 1.5 // 1 → 10, 3 → 7 elif (change < 5) score = 7 - (change - 3) * 1.5 // 3 → 7, 5 → 4 elif (change < 10) score = 4 - (change - 5) * 0.4 // 5 → 4, 10 → 2 else score = max(0, 2 - (change - 10) * 0.1)
if (no fspData OR totalEpochs <= 0) score = 5 // active window = epochs from the provider's first paid epoch to now missed = (active-window epochs) - (epochs the provider was paid) score = max(0, 10 - missed * 3)
if (amount <= 0 OR maxAmount <= 0) score = 0 score = min(8, round((amount / maxAmount) * 8 * 10) / 10)
if (no name) score = 0 elif (name starts with 0x or matches hex regex) score = 0 elif (name.length < 4) score = 4 else score = 8
ownBond = operator's own P-Chain node bond (FLR) total = ownBond + delegated WFLR vote power alignment = piecewise-linear ratio curve (0% → 0 … ≥10% → 7) absolute = min(7, ownBond / 5,000,000 * 7) // saturates at 5M FLR score = max(alignment, absolute)
fspRewardRate, delegationFeePercentage, wNatWeight, and votePowerDailyChangePct.providersuccessrate.secondary for accuracy, plus the entityminimalconditionslatest.ftso_scaling / ftso_fast_updates / fdc flags for V2 status.RewardClaimed events with claimType=3 referencing your nodeIDs. If there are none recent for any of your nodes, you'll show as MIRROR-inactive on the score.// Step 1 — Raw composite (sum of 13 dimensions, max 177)
raw = rewardRate + accuracy + consistency + v2 + fee + mirror
+ delegators + participation + stability + compliance
+ distributed + identity + selfBond
// An UNMEASURED reward rate is not a FAILED one. When a provider is
// demonstrably distributing but we hold no rate figure for it, the
// Reward Rate weight leaves the DENOMINATOR rather than scoring 0
// against it — we score what we measured, over what we could measure.
maxRaw = (rewardRate figure missing AND isActive) ? 177 - 25 : 177
normalized = round((raw / maxRaw) * 100)
// Step 2 — Vote power dilution penalty
// Larger providers deliver lower per-FLR returns due to dilution.
// Penalty applied AFTER normalization.
if (votePower > 1_340_000_000) normalized -= 3
elif (votePower > 1_000_000_000) normalized -= 1
normalized = max(0, normalized)
// Step 3 — Dynamic weight redistribution
// Across the active provider set, dimensions where everyone clusters
// (stddev < 1.0) become non-discriminating. Their weight gets
// redistributed evenly across dimensions where the spread is wider
// (stddev >= 1.0). The redistribution recomputes the score:
//
// for each dim d:
// multiplier[d] = 1.0 if stddev[d] < 1.0
// = (weight[d] + bonus) / weight[d] otherwise
// bonus = sum(weights of low-stddev dims) / count(high-stddev dims)
//
// adjusted_raw = sum(breakdown[d] * multiplier[d])
// adjusted_max = sum(weight[d] * multiplier[d])
// final = round((adjusted_raw / adjusted_max) * 100)
// Step 4 — Final score (capped at 100)
score = min(100, final)services/ftso/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, which recomputes every active provider's score every 5 minutes (the same run that rescores P-Chain validators). Inputs (Flaremetrics, FSE, FSP rewards, V2 RewardManager events) are fetched fresh on every run.