Skip to content

Commit bf9882d

Browse files
docs: cross-file coherence pass — endpoint costs, crawl intervals, retention, DVM format
Audited every doc (README, IMPACT-STATEMENT, DEPLOY.md, INTEGRATION.md, methodology.html, index.html, mcp-server.json, .env.example) against the source of truth (src/config/*.ts, src/routes/*.ts, src/nostr/dvm.ts, live prod) and fixed the remaining inconsistencies. README.md - `/api/agent/{hash}/attestations` listed as free → corrected to **1 sat**. Source of truth: `src/routes/attestation.ts:20` wires the endpoint through the `apertureGateAuth` middleware, and live prod responds 402 without an L402 token. INTEGRATION.md already had this right. public/index.html - `/api/agent/{hash}/attestations` added to the "Detailed Queries (1 sat)" section so the landing page matches README's endpoint table. IMPACT-STATEMENT.md - Business-model table: `/api/agent/:hash/attestations` added to the 1-sat row alongside the other detailed queries. public/methodology.html - "Unique Position" list: added **Brainstorm** to the other NIP-85 implementations (it was in IMPACT-STATEMENT/README but missing here). Rewrote the paragraph to highlight the live-verified interop on the shared canonical `rank` tag. - DVM section: replaced the vague "parameter target" description with the actual request format — `["j", "trust-check"]` + `["i", "<ln_pubkey>", "text"]` — matching `src/nostr/dvm.ts` constants and the README. Added the kind 31990 handler-info publish note. DEPLOY.md - Section 7 crawl-intervals table: `CRAWL_INTERVAL_PROBE_MS` 3,600,000 → **1,800,000** (30 min) and `PROBE_MAX_PER_SECOND` 10 → **15**. Both defaults now match `src/config.ts` and `.env.example`. - Section 8 snapshot-retention: rewritten from scratch. The old text described a 3-tier (<7d / 7-30d / >30d) policy embedded in `runCrawl()`, none of which is accurate. The real policy is a flat 45-day cutoff on `score_snapshots` (with separate 14-day cutoffs for probe/channel/fee snapshots) applied by a dedicated 24-hour cron inside the crawler process, using 50k-row chunks to keep the SQLite WAL bounded. Source: `src/config/retention.ts`. - Section 1 Aperture config snippet: replaced the `YOUR_VOLTAGE_NODE. voltageapp.io` placeholder host with `localhost:10009`, and `tlscertpath` / `macaroonpath` now point directly at `<LND_DATA_DIR>/...` so Aperture picks up an LND cert regeneration via a simple `systemctl restart aperture`. - Section 2 nginx config: rewritten to match the real prod routing. The old regex `^/api/(agent|agents|decide|profile)` would have captured `/api/agents/top` and sent it through Aperture (making it 1-sat), but that endpoint is free in README + landing page + prod. New regex `^/api/agent/[a-f0-9]+` only matches paths with a hex hash after `/api/agent/`, so `/api/agents/top` correctly falls through to Express. Routing-logic bullets rewritten to match endpoint-by-endpoint. - Section 7 post-crawl note: removed the "top 50 agents" claim (bulk scoring touches every eligible agent, not a top-N subset). src/crawler/lndGraphCrawler.ts - Top-of-file comment: stale `~17,000 nodes` → `~14,000 active Lightning nodes after UTXO validation`. Tests: 464 / 34 files green.
1 parent f41bca1 commit bf9882d

6 files changed

Lines changed: 114 additions & 52 deletions

File tree

DEPLOY.md

Lines changed: 89 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@ autocert: false
4040

4141
authenticator:
4242
lnd:
43-
host: "YOUR_VOLTAGE_NODE.voltageapp.io:10009"
44-
macaroonpath: "/etc/aperture/admin.macaroon"
45-
tlscertpath: "/etc/aperture/tls.cert"
43+
# gRPC host of the LND node Aperture uses to mint L402 invoices.
44+
# Prefer "localhost:10009" when LND runs on the same host — this avoids
45+
# copying macaroons/certs around and picks up LND cert regenerations
46+
# automatically (Aperture re-reads `tlspath` on restart).
47+
host: "localhost:10009"
48+
macaroonpath: "<LND_DATA_DIR>/data/chain/bitcoin/mainnet/admin.macaroon"
49+
tlscertpath: "<LND_DATA_DIR>/tls.cert"
4650

4751
servicesettings:
4852
- name: "satrank"
@@ -60,8 +64,8 @@ dbdir: "/var/lib/aperture"
6064
- `price: 1` — 1 satoshi per query
6165
- `duration: 31536000` — L402 token valid for 1 year (365 days in seconds)
6266
- `pathregexp` — only agent/agents endpoints require payment; health/stats/version are free
63-
- Copy your LND admin macaroon to `/etc/aperture/admin.macaroon`
64-
- Copy your LND TLS cert to `/etc/aperture/tls.cert`
67+
- Replace `<LND_DATA_DIR>` with the absolute path to your LND data directory (where `tls.cert` and `data/chain/bitcoin/mainnet/admin.macaroon` live). Pointing Aperture at LND's live files instead of a copy means a cert regeneration on the LND side is picked up by `systemctl restart aperture` without any file juggling.
68+
- `host: "localhost:10009"` assumes LND is on the same host and listens on the loopback interface (recommended — see the `restlisten=127.0.0.1:10009` convention in `lnd.conf`). If LND is remote, replace with `hostname:port` and add that IP to LND's `tlsextraip`.
6569

6670
### Systemd: /etc/systemd/system/aperture.service
6771

@@ -104,54 +108,81 @@ sudo systemctl enable --now aperture
104108
### /etc/nginx/sites-available/satrank.dev
105109

106110
```nginx
111+
server {
112+
listen 80;
113+
server_name satrank.dev;
114+
return 301 https://$host$request_uri;
115+
}
116+
107117
server {
108118
listen 443 ssl http2;
109119
server_name satrank.dev;
110120
111121
ssl_certificate /etc/letsencrypt/live/satrank.dev/fullchain.pem;
112122
ssl_certificate_key /etc/letsencrypt/live/satrank.dev/privkey.pem;
113123
114-
# L402-gated endpoints — proxy through Aperture
115-
# Matches: /api/agent/*, /api/agents/*, /api/decide, /api/profile/*
116-
location ~ ^/api/(agent|agents|decide|profile) {
117-
proxy_pass http://127.0.0.1:8443;
124+
# L402-gated endpoints — proxy through Aperture.
125+
# `/api/agent/{hash}` and `/api/agent/{hash}/{verdict,history,attestations}`
126+
# all match because the regex requires a 64-hex-char id after `/agent/`.
127+
# `/api/agents/top`, `/api/agents/movers`, `/api/agents/search` do NOT match
128+
# (no hex id after `/agents/`) — they fall through to Express and are free.
129+
location ~ ^/api/agent/[a-f0-9]+ {
130+
proxy_pass https://127.0.0.1:8443;
131+
proxy_ssl_verify off;
118132
proxy_set_header Host $host;
119133
proxy_set_header X-Real-IP $remote_addr;
120134
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
121135
proxy_set_header X-Forwarded-Proto $scheme;
122136
}
123137

124-
# Free endpoint — report goes direct to Express (API key auth, no L402)
125-
location = /api/report {
126-
proxy_pass http://127.0.0.1:3000;
138+
# Explicit paid routes (exact match on /api/decide, prefix on /api/profile/).
139+
location = /api/decide {
140+
proxy_pass https://127.0.0.1:8443;
141+
proxy_ssl_verify off;
127142
proxy_set_header Host $host;
128143
proxy_set_header X-Real-IP $remote_addr;
129144
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
130145
proxy_set_header X-Forwarded-Proto $scheme;
131146
}
132147

133-
# Free endpoints — direct to Express (health, stats, attestations, etc.)
134-
location /api/ {
148+
location ~ ^/api/profile/ {
149+
proxy_pass https://127.0.0.1:8443;
150+
proxy_ssl_verify off;
151+
proxy_set_header Host $host;
152+
proxy_set_header X-Real-IP $remote_addr;
153+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
154+
proxy_set_header X-Forwarded-Proto $scheme;
155+
}
156+
157+
# Free endpoint — report goes direct to Express (API key auth, no L402).
158+
location = /api/report {
135159
proxy_pass http://127.0.0.1:3000;
136160
proxy_set_header Host $host;
137161
proxy_set_header X-Real-IP $remote_addr;
138162
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
139163
proxy_set_header X-Forwarded-Proto $scheme;
140164
}
141165

142-
# Static assets (landing page, favicon)
166+
# NIP-05 (.well-known/nostr.json) — public JSON, needs CORS.
167+
location /.well-known/nostr.json {
168+
proxy_pass http://127.0.0.1:3000;
169+
proxy_set_header Host $host;
170+
add_header Access-Control-Allow-Origin * always;
171+
}
172+
173+
# Catch-all — every non-paid `/api/*` (health, stats, agents/top,
174+
# agents/movers, agents/search, ping, verdicts, attestations, docs,
175+
# openapi.json), plus static assets (landing page, methodology, icons).
176+
# `/api/verdicts` is L402-gated at the Express level via `apertureGateAuth`,
177+
# so reaching Express direct returns 402 for external callers.
143178
location / {
144179
proxy_pass http://127.0.0.1:3000;
145180
proxy_set_header Host $host;
146181
proxy_set_header X-Real-IP $remote_addr;
182+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
183+
proxy_set_header X-Forwarded-Proto $scheme;
147184
}
148185
}
149-
150-
server {
151-
listen 80;
152-
server_name satrank.dev;
153-
return 301 https://$host$request_uri;
154-
}
155186
```
156187

157188
```bash
@@ -160,11 +191,15 @@ sudo certbot --nginx -d satrank.dev
160191
sudo nginx -t && sudo systemctl reload nginx
161192
```
162193

163-
**Routing logic:**
164-
- `/api/agent/*`, `/api/agents/*`, `/api/decide`, `/api/profile/*` → Aperture (L402, 1 sat)
165-
- `/api/report` → Express direct (API key auth, free)
166-
- `/api/health`, `/api/stats`, `/api/attestations`, `/api/docs` → Express direct (free)
167-
- `/`, `/app.js`, `/favicon.png` → Express static (free)
194+
**Routing logic (matches README + landing page cost tables):**
195+
- `/api/agent/{hash}`, `/api/agent/{hash}/verdict`, `/api/agent/{hash}/history`, `/api/agent/{hash}/attestations` → nginx → Aperture → Express (L402, 1 sat)
196+
- `/api/decide` → nginx → Aperture → Express (L402, 1 sat)
197+
- `/api/profile/{id}` → nginx → Aperture → Express (L402, 1 sat)
198+
- `/api/verdicts` → nginx → Express direct, gated at Express by `apertureGateAuth` middleware (L402, 1 sat/batch — the middleware returns 402 for non-loopback callers)
199+
- `/api/report`, `/api/attestations` → nginx → Express direct (free, X-API-Key required)
200+
- `/api/health`, `/api/stats`, `/api/ping/{pubkey}`, `/api/agents/top`, `/api/agents/movers`, `/api/agents/search`, `/api/docs`, `/api/openapi.json` → nginx → Express direct (free, no auth)
201+
- `/`, static assets, `/methodology.html` → nginx → Express static (free)
202+
- `/.well-known/nostr.json` → nginx → Express direct (free, CORS enabled for NIP-05)
168203

169204
## 3. SatRank (Docker)
170205

@@ -338,8 +373,8 @@ Each data source runs on its own timer in `--cron` mode. At startup, a full craw
338373
| `CRAWL_INTERVAL_OBSERVER_MS` | `300000` (5 min) | Observer Protocol transactions |
339374
| `CRAWL_INTERVAL_LND_GRAPH_MS` | `3600000` (1 hour) | LND full graph (~14k active nodes on mainnet) |
340375
| `CRAWL_INTERVAL_LNPLUS_MS` | `86400000` (24 hours) | LN+ community ratings |
341-
| `CRAWL_INTERVAL_PROBE_MS` | `3600000` (1 hour) | Route probe (reachability check) |
342-
| `PROBE_MAX_PER_SECOND` | `10` | Max probes per second (rate limiter) |
376+
| `CRAWL_INTERVAL_PROBE_MS` | `1800000` (30 min) | Route probe (reachability check) |
377+
| `PROBE_MAX_PER_SECOND` | `15` | Max probes per second (rate limiter) |
343378
| `PROBE_AMOUNT_SATS` | `1000` | Amount in sats to test routes with |
344379

345380
Override in `.env.production`:
@@ -356,16 +391,32 @@ CRAWL_INTERVAL_LND_GRAPH_MS=21600000 # 6 hours
356391
CRAWL_INTERVAL_LNPLUS_MS=86400000 # 24 hours
357392
```
358393

359-
After each Observer and LND crawl, scores are pre-computed for the top 50 agents and old snapshots are purged.
394+
After each LND crawl, the scoring pipeline runs in two passes: first any unscored
395+
agents that accumulated since the last cycle (bulk scoring, unscored), then all
396+
previously scored agents are rescored with fresh data (bulk rescore). On a normal
397+
cycle this touches every eligible agent in the index, not a top-N subset. Logs
398+
show exact counts (`scored X/Y errors=0`) for each pass.
360399

361400
## 8. Snapshot retention
362401

363-
The `score_snapshots` table grows with each score computation (~50 agents every 5 minutes = ~14,400 rows/day).
364-
The crawler automatically purges old snapshots after each crawl run:
365-
366-
- **< 7 days**: all snapshots retained
367-
- **7–30 days**: 1 snapshot per agent per day (deduplication via `ROW_NUMBER()`)
368-
- **> 30 days**: deleted
369-
370-
This runs inside `runCrawl()` at the end of each cycle (cron or single run).
371-
No additional cron job is needed — the purge is embedded in the crawler process.
402+
The `score_snapshots` table grows with every score computation — on the production
403+
mainnet instance each cycle writes ~7,000+ rows, and the retention cron keeps the
404+
last 45 days.
405+
406+
The retention policy is defined in `src/config/retention.ts` and applied by a
407+
dedicated cron inside the crawler process (not embedded in each crawl):
408+
409+
- **`RETENTION_POLICIES`** (flat cutoff per table):
410+
- `probe_results` — 14 days (regularity uses the last 7, kept 2× for margin)
411+
- `score_snapshots` — **45 days** (delta windows up to 30d, kept 1.5× for margin)
412+
- `channel_snapshots` — 14 days
413+
- `fee_snapshots` — 14 days
414+
- **`RETENTION_CHUNK_SIZE`** — deletes run in chunks of 50,000 rows per
415+
transaction so the SQLite WAL never balloons (previous multi-million-row
416+
monolithic `DELETE` grew the WAL past 1 GB and stalled — that's why chunking).
417+
- **`RETENTION_INTERVAL_MS`** — the retention cron runs every **24 hours**,
418+
independent from the crawler's data ingestion cycle. At startup it also runs
419+
once immediately.
420+
421+
The retention cron is started from `src/crawler/run.ts` and logs each table's
422+
`{deleted, durationMs}` after every sweep.

IMPACT-STATEMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ The freemium architecture is limpid and already profitable at the margin:
207207
| **NIP-85 scores (kind 30382 on 3 relays)** | **free** | Distribution, adoption, composability with other NIP-85 providers |
208208
| **NIP-05, NIP-90 DVM, `/api/ping`, `/api/agents/top`, `/api/stats`, `/api/health`** | **free** | Discovery, live reachability, public statistics |
209209
| **`/api/decide` (personalized GO/NO-GO + pathfinding)** | **1 sat via L402** | Primary revenue — the personalized oracle call |
210-
| **`/api/profile`, `/api/agent/:hash`, `/api/agent/:hash/verdict`, `/api/agent/:hash/history`, `/api/verdicts`** | **1 sat via L402** | Detailed queries — secondary revenue |
210+
| **`/api/profile`, `/api/agent/:hash`, `/api/agent/:hash/verdict`, `/api/agent/:hash/history`, `/api/agent/:hash/attestations`, `/api/verdicts`** | **1 sat via L402** | Detailed queries — secondary revenue |
211211
| **`/api/report`, `/api/attestations`** | **free** (API key for identity) | Closes the feedback loop — reports improve `P_empirical` in future decide responses |
212212

213213
Why it works:

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ curl https://satrank.dev/api/agent/<hash>/verdict
156156
| GET | `/api/agent/:hash` | Detailed score + evidence | 1 sat |
157157
| GET | `/api/agent/:hash/verdict` | SAFE/RISKY/UNKNOWN + flags + risk profile | 1 sat |
158158
| GET | `/api/agent/:hash/history` | Score snapshots with deltas | 1 sat |
159-
| GET | `/api/agent/:hash/attestations` | Received attestations | free |
159+
| GET | `/api/agent/:hash/attestations` | Received attestations (list) | 1 sat |
160160
| GET | `/api/agents/top` | Leaderboard by score | free |
161161
| GET | `/api/agents/movers` | Top 7-day movers | free |
162162
| GET | `/api/agents/search?alias=…` | Search by alias | free |

public/index.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,11 @@ <h2>Detailed Queries <span class="api-tag paid-tag">1 sat</span></h2>
260260
<span class="endpoint-path">/api/agent/:hash/history</span>
261261
<span class="endpoint-desc">Score history</span>
262262
</li>
263+
<li>
264+
<span class="method method-get">GET</span>
265+
<span class="endpoint-path">/api/agent/:hash/attestations</span>
266+
<span class="endpoint-desc">Received attestations (list)</span>
267+
</li>
263268
<li>
264269
<span class="method method-post">POST</span>
265270
<span class="endpoint-path">/api/verdicts</span>

public/methodology.html

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -970,20 +970,25 @@ <h3>Why Nostr?</h3>
970970
<h3>DVM Trust-Check (NIP-90)</h3>
971971
<p>
972972
SatRank also operates a Data Vending Machine (NIP-90) for real-time trust queries.
973-
Send a kind <code>5900</code> event with parameter <code>target</code> set to any
974-
Lightning node pubkey, and receive a kind <code>6900</code> response with the trust
975-
score, verdict, and reachability. Unknown nodes trigger an on-demand <code>QueryRoutes</code>
976-
probe so the answer reflects the live graph, not just the cached snapshot.
973+
Publish a kind <code>5900</code> job request with <code>["j", "trust-check"]</code>
974+
and <code>["i", "&lt;ln_pubkey&gt;", "text"]</code> tags, and SatRank responds with
975+
a signed kind <code>6900</code> event containing the trust score, verdict, and
976+
reachability. Unknown nodes trigger an on-demand <code>QueryRoutes</code> probe so
977+
the answer reflects the live graph, not just the cached snapshot. The DVM is
978+
discoverable via a kind <code>31990</code> handler-info event published to the
979+
three canonical relays on every startup. Free, no payment required.
977980
</p>
978981

979982
<h3>Unique Position</h3>
980983
<p>
981-
Every other NIP-85 implementation (wot-scoring, nostr-wot-sdk, nostr-wot-oracle, Vertex)
982-
operates on the Nostr social graph &mdash; scoring based on follows, mutes, and zaps.
983-
SatRank is the only provider that bridges the Lightning payment graph into NIP-85. That
984-
means a wallet or agent can query <strong>both</strong> social trust and payment
985-
reliability from the same Nostr relay infrastructure, without running two parallel
986-
pipelines.
984+
Every other NIP-85 implementation (Brainstorm, wot-scoring, nostr-wot-sdk,
985+
nostr-wot-oracle, Vertex) operates on the Nostr social graph &mdash; scoring based on
986+
follows, mutes, and zaps. SatRank is the only provider that bridges the Lightning
987+
payment graph into NIP-85. Because Brainstorm and SatRank both use the canonical
988+
<code>rank</code> tag on kind <code>30382</code> events, a wallet or agent can list
989+
both in its kind <code>10040</code> and receive social trust <em>and</em> payment
990+
reliability from the same Nostr relay connection &mdash; verified live on
991+
<code>relay.damus.io</code> and <code>nos.lol</code>, where both providers publish.
987992
</p>
988993

989994
<h3 id="declare-provider">Declaring SatRank as a Trusted Provider (kind 10040)</h3>

src/crawler/lndGraphCrawler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
// Indexes Lightning Network nodes from our LND node's graph view
2-
// Primary source — replaces mempool.space for full graph coverage (~17,000 nodes)
1+
// Indexes Lightning Network nodes from our LND node's graph view.
2+
// Primary source — replaces mempool.space for full graph coverage.
3+
// Mainnet today: ~14,000 active Lightning nodes after UTXO validation.
34
import type { AgentRepository } from '../repositories/agentRepository';
45
import type { ChannelSnapshotRepository } from '../repositories/channelSnapshotRepository';
56
import type { FeeSnapshotRepository } from '../repositories/feeSnapshotRepository';

0 commit comments

Comments
 (0)