Overview
SatRank produces a composite trust score between 0 and 100 for each agent, computed from 5 independently measurable components. The score is deterministic: the same inputs always produce the same output. All data sources are verifiable through public URLs.
Components and Weights
The final score is a weighted sum of 5 components, each scored 0–100:
score = volume * 0.25 + reputation * 0.30 + seniority * 0.15 + regularity * 0.15 + diversity * 0.15
| Component | Weight | Measures |
|---|---|---|
| Volume | 25% | Transaction throughput or channel count |
| Reputation | 30% | Graph centrality, peer trust (BTC/channel), attestations |
| Seniority | 15% | Time on the network since first observation |
| Regularity | 15% | Consistency of activity over time |
| Diversity | 15% | Breadth of counterparties or capacity |
Formulas
Volume (Lightning nodes)
Logarithmic scale with a fixed reference of 500 channels. Spreads the full 0–100 range across the real network distribution:
volume = min(100, round(log(channels + 1) / log(501) * 100))
5 channels: ~26. 20: ~48. 50: ~63. 100: ~74. 500+: 100.
Volume (Observer Protocol agents)
Logarithmic scale normalized to a reference of 1000 verified transactions:
volume = min(100, round(log(count + 1) / log(1001) * 100))
Reputation (Lightning nodes)
Combines two objective, on-chain signals: graph centrality and peer trust.
// Centrality (max 50 pts) — position in the network graph centrality = 25 * exp(-hubnessRank / 100) + 25 * exp(-betweennessRank / 100) // Peer trust (max 50 pts) — how much BTC others lock in channels with you btcPerChannel = capacitySats / 100_000_000 / channels peerTrust = min(50, round(log10(btcPerChannel * 100 + 1) / log10(201) * 50)) reputation = min(100, centrality + peerTrust)
Centrality measures how well-connected a node is in the Lightning graph. Peer trust measures how much real capital other nodes commit to channels with you — skin in the game. Neither signal depends on external platforms or user ratings.
LN+ community ratings (from lightningnetwork.plus) contribute a separate bonus of up to +8 points on the final score. This keeps the social signal without making 30% of the score depend on a third-party platform.
Reputation (Observer Protocol agents)
Weighted average of attestation scores with exponential time decay (half-life: 30 days). Each attestation's weight is multiplied by the attester's own score, normalized to [0, 1]. See Anti-Gaming for penalty mechanisms.
When no attestations exist yet, reputation is 0 and its 30% weight is redistributed
to the other 4 components (volume * 0.36 + seniority * 0.21 + regularity * 0.21 + diversity * 0.21).
This allows new agents to score fairly based on their transaction history alone, while
attestations build over time.
Seniority
Exponential growth curve with diminishing returns (2-year half-life):
days = (now - firstSeen) / 86400 seniority = round(100 * (1 - exp(-days / 730)))
30 days: ~4. 6 months: ~22. 1 year: ~39. 2 years: ~61. 3 years: ~78. 5 years: ~93.
Regularity (Lightning nodes)
Multi-axis consistency over 7 days. A node that is always reachable but whose latency varies wildly or whose routing paths flap is less reliable than one that is rock-steady, and the score reflects that. Pure uptime alone used to saturate at 100 for ~77% of scored agents and stopped differentiating the top cluster.
// Primary: multi-axis (requires >= 3 probes in 7 days)
regularity = uptime_ratio * 70 // reachable / total probes
+ latency_consistency * 20 // exp(-stddev/mean) across reachable latencies
+ hop_stability * 10 // 1 - clamp(stddev_hops / 3, 0, 1)
// Fallback: gossip recency (when < 3 probes available)
regularity = min(100, round(100 * exp(-daysSinceGossip / 90)))
Stable, fast, rock-steady: ~100. 100% uptime with noticeable latency jitter: ~85. 80% uptime with stable routes: ~86. 50% uptime: ~65. Dead: 0.
Regularity (Observer Protocol agents)
Inverse coefficient of variation (CV) of transaction intervals. Requires at least 3 transactions. Regular intervals score high; erratic patterns score low.
cv = stddev(intervals) / mean(intervals) regularity = min(100, round(100 * exp(-cv)))
Diversity (Lightning nodes)
Number of unique peers (distinct nodes sharing a channel), extracted from the validated LND graph. A node with 500 BTC concentrated on 2 peers offers no routing diversity; a node with 0.5 BTC spread across 20 peers is a real contributor to network health. Peer count is the real signal; capacity is a correlated but weaker proxy.
diversity = min(100, round(log(unique_peers + 1) / log(501) * 100))
1 peer: ~11. 10 peers: ~38. 50 peers: ~63. 200 peers: ~85. 500+ peers: 100.
Falls back to capacity-based scoring only for fresh agents the LND crawler has not yet indexed.
Diversity (Observer Protocol agents)
diversity = min(100, round(log(uniqueCounterparties + 1) / log(51) * 100))
Additional Bonuses
Agents with verified Observer Protocol transactions receive +0.5 points per verified tx, capped at +15. This applies to all agent types, including Lightning nodes that also have Observer Protocol activity.
Each scored query is a demand signal. The bonus is min(10, round(log2(queryCount + 1) * 2)). An agent queried 100 times receives +13 (capped at +10).
Positive ratings from LN+ contribute a bonus: min(8, round(log2(positive + 1) * ratingsRatio * 3)) where ratingsRatio = positive / (positive + negative + 1). This keeps the social signal as a reward, not a requirement.
Confidence Levels
Confidence indicates how much data backs the score. It is based on total data points (transactions, attestations, channels, and LN+ ratings):
| Level | Data Points | Interpretation |
|---|---|---|
| very_low | < 5 | Insufficient data. Score is unreliable. |
| low | 5 – 19 | Limited data. Use with caution. |
| medium | 20 – 99 | Moderate evidence. Score is indicative. |
| high | 100 – 499 | Substantial evidence. Score is reliable. |
| very_high | ≥ 500 | Extensive evidence. High trust in score accuracy. |
Data Sources
Primary source for agent-to-agent transactions. Provides transaction records with payment hashes, preimages, status (verified/pending/failed), and protocol type (L402, keysend, bolt11). Transactions are the basis for volume, regularity, and diversity computation for non-Lightning agents.
Full Lightning Network graph from our own LND node, backed by a full Bitcoin Core (bitcoind v28.1) node for channel validation. Currently indexing 13,913 active Lightning nodes — cryptographically verified against the Bitcoin UTXO set — with 4,225 stale nodes (not seen in 90+ days) preserved for history but excluded from scoring. Coverage grows automatically through auto-indexation: every query for an unknown node triggers background indexing. The more agents use SatRank, the more complete the graph becomes. All numbers here are mirrored from the live /api/stats endpoint — if this paragraph ever disagrees with the landing page, the landing page wins.
Lightning Network graph data via public API. Used as fallback when the LND node is unavailable or not synced. Limited to top 100 nodes by connectivity. Verifiable at https://mempool.space/lightning/node/{publicKey}.
Community reputation platform. Provides positive/negative ratings and graph centrality metrics (hubness, betweenness, hopness). Centrality data feeds the reputation component; user ratings contribute a separate bonus (max +8 pts). Verifiable at https://lightningnetwork.plus/nodes/{publicKey}.
Periodic reachability checks via LND’s QueryRoutes API. Each indexed node is probed every 30 minutes to determine whether a route exists from SatRank’s node. Hot nodes — those recently queried via /decide or /ping within the last 2 hours — are probed first in each cycle so live traffic gets the freshest data. The unreachable flag in the verdict comes from probe data. Uptime ratios over 7 days are computed and exposed in the agent profile.
Data Validation
All channel data is validated against the Bitcoin UTXO set via a full bitcoind
node (v28.1). This means every channel’s capacity is cryptographically verified —
not estimated from gossip alone. Nodes running lightweight backends (Neutrino, SPV) cannot
perform this validation, which is why many Lightning explorers report inflated node counts
and phantom channels.
SatRank’s full-node validation reduced the apparent graph by 4,225 nodes to 13,913 verified active nodes. The excluded nodes are stale (not seen in 90+ days) and are preserved in the database but excluded from scoring, publishing, and statistics. If a stale node reappears in the gossip with a recent update, it is automatically restored to the active set — no history is lost.
This is the reason SatRank’s numbers will diverge from other Lightning dashboards that rely on Neutrino or SPV. Both pictures are internally consistent; only one is validated end-to-end against Bitcoin.
Anti-Gaming Mechanisms
SatRank employs multiple layers of defense against score manipulation. The mechanisms are described conceptually below; exact thresholds are proprietary to prevent circumvention.
- Mutual attestation detection. When agent A attests agent B and B attests A, both attestations are detected and heavily penalized. The effective score from these attestations is capped.
- Circular cluster detection. Cycles up to depth 4 (A→B→C→D→A) are identified via BFS. Attestations from cycle members receive reduced weight and capped effective scores.
- Attestation concentration. When a disproportionate number of attestations come from too few unique attesters, a penalty is applied. This discourages sybil attestation farms.
- New attester weight reduction. Attesters younger than a minimum age carry significantly reduced weight. This prevents rapid bootstrapping of fake reputation.
- Attester reliability weighting. Each attester's weight is proportional to their own score. Unknown attesters (score 0) carry minimal weight.
- Temporal decay. Attestation weight decays exponentially over time. Old attestations matter less than recent ones. This ensures scores reflect current behavior.
- Negative ratings dilution. Community negative ratings on LN+ reduce the LN+ bonus. A node with more negatives than positives receives no bonus.
- Manual source penalty. Agents registered manually (not discovered via protocol or graph) face a linear penalty until they accumulate sufficient verified transactions.
Limitations & Centralization Risk
Our scores correlate with node size. Volume, capacity, seniority, and peer trust all favor large, established nodes. Using the total score as the sole routing criterion will concentrate traffic on dominant nodes.
We recommend agents use individual components and evidence to make nuanced decisions.
A small node with high reputation and low volume may be a better choice for network
resilience than a large node with a high total score. The /agents/top?sort_by=reputation
endpoint lets you discover these nodes directly.
The total score measures individual reliability, not value to the network. These are different things.
As the attestation system matures, peer-to-peer trust signals will increasingly complement infrastructure metrics, enabling trust to emerge from consensus rather than size.
Evidence and Verification
Every score response includes an evidence object containing verifiable source data:
- Transactions: Total count, verified count, and a sample of the 5 most recent transactions with tx IDs, protocol, amount bucket, and verification status.
- Lightning Graph: Public key, channel count, capacity in sats, and a link to
mempool.spacewhere you can independently verify. - Reputation: Hubness and betweenness centrality ranks, BTC per channel (peer trust), and a link to
lightningnetwork.plusfor independent verification of community ratings. - Popularity: Query count and the computed popularity bonus, showing how much demand contributed to the score.
The evidence structure enables any consumer to cross-check the score against primary sources without trusting SatRank exclusively.
Worked Example: ACINQ
ACINQ is a major Lightning node operator with ~2,004 channels and hundreds of BTC of verified capacity. Here is how the score is computed with full-node validation:
| Component | Input | Score | Weighted |
|---|---|---|---|
| Volume | 2,004 channels (log scale, ref 500) | 100 | 100 × 0.25 = 25.0 |
| Reputation | Hubness + betweenness top-10, peer trust from validated capacity | ~77 | 77 × 0.30 = 23.1 |
| Seniority | ~2,921 days (8 years, half-life 730d) | ~98 | 98 × 0.15 = 14.7 |
| Regularity | Reachable every probe, ~5% latency jitter, stable 1-hop routes | ~85 | 85 × 0.15 = 12.75 |
| Diversity | ~1,800 unique peers (validated against the LND graph) | 100 | 100 × 0.15 = 15.0 |
| Base score | ~91 | ||
| + Popularity bonus (log2-scaled queries) | +2 | ||
| Final score | ~93* | ||
* Values updated April 2026 after the v15 scoring calibration. Under pre-v15 scoring, ACINQ had regularity=100 (uptime only) and received two probe bonuses (+3 low-latency, +2 short-hop) that duplicated the regularity signal, landing at 100. The new multi-axis regularity reflects natural latency variance and route stability, and the probe bonuses are removed; the resulting 93 is the honest measure of ACINQ’s reliability. Actual scores are computed in real-time and may differ slightly. ACINQ has 0 LN+ ratings, so no LN+ bonus is applied.
Verdict API — SAFE / RISKY / UNKNOWN
An AI agent evaluating a 10,000 sat transaction has less than 200ms to decide. A composite score from 0 to 100 requires interpretation — what threshold is safe? How should a score of 47 be treated differently from 52? These are questions an autonomous agent should not have to answer in real-time.
The Verdict API (GET /api/agent/{hash}/verdict) returns a single, actionable
decision: SAFE, RISKY, or UNKNOWN.
No interpretation required.
Decision Logic
| Verdict | Condition |
|---|---|
| RISKY | Score < 30 with confidence ≥ low (0.25), OR delta_7d < −15, OR negative_reputation flag, OR fraud_reported flag |
| SAFE | Score ≥ 47, no critical flags, confidence ≥ medium (0.5) |
| UNKNOWN | Agent not found, OR score 30–46, OR score < 30 with very low confidence, OR score ≥ 47 with confidence < medium |
Why 47, not 50? Until v15 the SAFE threshold was 50 on a 0–100 scale that practically saturated at 100 (seven nodes tied at the cap). The v15 calibration replaced two proxies with real signals: diversity now measures unique peers from the validated LND graph instead of BTC capacity, and regularity now measures multi-axis consistency (uptime, latency, route stability) instead of uptime alone. This corrected roughly 1,000 nodes whose previous scores were inflated by the old proxies — nodes with concentrated BTC on a handful of peers, for instance, saw their diversity drop from 100 to about 18. The best nodes realistically score 93–94 post-calibration, so the SAFE threshold was moved from 50 to 47 to preserve the exact semantic: “SAFE = top 50% of the theoretical maximum.” 47/94 ≈ 50/100. The meaning of SAFE is unchanged; the number follows the new scale.
UNKNOWN is the honest answer when data is insufficient to commit either way. A score of 42
with low confidence could be a legitimate new node or a sophisticated fake — the system
signals uncertainty rather than forcing a premature judgment. Agents receiving UNKNOWN should
retry after the node accumulates more history, or use caller_pubkey for a
personal trust graph assessment.
Confidence Mapping
Confidence is a numeric value (0–1) derived from the existing confidence level:
very_low=0.1, low=0.25, medium=0.5,
high=0.75, very_high=0.9. UNKNOWN agents return
confidence 0.
Flags
Flags provide machine-readable context for the verdict:
new_agent— first seen less than 30 days agolow_volume— fewer than 10 transactionsrapid_decline— score dropped more than 10 points in 7 daysrapid_rise— score increased more than 15 points in 7 daysnegative_reputation— negative ratings exceed positive ratingshigh_demand— queried more than 50 times (popular agent)no_reputation_data— no LN+ data availablefraud_reported— at least one attestation with category “fraud”dispute_reported— at least one attestation with category “dispute”unreachable— no route from SatRank’s node to the target (fresh probe only)unreachable_from_caller— no route from the caller’s position to the target (requirescaller_pubkey, see Pathfinding)stale_gossip— no gossip update in 7–14 dayszombie_gossip— no gossip update in 14+ dayscapacity_drain— capacity dropped 30%+ in 24 hourssevere_capacity_drain— capacity dropped 50%+ in 24 hours
Structured Attestation Categories
When submitting attestations via POST /api/attestation, agents can now include
a category field: successful_transaction,
failed_transaction, dispute, fraud,
unresponsive, or general (default).
Attestations with category fraud or dispute automatically trigger the
corresponding flag in the verdict, ensuring the agent is classified as RISKY
regardless of their score. This is the foundation for a decentralized dispute resolution layer.
Attestations are free. They are the fuel of the trust network. Submitting attestations requires an API key for identity verification, but no Lightning payment (no L402). The more agents attest, the more accurate the scoring becomes for everyone.
Auto-Indexation
When you query /agent/{pubkey} or /verdict for an unknown Lightning
pubkey (66 hex characters starting with 02 or 03), SatRank returns
202 Accepted and begins indexing that node in the background via our LND graph
connection. Retry after 10 seconds to get the score.
This means the index grows organically as agents query new counterparties — usage drives coverage. The system is rate-limited (configurable, default 10 auto-indexes per minute) to prevent abuse, and deduplicates concurrent requests for the same pubkey.
Batch verdicts (POST /api/verdicts) also trigger auto-indexation for unknown
Lightning pubkeys, capped at 2 per batch to prevent a single request from consuming the
entire rate budget.
Batch Verdict
POST /api/verdicts accepts up to 100 hashes in a single request, returning an
array of verdicts. This is 10x more efficient than individual queries for agents that need to
screen multiple counterparties before transacting. Unknown Lightning pubkeys trigger
auto-indexation automatically.
Personal Trust Graph
A global score tells you what the network thinks. A personal trust graph tells you what your network thinks. This is the difference between “score 72” and “3 agents you trust have positively attested this node.”
When calling GET /api/agent/{hash}/verdict, pass your own pubkey hash via the
caller_pubkey query parameter or X-Caller-Pubkey header. SatRank
traverses the attestation graph to compute a trust distance:
| Distance | Meaning | How it’s computed |
|---|---|---|
| 0 | Direct trust | You have positively attested this agent (score ≥ 70) |
| 1 | Friend-of-friend | An agent you positively attested has also positively attested the target |
| 2 | Two degrees | An agent attested by someone you trust has attested the target |
| null | No connection | No path found within 2 hops in the attestation graph |
The response also includes sharedConnections (number of mutual trusted agents)
and strongestConnection (alias of the highest-scoring shared agent).
The trust graph builds naturally as agents submit attestations after transactions. No registration or explicit “follow” mechanism is needed — the attestation history is the social graph.
If caller_pubkey is not provided, personalTrust is null
and the verdict uses only the global score. There is no regression for existing consumers.
Risk Profiles
A score of 55 for a 3-year-old hub means something very different than 55 for a 10-day-old node. Risk profiles classify agents into behavioral categories based on observable properties, giving the verdict context that a single number cannot convey.
| Profile | Risk | Criteria |
|---|---|---|
established_hub |
Low | Score ≥ 70, seniority > 365 days, > 200 transactions |
small_reliable |
Low | Score 40–69, seniority > 365 days, regularity > 60 |
growing_node |
Medium | delta_7d > 10, seniority < 180 days |
new_unproven |
High | Seniority < 30 days, total transactions < 5 |
declining_node |
High | delta_7d < −10, trend = falling |
suspicious_rapid_rise |
High | delta_7d > 20, seniority < 60 days |
default |
Unknown | Does not match any specific profile |
Profiles are evaluated in priority order (suspicious patterns first, then established trust,
then growth). The first match wins. The profile is a complement to the verdict,
not a replacement — an established_hub classified as RISKY due to a fraud
attestation is more alarming than a new_unproven classified as RISKY for having
no history.
Each profile includes a human-readable description with the specific values
that triggered the classification (e.g., “Active for 45 days, rapid score increase
(+18 in 7d). Monitor closely.”).
Score History & Deltas
A score is a snapshot. A delta is a trajectory. SatRank stores score snapshots over time and computes deltas at three intervals:
| Field | Window | Use Case |
|---|---|---|
delta24h | 24 hours | Detect sudden drops or spikes after an incident |
delta7d | 7 days | Primary trend signal. Used by verdict logic and risk profiles |
delta30d | 30 days | Long-term trajectory. Identifies sustained improvement or degradation |
All deltas are null when no historical snapshot exists for the given window.
A delta of 0 means the score was measured at both endpoints and did not change.
Trend Direction
Derived from delta7d:
if delta7d is null or between -2 and +2 → stable if delta7d > 2 → rising if delta7d < -2 → falling
The ±2 dead zone prevents noise from minor score fluctuations. Only meaningful
directional changes register as rising or falling.
History Endpoint
GET /api/agent/{hash}/history returns paginated score snapshots with per-entry
deltas (difference from previous snapshot), plus an aggregate delta summary for the agent:
{
"data": [
{ "score": 72, "components": {...}, "computedAt": "2026-04-01T...", "delta": +3 },
{ "score": 69, "components": {...}, "computedAt": "2026-03-31T...", "delta": -1 },
...
],
"delta": { "delta24h": 3, "delta7d": 8, "delta30d": 12, "trend": "rising" },
"meta": { "total": 45, "limit": 20, "offset": 0 }
}
Top Movers
GET /api/agents/movers returns the 5 agents with the largest score increases
and the 5 with the largest decreases over the past 7 days. This surfaces emerging trust
signals and developing problems before they reach the individual agent level.
Alerts
The score response (GET /api/agent/{hash}) includes an alerts
array with machine-readable signals about the agent's current state. Alerts are computed
from deltas, age, and activity:
| Alert Type | Severity | Trigger |
|---|---|---|
score_drop |
warning or critical | delta_7d ≤ −10 (warning) or ≤ −20 (critical) |
score_surge |
info | delta_7d ≥ +15 |
new_agent |
info | First seen ≤ 7 days ago |
inactive |
info | Last seen ≥ 60 days ago |
Alerts complement verdicts. A SAFE agent with a score_drop alert
is still safe today but may not be next week. A score_surge on a new agent
warrants the same scrutiny as the suspicious_rapid_rise risk profile.
Alerts differ from verdict flags: flags affect the verdict decision itself
(fraud_reported forces RISKY), while alerts are informational and do not
change the verdict outcome.
Personalized Pathfinding
Probe data tells you whether our node can reach a target. Personalized pathfinding tells you whether you can reach the target. This is the difference between “reachable from SatRank” and “reachable from your position in the graph.”
When calling GET /api/agent/{hash}/verdict with a caller_pubkey,
SatRank performs a real-time route query from the caller’s position to the target using
LND’s QueryRoutes API with the source_pub_key parameter.
No payment is sent — this is a read-only graph operation.
How It Works
- The caller provides their pubkey hash (or 66-char Lightning pubkey) via
caller_pubkey - SatRank looks up both the caller’s and target’s Lightning public keys in the agents table
- If both are indexed Lightning nodes, SatRank calls
GET /v1/graph/routes/{target}/{amt}?source_pub_key={caller}on our LND node - LND computes Dijkstra pathfinding on the public Lightning graph from the caller’s topological position
- The result is returned as a
pathfindingfield in the verdict response
Response Fields
| Field | Type | Description |
|---|---|---|
reachable | boolean | Whether a route exists from the caller to the target |
hops | integer | null | Number of hops in the best route |
estimatedFeeMsat | integer | null | Estimated routing fee in millisatoshis |
alternatives | integer | Number of alternative routes found |
latencyMs | integer | Route computation time in milliseconds |
source | string | Always lnd_queryroutes |
When Pathfinding Is Unavailable
pathfinding is null when: caller_pubkey is not provided,
the caller is not an indexed Lightning node, the target has no Lightning pubkey, or the LND
node is unreachable. The verdict still returns normally using cached probe data and the global
score — pathfinding is additive, never blocking.
The unreachable_from_caller Flag
When pathfinding finds no route from the caller to the target, the flag
unreachable_from_caller is added to the verdict. This is distinct from the
unreachable flag (no route from our node). A target can be reachable
from SatRank but not from the caller, or vice versa.
Caching
Results are cached for 5 minutes per (caller, target) pair. The Lightning graph topology changes slowly enough that a 5-minute TTL provides accurate results while keeping LND query volume manageable.
Competitive Advantage
No free Lightning service provides personalized pathfinding as an API. mempool.space shows the graph. Amboss shows node statistics. 1ML shows connectivity. None of them answer the question: “Can I pay this node, and what will it cost me?”
Decision Engine
Scores answer “how trustworthy is this agent?” The decision engine answers “should I transact with this agent right now?” It combines the trust score with real-time signals — route availability, probe uptime, and historical outcomes — into a single GO / NO-GO decision.
| Endpoint | Auth | Description |
|---|---|---|
POST /api/decide | 1 sat | GO / NO-GO with success probability |
POST /api/report | API Key | Report transaction outcome (free) |
GET /api/profile/{id} | 1 sat | Agent profile with reports, uptime, rank |
Decide
The decide endpoint returns a boolean go (true/false) and a
successRate (0–1) composed from 4 probability signals:
P_trust = sigmoid(score, midpoint=50, steepness=0.1) // SatRank score → probability P_routable = 1.0 if pathfinding finds a route, else 0.0 // Can I reach this node? P_available = probe uptime over 7 days (0-1) // Is this node online? P_empirical = weighted success rate from past reports // Did other agents succeed?
When fewer than 10 reports from 5+ unique reporters exist (proxy mode):
successRate = P_trust * 0.4 + P_routable * 0.3 + P_available * 0.3
When 10+ reports from 5+ unique reporters exist (empirical mode):
successRate = P_empirical * 0.45 + P_trust * 0.10 + P_routable * 0.225 + P_available * 0.225
P_trust is kept at 10% even in empirical mode as a safety net. Reports measure past success, but a node whose infrastructure is degrading (channels closing, probe instability) should still trigger a warning even if its last 15 reports were positive.
The threshold requires both sufficient data and diverse reporters — 10 reports from a single agent do not trigger empirical mode. This prevents self-reporting manipulation.
go = true when successRate ≥ 0.5 and no critical flags
(fraud_reported, negative_reputation).
Report
After a transaction, agents report the outcome: success,
failure, or timeout. Reports are
free (no payment required) to maximize participation.
- Reporter weighting: Each report’s weight is proportional to the reporter’s own SatRank score (floor 0.3, cap 1.0). High-trust reporters have more influence.
- Preimage verification: If the reporter provides
paymentHash+preimageand SHA256(preimage) = paymentHash, the report receives a 2× weight bonus. This cryptographic proof of payment makes the report unforgeable. - Deduplication: Maximum 1 report per (reporter, target) per hour.
- Rate limiting: Maximum 20 reports per reporter per minute.
- Self-report rejection: An agent cannot report on itself.
Feedback Loop
The decide → pay → report cycle creates a closed feedback loop.
The more agents report outcomes, the more accurate P_empirical becomes,
and the better future decisions perform. This is SatRank’s flywheel:
usage generates data, data improves decisions, better decisions attract more usage.
Predictive Signals
Scores describe the present. Predictive signals answer: will this node still be here in 7 days? All signals below are derived from data already in the index — no external dependencies, no machine learning.
Survival Score (0–100)
Predicts whether a node will remain reachable, based on three weighted signals:
| Signal | Weight | Source | Thresholds |
|---|---|---|---|
| Score Trajectory | 40% | Score snapshots (7-day slope) | Slope < −2/day: −40. Slope −1 to −2: −20. Stable/positive: 0. |
| Probe Stability | 40% | Probe results (uptime ratio) | 0% uptime: −40. < 50%: −30. < 80%: −15. ≥ 80%: 0. |
| Gossip Freshness | 20% | Last gossip timestamp | > 14 days: −20 (zombie). > 7 days: −10 (stale). ≤ 7 days: 0. |
survival_score = clamp(0, 100, 100 + sum(adjustments)) prediction = score > 70 ? "stable" : score > 40 ? "at_risk" : "likely_dead"
Channel Flow
Net change in channel count and capacity over 7 days. A node losing 3+ channels/week
is declining. A node gaining 3+ is growing. Derived from
hourly channel snapshots stored during each LND graph crawl.
Capacity Drain Rate
Percentage change in total capacity over 24 hours and 7 days. A drain rate
≤ −30% triggers the capacity_drain flag. ≤ −50% triggers
severe_capacity_drain. A node hemorrhaging capacity is likely closing
channels or being force-closed by peers.
Fee Volatility Index (FVI)
Measures how often a node changes its fee policies. Computed as the average number
of fee changes per channel per day, normalized to 0–100. Interpretation:
< 10 = stable, < 30 = moderate,
≥ 30 = volatile. A node that spikes fees from 1 ppm to 1000 ppm
overnight is volatile and may indicate distress or adversarial behavior.
Gossip Freshness Flags
Two flags based on the last gossip update timestamp:
stale_gossip— no gossip update in 7–14 days. The node may be going offline.zombie_gossip— no gossip update in 14+ days. The node is almost certainly dead but its channels haven’t expired yet.
L402 Payment as Data
Accessing scored endpoints requires an L402 micropayment of 1 satoshi. This is not just an access control mechanism — it is a data point.
Each paid query is an anonymous, permissionless signal of demand. An agent that is queried frequently is one that others care about — partners evaluating transactions, services checking trust, or researchers monitoring the network. This demand signal is captured in the popularity bonus (max +10 points).
Unlike page views or API hits, L402 queries have a nonzero cost, which filters out noise. A bot scraping every agent generates signal at 1 sat per query — sustainable for legitimate research, expensive for spam.
Nostr Distribution (NIP-85)
SatRank publishes trust scores as Nostr Trusted Assertions (NIP-85, kind 30382).
Every 6 hours, scores for all active agents above threshold (score ≥ 30) are published
to three public relays:
wss://relay.damus.iowss://nos.lolwss://relay.primal.net
Each event carries the NIP-85 canonical rank tag (normalized 0-100 score) alongside
SatRank-specific tags: the 5 scoring components (volume, reputation, seniority, regularity,
diversity), verdict (SAFE / RISKY / UNKNOWN), reachability, and a survival prediction. Strict
NIP-85 consumers read rank without needing SatRank-specific knowledge; clients
that want the richer signal read the component tags too. Events are replaceable (same
d-tag per agent pubkey), so clients always see the latest score with no duplication.
Scope note. NIP-85’s “User as Subject” is defined for a 32-byte pubkey. SatRank extends the semantics to Lightning node pubkeys — same secp256k1 format, different key space. We’re the first provider to bridge the Lightning payment graph into the NIP-85 ecosystem; every other implementation operates on the Nostr social graph.
Dual publishing — extension + strict conformance
Because NIP-85 as written targets 32-byte Nostr pubkeys and SatRank’s primary signal
targets 66-byte Lightning pubkeys, we publish kind 30382 events in two
indexed namespaces so clients can read either one:
-
Stream A — Lightning-indexed (extension, ~2,400 events / cycle).
d = <lightning_pubkey>. The default broad stream covering every active Lightning node with score ≥ 30. Clients that understand Lightning pubkeys can query the entire payment graph this way. -
Stream B — Nostr-indexed (strict NIP-85).
d = <nostr_pubkey>. Published for every(nostr_pubkey, ln_pubkey)pair SatRank can verify cryptographically. A strict NIP-85 client filtering on{kinds:[30382], authors:[<satrank>], "#d":[<user_npub_hex>]}gets a trust assertion about that Nostr user’s Lightning operations, with zero SatRank-specific knowledge of the Lightning key space.
How we build the mapping. NIP-57 zap receipts (kind 9735)
contain the recipient’s Nostr pubkey in the p tag and the paid invoice in
the bolt11 tag. The BOLT11 invoice’s destination (payee_node_key)
is cryptographically embedded in the invoice signature — we decode it and cross-reference
with the p tag to get a verified (nostr_pubkey, ln_pubkey) tuple. The
miner paginates backwards across 6 relays
(damus.io, nos.lol, relay.primal.net,
relay.nostr.band, nostr.wine, relay.snort.social)
using NIP-01 until-based pagination, walking up to 40 pages of 500 events per
relay with a 60-day age wall. The first paginated run processed ~20,000 distinct receipts
(100 % BOLT11-decodable), resolved 302 distinct Lightning pubkeys, and produced 281
self-hosted candidates — from which 65 survive the production filter below.
Custodian filter. Any ln_pubkey paired with more than 1 distinct Nostr
pubkey in the mining sample is rejected as likely custodial or shared (multiple users on
the same wallet provider), and any node whose alias matches known custodial / LSP patterns
(zlnd*, lndus*, *coordinator*, *.cash,
zeus, alby, wos, cashu,
minibits, phoenix, breez, muun,
primal, nwc, fountain, wavlake,
fedi, fewsats, lightspark, voltage,
strike, …) is also rejected. What remains is a conservative set of
self-hosted operators, each paired with exactly the Nostr identity that zaps them. An
optional relaxed mode (ALLOW_SHARED_LNPK=1) publishes up to 5 nostr pubkeys
per ln_pubkey for shared family/team nodes; the default is strict = 1.
Stream B events carry the same rank / verdict /
component tags as Stream A, plus ln_pubkey (for traceability back to the
Lightning node), subject_type (mined_mapping or
self_declaration), source (nip57_zap_receipt), and
zap_count (evidence strength). Clients already consuming Stream A need zero
extra code to parse Stream B — they’re identical except for the d tag.
The mining and publishing logic live in
scripts/nostr-mine-zap-mappings.ts and
scripts/nostr-publish-nostr-indexed.ts; the one-shot self-declaration
(SatRank’s own Nostr pubkey → SatRank’s own Lightning node) is in
scripts/nostr-publish-self-declaration.ts.
Why Nostr?
Nostr is the natural distribution layer for trust data in the Bitcoin ecosystem:
- Sovereign identity. Agents sign their own events; no platform dependency.
- Relay redundancy. Data replicates across multiple relays with no single point of failure.
- Native discovery. Agents already transact via Lightning on Nostr (zaps, DVMs), so they find SatRank in the infrastructure they already use.
- Standard format. NIP-85 is an established protocol for trust assertions; consumers don’t need SatRank-specific SDKs.
DVM Trust-Check (NIP-90)
SatRank also operates a Data Vending Machine (NIP-90) for real-time trust queries.
Publish a kind 5900 job request with ["j", "trust-check"]
and ["i", "<ln_pubkey>", "text"] tags, and SatRank responds with
a signed kind 6900 event containing the trust score, verdict, and
reachability. Unknown nodes trigger an on-demand QueryRoutes probe so
the answer reflects the live graph, not just the cached snapshot. The DVM is
discoverable via a kind 31990 handler-info event published to the
three canonical relays on every startup. Free, no payment required.
Unique Position
Every other NIP-85 implementation (Brainstorm, wot-scoring, nostr-wot-sdk,
nostr-wot-oracle, Vertex) operates on the Nostr social graph — scoring based on
follows, mutes, and zaps. SatRank is the only provider that bridges the Lightning
payment graph into NIP-85. Because Brainstorm and SatRank both use the canonical
rank tag on kind 30382 events, a wallet or agent can list
both in its kind 10040 and receive social trust and payment
reliability from the same Nostr relay connection — verified live on
relay.damus.io and nos.lol, where both providers publish.
Declaring SatRank as a Trusted Provider (kind 10040)
NIP-85 kind 10040 is the user-side declaration that lists your trusted providers
for each <kind:tag> pair. To add SatRank to your own trusted provider list, publish a
kind 10040 event with one tag per (provider, relay) combo:
{
"kind": 10040,
"tags": [
["30382:rank", "5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4", "wss://relay.damus.io"],
["30382:rank", "5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4", "wss://nos.lol"],
["30382:rank", "5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4", "wss://relay.primal.net"]
],
"content": ""
}
Multiple relay entries are independent — a client picks whichever relay is reachable
for the user. You can combine SatRank with other NIP-85 providers in the same
kind 10040 event: each provider gets its own row, and clients query all of them
in parallel.
SatRank itself publishes a self-declaration kind 10040 from its service key so
clients have an on-chain reference example to copy. Query it with:
["REQ", "satrank-10040", {
"kinds": [10040],
"authors": ["5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4"]
}]
Live circuit — kind 0 / 30382 / 10040
This page is not just documentation — it is an end-to-end probe of the SatRank Nostr
circuit. On every visit, your browser opens a WebSocket to each of the three canonical relays
(relay.damus.io, nos.lol, relay.primal.net) and issues a
REQ for kind 0 (profile), kind 30382 (trusted assertions) and kind 10040 (trusted
provider declaration) authored by SatRank’s service pubkey. The results below are the
real events currently on the network — if the circuit is broken, this block tells you
directly. If the self-declaration has not yet been published, the kind 10040 count will
honestly read 0 until the script in scripts/nostr-publish-10040.ts
is run.
NIP-05: satrank@satrank.dev
Pubkey: 5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4