csm-dashboard 0.2.1__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/Dockerfile +8 -11
  2. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/PKG-INFO +90 -25
  3. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/README.md +89 -24
  4. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/pyproject.toml +1 -1
  5. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/abis/CSAccounting.json +22 -0
  6. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/abis/stETH.json +10 -0
  7. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/cli/commands.py +252 -33
  8. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/core/config.py +3 -0
  9. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/core/types.py +74 -3
  10. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/data/etherscan.py +60 -0
  11. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/data/ipfs_logs.py +42 -2
  12. csm_dashboard-0.3.0/src/data/lido_api.py +140 -0
  13. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/data/onchain.py +190 -0
  14. csm_dashboard-0.3.0/src/services/operator_service.py +630 -0
  15. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/web/routes.py +60 -2
  16. csm_dashboard-0.2.1/src/data/lido_api.py +0 -35
  17. csm_dashboard-0.2.1/src/services/operator_service.py +0 -320
  18. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/.dockerignore +0 -0
  19. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/.env.example +0 -0
  20. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/.github/workflows/docker-publish.yaml +0 -0
  21. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/.github/workflows/release.yaml +0 -0
  22. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/.gitignore +0 -0
  23. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/docker-compose.yml +0 -0
  24. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/img/csm-dash-cli.png +0 -0
  25. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/img/csm-dash-web.png +0 -0
  26. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/img/logo.png +0 -0
  27. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/requirements.txt +0 -0
  28. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/__init__.py +0 -0
  29. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/abis/CSFeeDistributor.json +0 -0
  30. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/abis/CSModule.json +0 -0
  31. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/abis/__init__.py +0 -0
  32. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/cli/__init__.py +0 -0
  33. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/core/__init__.py +0 -0
  34. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/core/contracts.py +0 -0
  35. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/data/__init__.py +0 -0
  36. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/data/beacon.py +0 -0
  37. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/data/cache.py +0 -0
  38. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/data/known_cids.py +0 -0
  39. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/data/rewards_tree.py +0 -0
  40. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/data/strikes.py +0 -0
  41. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/main.py +0 -0
  42. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/services/__init__.py +0 -0
  43. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/web/__init__.py +0 -0
  44. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/src/web/app.py +0 -0
  45. {csm_dashboard-0.2.1 → csm_dashboard-0.3.0}/tests/__init__.py +0 -0
@@ -17,6 +17,11 @@ ENV PATH="/opt/venv/bin:$PATH"
17
17
  RUN pip install --upgrade pip && \
18
18
  pip install --no-cache-dir -r requirements.txt
19
19
 
20
+ # Copy package files and install
21
+ COPY pyproject.toml README.md ./
22
+ COPY src ./src
23
+ RUN pip install --no-cache-dir .
24
+
20
25
  # Final stage
21
26
  FROM python:3.11-slim
22
27
 
@@ -27,21 +32,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
27
32
  curl \
