Route reliability for Lightning payments. Built for the agentic economy.
SatRank is a trust oracle for the Lightning Network. Before each payment, an agent queries SatRank for a GO/NO-GO decision — one request, one answer, 1 sat via L402.
- Backed by a full bitcoind v28.1 node + LND, not Neutrino or gossip — every channel capacity is UTXO-validated.
- Tracks 13,913 active Lightning nodes (schema v15, post-migration), probes them every 30 minutes, publishes trust assertions on Nostr every 6 hours.
- ~60 % of the Lightning graph is unreachable in routing ("phantom nodes") — the exact rate varies probe-to-probe and is published live by
/api/stats. SatRank tells you which nodes are actually alive. - First NIP-85 provider bridging the Lightning payment graph into the Web of Trust. Every other NIP-85 implementation scores the Nostr social graph — SatRank scores who you can actually pay.
Any Nostr client can read SatRank's Lightning trust scores without an SDK, an API key, or a Lightning payment:
# 1) Install nak — the command-line Nostr utility (https://github.com/fiatjaf/nak)
go install github.com/fiatjaf/nak@latest
# 2) Fetch the 5 latest trust assertions for Lightning nodes
nak req -k 30382 -a 5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4 \
--limit 5 wss://relay.damus.io
# 3) The returned events include a `rank` tag (0-100 normalized trust score),
# a `verdict` tag (SAFE/RISKY/UNKNOWN), a `reachable` tag, five component
# scores, an alias, and the Lightning pubkey in the `d` tag. Parse any of
# them with jq or your favorite Nostr client.Prefer HTTP? SatRank also exposes the same data via a free GET /api/agents/top endpoint and a paid POST /api/decide endpoint gated by L402 (1 sat/query).
flowchart LR
subgraph Trust_Root["Trust root (on-prem)"]
BTC[bitcoind v28.1<br/>full node]
LND[LND<br/>+ gossip + QueryRoutes]
end
BTC --> LND
LND --> GC[Graph crawler<br/>hourly]
LND --> PC[Probe crawler<br/>every 30 min]
LND --> LP[LN+ crawler<br/>daily]
OBS[Observer Protocol<br/>5 min] --> SE
GC --> DB[(SQLite<br/>schema v15)]
PC --> DB
LP --> DB
DB --> SE[Scoring engine<br/>5 components + anti-gaming]
SE --> API[REST API<br/>40+ endpoints]
SE --> NP[NIP-85 publisher<br/>kind 30382:rank<br/>every 6h]
SE --> DVM[DVM<br/>kind 5900 → 6900]
NP --> R1((relay.damus.io))
NP --> R2((nos.lol))
NP --> R3((relay.primal.net))
DVM --> R1
DVM --> R2
DVM --> R3
API -->|free| Agents
API -->|1 sat L402<br/>via Aperture| L402[Aperture<br/>paywall]
L402 --> Agents[Autonomous agents]
R1 --> Clients[Any Nostr client<br/>nak / nostcat / njump]
R2 --> Clients
R3 --> Clients
User[Any Nostr user] -->|kind 10040<br/>trust declaration| R1
The full path: bitcoind → LND → crawlers/probes → scoring engine → NIP-85 publisher + L402 API + DVM → 3 relays / HTTP clients / autonomous agents. Every layer is reproducible from the code in this repo.
| Metric | Value | Source |
|---|---|---|
| Active Lightning nodes indexed | 13,913 | /api/stats totalAgents |
| Stale (not seen 90+ days, excluded from scoring) | 4,225 | /api/stats |
| Phantom rate (unreachable in routing) | ~60 % (live) | /api/stats phantomRate |
| Verified reachable | ~5,300-5,500 (live) | /api/stats verifiedReachable |
| Total channels | ~88,950 (live) | /api/stats totalChannels |
| Network capacity (validated) | ~9,630 BTC (live) | /api/stats networkCapacityBtc |
| Probes executed / 24 h | ~255,000-265,000 (live · 24 h rolling) | /api/stats probes24h |
| NIP-85 events published per cycle | ~2,400 (score ≥ 30) | crawler log, count=2424 on 2026-04-09 |
| Score snapshots stored | 921,968 | sqlite3 … 'SELECT COUNT(*) FROM score_snapshots' |
| Tests (vitest) | 464 / 34 files green | npm test |
| Schema version | v15 | SELECT * FROM schema_version |
Numbers are pulled live from /api/stats (free endpoint, no auth). The landing page at satrank.dev renders them client-side on every visit.
Composite score 0-100 computed from 5 weighted components:
| Component | Weight | Measures |
|---|---|---|
| Volume | 25 % | Channels (log scale, ref 500) or verified transactions (log) |
| Reputation | 30 % | Graph centrality (hubness + betweenness) + peer trust (BTC/channel). LN+ ratings as bonus, max +8 |
| Seniority | 15 % | Days since first seen, exponential growth with 2-year half-life |
| Regularity | 15 % | Multi-axis consistency over 7 d (uptime 70 % + latency stability 20 % + hop stability 10 %) |
| Diversity | 15 % | Unique peers (log, ref 500) or unique counterparties (log) |
Additional bonuses: verified-tx bonus up to +15, popularity bonus up to +10, LN+ ratings bonus up to +8.
Anti-gaming:
- Mutual attestation loop detection (A↔B) with 95 % penalty
- Circular cluster detection (A→B→C→A) with 90 % penalty
- Extended cycle detection via BFS (up to 4 hops) with 90 % penalty
- Minimum 7-day seniority required to attest
- Attester score weighting (PageRank-like recursion)
- Attestation source concentration penalty
Verdict thresholds (GET /api/agent/{hash}/verdict): SAFE ≥ 47 (post-v15 recalibration), UNKNOWN 30-46, RISKY < 30 or critical flags. See public/methodology.html for the full rationale.
# GO / NO-GO decision with success probability (1 sat via L402)
curl -X POST https://satrank.dev/api/decide \
-H "Content-Type: application/json" \
-d '{"target": "<hash>", "caller": "<your-hash>"}'
# Report transaction outcome (free — no L402)
curl -X POST https://satrank.dev/api/report \
-H "Content-Type: application/json" \
-H "X-API-Key: <key>" \
-d '{"target": "<hash>", "reporter": "<your-hash>", "outcome": "success"}'
# Agent profile with reports, uptime, rank
curl https://satrank.dev/api/profile/<hash>
# Real-time reachability check (free)
curl https://satrank.dev/api/ping/<ln-pubkey>
curl "https://satrank.dev/api/ping/<ln-pubkey>?from=<your-ln-pubkey>"curl https://satrank.dev/api/agent/<hash>/verdict
# Returns: SAFE / RISKY / UNKNOWN with confidence, flags, risk profile, personalized pathfinding| Method | Endpoint | Purpose | Cost |
|---|---|---|---|
| POST | /api/decide |
GO/NO-GO with success probability + personalized pathfinding | 1 sat |
| POST | /api/report |
Report transaction outcome (weighted by reporter score) | free (API key) |
| GET | /api/profile/:id |
Full agent profile with reports, uptime, rank | 1 sat |
| GET | /api/ping/:pubkey |
Real-time reachability (QueryRoutes live) | free |
| GET | /api/agent/:hash |
Detailed score + evidence | 1 sat |
| GET | /api/agent/:hash/verdict |
SAFE/RISKY/UNKNOWN + flags + risk profile | 1 sat |
| GET | /api/agent/:hash/history |
Score snapshots with deltas | 1 sat |
| GET | /api/agent/:hash/attestations |
Received attestations (list) | 1 sat |
| GET | /api/agents/top |
Leaderboard by score | free |
| GET | /api/agents/movers |
Top 7-day movers | free |
| GET | /api/agents/search?alias=… |
Search by alias | free |
| POST | /api/verdicts |
Batch verdicts for up to 100 hashes | 1 sat / batch |
| POST | /api/attestations |
Submit attestation | free (API key) |
| GET | /api/health |
DB status, schema version, agents indexed, uptime | free |
| GET | /api/stats |
Network statistics | free |
| GET | /api/docs |
Interactive Swagger UI | free |
| GET | /api/openapi.json |
OpenAPI 3 spec | free |
| GET | /.well-known/nostr.json |
NIP-05 handler (satrank@satrank.dev) | free |
Live Swagger UI: satrank.dev/api/docs.
SatRank is a NIP-85 Trusted Assertion provider. It publishes kind 30382:rank assertions for Lightning Network nodes, declares itself as a provider via kind 10040, operates a NIP-90 DVM for real-time trust checks, and exposes NIP-05 verification.
NIP-85 scope note. NIP-85's "User as Subject" is defined generically for a 32-byte pubkey; SatRank extends the semantics to Lightning node pubkeys (same secp256k1 format, different key space). The canonical rank tag is published alongside SatRank-specific tags so strict NIP-85 consumers can read assertions without SatRank-specific knowledge, while clients that want the richer signal can read the component tags too.
rank(0-100 normalized trust score) — the canonical NIP-85 tagverdict(SAFE/RISKY/UNKNOWN)reachable(boolean, from live probes)survival(stable / at_risk / likely_dead)- five component scores:
volume,reputation,seniority,regularity,diversity aliasand the Lightning pubkey in thedtag (replaceable events)
Frequency: every 6 hours. Nodes published per cycle: ~2,400 (score ≥ 30 on ~13,900 active).
Event format:
{
"kind": 30382,
"tags": [
["d", "<lightning_pubkey>"],
["n", "lightning"],
["rank", "94"],
["alias", "Kraken"],
["score", "94"],
["verdict", "SAFE"],
["reachable", "true"],
["survival", "stable"],
["volume", "100"],
["reputation", "79"],
["seniority", "87"],
["regularity", "100"],
["diversity", "100"]
],
"content": ""
}Query from any Nostr client:
["REQ", "satrank", {"kinds": [30382], "authors": ["5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4"]}]
SatRank publishes kind 30382 events in two indexed namespaces to serve both the letter and the spirit of NIP-85:
- Lightning-indexed (extension).
d = <66-char Lightning node pubkey>— the default stream. Emitted every 6 hours for every active node with score ≥ 30 (~2,400 events per cycle). This extends NIP-85's "User as Subject" semantics from 32-byte Nostr pubkeys to 66-byte Lightning pubkeys (same secp256k1, different key space). It's the stream that makes the Lightning payment graph queryable via NIP-85 for the first time. - Nostr-indexed (strict).
d = <64-char Nostr pubkey>— strictly conformant to NIP-85's subject-key requirement. Emitted for every (Nostr operator, Lightning node) pair SatRank can verify cryptographically.
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 the Lightning node that settled the payment — cryptographically guaranteed by the invoice signature. Cross-referencing p and payee_node_key gives a verifiable (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) via NIP-01 until-based pagination, walking up to 40 pages × 500 events per relay with a 60-day age wall.
First production run (2026-04-09): ~20,000 distinct zap receipts decoded, 302 distinct Lightning pubkeys extracted, 281 self-hosted candidates after the ≤5-nostr heuristic, 65 events published after all filters (score ≥ 30, not stale, custodian alias rejected, exactly 1 nostr pk per ln pk). With the relaxed ALLOW_SHARED_LNPK=1 mode: 94 events.
Custodian filtering. The same BOLT11 destination across many distinct Nostr pubkeys means a custodial wallet (Wallet of Satoshi, Alby, Minibits, …). SatRank drops any ln_pubkey paired with more than 1 distinct Nostr pubkey in the mining sample AND any node whose alias matches a known custodial/LSP pattern (zlnd*, lndus*, *coordinator*, *.cash, zeus, alby, wos, cashu, minibits, phoenix, breez, muun, primal, nwc, fountain, wavlake, fedi, fewsats, lightspark, voltage, strike, …). What remains is a conservative set of self-hosted operators.
Commands:
# 1) Mine zap receipts across the canonical relays (+ Primal cache), build the mapping
npx tsx scripts/nostr-mine-zap-mappings.ts
# → scripts/nostr-mappings.json
# 2) Preview the strict events that would be published from the mined mappings
DRY_RUN=1 DB_PATH=./data/satrank.db \
npx tsx scripts/nostr-publish-nostr-indexed.ts
# 3) Publish for real (once you're happy with the preview)
NOSTR_PRIVATE_KEY=<hex> DB_PATH=./data/satrank.db \
npx tsx scripts/nostr-publish-nostr-indexed.ts
# 4) One-shot self-declaration event: SatRank's own Nostr pk → SatRank's Lightning node
NOSTR_PRIVATE_KEY=<hex> DB_PATH=./data/satrank.db \
npx tsx scripts/nostr-publish-self-declaration.tsBoth streams carry the same score payload (rank, verdict, reachable, survival, 5 components). A strictly-conformant NIP-85 client can filter on "#d": ["<nostr_pubkey>"] and get a score assertion without needing any SatRank-specific knowledge of the Lightning key space.
Any Nostr user can declare SatRank as their trusted provider for Lightning node trust assertions by publishing 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. You can combine SatRank with other NIP-85 providers in the same kind 10040 event. For example, a client using Brainstorm (NosFabrica/brainstorm) for social-graph trust assertions can add SatRank's row to the same 10040 declaration:
{
"kind": 10040,
"tags": [
["30382:followRank", "<BRAINSTORM_PUBKEY>", "wss://relay.damus.io"],
["30382:rank", "5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4", "wss://relay.damus.io"]
],
"content": ""
}A client fulfilling 30382:rank queries will then receive both providers' assertions in parallel — social trust from Brainstorm, Lightning payment reliability from SatRank — with no SatRank-specific SDK or API key. This is the point of NIP-85: interoperability by design.
Quick-paste with nak:
nak event -k 10040 \
--tag '30382:rank;5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4;wss://relay.damus.io' \
--sec <your-nsec> wss://relay.damus.ioSatRank itself publishes a self-declaration kind 10040 from its service key (see scripts/nostr-publish-10040.ts) so clients have an on-chain reference example to copy.
Why rank is free (and /api/decide is not). Global scores are the trailer — great for discovery and social integration. The personalized /api/decide (pathfinding from YOUR position, survival, P_empirical) is the film — 1 sat via L402.
SatRank runs a DVM that responds to trust-check job requests on Nostr. Any agent can publish a kind 5900 event with ["j", "trust-check"] and ["i", "<ln_pubkey>", "text"], and SatRank responds with a signed kind 6900 containing score, verdict, and reachability. Free, no payment required. Unknown nodes trigger an on-demand QueryRoutes probe so the answer reflects the live graph.
satrank@satrank.devresolves to the service pubkey via/.well-known/nostr.json- Relays:
relay.damus.io,nos.lol,relay.primal.net - Service pubkey:
5d11d46de1ba4d3295a33658df12eebb5384d6d6679f05b65fec3c86707de7d4 npub1t5gagm0phfxn99drxevd7yhwhdfcf4kkv70stdjlas7gvuraul2q27lpl4
The script below queries each canonical relay for kind 0, 30382, and 10040 events and prints a per-relay summary. Non-zero exit if any of the three kinds is missing.
npx tsx scripts/nostr-verify.tsSatRank is designed to be consumed piecewise by anyone, with zero lock-in:
- Any Nostr client can read kind
30382:rankevents without an SDK, an API key, or a Lightning payment. - The NIP-90 DVM (kind 5900 → 6900) gives agents a signed real-time trust check over Nostr with zero account creation.
- The MCP Server exposes 12 tools (
decide,ping,report,get_profile,get_verdict,get_batch_verdicts,get_top_agents,search_agents,get_network_stats,get_top_movers,submit_attestation,get_agent_score) to any MCP-aware client (Claude Code, Cursor, VS Code, …). Listed on glama.ai/mcp/servers. - The open-source TypeScript SDK (
@satrank/sdk,sdk/) reduces the decide → pay → report loop to a singleclient.transact(…)call. - L402 / 402index.io. SatRank is listed on 402index.io as a paid L402 endpoint, discoverable alongside every other L402-enabled service.
- OpenAPI spec at
/api/openapi.jsonand interactive Swagger UI at/api/docsfor code generators.
SatRank exposes a Model Context Protocol server for agent-native access via stdio. Install in Claude Code:
claude mcp add satrank -- npx tsx src/mcp/server.tsOr with environment variables:
claude mcp add satrank -e DB_PATH=./data/satrank.db -e SATRANK_API_KEY=<key> -- npx tsx src/mcp/server.tsInstall in Cursor / VS Code by adding the following to .cursor/mcp.json or .vscode/mcp.json:
{
"mcpServers": {
"satrank": {
"command": "npx",
"args": ["tsx", "src/mcp/server.ts"],
"cwd": "/path/to/satrank",
"env": {
"DB_PATH": "./data/satrank.db",
"SATRANK_API_KEY": "your-api-key"
}
}
}
}Run manually:
npm run mcp # Development
npm run mcp:prod # Productionnpm install @satrank/sdkimport { SatRankClient } from '@satrank/sdk';
const client = new SatRankClient('https://satrank.dev');
// Full cycle in one line: decide → pay → report
const result = await client.transact('<target-hash>', '<your-hash>', async () => {
const payment = await myWallet.pay(invoice);
return { success: payment.ok, preimage: payment.preimage, paymentHash: payment.hash };
});
// result.paid, result.decision.go, result.report.weight
// Or step by step
const decision = await client.decide({ target: '<hash>', caller: '<your-hash>' });
const profile = await client.getProfile('<hash>');
const verdict = await client.getVerdict('<hash>');A fully commented walkthrough of the flow — health, stats, leaderboard, ping, paid decide, report, profile, Nostr distribution — lives in scripts/demo.sh:
BASE_URL=https://satrank.dev ./scripts/demo.shEvery curl is preceded by a plain-English banner explaining what the step is and why it matters. The script is designed to be recorded end-to-end for the WoT-a-thon video submission.
- TypeScript strict mode, Node 22
- Express — REST API (40+ endpoints)
- better-sqlite3 — embedded database, WAL mode, chunked retention cron
- bitcoind v28.1 + LND — trust root, gossip ingestion, QueryRoutes probing
- nostr-tools — NIP-85 publishing, NIP-90 DVM, NIP-01 event signing
- zod — input validation at every API boundary
- pino — structured logging
- Aperture / L402 — Lightning paywall for
/api/decideand scored endpoints - Docker Compose — api + crawler containers with cap-drop-ALL, read-only FS, tmpfs, healthchecks
- vitest — 464 unit + integration tests across 34 files, all green on the submission commit
| Script | Description |
|---|---|
npm run dev |
Development with hot reload (tsx watch) |
npm run build |
TypeScript compilation |
npm start |
Production |
npm test |
Tests (vitest) |
npm run lint |
TypeScript check (tsc --noEmit) |
npm run crawl |
Observer Protocol + LND graph + probe crawlers |
npm run crawl:cron |
Crawler in cron mode |
npm run mcp |
MCP server (dev) |
npm run mcp:prod |
MCP server (production) |
npm run purge |
Purge stale data |
npm run backup |
Database backup |
npm run rollback |
Database rollback |
npm run calibrate |
Scoring calibration report |
npm run demo |
Attestation demo script |
npm run sdk:build |
Build TypeScript SDK |
npm run nostr:verify |
Live kind 0 / 30382 / 10040 check against all 3 relays |
npm run nostr:publish-10040 |
Publish SatRank's NIP-85 self-declaration (requires NOSTR_PRIVATE_KEY) |
scripts/demo.sh |
End-to-end curl walkthrough against any base URL |
- Decision API — GO/NO-GO with success probability, outcome reports, agent profiles
- Personalized pathfinding — real-time route from caller to target via LND QueryRoutes
- Aperture integration (L402 reverse proxy) — monetize queries in sats
- Observer Protocol crawler — automatic on-chain data ingestion
- Lightning graph crawler — channel topology and capacity via LND node (bitcoind v28.1 UTXO-validated)
- Route probe crawler — reachability testing every 30 minutes
- TypeScript SDK for agents (
@satrank/sdk) - Verdict API — SAFE/RISKY/UNKNOWN binary decision
- MCP server — agent-native access via stdio (12 tools)
- Auto-indexation — unknown pubkeys indexed on demand
- NIP-85 publisher (kind 30382 with canonical
ranktag) - NIP-85 kind 10040 self-declaration script
- NIP-90 DVM for real-time trust checks (kind 5900 → 6900)
- NIP-05 verification (
satrank@satrank.dev) - Chunked retention cleanup cron for time-series tables
- v15 scoring calibration — multi-axis regularity, unique-peers diversity
- 4tress connector — verified attestations
- Trust network visualization dashboard
- Per-component NIP-85 keys (
30382:volume,30382:reputation, …)
SatRank is the reliability check before every Lightning payment. ~60 % of the Lightning graph is phantom nodes — we tell you which endpoints are alive, score them on Nostr, and gate the personalized decision behind a single satoshi.
Project: satrank.dev · NIP-05: satrank@satrank.dev · Code: github.com/proofoftrust21/satrank · License: MIT