28
33
  && rm -rf /var/lib/apt/lists/*
29
34
 
30
- # Copy virtual environment from builder
35
+ # Copy virtual environment from builder (includes installed package)
31
36
  COPY --from=builder /opt/venv /opt/venv
32
37
 
33
- # Copy application code (includes abis inside src/)
34
- COPY src ./src
35
-
36
- # Create csm alias script
37
- RUN echo '#!/bin/sh\npython -m src.main "$@"' > /usr/local/bin/csm && \
38
- chmod +x /usr/local/bin/csm
39
-
40
38
  # Set environment variables
41
39
  ENV PATH="/opt/venv/bin:$PATH" \
42
40
  PYTHONUNBUFFERED=1 \
43
- PYTHONDONTWRITEBYTECODE=1 \
44
- PYTHONPATH=/app
41
+ PYTHONDONTWRITEBYTECODE=1
45
42
 
46
43
  # Health check
47
44
  HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
@@ -50,5 +47,5 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
50
47
  # Expose port for web dashboard
51
48
  EXPOSE 3000
52
49
 
53
- # Default command - run the web dashboard using csm alias
50
+ # Default command - csm is now an entry point from pip install
54
51
  CMD ["csm", "serve", "--host", "0.0.0.0", "--port", "3000"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: csm-dashboard
3
- Version: 0.2.1
3
+ Version: 0.3.0
4
4
  Summary: Lido CSM Operator Dashboard for tracking validator earnings
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: fastapi>=0.104
@@ -31,8 +31,11 @@ Track your Lido Community Staking Module (CSM) validator earnings, excess bond,
31
31
  - Look up operator by Ethereum address (manager or rewards address) or operator ID
32
32
  - View current bond vs required bond (excess is claimable)
33
33
  - Track cumulative rewards and unclaimed amounts
34
+ - Operator type detection (Permissionless, ICS/Legacy EA, etc.)
34
35
  - Detailed validator status from beacon chain (with `--detailed` flag)
35
36
  - APY metrics: reward APY, bond APY (stETH rebase), and net APY
37
+ - Full distribution history with per-frame breakdown (with `--history` flag)
38
+ - Withdrawal/claim history tracking (with `--withdrawals` flag)
36
39
  - JSON output for scripting and automation
37
40
  - CLI for quick terminal lookups
38
41
  - Web interface for browser-based monitoring
@@ -85,6 +88,7 @@ Available settings:
85
88
  - `BEACON_API_URL`: Beacon chain API (default: https://beaconcha.in/api/v1)
86
89
  - `BEACON_API_KEY`: Optional API key for beaconcha.in (higher rate limits)
87
90
  - `ETHERSCAN_API_KEY`: Optional API key for Etherscan (recommended for accurate historical data)
91
+ - `THEGRAPH_API_KEY`: Optional API key for The Graph (enables historical Bond APY per distribution frame)
88
92
  - `CACHE_TTL_SECONDS`: Cache duration in seconds (default: 300)
89
93
 
90
94
  ## Usage
@@ -128,6 +132,8 @@ csm rewards [ADDRESS] [OPTIONS]
128
132
  | `ADDRESS` | | Ethereum address (required unless `--id` is provided) |
129
133
  | `--id` | `-i` | Operator ID (skips address lookup, faster) |
130
134
  | `--detailed` | `-d` | Include validator status from beacon chain and APY metrics |
135
+ | `--history` | `-H` | Show all historical distribution frames with per-frame APY |
136
+ | `--withdrawals` | `-w` | Include withdrawal/claim history |
131
137
  | `--json` | `-j` | Output as JSON (same format as API) |
132
138
  | `--rpc` | `-r` | Custom RPC URL |
133
139
 
@@ -143,6 +149,12 @@ csm rewards --id 42
143
149
  # Get detailed validator info and APY
144
150
  csm rewards --id 42 --detailed
145
151
 
152
+ # Show full distribution history with Previous/Current/Lifetime columns
153
+ csm rewards --id 42 --history
154
+
155
+ # Include withdrawal history
156
+ csm rewards --id 42 --withdrawals
157
+
146
158
  # JSON output for scripting
147
159
  csm rewards --id 42 --json
148
160
 
@@ -226,17 +238,16 @@ csm rewards --id 333 --json
226
238
  "operator_id": 333,
227
239
  "manager_address": "0x6ac683C503CF210CCF88193ec7ebDe2c993f63a4",
228
240
  "reward_address": "0x55915Cf2115c4D6e9085e94c8dAD710cabefef31",
241
+ "curve_id": 2,
242
+ "operator_type": "Permissionless",
229
243
  "rewards": {
230
- "current_bond_eth": 651.5523536856277,
244
+ "current_bond_eth": 651.55,
231
245
  "required_bond_eth": 650.2,
232
- "excess_bond_eth": 1.3523536856277778,
233
- "cumulative_rewards_shares": 8973877501313655495,
234
- "cumulative_rewards_eth": 10.9642938931415,
235
- "distributed_shares": 7867435720490255061,
236
- "distributed_eth": 9.61244204773546,
237
- "unclaimed_shares": 1106441780823400434,
238
- "unclaimed_eth": 1.3518518454060409,
239
- "total_claimable_eth": 2.7042055310338187
246
+ "excess_bond_eth": 1.35,
247
+ "cumulative_rewards_eth": 10.96,
248
+ "distributed_eth": 9.61,
249
+ "unclaimed_eth": 1.35,
250
+ "total_claimable_eth": 2.70
240
251
  },
241
252
  "validators": {
242
253
  "total": 500,
@@ -251,44 +262,68 @@ With `--detailed`, additional fields are included:
251
262
  ```json
252
263
  {
253
264
  "operator_id": 333,
254
- "...": "...",
265
+ "curve_id": 2,
266
+ "operator_type": "Permissionless",
255
267
  "validators": {
256
268
  "total": 500,
257
269
  "active": 500,
258
270
  "exited": 0,
259
271
  "by_status": {
260
- "active": 100,
272
+ "active": 500,
261
273
  "pending": 0,
262
274
  "exiting": 0,
263
275
  "exited": 0,
264
- "slashed": 0,
265
- "unknown": 0
276
+ "slashed": 0
266
277
  }
267
278
  },
268
279
  "performance": {
269
280
  "avg_effectiveness": 98.5
270
281
  },
271
282
  "apy": {
272
- "historical_reward_apy_28d": 2.21,
273
- "historical_reward_apy_ltd": 2.03,
274
- "bond_apy": 2.54,
275
- "net_apy_28d": 4.75,
276
- "net_apy_ltd": 4.57
283
+ "current_distribution_apy": 2.77,
284
+ "current_bond_apr": 2.56,
285
+ "net_apy_28d": 5.33,
286
+ "lifetime_reward_apy": 2.80,
287
+ "lifetime_bond_apy": 2.60,
288
+ "lifetime_net_apy": 5.40
277
289
  },
278
290
  "active_since": "2025-02-16T12:00:00"
279
291
  }
280
292
  ```
281
293
 
294
+ With `--history`, you also get the full distribution frame history:
295
+
296
+ ```json
297
+ {
298
+ "apy": {
299
+ "frames": [
300
+ {
301
+ "frame_number": 1,
302
+ "start_date": "2025-03-14T00:00:00",
303
+ "end_date": "2025-04-11T00:00:00",
304
+ "rewards_eth": 1.2345,
305
+ "validator_count": 500,
306
+ "duration_days": 28.0,
307
+ "apy": 2.85
308
+ }
309
+ ]
310
+ }
311
+ }
312
+ ```
313
+
282
314
  ## API Endpoints
283
315
 
284
316
  - `GET /api/operator/{address_or_id}` - Get operator rewards data
285
317
  - Query param: `?detailed=true` for validator status and APY
318
+ - Query param: `?history=true` for all historical distribution frames
319
+ - Query param: `?withdrawals=true` for withdrawal/claim history
320
+ - `GET /api/operator/{address_or_id}/strikes` - Get detailed validator strikes
286
321
  - `GET /api/operators` - List all operators with rewards
287
322
  - `GET /api/health` - Health check
288
323
 
289
324
  ## Understanding APY Metrics
290
325
 
291
- The dashboard shows three APY metrics when using the `--detailed` flag:
326
+ The dashboard shows three APY metrics when using the `--detailed` or `--history` flags:
292
327
 
293
328
  | Metric | What It Means |
294
329
  |--------|---------------|
@@ -296,16 +331,34 @@ The dashboard shows three APY metrics when using the `--detailed` flag:
296
331
  | **Bond APY** | Automatic growth of your stETH bond from protocol rebasing (same for all operators) |
297
332
  | **NET APY** | Total return = Reward APY + Bond APY |
298
333
 
334
+ ### Display Modes
335
+
336
+ - **`--detailed`**: Shows only the Current frame column (simpler view)
337
+ - **`--history`**: Shows Previous, Current, and Lifetime columns with full distribution history
338
+
299
339
  ### How APY is Calculated
300
340
 
301
- **Reward APY** is calculated from actual reward distribution data published by Lido. Every ~28 days, Lido calculates how much each operator earned and publishes a "distribution frame" to IPFS (a decentralized file storage network). The dashboard fetches all these historical frames to calculate both 28-day and lifetime APY.
341
+ **Reward APY** is calculated from actual reward distribution data published by Lido. Every ~28 days, Lido calculates how much each operator earned and publishes a "distribution frame" to IPFS (a decentralized file storage network). The dashboard fetches all these historical frames to calculate APY.
342
+
343
+ - **Current APY**: Based on the most recent distribution frame (~28 days)
344
+ - **Previous APY**: Based on the second-to-last distribution frame
345
+ - **Lifetime APY**: Duration-weighted average of all frames, using **per-frame bond requirements** for accuracy
346
+
347
+ The **Lifetime APY** calculation is particularly sophisticated: it uses each frame's actual validator count to determine the bond requirement for that period, then calculates a duration-weighted average. This produces accurate lifetime APY even for operators who have grown significantly over time.
348
+
349
+ **Bond APY** represents the stETH rebase rate—the automatic growth of your bond due to Ethereum staking rewards. This rate is set by the Lido protocol and applies equally to all operators.
350
+
351
+ > **Note**: With a Graph API key configured (`THEGRAPH_API_KEY`), Bond APY shows the actual historical stETH rate for each distribution frame. Without the API key, it falls back to the current rate (marked with an asterisk).
302
352
 
303
- - **28-Day APY**: Based on the most recent ~28 days of reward distributions
304
- - **Lifetime APY**: Based on all periods where you earned rewards (excludes ramp-up periods with no rewards to avoid misleadingly low numbers)
353
+ ### Operator Types
305
354
 
306
- **Bond APY** represents the stETH rebase rate—the automatic growth of your bond due to Ethereum staking rewards. This rate is set by the Lido protocol and applies equally to all operators. The dashboard shows the current 7-day average rate from Lido's API.
355
+ The dashboard detects your operator type from the CSAccounting bond curve:
307
356
 
308
- > **Note**: Bond APY shows the current stETH rate for both 28-Day and Lifetime columns, as historical rates aren't readily available.
357
+ | Type | Description |
358
+ |------|-------------|
359
+ | **Permissionless** | Standard operators (Curve 2, current default) |
360
+ | **Permissionless (Legacy)** | Early permissionless operators (Curve 0, deprecated) |
361
+ | **ICS/Legacy EA** | Incentivized Community Stakers / Early Adopters (Curve 1) |
309
362
 
310
363
  ### Why You Might Want an Etherscan API Key
311
364
 
@@ -324,6 +377,18 @@ The actual reward data lives on IPFS and is always accessible. However, to *disc
324
377
 
325
378
  The free tier allows 5 calls/second, which is plenty for this dashboard.
326
379
 
380
+ ### Why You Might Want a Graph API Key
381
+
382
+ The Graph provides historical stETH APR data from the Lido subgraph. Without this API key, Bond APY calculations use the current rate for all periods, which is less accurate.
383
+
384
+ **How to get one (free):**
385
+ 1. Go to [thegraph.com/studio](https://thegraph.com/studio/)
386
+ 2. Connect your wallet and create an account
387
+ 3. Go to "API Keys" and create a new key
388
+ 4. Add to your `.env` file: `THEGRAPH_API_KEY=your_key_here`
389
+
390
+ The free tier includes 100,000 queries/month, which is plenty for this dashboard.
391
+
327
392
  ## Data Sources
328
393
 
329
394
  - **On-chain contracts**: CSModule, CSAccounting, CSFeeDistributor, stETH
@@ -11,8 +11,11 @@ Track your Lido Community Staking Module (CSM) validator earnings, excess bond,
11
11
  - Look up operator by Ethereum address (manager or rewards address) or operator ID
12
12
  - View current bond vs required bond (excess is claimable)
13
13
  - Track cumulative rewards and unclaimed amounts
14
+ - Operator type detection (Permissionless, ICS/Legacy EA, etc.)
14
15
  - Detailed validator status from beacon chain (with `--detailed` flag)
15
16
  - APY metrics: reward APY, bond APY (stETH rebase), and net APY
17
+ - Full distribution history with per-frame breakdown (with `--history` flag)
18
+ - Withdrawal/claim history tracking (with `--withdrawals` flag)
16
19
  - JSON output for scripting and automation
17
20
  - CLI for quick terminal lookups
18
21
  - Web interface for browser-based monitoring
@@ -65,6 +68,7 @@ Available settings:
65
68
  - `BEACON_API_URL`: Beacon chain API (default: https://beaconcha.in/api/v1)
66
69
  - `BEACON_API_KEY`: Optional API key for beaconcha.in (higher rate limits)
67
70
  - `ETHERSCAN_API_KEY`: Optional API key for Etherscan (recommended for accurate historical data)
71
+ - `THEGRAPH_API_KEY`: Optional API key for The Graph (enables historical Bond APY per distribution frame)
68
72
  - `CACHE_TTL_SECONDS`: Cache duration in seconds (default: 300)
69
73
 
70
74
  ## Usage
@@ -108,6 +112,8 @@ csm rewards [ADDRESS] [OPTIONS]
108
112
  | `ADDRESS` | | Ethereum address (required unless `--id` is provided) |
109
113
  | `--id` | `-i` | Operator ID (skips address lookup, faster) |
110
114
  | `--detailed` | `-d` | Include validator status from beacon chain and APY metrics |
115
+ | `--history` | `-H` | Show all historical distribution frames with per-frame APY |
116
+ | `--withdrawals` | `-w` | Include withdrawal/claim history |
111
117
  | `--json` | `-j` | Output as JSON (same format as API) |
112
118
  | `--rpc` | `-r` | Custom RPC URL |
113
119
 
@@ -123,6 +129,12 @@ csm rewards --id 42
123
129
  # Get detailed validator info and APY
124
130
  csm rewards --id 42 --detailed
125
131
 
132
+ # Show full distribution history with Previous/Current/Lifetime columns
133
+ csm rewards --id 42 --history
134
+
135
+ # Include withdrawal history
136
+ csm rewards --id 42 --withdrawals
137
+
126
138
  # JSON output for scripting
127
139
  csm rewards --id 42 --json
128
140
 
@@ -206,17 +218,16 @@ csm rewards --id 333 --json
206
218
  "operator_id": 333,
207
219
  "manager_address": "0x6ac683C503CF210CCF88193ec7ebDe2c993f63a4",
208
220
  "reward_address": "0x55915Cf2115c4D6e9085e94c8dAD710cabefef31",
221
+ "curve_id": 2,
222
+ "operator_type": "Permissionless",
209
223
  "rewards": {
210
- "current_bond_eth": 651.5523536856277,
224
+ "current_bond_eth": 651.55,
211
225
  "required_bond_eth": 650.2,
212
- "excess_bond_eth": 1.3523536856277778,
213
- "cumulative_rewards_shares": 8973877501313655495,
214
- "cumulative_rewards_eth": 10.9642938931415,
215
- "distributed_shares": 7867435720490255061,
216
- "distributed_eth": 9.61244204773546,
217
- "unclaimed_shares": 1106441780823400434,
218
- "unclaimed_eth": 1.3518518454060409,
219
- "total_claimable_eth": 2.7042055310338187
226
+ "excess_bond_eth": 1.35,
227
+ "cumulative_rewards_eth": 10.96,
228
+ "distributed_eth": 9.61,
229
+ "unclaimed_eth": 1.35,
230
+ "total_claimable_eth": 2.70
220
231
  },
221
232
  "validators": {
222
233
  "total": 500,
@@ -231,44 +242,68 @@ With `--detailed`, additional fields are included:
231
242
  ```json
232
243
  {
233
244
  "operator_id": 333,
234
- "...": "...",
245
+ "curve_id": 2,
246
+ "operator_type": "Permissionless",
235
247
  "validators": {
236
248
  "total": 500,
237
249
  "active": 500,
238
250
  "exited": 0,
239
251
  "by_status": {
240
- "active": 100,
252
+ "active": 500,
241
253
  "pending": 0,
242
254
  "exiting": 0,
243
255
  "exited": 0,
244
- "slashed": 0,
245
- "unknown": 0
256
+ "slashed": 0
246
257
  }
247
258
  },
248
259
  "performance": {
249
260
  "avg_effectiveness": 98.5
250
261
  },
251
262
  "apy": {
252
- "historical_reward_apy_28d": 2.21,
253
- "historical_reward_apy_ltd": 2.03,
254
- "bond_apy": 2.54,
255
- "net_apy_28d": 4.75,
256
- "net_apy_ltd": 4.57
263
+ "current_distribution_apy": 2.77,
264
+ "current_bond_apr": 2.56,
265
+ "net_apy_28d": 5.33,
266
+ "lifetime_reward_apy": 2.80,
267
+ "lifetime_bond_apy": 2.60,
268
+ "lifetime_net_apy": 5.40
257
269
  },
258
270
  "active_since": "2025-02-16T12:00:00"
259
271
  }
260
272
  ```
261
273
 
274
+ With `--history`, you also get the full distribution frame history:
275
+
276
+ ```json
277
+ {
278
+ "apy": {
279
+ "frames": [
280
+ {
281
+ "frame_number": 1,
282
+ "start_date": "2025-03-14T00:00:00",
283
+ "end_date": "2025-04-11T00:00:00",
284
+ "rewards_eth": 1.2345,
285
+ "validator_count": 500,
286
+ "duration_days": 28.0,
287
+ "apy": 2.85
288
+ }
289
+ ]
290
+ }
291
+ }
292
+ ```
293
+
262
294
  ## API Endpoints
263
295
 
264
296
  - `GET /api/operator/{address_or_id}` - Get operator rewards data
265
297
  - Query param: `?detailed=true` for validator status and APY
298
+ - Query param: `?history=true` for all historical distribution frames
299
+ - Query param: `?withdrawals=true` for withdrawal/claim history
300
+ - `GET /api/operator/{address_or_id}/strikes` - Get detailed validator strikes
266
301
  - `GET /api/operators` - List all operators with rewards
267
302
  - `GET /api/health` - Health check
268
303
 
269
304
  ## Understanding APY Metrics
270
305
 
271
- The dashboard shows three APY metrics when using the `--detailed` flag:
306
+ The dashboard shows three APY metrics when using the `--detailed` or `--history` flags:
272
307
 
273
308
  | Metric | What It Means |
274
309
  |--------|---------------|
@@ -276,16 +311,34 @@ The dashboard shows three APY metrics when using the `--detailed` flag:
276
311
  | **Bond APY** | Automatic growth of your stETH bond from protocol rebasing (same for all operators) |
277
312
  | **NET APY** | Total return = Reward APY + Bond APY |
278
313
 
314
+ ### Display Modes
315
+
316
+ - **`--detailed`**: Shows only the Current frame column (simpler view)
317
+ - **`--history`**: Shows Previous, Current, and Lifetime columns with full distribution history
318
+
279
319
  ### How APY is Calculated
280
320
 
281
- **Reward APY** is calculated from actual reward distribution data published by Lido. Every ~28 days, Lido calculates how much each operator earned and publishes a "distribution frame" to IPFS (a decentralized file storage network). The dashboard fetches all these historical frames to calculate both 28-day and lifetime APY.
321
+ **Reward APY** is calculated from actual reward distribution data published by Lido. Every ~28 days, Lido calculates how much each operator earned and publishes a "distribution frame" to IPFS (a decentralized file storage network). The dashboard fetches all these historical frames to calculate APY.
322
+
323
+ - **Current APY**: Based on the most recent distribution frame (~28 days)
324
+ - **Previous APY**: Based on the second-to-last distribution frame
325
+ - **Lifetime APY**: Duration-weighted average of all frames, using **per-frame bond requirements** for accuracy
326
+
327
+ The **Lifetime APY** calculation is particularly sophisticated: it uses each frame's actual validator count to determine the bond requirement for that period, then calculates a duration-weighted average. This produces accurate lifetime APY even for operators who have grown significantly over time.
328
+
329
+ **Bond APY** represents the stETH rebase rate—the automatic growth of your bond due to Ethereum staking rewards. This rate is set by the Lido protocol and applies equally to all operators.
330
+
331
+ > **Note**: With a Graph API key configured (`THEGRAPH_API_KEY`), Bond APY shows the actual historical stETH rate for each distribution frame. Without the API key, it falls back to the current rate (marked with an asterisk).
282
332
 
283
- - **28-Day APY**: Based on the most recent ~28 days of reward distributions
284
- - **Lifetime APY**: Based on all periods where you earned rewards (excludes ramp-up periods with no rewards to avoid misleadingly low numbers)
333
+ ### Operator Types
285
334
 
286
- **Bond APY** represents the stETH rebase rate—the automatic growth of your bond due to Ethereum staking rewards. This rate is set by the Lido protocol and applies equally to all operators. The dashboard shows the current 7-day average rate from Lido's API.
335
+ The dashboard detects your operator type from the CSAccounting bond curve:
287
336
 
288
- > **Note**: Bond APY shows the current stETH rate for both 28-Day and Lifetime columns, as historical rates aren't readily available.
337
+ | Type | Description |
338
+ |------|-------------|
339
+ | **Permissionless** | Standard operators (Curve 2, current default) |
340
+ | **Permissionless (Legacy)** | Early permissionless operators (Curve 0, deprecated) |
341
+ | **ICS/Legacy EA** | Incentivized Community Stakers / Early Adopters (Curve 1) |
289
342
 
290
343
  ### Why You Might Want an Etherscan API Key
291
344
 
@@ -304,6 +357,18 @@ The actual reward data lives on IPFS and is always accessible. However, to *disc
304
357
 
305
358
  The free tier allows 5 calls/second, which is plenty for this dashboard.
306
359
 
360
+ ### Why You Might Want a Graph API Key
361
+
362
+ The Graph provides historical stETH APR data from the Lido subgraph. Without this API key, Bond APY calculations use the current rate for all periods, which is less accurate.
363
+
364
+ **How to get one (free):**
365
+ 1. Go to [thegraph.com/studio](https://thegraph.com/studio/)
366
+ 2. Connect your wallet and create an account
367
+ 3. Go to "API Keys" and create a new key
368
+ 4. Add to your `.env` file: `THEGRAPH_API_KEY=your_key_here`
369
+
370
+ The free tier includes 100,000 queries/month, which is plenty for this dashboard.
371
+
307
372
  ## Data Sources
308
373
 
309
374
  - **On-chain contracts**: CSModule, CSAccounting, CSFeeDistributor, stETH
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "csm-dashboard"
3
- version = "0.2.1"
3
+ version = "0.3.0"
4
4
  description = "Lido CSM Operator Dashboard for tracking validator earnings"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -33,5 +33,27 @@
33
33
  "outputs": [
34
34
  {"name": "", "type": "uint256"}
35
35
  ]
36
+ },
37
+ {
38
+ "name": "getBondCurve",
39
+ "type": "function",
40
+ "stateMutability": "view",
41
+ "inputs": [
42
+ {"name": "nodeOperatorId", "type": "uint256"}
43
+ ],
44
+ "outputs": [
45
+ {"name": "points", "type": "uint256[]"}
46
+ ]
47
+ },
48
+ {
49
+ "name": "getBondCurveId",
50
+ "type": "function",
51
+ "stateMutability": "view",
52
+ "inputs": [
53
+ {"name": "nodeOperatorId", "type": "uint256"}
54
+ ],
55
+ "outputs": [
56
+ {"name": "", "type": "uint256"}
57
+ ]
36
58
  }
37
59
  ]
@@ -38,5 +38,15 @@
38
38
  "outputs": [
39
39
  {"name": "", "type": "uint256"}
40
40
  ]
41
+ },
42
+ {
43
+ "name": "Transfer",
44
+ "type": "event",
45
+ "anonymous": false,
46
+ "inputs": [
47
+ {"indexed": true, "name": "from", "type": "address"},
48
+ {"indexed": true, "name": "to", "type": "address"},
49
+ {"indexed": false, "name": "value", "type": "uint256"}
50
+ ]
41
51
  }
42
52
  ]