@kadi.build/deploy-ability 0.0.1

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 (198) hide show
  1. package/README.md +523 -0
  2. package/dist/constants.d.ts +82 -0
  3. package/dist/constants.d.ts.map +1 -0
  4. package/dist/constants.js +82 -0
  5. package/dist/constants.js.map +1 -0
  6. package/dist/errors/certificate-error.d.ts +95 -0
  7. package/dist/errors/certificate-error.d.ts.map +1 -0
  8. package/dist/errors/certificate-error.js +111 -0
  9. package/dist/errors/certificate-error.js.map +1 -0
  10. package/dist/errors/deployment-error.d.ts +122 -0
  11. package/dist/errors/deployment-error.d.ts.map +1 -0
  12. package/dist/errors/deployment-error.js +185 -0
  13. package/dist/errors/deployment-error.js.map +1 -0
  14. package/dist/errors/index.d.ts +13 -0
  15. package/dist/errors/index.d.ts.map +1 -0
  16. package/dist/errors/index.js +18 -0
  17. package/dist/errors/index.js.map +1 -0
  18. package/dist/errors/profile-error.d.ts +106 -0
  19. package/dist/errors/profile-error.d.ts.map +1 -0
  20. package/dist/errors/profile-error.js +127 -0
  21. package/dist/errors/profile-error.js.map +1 -0
  22. package/dist/errors/provider-error.d.ts +104 -0
  23. package/dist/errors/provider-error.d.ts.map +1 -0
  24. package/dist/errors/provider-error.js +120 -0
  25. package/dist/errors/provider-error.js.map +1 -0
  26. package/dist/errors/wallet-error.d.ts +131 -0
  27. package/dist/errors/wallet-error.d.ts.map +1 -0
  28. package/dist/errors/wallet-error.js +154 -0
  29. package/dist/errors/wallet-error.js.map +1 -0
  30. package/dist/index.d.ts +49 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +53 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/targets/akash/bid-selectors.d.ts +251 -0
  35. package/dist/targets/akash/bid-selectors.d.ts.map +1 -0
  36. package/dist/targets/akash/bid-selectors.js +322 -0
  37. package/dist/targets/akash/bid-selectors.js.map +1 -0
  38. package/dist/targets/akash/bid-types.d.ts +297 -0
  39. package/dist/targets/akash/bid-types.d.ts.map +1 -0
  40. package/dist/targets/akash/bid-types.js +89 -0
  41. package/dist/targets/akash/bid-types.js.map +1 -0
  42. package/dist/targets/akash/blockchain-client.d.ts +577 -0
  43. package/dist/targets/akash/blockchain-client.d.ts.map +1 -0
  44. package/dist/targets/akash/blockchain-client.js +803 -0
  45. package/dist/targets/akash/blockchain-client.js.map +1 -0
  46. package/dist/targets/akash/certificate-manager.d.ts +228 -0
  47. package/dist/targets/akash/certificate-manager.d.ts.map +1 -0
  48. package/dist/targets/akash/certificate-manager.js +395 -0
  49. package/dist/targets/akash/certificate-manager.js.map +1 -0
  50. package/dist/targets/akash/constants.d.ts +231 -0
  51. package/dist/targets/akash/constants.d.ts.map +1 -0
  52. package/dist/targets/akash/constants.js +225 -0
  53. package/dist/targets/akash/constants.js.map +1 -0
  54. package/dist/targets/akash/deployer.d.ts +136 -0
  55. package/dist/targets/akash/deployer.d.ts.map +1 -0
  56. package/dist/targets/akash/deployer.js +599 -0
  57. package/dist/targets/akash/deployer.js.map +1 -0
  58. package/dist/targets/akash/environment.d.ts +241 -0
  59. package/dist/targets/akash/environment.d.ts.map +1 -0
  60. package/dist/targets/akash/environment.js +245 -0
  61. package/dist/targets/akash/environment.js.map +1 -0
  62. package/dist/targets/akash/index.d.ts +1113 -0
  63. package/dist/targets/akash/index.d.ts.map +1 -0
  64. package/dist/targets/akash/index.js +909 -0
  65. package/dist/targets/akash/index.js.map +1 -0
  66. package/dist/targets/akash/lease-monitor.d.ts +51 -0
  67. package/dist/targets/akash/lease-monitor.d.ts.map +1 -0
  68. package/dist/targets/akash/lease-monitor.js +110 -0
  69. package/dist/targets/akash/lease-monitor.js.map +1 -0
  70. package/dist/targets/akash/logs.d.ts +71 -0
  71. package/dist/targets/akash/logs.d.ts.map +1 -0
  72. package/dist/targets/akash/logs.js +311 -0
  73. package/dist/targets/akash/logs.js.map +1 -0
  74. package/dist/targets/akash/logs.types.d.ts +102 -0
  75. package/dist/targets/akash/logs.types.d.ts.map +1 -0
  76. package/dist/targets/akash/logs.types.js +9 -0
  77. package/dist/targets/akash/logs.types.js.map +1 -0
  78. package/dist/targets/akash/pricing.d.ts +247 -0
  79. package/dist/targets/akash/pricing.d.ts.map +1 -0
  80. package/dist/targets/akash/pricing.js +246 -0
  81. package/dist/targets/akash/pricing.js.map +1 -0
  82. package/dist/targets/akash/provider-client.d.ts +114 -0
  83. package/dist/targets/akash/provider-client.d.ts.map +1 -0
  84. package/dist/targets/akash/provider-client.js +318 -0
  85. package/dist/targets/akash/provider-client.js.map +1 -0
  86. package/dist/targets/akash/provider-metadata.d.ts +228 -0
  87. package/dist/targets/akash/provider-metadata.d.ts.map +1 -0
  88. package/dist/targets/akash/provider-metadata.js +14 -0
  89. package/dist/targets/akash/provider-metadata.js.map +1 -0
  90. package/dist/targets/akash/provider-service.d.ts +133 -0
  91. package/dist/targets/akash/provider-service.d.ts.map +1 -0
  92. package/dist/targets/akash/provider-service.js +391 -0
  93. package/dist/targets/akash/provider-service.js.map +1 -0
  94. package/dist/targets/akash/query-client.d.ts +125 -0
  95. package/dist/targets/akash/query-client.d.ts.map +1 -0
  96. package/dist/targets/akash/query-client.js +332 -0
  97. package/dist/targets/akash/query-client.js.map +1 -0
  98. package/dist/targets/akash/sdl-generator.d.ts +31 -0
  99. package/dist/targets/akash/sdl-generator.d.ts.map +1 -0
  100. package/dist/targets/akash/sdl-generator.js +279 -0
  101. package/dist/targets/akash/sdl-generator.js.map +1 -0
  102. package/dist/targets/akash/types.d.ts +285 -0
  103. package/dist/targets/akash/types.d.ts.map +1 -0
  104. package/dist/targets/akash/types.js +54 -0
  105. package/dist/targets/akash/types.js.map +1 -0
  106. package/dist/targets/akash/wallet-manager.d.ts +526 -0
  107. package/dist/targets/akash/wallet-manager.d.ts.map +1 -0
  108. package/dist/targets/akash/wallet-manager.js +953 -0
  109. package/dist/targets/akash/wallet-manager.js.map +1 -0
  110. package/dist/targets/local/compose-generator.d.ts +244 -0
  111. package/dist/targets/local/compose-generator.d.ts.map +1 -0
  112. package/dist/targets/local/compose-generator.js +324 -0
  113. package/dist/targets/local/compose-generator.js.map +1 -0
  114. package/dist/targets/local/deployer.d.ts +82 -0
  115. package/dist/targets/local/deployer.d.ts.map +1 -0
  116. package/dist/targets/local/deployer.js +367 -0
  117. package/dist/targets/local/deployer.js.map +1 -0
  118. package/dist/targets/local/engine-manager.d.ts +155 -0
  119. package/dist/targets/local/engine-manager.d.ts.map +1 -0
  120. package/dist/targets/local/engine-manager.js +250 -0
  121. package/dist/targets/local/engine-manager.js.map +1 -0
  122. package/dist/targets/local/index.d.ts +40 -0
  123. package/dist/targets/local/index.d.ts.map +1 -0
  124. package/dist/targets/local/index.js +43 -0
  125. package/dist/targets/local/index.js.map +1 -0
  126. package/dist/targets/local/network-manager.d.ts +160 -0
  127. package/dist/targets/local/network-manager.d.ts.map +1 -0
  128. package/dist/targets/local/network-manager.js +337 -0
  129. package/dist/targets/local/network-manager.js.map +1 -0
  130. package/dist/targets/local/types.d.ts +327 -0
  131. package/dist/targets/local/types.d.ts.map +1 -0
  132. package/dist/targets/local/types.js +9 -0
  133. package/dist/targets/local/types.js.map +1 -0
  134. package/dist/types/common.d.ts +585 -0
  135. package/dist/types/common.d.ts.map +1 -0
  136. package/dist/types/common.js +13 -0
  137. package/dist/types/common.js.map +1 -0
  138. package/dist/types/index.d.ts +15 -0
  139. package/dist/types/index.d.ts.map +1 -0
  140. package/dist/types/index.js +12 -0
  141. package/dist/types/index.js.map +1 -0
  142. package/dist/types/options.d.ts +329 -0
  143. package/dist/types/options.d.ts.map +1 -0
  144. package/dist/types/options.js +10 -0
  145. package/dist/types/options.js.map +1 -0
  146. package/dist/types/profiles.d.ts +329 -0
  147. package/dist/types/profiles.d.ts.map +1 -0
  148. package/dist/types/profiles.js +27 -0
  149. package/dist/types/profiles.js.map +1 -0
  150. package/dist/types/results.d.ts +443 -0
  151. package/dist/types/results.d.ts.map +1 -0
  152. package/dist/types/results.js +64 -0
  153. package/dist/types/results.js.map +1 -0
  154. package/dist/types/validators.d.ts +118 -0
  155. package/dist/types/validators.d.ts.map +1 -0
  156. package/dist/types/validators.js +198 -0
  157. package/dist/types/validators.js.map +1 -0
  158. package/dist/utils/command-runner.d.ts +128 -0
  159. package/dist/utils/command-runner.d.ts.map +1 -0
  160. package/dist/utils/command-runner.js +210 -0
  161. package/dist/utils/command-runner.js.map +1 -0
  162. package/dist/utils/index.d.ts +10 -0
  163. package/dist/utils/index.d.ts.map +1 -0
  164. package/dist/utils/index.js +10 -0
  165. package/dist/utils/index.js.map +1 -0
  166. package/dist/utils/logger.d.ts +68 -0
  167. package/dist/utils/logger.d.ts.map +1 -0
  168. package/dist/utils/logger.js +93 -0
  169. package/dist/utils/logger.js.map +1 -0
  170. package/dist/utils/profile-loader.d.ts +76 -0
  171. package/dist/utils/profile-loader.d.ts.map +1 -0
  172. package/dist/utils/profile-loader.js +194 -0
  173. package/dist/utils/profile-loader.js.map +1 -0
  174. package/dist/utils/registry/index.d.ts +27 -0
  175. package/dist/utils/registry/index.d.ts.map +1 -0
  176. package/dist/utils/registry/index.js +29 -0
  177. package/dist/utils/registry/index.js.map +1 -0
  178. package/dist/utils/registry/manager.d.ts +319 -0
  179. package/dist/utils/registry/manager.d.ts.map +1 -0
  180. package/dist/utils/registry/manager.js +671 -0
  181. package/dist/utils/registry/manager.js.map +1 -0
  182. package/dist/utils/registry/setup.d.ts +135 -0
  183. package/dist/utils/registry/setup.d.ts.map +1 -0
  184. package/dist/utils/registry/setup.js +207 -0
  185. package/dist/utils/registry/setup.js.map +1 -0
  186. package/dist/utils/registry/transformer.d.ts +92 -0
  187. package/dist/utils/registry/transformer.d.ts.map +1 -0
  188. package/dist/utils/registry/transformer.js +131 -0
  189. package/dist/utils/registry/transformer.js.map +1 -0
  190. package/dist/utils/registry/types.d.ts +241 -0
  191. package/dist/utils/registry/types.d.ts.map +1 -0
  192. package/dist/utils/registry/types.js +10 -0
  193. package/dist/utils/registry/types.js.map +1 -0
  194. package/docs/EXAMPLES.md +293 -0
  195. package/docs/PLACEMENT.md +433 -0
  196. package/docs/STORAGE.md +318 -0
  197. package/docs/building-provider-reliability-tracker.md +2581 -0
  198. package/package.json +109 -0
@@ -0,0 +1,2581 @@
1
+ # Building a Provider Reliability Tracker for Akash Network
2
+
3
+ > **Purpose:** A comprehensive guide to building your own provider monitoring and reliability tracking service for the Akash Network.
4
+ >
5
+ > **Author:** KADI Infrastructure Team
6
+ > **Last Updated:** 2025-10-15
7
+ > **Difficulty:** Intermediate
8
+ > **Estimated Time:** 1-2 weeks for full implementation
9
+
10
+ ---
11
+
12
+ ## Table of Contents
13
+
14
+ 1. [Introduction](#introduction)
15
+ 2. [Understanding the Components](#understanding-the-components)
16
+ 3. [Architecture Overview](#architecture-overview)
17
+ 4. [Prerequisites](#prerequisites)
18
+ 5. [Implementation Guide](#implementation-guide)
19
+ 6. [Deployment](#deployment)
20
+ 7. [Maintenance](#maintenance)
21
+ 8. [Advanced Features](#advanced-features)
22
+
23
+ ---
24
+
25
+ ## Introduction
26
+
27
+ ### What is a Provider Reliability Tracker?
28
+
29
+ A provider reliability tracker is a service that continuously monitors Akash Network providers to:
30
+ - Track uptime/downtime over time (1 day, 7 days, 30 days)
31
+ - Monitor response times and availability
32
+ - Aggregate provider metadata (location, version, capabilities)
33
+ - Provide an API for querying provider statistics
34
+
35
+ This data helps users make informed decisions when selecting providers for their deployments.
36
+
37
+ ### What is a Blockchain Indexer?
38
+
39
+ A **blockchain indexer** is a service that:
40
+ 1. **Monitors blockchain events** - Watches for new blocks and transactions
41
+ 2. **Extracts relevant data** - Parses transactions to find specific information
42
+ 3. **Stores data in a queryable format** - Saves to a database for fast access
43
+ 4. **Provides fast lookups** - Offers API endpoints to query historical data
44
+
45
+ **Why do we need it?**
46
+
47
+ The Akash blockchain stores provider information, but querying it directly is:
48
+ - **Slow** - Each query requires RPC calls to blockchain nodes
49
+ - **Limited** - You can only query current state, not historical trends
50
+ - **Resource-intensive** - Repeated queries burden the network
51
+
52
+ An indexer solves this by:
53
+ - **Pre-fetching data** - Queries blockchain once, stores results
54
+ - **Enabling historical queries** - Tracks changes over time
55
+ - **Fast API responses** - Returns data from local database (milliseconds vs seconds)
56
+
57
+ **Example:**
58
+ - Without indexer: "Is this provider online right now?" → Query blockchain → 2-5 seconds
59
+ - With indexer: "What was this provider's uptime last month?" → Query local DB → 10-50ms
60
+
61
+ ### What Problem Are We Solving?
62
+
63
+ When deploying to Akash, you receive multiple provider bids. But without reliability data:
64
+ - ❌ You can't tell which providers are reliable
65
+ - ❌ You don't know historical uptime
66
+ - ❌ You can't identify providers that frequently go offline
67
+ - ❌ You're choosing blindly based only on price
68
+
69
+ With a reliability tracker:
70
+ - ✅ See provider uptime percentages (99.8% vs 85%)
71
+ - ✅ Filter out unreliable providers
72
+ - ✅ Make data-driven deployment decisions
73
+ - ✅ Track provider performance over time
74
+
75
+ ### Existing Solutions
76
+
77
+ **Cloudmos (formerly Akash Console)** runs their own tracker:
78
+ - API: `https://api.cloudmos.io/v1/providers`
79
+ - Indexes 100+ providers
80
+ - Tracks uptime, location, versions
81
+ - Free to use BUT:
82
+ - Closed source
83
+ - Centralized (single point of failure)
84
+ - May have rate limits or access restrictions
85
+
86
+ **Building your own gives you:**
87
+ - Full control over data collection
88
+ - Customizable metrics
89
+ - No rate limits
90
+ - Can add custom features
91
+ - Own your infrastructure
92
+
93
+ ---
94
+
95
+ ## Understanding the Components
96
+
97
+ Before building, let's understand each component:
98
+
99
+ ### 1. Blockchain Indexer
100
+
101
+ **Purpose:** Discover and track providers registered on Akash Network
102
+
103
+ **What it does:**
104
+ ```
105
+ Every 10-30 minutes:
106
+
107
+ Query blockchain for all providers
108
+
109
+ Check for new providers or updates
110
+
111
+ Store in database
112
+
113
+ Update provider metadata
114
+ ```
115
+
116
+ **Key data collected:**
117
+ - Provider address (unique identifier)
118
+ - Host URI (provider's API endpoint)
119
+ - Attributes (audited-by, region, capabilities)
120
+ - Registration height (when they joined)
121
+
122
+ ### 2. Health Checker
123
+
124
+ **Purpose:** Continuously ping providers to check if they're online
125
+
126
+ **What it does:**
127
+ ```
128
+ Every 5-10 minutes:
129
+
130
+ For each known provider:
131
+
132
+ Send HTTP request to /status endpoint
133
+
134
+ Record: online/offline, response time
135
+
136
+ Store result in time-series database
137
+ ```
138
+
139
+ **Key metrics collected:**
140
+ - Is online? (boolean)
141
+ - Response time (milliseconds)
142
+ - Error message (if offline)
143
+ - Timestamp
144
+
145
+ ### 3. Metrics Calculator
146
+
147
+ **Purpose:** Aggregate health check data into uptime percentages
148
+
149
+ **What it does:**
150
+ ```
151
+ Every 1 hour:
152
+
153
+ For each provider:
154
+
155
+ Calculate uptime for 1d, 7d, 30d periods
156
+
157
+ Update provider_metrics table
158
+
159
+ Calculate averages, percentages
160
+ ```
161
+
162
+ **Calculations:**
163
+ ```
164
+ uptime_7d = (successful_checks / total_checks) over last 7 days
165
+
166
+ Example:
167
+ - 2016 checks in 7 days (144 checks/day × 7 days, checking every 10 minutes)
168
+ - 2010 successful, 6 failed
169
+ - Uptime = 2010/2016 = 99.7%
170
+ ```
171
+
172
+ ### 4. API Server
173
+
174
+ **Purpose:** Serve provider data to your applications
175
+
176
+ **What it provides:**
177
+ ```
178
+ GET /v1/providers
179
+ → Returns all providers with uptime stats
180
+
181
+ GET /v1/providers/:address
182
+ → Returns detailed info for one provider
183
+
184
+ GET /v1/providers/:address/history
185
+ → Returns historical uptime data
186
+ ```
187
+
188
+ ---
189
+
190
+ ## Architecture Overview
191
+
192
+ ```
193
+ ┌─────────────────────────────────────────────────────────────────┐
194
+ │ Akash Network Blockchain │
195
+ │ (Source of truth for provider registrations) │
196
+ └────────────────────┬────────────────────────────────────────────┘
197
+
198
+ │ RPC Queries (every 10-30 min)
199
+
200
+
201
+ ┌──────────────────────┐
202
+ │ Blockchain Indexer │
203
+ │ Discovers providers │
204
+ └──────────┬───────────┘
205
+
206
+ │ Stores provider info
207
+
208
+
209
+ ┌──────────────────────┐
210
+ │ PostgreSQL Database │◄───────────┐
211
+ │ (Provider registry) │ │
212
+ └──────────┬───────────┘ │
213
+ │ │
214
+ │ Reads provider list │ Stores health checks
215
+ │ │
216
+ ▼ │
217
+ ┌──────────────────────┐ │
218
+ │ Health Checker │────────────┘
219
+ │ Pings all providers │
220
+ │ every 5-10 minutes │
221
+ └──────────────────────┘
222
+
223
+ │ Triggers every hour
224
+
225
+
226
+ ┌──────────────────────┐
227
+ │ Metrics Calculator │
228
+ │ Computes uptime % │
229
+ └──────────┬───────────┘
230
+
231
+ │ Updates metrics
232
+
233
+
234
+ ┌──────────────────────┐
235
+ │ PostgreSQL Database │
236
+ │ (Metrics table) │
237
+ └──────────┬───────────┘
238
+
239
+ │ Queries data
240
+
241
+
242
+ ┌──────────────────────┐
243
+ │ API Server │
244
+ │ Express/Fastify │
245
+ └──────────┬───────────┘
246
+
247
+ │ HTTP/JSON
248
+
249
+
250
+ ┌──────────────────────┐
251
+ │ Your Applications │
252
+ │ (kadi-deploy, etc) │
253
+ └──────────────────────┘
254
+ ```
255
+
256
+ ### Data Flow Example
257
+
258
+ **Scenario:** A new provider joins Akash Network
259
+
260
+ ```
261
+ 1. Provider registers on blockchain
262
+
263
+ 2. Indexer detects new provider (within 30 min)
264
+
265
+ 3. Indexer stores provider in database
266
+
267
+ 4. Health Checker sees new provider in database
268
+
269
+ 5. Health Checker starts pinging provider every 10 min
270
+
271
+ 6. After 24 hours, enough data exists
272
+
273
+ 7. Metrics Calculator computes uptime_1d
274
+
275
+ 8. API serves provider data with uptime stats
276
+
277
+ 9. Your deployment tool uses data to filter providers
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Prerequisites
283
+
284
+ ### Knowledge Requirements
285
+
286
+ - **TypeScript/Node.js** - Core implementation language
287
+ - **SQL/PostgreSQL** - Database queries and schema design
288
+ - **REST APIs** - Building and consuming HTTP APIs
289
+ - **Async Programming** - Handling concurrent operations
290
+ - **Blockchain basics** - Understanding RPC, queries, addresses
291
+
292
+ ### System Requirements
293
+
294
+ **Development:**
295
+ - Node.js 18+ or Bun
296
+ - PostgreSQL 14+ (or Docker with PostgreSQL image)
297
+ - 4GB RAM minimum
298
+ - 10GB storage
299
+
300
+ **Production:**
301
+ - VPS or cloud instance (2-4GB RAM recommended)
302
+ - PostgreSQL with TimescaleDB extension (optional but recommended)
303
+ - 50GB+ storage (for historical data)
304
+ - Reliable network connection
305
+
306
+ ### Dependencies
307
+
308
+ ```json
309
+ {
310
+ "dependencies": {
311
+ "@akashnetwork/akashjs": "^0.7.0",
312
+ "@akashnetwork/akash-api": "^1.0.0",
313
+ "pg": "^8.11.0",
314
+ "express": "^4.18.2",
315
+ "node-cron": "^3.0.2",
316
+ "axios": "^1.6.0"
317
+ },
318
+ "devDependencies": {
319
+ "@types/node": "^20.0.0",
320
+ "@types/pg": "^8.10.0",
321
+ "@types/express": "^4.17.17",
322
+ "typescript": "^5.2.0",
323
+ "tsx": "^4.7.0"
324
+ }
325
+ }
326
+ ```
327
+
328
+ ---
329
+
330
+ ## Implementation Guide
331
+
332
+ ### Step 1: Project Setup
333
+
334
+ Create the project structure:
335
+
336
+ ```bash
337
+ mkdir akash-provider-tracker
338
+ cd akash-provider-tracker
339
+ npm init -y
340
+
341
+ # Install dependencies
342
+ npm install @akashnetwork/akashjs @akashnetwork/akash-api pg express node-cron axios
343
+ npm install -D @types/node @types/pg @types/express typescript tsx
344
+
345
+ # Initialize TypeScript
346
+ npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --esModuleInterop true --outDir dist
347
+ ```
348
+
349
+ Project structure:
350
+ ```
351
+ akash-provider-tracker/
352
+ ├── src/
353
+ │ ├── indexer/
354
+ │ │ └── blockchain-indexer.ts
355
+ │ ├── health/
356
+ │ │ └── health-checker.ts
357
+ │ ├── metrics/
358
+ │ │ └── metrics-calculator.ts
359
+ │ ├── api/
360
+ │ │ └── server.ts
361
+ │ ├── database/
362
+ │ │ ├── client.ts
363
+ │ │ └── schema.sql
364
+ │ ├── types/
365
+ │ │ └── index.ts
366
+ │ └── index.ts
367
+ ├── package.json
368
+ ├── tsconfig.json
369
+ └── README.md
370
+ ```
371
+
372
+ ### Step 2: Database Schema
373
+
374
+ Create `src/database/schema.sql`:
375
+
376
+ ```sql
377
+ -- Enable UUID extension
378
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
379
+
380
+ -- Providers table
381
+ -- Stores basic information about each provider
382
+ CREATE TABLE providers (
383
+ -- Primary key: provider's blockchain address
384
+ address TEXT PRIMARY KEY,
385
+
386
+ -- Provider's API endpoint (e.g., https://provider.example.com:8443)
387
+ host_uri TEXT NOT NULL,
388
+
389
+ -- Optional human-readable name
390
+ name TEXT,
391
+
392
+ -- Blockchain height when provider was registered
393
+ created_height BIGINT,
394
+
395
+ -- Whether provider is audited by at least one auditor
396
+ is_audited BOOLEAN DEFAULT FALSE,
397
+
398
+ -- Provider attributes as JSON (flexible storage)
399
+ attributes JSONB DEFAULT '[]'::jsonb,
400
+
401
+ -- Metadata timestamps
402
+ first_seen_at TIMESTAMP DEFAULT NOW(),
403
+ last_updated_at TIMESTAMP DEFAULT NOW(),
404
+
405
+ -- Indexes for fast queries
406
+ CONSTRAINT valid_host_uri CHECK (host_uri ~ '^https?://')
407
+ );
408
+
409
+ CREATE INDEX idx_providers_is_audited ON providers(is_audited);
410
+ CREATE INDEX idx_providers_created_height ON providers(created_height);
411
+
412
+ -- Health checks table
413
+ -- Time-series data: one row per check per provider
414
+ CREATE TABLE health_checks (
415
+ -- Unique ID for each check
416
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
417
+
418
+ -- Which provider was checked
419
+ provider_address TEXT NOT NULL REFERENCES providers(address) ON DELETE CASCADE,
420
+
421
+ -- When the check was performed
422
+ checked_at TIMESTAMP NOT NULL DEFAULT NOW(),
423
+
424
+ -- Result: was the provider online?
425
+ is_online BOOLEAN NOT NULL,
426
+
427
+ -- How long did the provider take to respond (NULL if offline)
428
+ response_time_ms INTEGER,
429
+
430
+ -- HTTP status code (200, 404, 500, etc)
431
+ http_status INTEGER,
432
+
433
+ -- Error message if check failed
434
+ error_message TEXT,
435
+
436
+ -- Additional metadata from /status endpoint
437
+ metadata JSONB
438
+ );
439
+
440
+ -- Indexes for fast time-series queries
441
+ CREATE INDEX idx_health_checks_provider ON health_checks(provider_address, checked_at DESC);
442
+ CREATE INDEX idx_health_checks_time ON health_checks(checked_at DESC);
443
+
444
+ -- Optional: Convert to TimescaleDB hypertable for better performance
445
+ -- Requires TimescaleDB extension
446
+ -- SELECT create_hypertable('health_checks', 'checked_at');
447
+
448
+ -- Provider metrics table
449
+ -- Aggregated statistics, updated hourly
450
+ CREATE TABLE provider_metrics (
451
+ -- One row per provider
452
+ provider_address TEXT PRIMARY KEY REFERENCES providers(address) ON DELETE CASCADE,
453
+
454
+ -- Uptime percentages (0.0 to 1.0)
455
+ uptime_1d DECIMAL(5,4), -- Last 24 hours
456
+ uptime_7d DECIMAL(5,4), -- Last 7 days
457
+ uptime_30d DECIMAL(5,4), -- Last 30 days
458
+
459
+ -- Current status
460
+ is_currently_online BOOLEAN,
461
+ last_online_at TIMESTAMP,
462
+ last_offline_at TIMESTAMP,
463
+
464
+ -- Response time statistics (milliseconds)
465
+ avg_response_time_1d INTEGER,
466
+ avg_response_time_7d INTEGER,
467
+ avg_response_time_30d INTEGER,
468
+
469
+ -- Count of checks performed
470
+ total_checks_1d INTEGER DEFAULT 0,
471
+ total_checks_7d INTEGER DEFAULT 0,
472
+ total_checks_30d INTEGER DEFAULT 0,
473
+
474
+ -- Metadata
475
+ last_calculated_at TIMESTAMP DEFAULT NOW()
476
+ );
477
+
478
+ CREATE INDEX idx_metrics_uptime_7d ON provider_metrics(uptime_7d DESC);
479
+ CREATE INDEX idx_metrics_currently_online ON provider_metrics(is_currently_online);
480
+
481
+ -- Provider locations table (optional)
482
+ -- IP geolocation data
483
+ CREATE TABLE provider_locations (
484
+ provider_address TEXT PRIMARY KEY REFERENCES providers(address) ON DELETE CASCADE,
485
+
486
+ -- Geographic data
487
+ country TEXT,
488
+ country_code TEXT,
489
+ region TEXT,
490
+ region_code TEXT,
491
+ city TEXT,
492
+ latitude DECIMAL(10,8),
493
+ longitude DECIMAL(11,8),
494
+ timezone TEXT,
495
+
496
+ -- IP information
497
+ ip_address TEXT,
498
+
499
+ -- When location was determined
500
+ detected_at TIMESTAMP DEFAULT NOW()
501
+ );
502
+
503
+ CREATE INDEX idx_locations_country ON provider_locations(country_code);
504
+
505
+ -- Provider versions table
506
+ -- Track provider software versions over time
507
+ CREATE TABLE provider_versions (
508
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
509
+ provider_address TEXT NOT NULL REFERENCES providers(address) ON DELETE CASCADE,
510
+
511
+ -- Version information
512
+ akash_version TEXT,
513
+ cosmos_sdk_version TEXT,
514
+
515
+ -- Kubernetes version (from /version endpoint)
516
+ k8s_version TEXT,
517
+
518
+ -- When this version was detected
519
+ detected_at TIMESTAMP DEFAULT NOW()
520
+ );
521
+
522
+ CREATE INDEX idx_versions_provider ON provider_versions(provider_address, detected_at DESC);
523
+
524
+ -- Create view for easy querying
525
+ CREATE VIEW provider_details AS
526
+ SELECT
527
+ p.address,
528
+ p.host_uri,
529
+ p.name,
530
+ p.is_audited,
531
+ p.created_height,
532
+ p.first_seen_at,
533
+ m.uptime_1d,
534
+ m.uptime_7d,
535
+ m.uptime_30d,
536
+ m.is_currently_online,
537
+ m.last_online_at,
538
+ m.avg_response_time_7d,
539
+ l.country,
540
+ l.country_code,
541
+ l.region,
542
+ l.city,
543
+ l.latitude,
544
+ l.longitude
545
+ FROM providers p
546
+ LEFT JOIN provider_metrics m ON p.address = m.provider_address
547
+ LEFT JOIN provider_locations l ON p.address = l.provider_address;
548
+ ```
549
+
550
+ ### Step 3: Database Client
551
+
552
+ Create `src/database/client.ts`:
553
+
554
+ ```typescript
555
+ import pg from 'pg';
556
+ const { Pool } = pg;
557
+
558
+ /**
559
+ * Database configuration
560
+ */
561
+ interface DatabaseConfig {
562
+ host: string;
563
+ port: number;
564
+ database: string;
565
+ user: string;
566
+ password: string;
567
+ }
568
+
569
+ /**
570
+ * Database client singleton
571
+ */
572
+ class DatabaseClient {
573
+ private static instance: DatabaseClient;
574
+ private pool: pg.Pool;
575
+
576
+ private constructor(config: DatabaseConfig) {
577
+ this.pool = new Pool({
578
+ host: config.host,
579
+ port: config.port,
580
+ database: config.database,
581
+ user: config.user,
582
+ password: config.password,
583
+ max: 20, // Maximum number of connections
584
+ idleTimeoutMillis: 30000,
585
+ connectionTimeoutMillis: 2000,
586
+ });
587
+
588
+ // Handle pool errors
589
+ this.pool.on('error', (err) => {
590
+ console.error('Unexpected database pool error:', err);
591
+ });
592
+ }
593
+
594
+ /**
595
+ * Get database client instance (singleton)
596
+ */
597
+ static getInstance(config?: DatabaseConfig): DatabaseClient {
598
+ if (!DatabaseClient.instance) {
599
+ if (!config) {
600
+ throw new Error('Database config required for first initialization');
601
+ }
602
+ DatabaseClient.instance = new DatabaseClient(config);
603
+ }
604
+ return DatabaseClient.instance;
605
+ }
606
+
607
+ /**
608
+ * Execute a query
609
+ */
610
+ async query<T = any>(text: string, params?: any[]): Promise<pg.QueryResult<T>> {
611
+ const start = Date.now();
612
+ try {
613
+ const result = await this.pool.query<T>(text, params);
614
+ const duration = Date.now() - start;
615
+
616
+ // Log slow queries (> 1 second)
617
+ if (duration > 1000) {
618
+ console.warn(`Slow query (${duration}ms):`, text);
619
+ }
620
+
621
+ return result;
622
+ } catch (error) {
623
+ console.error('Database query error:', error);
624
+ console.error('Query:', text);
625
+ console.error('Params:', params);
626
+ throw error;
627
+ }
628
+ }
629
+
630
+ /**
631
+ * Execute multiple queries in a transaction
632
+ */
633
+ async transaction<T>(callback: (client: pg.PoolClient) => Promise<T>): Promise<T> {
634
+ const client = await this.pool.connect();
635
+
636
+ try {
637
+ await client.query('BEGIN');
638
+ const result = await callback(client);
639
+ await client.query('COMMIT');
640
+ return result;
641
+ } catch (error) {
642
+ await client.query('ROLLBACK');
643
+ throw error;
644
+ } finally {
645
+ client.release();
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Close database connection pool
651
+ */
652
+ async close(): Promise<void> {
653
+ await this.pool.end();
654
+ }
655
+
656
+ /**
657
+ * Check if database is healthy
658
+ */
659
+ async healthCheck(): Promise<boolean> {
660
+ try {
661
+ await this.query('SELECT 1');
662
+ return true;
663
+ } catch (error) {
664
+ console.error('Database health check failed:', error);
665
+ return false;
666
+ }
667
+ }
668
+ }
669
+
670
+ // Export singleton instance
671
+ export const getDatabaseClient = (config?: DatabaseConfig) => {
672
+ return DatabaseClient.getInstance(config);
673
+ };
674
+
675
+ export default DatabaseClient;
676
+ ```
677
+
678
+ ### Step 4: Type Definitions
679
+
680
+ Create `src/types/index.ts`:
681
+
682
+ ```typescript
683
+ /**
684
+ * Provider information from blockchain
685
+ */
686
+ export interface Provider {
687
+ address: string;
688
+ hostUri: string;
689
+ name?: string;
690
+ createdHeight: number;
691
+ isAudited: boolean;
692
+ attributes: ProviderAttribute[];
693
+ }
694
+
695
+ /**
696
+ * Provider attribute (key-value pair with auditor info)
697
+ */
698
+ export interface ProviderAttribute {
699
+ key: string;
700
+ value: string;
701
+ auditedBy?: string[];
702
+ }
703
+
704
+ /**
705
+ * Health check result
706
+ */
707
+ export interface HealthCheck {
708
+ id?: string;
709
+ providerAddress: string;
710
+ checkedAt: Date;
711
+ isOnline: boolean;
712
+ responseTimeMs?: number;
713
+ httpStatus?: number;
714
+ errorMessage?: string;
715
+ metadata?: Record<string, any>;
716
+ }
717
+
718
+ /**
719
+ * Provider metrics (aggregated statistics)
720
+ */
721
+ export interface ProviderMetrics {
722
+ providerAddress: string;
723
+ uptime1d?: number; // 0.0 to 1.0
724
+ uptime7d?: number;
725
+ uptime30d?: number;
726
+ isCurrentlyOnline: boolean;
727
+ lastOnlineAt?: Date;
728
+ lastOfflineAt?: Date;
729
+ avgResponseTime1d?: number;
730
+ avgResponseTime7d?: number;
731
+ avgResponseTime30d?: number;
732
+ totalChecks1d: number;
733
+ totalChecks7d: number;
734
+ totalChecks30d: number;
735
+ lastCalculatedAt: Date;
736
+ }
737
+
738
+ /**
739
+ * Provider location data
740
+ */
741
+ export interface ProviderLocation {
742
+ providerAddress: string;
743
+ country?: string;
744
+ countryCode?: string;
745
+ region?: string;
746
+ regionCode?: string;
747
+ city?: string;
748
+ latitude?: number;
749
+ longitude?: number;
750
+ timezone?: string;
751
+ ipAddress?: string;
752
+ }
753
+
754
+ /**
755
+ * Provider status endpoint response
756
+ * (what we get from https://provider.example.com/status)
757
+ */
758
+ export interface ProviderStatus {
759
+ cluster: {
760
+ leases: number;
761
+ inventory: {
762
+ error?: string;
763
+ active: ResourceUsage[];
764
+ pending: ResourceUsage[];
765
+ available: {
766
+ nodes: ResourceCapacity[];
767
+ };
768
+ };
769
+ };
770
+ bidengine: {
771
+ orders: number;
772
+ };
773
+ manifest: {
774
+ deployments: number;
775
+ };
776
+ cluster_public_hostname?: string;
777
+ address: string;
778
+ }
779
+
780
+ export interface ResourceUsage {
781
+ cpu: number;
782
+ gpu: number;
783
+ memory: number;
784
+ storage_ephemeral: number;
785
+ }
786
+
787
+ export interface ResourceCapacity {
788
+ cpu: number;
789
+ gpu: number;
790
+ memory: number;
791
+ storage_ephemeral: number;
792
+ }
793
+
794
+ /**
795
+ * Provider version endpoint response
796
+ * (what we get from https://provider.example.com/version)
797
+ */
798
+ export interface ProviderVersion {
799
+ akash: {
800
+ version: string;
801
+ commit: string;
802
+ buildTags: string;
803
+ go: string;
804
+ cosmosSdkVersion: string;
805
+ };
806
+ kube: {
807
+ major: string;
808
+ minor: string;
809
+ gitVersion: string;
810
+ gitCommit: string;
811
+ gitTreeState: string;
812
+ buildDate: string;
813
+ goVersion: string;
814
+ compiler: string;
815
+ platform: string;
816
+ };
817
+ }
818
+
819
+ /**
820
+ * Complete provider details (for API responses)
821
+ */
822
+ export interface ProviderDetails {
823
+ address: string;
824
+ hostUri: string;
825
+ name?: string;
826
+ isAudited: boolean;
827
+ createdHeight: number;
828
+ firstSeenAt: Date;
829
+
830
+ // Metrics
831
+ uptime1d?: number;
832
+ uptime7d?: number;
833
+ uptime30d?: number;
834
+ isCurrentlyOnline: boolean;
835
+ lastOnlineAt?: Date;
836
+ avgResponseTime7d?: number;
837
+
838
+ // Location
839
+ country?: string;
840
+ countryCode?: string;
841
+ region?: string;
842
+ city?: string;
843
+ latitude?: number;
844
+ longitude?: number;
845
+
846
+ // Additional
847
+ attributes: ProviderAttribute[];
848
+ }
849
+ ```
850
+
851
+ ### Step 5: Blockchain Indexer
852
+
853
+ Create `src/indexer/blockchain-indexer.ts`:
854
+
855
+ ```typescript
856
+ import { getRpc } from '@akashnetwork/akashjs/build/rpc';
857
+ import { QueryClientImpl as QueryProviderClient } from '@akashnetwork/akash-api/akash/provider/v1beta3';
858
+ import { getDatabaseClient } from '../database/client.js';
859
+ import type { Provider, ProviderAttribute } from '../types/index.js';
860
+
861
+ /**
862
+ * Blockchain Indexer
863
+ *
864
+ * Discovers providers registered on Akash blockchain and stores them in database.
865
+ * Runs periodically to catch new providers and updates.
866
+ */
867
+ export class BlockchainIndexer {
868
+ private rpcEndpoint: string;
869
+ private db: ReturnType<typeof getDatabaseClient>;
870
+ private isRunning = false;
871
+
872
+ constructor(rpcEndpoint: string = 'https://rpc.akashnet.net:443') {
873
+ this.rpcEndpoint = rpcEndpoint;
874
+ this.db = getDatabaseClient();
875
+ }
876
+
877
+ /**
878
+ * Start the indexer (runs in background)
879
+ */
880
+ async start(intervalMinutes: number = 30): Promise<void> {
881
+ console.log('🔍 Starting blockchain indexer...');
882
+ console.log(` RPC: ${this.rpcEndpoint}`);
883
+ console.log(` Interval: ${intervalMinutes} minutes`);
884
+
885
+ // Initial run
886
+ await this.indexProviders();
887
+
888
+ // Schedule periodic runs
889
+ setInterval(async () => {
890
+ if (!this.isRunning) {
891
+ await this.indexProviders();
892
+ }
893
+ }, intervalMinutes * 60 * 1000);
894
+ }
895
+
896
+ /**
897
+ * Main indexing logic
898
+ */
899
+ private async indexProviders(): Promise<void> {
900
+ if (this.isRunning) {
901
+ console.log('⏩ Indexer already running, skipping...');
902
+ return;
903
+ }
904
+
905
+ this.isRunning = true;
906
+ const startTime = Date.now();
907
+
908
+ try {
909
+ console.log('🔄 Fetching providers from blockchain...');
910
+
911
+ // Step 1: Connect to Akash RPC
912
+ const rpc = await getRpc(this.rpcEndpoint);
913
+ const queryClient = new QueryProviderClient(rpc);
914
+
915
+ // Step 2: Query all providers
916
+ // Note: This returns paginated results, we need to fetch all pages
917
+ let allProviders: any[] = [];
918
+ let nextKey: Uint8Array | undefined = undefined;
919
+
920
+ do {
921
+ const response = await queryClient.Providers({
922
+ pagination: {
923
+ key: nextKey || new Uint8Array(),
924
+ offset: 0n,
925
+ limit: 100n,
926
+ countTotal: true,
927
+ reverse: false,
928
+ },
929
+ });
930
+
931
+ if (response.providers) {
932
+ allProviders = allProviders.concat(response.providers);
933
+ }
934
+
935
+ nextKey = response.pagination?.nextKey;
936
+ } while (nextKey && nextKey.length > 0);
937
+
938
+ console.log(` Found ${allProviders.length} providers on blockchain`);
939
+
940
+ // Step 3: Process each provider
941
+ let newProviders = 0;
942
+ let updatedProviders = 0;
943
+
944
+ for (const provider of allProviders) {
945
+ const transformed = this.transformProvider(provider);
946
+ const exists = await this.providerExists(transformed.address);
947
+
948
+ if (exists) {
949
+ await this.updateProvider(transformed);
950
+ updatedProviders++;
951
+ } else {
952
+ await this.insertProvider(transformed);
953
+ newProviders++;
954
+ }
955
+ }
956
+
957
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
958
+ console.log(`✅ Indexing complete in ${duration}s`);
959
+ console.log(` New providers: ${newProviders}`);
960
+ console.log(` Updated providers: ${updatedProviders}`);
961
+ console.log(` Total in database: ${allProviders.length}`);
962
+
963
+ } catch (error) {
964
+ console.error('❌ Indexer error:', error);
965
+ throw error;
966
+ } finally {
967
+ this.isRunning = false;
968
+ }
969
+ }
970
+
971
+ /**
972
+ * Transform blockchain provider data to our format
973
+ */
974
+ private transformProvider(provider: any): Provider {
975
+ // Extract attributes
976
+ const attributes: ProviderAttribute[] = (provider.attributes || []).map((attr: any) => ({
977
+ key: attr.key || '',
978
+ value: attr.value || '',
979
+ auditedBy: attr.auditedBy || [],
980
+ }));
981
+
982
+ // Check if provider is audited (has "audited-by" attribute)
983
+ const isAudited = attributes.some(attr => attr.key === 'audited-by' && attr.value);
984
+
985
+ return {
986
+ address: provider.owner || '',
987
+ hostUri: provider.hostUri || '',
988
+ name: undefined, // Will be populated from /status endpoint by health checker
989
+ createdHeight: Number(provider.createdHeight || 0),
990
+ isAudited,
991
+ attributes,
992
+ };
993
+ }
994
+
995
+ /**
996
+ * Check if provider exists in database
997
+ */
998
+ private async providerExists(address: string): Promise<boolean> {
999
+ const result = await this.db.query(
1000
+ 'SELECT address FROM providers WHERE address = $1',
1001
+ [address]
1002
+ );
1003
+ return result.rows.length > 0;
1004
+ }
1005
+
1006
+ /**
1007
+ * Insert new provider into database
1008
+ */
1009
+ private async insertProvider(provider: Provider): Promise<void> {
1010
+ await this.db.query(
1011
+ `INSERT INTO providers
1012
+ (address, host_uri, name, created_height, is_audited, attributes, first_seen_at, last_updated_at)
1013
+ VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())`,
1014
+ [
1015
+ provider.address,
1016
+ provider.hostUri,
1017
+ provider.name,
1018
+ provider.createdHeight,
1019
+ provider.isAudited,
1020
+ JSON.stringify(provider.attributes),
1021
+ ]
1022
+ );
1023
+ }
1024
+
1025
+ /**
1026
+ * Update existing provider in database
1027
+ */
1028
+ private async updateProvider(provider: Provider): Promise<void> {
1029
+ await this.db.query(
1030
+ `UPDATE providers
1031
+ SET host_uri = $2,
1032
+ is_audited = $3,
1033
+ attributes = $4,
1034
+ last_updated_at = NOW()
1035
+ WHERE address = $1`,
1036
+ [
1037
+ provider.address,
1038
+ provider.hostUri,
1039
+ provider.isAudited,
1040
+ JSON.stringify(provider.attributes),
1041
+ ]
1042
+ );
1043
+ }
1044
+
1045
+ /**
1046
+ * Get count of providers in database
1047
+ */
1048
+ async getProviderCount(): Promise<number> {
1049
+ const result = await this.db.query('SELECT COUNT(*) as count FROM providers');
1050
+ return parseInt(result.rows[0].count);
1051
+ }
1052
+ }
1053
+ ```
1054
+
1055
+ ### Step 6: Health Checker
1056
+
1057
+ Create `src/health/health-checker.ts`:
1058
+
1059
+ ```typescript
1060
+ import axios from 'axios';
1061
+ import { getDatabaseClient } from '../database/client.js';
1062
+ import type { HealthCheck, ProviderStatus, ProviderVersion } from '../types/index.js';
1063
+
1064
+ /**
1065
+ * Health Checker
1066
+ *
1067
+ * Continuously pings all providers to check if they're online.
1068
+ * Records results in time-series database for uptime calculations.
1069
+ */
1070
+ export class HealthChecker {
1071
+ private db: ReturnType<typeof getDatabaseClient>;
1072
+ private isRunning = false;
1073
+ private checkTimeout: number;
1074
+ private userAgent: string;
1075
+
1076
+ constructor(checkTimeoutMs: number = 5000) {
1077
+ this.db = getDatabaseClient();
1078
+ this.checkTimeout = checkTimeoutMs;
1079
+ this.userAgent = 'KADI-Provider-Tracker/1.0';
1080
+ }
1081
+
1082
+ /**
1083
+ * Start the health checker (runs in background)
1084
+ */
1085
+ async start(intervalMinutes: number = 10): Promise<void> {
1086
+ console.log('🏥 Starting health checker...');
1087
+ console.log(` Check timeout: ${this.checkTimeout}ms`);
1088
+ console.log(` Interval: ${intervalMinutes} minutes`);
1089
+
1090
+ // Initial run
1091
+ await this.checkAllProviders();
1092
+
1093
+ // Schedule periodic checks
1094
+ setInterval(async () => {
1095
+ if (!this.isRunning) {
1096
+ await this.checkAllProviders();
1097
+ }
1098
+ }, intervalMinutes * 60 * 1000);
1099
+ }
1100
+
1101
+ /**
1102
+ * Check all providers
1103
+ */
1104
+ private async checkAllProviders(): Promise<void> {
1105
+ if (this.isRunning) {
1106
+ console.log('⏩ Health checker already running, skipping...');
1107
+ return;
1108
+ }
1109
+
1110
+ this.isRunning = true;
1111
+ const startTime = Date.now();
1112
+
1113
+ try {
1114
+ // Fetch all providers from database
1115
+ const result = await this.db.query<{ address: string; host_uri: string }>(
1116
+ 'SELECT address, host_uri FROM providers'
1117
+ );
1118
+
1119
+ const providers = result.rows;
1120
+ console.log(`🔍 Checking health of ${providers.length} providers...`);
1121
+
1122
+ // Check all providers in parallel (with concurrency limit)
1123
+ const concurrencyLimit = 10; // Check 10 providers at once
1124
+ const results: HealthCheck[] = [];
1125
+
1126
+ for (let i = 0; i < providers.length; i += concurrencyLimit) {
1127
+ const batch = providers.slice(i, i + concurrencyLimit);
1128
+ const batchResults = await Promise.all(
1129
+ batch.map(provider => this.checkProvider(provider.address, provider.host_uri))
1130
+ );
1131
+ results.push(...batchResults);
1132
+ }
1133
+
1134
+ // Store all results in database
1135
+ await this.storeHealthChecks(results);
1136
+
1137
+ // Calculate summary
1138
+ const onlineCount = results.filter(r => r.isOnline).length;
1139
+ const offlineCount = results.length - onlineCount;
1140
+ const avgResponseTime = results
1141
+ .filter(r => r.responseTimeMs)
1142
+ .reduce((sum, r) => sum + (r.responseTimeMs || 0), 0) / onlineCount;
1143
+
1144
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
1145
+ console.log(`✅ Health check complete in ${duration}s`);
1146
+ console.log(` Online: ${onlineCount} (${((onlineCount / results.length) * 100).toFixed(1)}%)`);
1147
+ console.log(` Offline: ${offlineCount}`);
1148
+ console.log(` Avg response time: ${avgResponseTime.toFixed(0)}ms`);
1149
+
1150
+ } catch (error) {
1151
+ console.error('❌ Health checker error:', error);
1152
+ throw error;
1153
+ } finally {
1154
+ this.isRunning = false;
1155
+ }
1156
+ }
1157
+
1158
+ /**
1159
+ * Check a single provider's health
1160
+ */
1161
+ private async checkProvider(address: string, hostUri: string): Promise<HealthCheck> {
1162
+ const checkedAt = new Date();
1163
+
1164
+ try {
1165
+ // Ping provider's /status endpoint
1166
+ const startTime = Date.now();
1167
+
1168
+ const response = await axios.get<ProviderStatus>(`${hostUri}/status`, {
1169
+ timeout: this.checkTimeout,
1170
+ headers: {
1171
+ 'User-Agent': this.userAgent,
1172
+ 'Accept': 'application/json',
1173
+ },
1174
+ validateStatus: () => true, // Don't throw on non-200 status
1175
+ });
1176
+
1177
+ const responseTimeMs = Date.now() - startTime;
1178
+ const isOnline = response.status === 200;
1179
+
1180
+ // Try to get provider name from status response
1181
+ if (isOnline && response.data.address) {
1182
+ await this.updateProviderName(address, response.data.cluster_public_hostname);
1183
+ }
1184
+
1185
+ return {
1186
+ providerAddress: address,
1187
+ checkedAt,
1188
+ isOnline,
1189
+ responseTimeMs,
1190
+ httpStatus: response.status,
1191
+ errorMessage: isOnline ? undefined : `HTTP ${response.status}`,
1192
+ metadata: isOnline ? {
1193
+ leases: response.data.cluster?.leases,
1194
+ deployments: response.data.manifest?.deployments,
1195
+ } : undefined,
1196
+ };
1197
+
1198
+ } catch (error) {
1199
+ // Provider is offline or unreachable
1200
+ let errorMessage = 'Unknown error';
1201
+
1202
+ if (axios.isAxiosError(error)) {
1203
+ if (error.code === 'ECONNREFUSED') {
1204
+ errorMessage = 'Connection refused';
1205
+ } else if (error.code === 'ETIMEDOUT') {
1206
+ errorMessage = 'Timeout';
1207
+ } else if (error.code === 'ENOTFOUND') {
1208
+ errorMessage = 'DNS resolution failed';
1209
+ } else if (error.code === 'ECONNRESET') {
1210
+ errorMessage = 'Connection reset';
1211
+ } else {
1212
+ errorMessage = error.message;
1213
+ }
1214
+ }
1215
+
1216
+ return {
1217
+ providerAddress: address,
1218
+ checkedAt,
1219
+ isOnline: false,
1220
+ errorMessage,
1221
+ };
1222
+ }
1223
+ }
1224
+
1225
+ /**
1226
+ * Update provider name in database (if we discovered it from /status)
1227
+ */
1228
+ private async updateProviderName(address: string, name?: string): Promise<void> {
1229
+ if (!name) return;
1230
+
1231
+ await this.db.query(
1232
+ 'UPDATE providers SET name = $2 WHERE address = $1 AND name IS NULL',
1233
+ [address, name]
1234
+ );
1235
+ }
1236
+
1237
+ /**
1238
+ * Store health check results in database
1239
+ */
1240
+ private async storeHealthChecks(checks: HealthCheck[]): Promise<void> {
1241
+ if (checks.length === 0) return;
1242
+
1243
+ // Build bulk insert query
1244
+ const values: any[] = [];
1245
+ const placeholders: string[] = [];
1246
+
1247
+ checks.forEach((check, index) => {
1248
+ const offset = index * 7;
1249
+ placeholders.push(
1250
+ `($${offset + 1}, $${offset + 2}, $${offset + 3}, $${offset + 4}, $${offset + 5}, $${offset + 6}, $${offset + 7})`
1251
+ );
1252
+ values.push(
1253
+ check.providerAddress,
1254
+ check.checkedAt,
1255
+ check.isOnline,
1256
+ check.responseTimeMs || null,
1257
+ check.httpStatus || null,
1258
+ check.errorMessage || null,
1259
+ check.metadata ? JSON.stringify(check.metadata) : null
1260
+ );
1261
+ });
1262
+
1263
+ const query = `
1264
+ INSERT INTO health_checks
1265
+ (provider_address, checked_at, is_online, response_time_ms, http_status, error_message, metadata)
1266
+ VALUES ${placeholders.join(', ')}
1267
+ `;
1268
+
1269
+ await this.db.query(query, values);
1270
+ }
1271
+
1272
+ /**
1273
+ * Get recent health checks for a provider
1274
+ */
1275
+ async getRecentChecks(providerAddress: string, limit: number = 100): Promise<HealthCheck[]> {
1276
+ const result = await this.db.query<HealthCheck>(
1277
+ `SELECT * FROM health_checks
1278
+ WHERE provider_address = $1
1279
+ ORDER BY checked_at DESC
1280
+ LIMIT $2`,
1281
+ [providerAddress, limit]
1282
+ );
1283
+
1284
+ return result.rows;
1285
+ }
1286
+ }
1287
+ ```
1288
+
1289
+ ### Step 7: Metrics Calculator
1290
+
1291
+ Create `src/metrics/metrics-calculator.ts`:
1292
+
1293
+ ```typescript
1294
+ import { getDatabaseClient } from '../database/client.js';
1295
+ import type { ProviderMetrics } from '../types/index.js';
1296
+
1297
+ /**
1298
+ * Metrics Calculator
1299
+ *
1300
+ * Aggregates health check data into uptime percentages and other statistics.
1301
+ * Runs hourly to keep metrics up to date.
1302
+ */
1303
+ export class MetricsCalculator {
1304
+ private db: ReturnType<typeof getDatabaseClient>;
1305
+ private isRunning = false;
1306
+
1307
+ constructor() {
1308
+ this.db = getDatabaseClient();
1309
+ }
1310
+
1311
+ /**
1312
+ * Start the metrics calculator (runs in background)
1313
+ */
1314
+ async start(intervalMinutes: number = 60): Promise<void> {
1315
+ console.log('📊 Starting metrics calculator...');
1316
+ console.log(` Interval: ${intervalMinutes} minutes`);
1317
+
1318
+ // Initial run
1319
+ await this.calculateAllMetrics();
1320
+
1321
+ // Schedule periodic calculations
1322
+ setInterval(async () => {
1323
+ if (!this.isRunning) {
1324
+ await this.calculateAllMetrics();
1325
+ }
1326
+ }, intervalMinutes * 60 * 1000);
1327
+ }
1328
+
1329
+ /**
1330
+ * Calculate metrics for all providers
1331
+ */
1332
+ private async calculateAllMetrics(): Promise<void> {
1333
+ if (this.isRunning) {
1334
+ console.log('⏩ Metrics calculator already running, skipping...');
1335
+ return;
1336
+ }
1337
+
1338
+ this.isRunning = true;
1339
+ const startTime = Date.now();
1340
+
1341
+ try {
1342
+ console.log('🔄 Calculating provider metrics...');
1343
+
1344
+ // Get all provider addresses
1345
+ const result = await this.db.query<{ address: string }>(
1346
+ 'SELECT address FROM providers'
1347
+ );
1348
+
1349
+ const providers = result.rows;
1350
+ console.log(` Processing ${providers.length} providers...`);
1351
+
1352
+ // Calculate metrics for each provider
1353
+ for (const provider of providers) {
1354
+ await this.calculateProviderMetrics(provider.address);
1355
+ }
1356
+
1357
+ const duration = ((Date.now() - startTime) / 1000).toFixed(2);
1358
+ console.log(`✅ Metrics calculation complete in ${duration}s`);
1359
+
1360
+ } catch (error) {
1361
+ console.error('❌ Metrics calculator error:', error);
1362
+ throw error;
1363
+ } finally {
1364
+ this.isRunning = false;
1365
+ }
1366
+ }
1367
+
1368
+ /**
1369
+ * Calculate metrics for a single provider
1370
+ */
1371
+ private async calculateProviderMetrics(providerAddress: string): Promise<void> {
1372
+ // Calculate uptime for different time periods
1373
+ const uptime1d = await this.calculateUptime(providerAddress, 1);
1374
+ const uptime7d = await this.calculateUptime(providerAddress, 7);
1375
+ const uptime30d = await this.calculateUptime(providerAddress, 30);
1376
+
1377
+ // Calculate average response times
1378
+ const avgResponseTime1d = await this.calculateAvgResponseTime(providerAddress, 1);
1379
+ const avgResponseTime7d = await this.calculateAvgResponseTime(providerAddress, 7);
1380
+ const avgResponseTime30d = await this.calculateAvgResponseTime(providerAddress, 30);
1381
+
1382
+ // Get check counts
1383
+ const totalChecks1d = await this.getCheckCount(providerAddress, 1);
1384
+ const totalChecks7d = await this.getCheckCount(providerAddress, 7);
1385
+ const totalChecks30d = await this.getCheckCount(providerAddress, 30);
1386
+
1387
+ // Get current status
1388
+ const currentStatus = await this.getCurrentStatus(providerAddress);
1389
+
1390
+ // Build metrics object
1391
+ const metrics: ProviderMetrics = {
1392
+ providerAddress,
1393
+ uptime1d,
1394
+ uptime7d,
1395
+ uptime30d,
1396
+ isCurrentlyOnline: currentStatus.isCurrentlyOnline,
1397
+ lastOnlineAt: currentStatus.lastOnlineAt,
1398
+ lastOfflineAt: currentStatus.lastOfflineAt,
1399
+ avgResponseTime1d,
1400
+ avgResponseTime7d,
1401
+ avgResponseTime30d,
1402
+ totalChecks1d,
1403
+ totalChecks7d,
1404
+ totalChecks30d,
1405
+ lastCalculatedAt: new Date(),
1406
+ };
1407
+
1408
+ // Upsert metrics into database
1409
+ await this.upsertMetrics(metrics);
1410
+ }
1411
+
1412
+ /**
1413
+ * Calculate uptime percentage for a time period
1414
+ */
1415
+ private async calculateUptime(providerAddress: string, days: number): Promise<number | undefined> {
1416
+ const startDate = new Date();
1417
+ startDate.setDate(startDate.getDate() - days);
1418
+
1419
+ const result = await this.db.query<{ online_count: string; total_count: string }>(
1420
+ `SELECT
1421
+ COUNT(CASE WHEN is_online THEN 1 END) as online_count,
1422
+ COUNT(*) as total_count
1423
+ FROM health_checks
1424
+ WHERE provider_address = $1
1425
+ AND checked_at >= $2`,
1426
+ [providerAddress, startDate]
1427
+ );
1428
+
1429
+ const row = result.rows[0];
1430
+ if (!row || parseInt(row.total_count) === 0) {
1431
+ return undefined; // Not enough data
1432
+ }
1433
+
1434
+ const onlineCount = parseInt(row.online_count);
1435
+ const totalCount = parseInt(row.total_count);
1436
+
1437
+ return onlineCount / totalCount;
1438
+ }
1439
+
1440
+ /**
1441
+ * Calculate average response time for a time period (in milliseconds)
1442
+ */
1443
+ private async calculateAvgResponseTime(
1444
+ providerAddress: string,
1445
+ days: number
1446
+ ): Promise<number | undefined> {
1447
+ const startDate = new Date();
1448
+ startDate.setDate(startDate.getDate() - days);
1449
+
1450
+ const result = await this.db.query<{ avg_response_time: string }>(
1451
+ `SELECT AVG(response_time_ms)::INTEGER as avg_response_time
1452
+ FROM health_checks
1453
+ WHERE provider_address = $1
1454
+ AND checked_at >= $2
1455
+ AND is_online = TRUE
1456
+ AND response_time_ms IS NOT NULL`,
1457
+ [providerAddress, startDate]
1458
+ );
1459
+
1460
+ const avgTime = result.rows[0]?.avg_response_time;
1461
+ return avgTime ? parseInt(avgTime) : undefined;
1462
+ }
1463
+
1464
+ /**
1465
+ * Get total number of checks performed in a time period
1466
+ */
1467
+ private async getCheckCount(providerAddress: string, days: number): Promise<number> {
1468
+ const startDate = new Date();
1469
+ startDate.setDate(startDate.getDate() - days);
1470
+
1471
+ const result = await this.db.query<{ count: string }>(
1472
+ `SELECT COUNT(*) as count
1473
+ FROM health_checks
1474
+ WHERE provider_address = $1
1475
+ AND checked_at >= $2`,
1476
+ [providerAddress, startDate]
1477
+ );
1478
+
1479
+ return parseInt(result.rows[0]?.count || '0');
1480
+ }
1481
+
1482
+ /**
1483
+ * Get current online/offline status
1484
+ */
1485
+ private async getCurrentStatus(
1486
+ providerAddress: string
1487
+ ): Promise<{
1488
+ isCurrentlyOnline: boolean;
1489
+ lastOnlineAt?: Date;
1490
+ lastOfflineAt?: Date;
1491
+ }> {
1492
+ // Get most recent check
1493
+ const recentCheck = await this.db.query<{ is_online: boolean; checked_at: Date }>(
1494
+ `SELECT is_online, checked_at
1495
+ FROM health_checks
1496
+ WHERE provider_address = $1
1497
+ ORDER BY checked_at DESC
1498
+ LIMIT 1`,
1499
+ [providerAddress]
1500
+ );
1501
+
1502
+ const isCurrentlyOnline = recentCheck.rows[0]?.is_online || false;
1503
+
1504
+ // Get last time provider was online
1505
+ const lastOnlineResult = await this.db.query<{ checked_at: Date }>(
1506
+ `SELECT checked_at
1507
+ FROM health_checks
1508
+ WHERE provider_address = $1
1509
+ AND is_online = TRUE
1510
+ ORDER BY checked_at DESC
1511
+ LIMIT 1`,
1512
+ [providerAddress]
1513
+ );
1514
+
1515
+ // Get last time provider was offline
1516
+ const lastOfflineResult = await this.db.query<{ checked_at: Date }>(
1517
+ `SELECT checked_at
1518
+ FROM health_checks
1519
+ WHERE provider_address = $1
1520
+ AND is_online = FALSE
1521
+ ORDER BY checked_at DESC
1522
+ LIMIT 1`,
1523
+ [providerAddress]
1524
+ );
1525
+
1526
+ return {
1527
+ isCurrentlyOnline,
1528
+ lastOnlineAt: lastOnlineResult.rows[0]?.checked_at,
1529
+ lastOfflineAt: lastOfflineResult.rows[0]?.checked_at,
1530
+ };
1531
+ }
1532
+
1533
+ /**
1534
+ * Insert or update provider metrics
1535
+ */
1536
+ private async upsertMetrics(metrics: ProviderMetrics): Promise<void> {
1537
+ await this.db.query(
1538
+ `INSERT INTO provider_metrics (
1539
+ provider_address,
1540
+ uptime_1d, uptime_7d, uptime_30d,
1541
+ is_currently_online, last_online_at, last_offline_at,
1542
+ avg_response_time_1d, avg_response_time_7d, avg_response_time_30d,
1543
+ total_checks_1d, total_checks_7d, total_checks_30d,
1544
+ last_calculated_at
1545
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
1546
+ ON CONFLICT (provider_address) DO UPDATE SET
1547
+ uptime_1d = EXCLUDED.uptime_1d,
1548
+ uptime_7d = EXCLUDED.uptime_7d,
1549
+ uptime_30d = EXCLUDED.uptime_30d,
1550
+ is_currently_online = EXCLUDED.is_currently_online,
1551
+ last_online_at = EXCLUDED.last_online_at,
1552
+ last_offline_at = EXCLUDED.last_offline_at,
1553
+ avg_response_time_1d = EXCLUDED.avg_response_time_1d,
1554
+ avg_response_time_7d = EXCLUDED.avg_response_time_7d,
1555
+ avg_response_time_30d = EXCLUDED.avg_response_time_30d,
1556
+ total_checks_1d = EXCLUDED.total_checks_1d,
1557
+ total_checks_7d = EXCLUDED.total_checks_7d,
1558
+ total_checks_30d = EXCLUDED.total_checks_30d,
1559
+ last_calculated_at = EXCLUDED.last_calculated_at`,
1560
+ [
1561
+ metrics.providerAddress,
1562
+ metrics.uptime1d,
1563
+ metrics.uptime7d,
1564
+ metrics.uptime30d,
1565
+ metrics.isCurrentlyOnline,
1566
+ metrics.lastOnlineAt,
1567
+ metrics.lastOfflineAt,
1568
+ metrics.avgResponseTime1d,
1569
+ metrics.avgResponseTime7d,
1570
+ metrics.avgResponseTime30d,
1571
+ metrics.totalChecks1d,
1572
+ metrics.totalChecks7d,
1573
+ metrics.totalChecks30d,
1574
+ metrics.lastCalculatedAt,
1575
+ ]
1576
+ );
1577
+ }
1578
+
1579
+ /**
1580
+ * Get metrics for a specific provider
1581
+ */
1582
+ async getProviderMetrics(providerAddress: string): Promise<ProviderMetrics | null> {
1583
+ const result = await this.db.query<ProviderMetrics>(
1584
+ 'SELECT * FROM provider_metrics WHERE provider_address = $1',
1585
+ [providerAddress]
1586
+ );
1587
+
1588
+ return result.rows[0] || null;
1589
+ }
1590
+ }
1591
+ ```
1592
+
1593
+ ### Step 8: API Server
1594
+
1595
+ Create `src/api/server.ts`:
1596
+
1597
+ ```typescript
1598
+ import express from 'express';
1599
+ import type { Request, Response } from 'express';
1600
+ import { getDatabaseClient } from '../database/client.js';
1601
+ import type { ProviderDetails } from '../types/index.js';
1602
+
1603
+ /**
1604
+ * API Server
1605
+ *
1606
+ * Exposes provider data via REST API endpoints.
1607
+ */
1608
+ export class ApiServer {
1609
+ private app: express.Application;
1610
+ private db: ReturnType<typeof getDatabaseClient>;
1611
+ private port: number;
1612
+
1613
+ constructor(port: number = 3000) {
1614
+ this.app = express();
1615
+ this.db = getDatabaseClient();
1616
+ this.port = port;
1617
+
1618
+ this.setupMiddleware();
1619
+ this.setupRoutes();
1620
+ }
1621
+
1622
+ /**
1623
+ * Setup Express middleware
1624
+ */
1625
+ private setupMiddleware(): void {
1626
+ // CORS headers
1627
+ this.app.use((req, res, next) => {
1628
+ res.header('Access-Control-Allow-Origin', '*');
1629
+ res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
1630
+ res.header('Access-Control-Allow-Headers', 'Content-Type');
1631
+ next();
1632
+ });
1633
+
1634
+ // JSON parsing
1635
+ this.app.use(express.json());
1636
+
1637
+ // Request logging
1638
+ this.app.use((req, res, next) => {
1639
+ const start = Date.now();
1640
+ res.on('finish', () => {
1641
+ const duration = Date.now() - start;
1642
+ console.log(`${req.method} ${req.path} - ${res.statusCode} (${duration}ms)`);
1643
+ });
1644
+ next();
1645
+ });
1646
+ }
1647
+
1648
+ /**
1649
+ * Setup API routes
1650
+ */
1651
+ private setupRoutes(): void {
1652
+ // Health check
1653
+ this.app.get('/health', (req, res) => {
1654
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
1655
+ });
1656
+
1657
+ // Get all providers
1658
+ this.app.get('/v1/providers', this.getAllProviders.bind(this));
1659
+
1660
+ // Get single provider by address
1661
+ this.app.get('/v1/providers/:address', this.getProviderByAddress.bind(this));
1662
+
1663
+ // Get provider history
1664
+ this.app.get('/v1/providers/:address/history', this.getProviderHistory.bind(this));
1665
+
1666
+ // Get provider statistics
1667
+ this.app.get('/v1/stats', this.getStats.bind(this));
1668
+
1669
+ // 404 handler
1670
+ this.app.use((req, res) => {
1671
+ res.status(404).json({ error: 'Not found' });
1672
+ });
1673
+
1674
+ // Error handler
1675
+ this.app.use((err: Error, req: Request, res: Response, next: any) => {
1676
+ console.error('API error:', err);
1677
+ res.status(500).json({ error: 'Internal server error' });
1678
+ });
1679
+ }
1680
+
1681
+ /**
1682
+ * GET /v1/providers
1683
+ * Returns all providers with their metrics
1684
+ */
1685
+ private async getAllProviders(req: Request, res: Response): Promise<void> {
1686
+ try {
1687
+ const result = await this.db.query<ProviderDetails>(
1688
+ `SELECT * FROM provider_details ORDER BY uptime_7d DESC NULLS LAST`
1689
+ );
1690
+
1691
+ res.json(result.rows);
1692
+ } catch (error) {
1693
+ console.error('Error fetching providers:', error);
1694
+ res.status(500).json({ error: 'Failed to fetch providers' });
1695
+ }
1696
+ }
1697
+
1698
+ /**
1699
+ * GET /v1/providers/:address
1700
+ * Returns detailed information for a specific provider
1701
+ */
1702
+ private async getProviderByAddress(req: Request, res: Response): Promise<void> {
1703
+ try {
1704
+ const { address } = req.params;
1705
+
1706
+ const result = await this.db.query<ProviderDetails>(
1707
+ `SELECT * FROM provider_details WHERE address = $1`,
1708
+ [address]
1709
+ );
1710
+
1711
+ if (result.rows.length === 0) {
1712
+ res.status(404).json({ error: 'Provider not found' });
1713
+ return;
1714
+ }
1715
+
1716
+ res.json(result.rows[0]);
1717
+ } catch (error) {
1718
+ console.error('Error fetching provider:', error);
1719
+ res.status(500).json({ error: 'Failed to fetch provider' });
1720
+ }
1721
+ }
1722
+
1723
+ /**
1724
+ * GET /v1/providers/:address/history
1725
+ * Returns historical health check data for a provider
1726
+ */
1727
+ private async getProviderHistory(req: Request, res: Response): Promise<void> {
1728
+ try {
1729
+ const { address } = req.params;
1730
+ const days = parseInt(req.query.days as string) || 7;
1731
+ const limit = Math.min(parseInt(req.query.limit as string) || 1000, 10000);
1732
+
1733
+ const startDate = new Date();
1734
+ startDate.setDate(startDate.getDate() - days);
1735
+
1736
+ const result = await this.db.query(
1737
+ `SELECT
1738
+ checked_at,
1739
+ is_online,
1740
+ response_time_ms,
1741
+ error_message
1742
+ FROM health_checks
1743
+ WHERE provider_address = $1
1744
+ AND checked_at >= $2
1745
+ ORDER BY checked_at DESC
1746
+ LIMIT $3`,
1747
+ [address, startDate, limit]
1748
+ );
1749
+
1750
+ res.json({
1751
+ provider: address,
1752
+ days,
1753
+ dataPoints: result.rows.length,
1754
+ history: result.rows,
1755
+ });
1756
+ } catch (error) {
1757
+ console.error('Error fetching provider history:', error);
1758
+ res.status(500).json({ error: 'Failed to fetch provider history' });
1759
+ }
1760
+ }
1761
+
1762
+ /**
1763
+ * GET /v1/stats
1764
+ * Returns overall statistics about the tracker
1765
+ */
1766
+ private async getStats(req: Request, res: Response): Promise<void> {
1767
+ try {
1768
+ // Total providers
1769
+ const totalResult = await this.db.query<{ count: string }>(
1770
+ 'SELECT COUNT(*) as count FROM providers'
1771
+ );
1772
+ const totalProviders = parseInt(totalResult.rows[0]?.count || '0');
1773
+
1774
+ // Online providers
1775
+ const onlineResult = await this.db.query<{ count: string }>(
1776
+ 'SELECT COUNT(*) as count FROM provider_metrics WHERE is_currently_online = TRUE'
1777
+ );
1778
+ const onlineProviders = parseInt(onlineResult.rows[0]?.count || '0');
1779
+
1780
+ // Average uptime
1781
+ const avgUptimeResult = await this.db.query<{ avg_uptime: string }>(
1782
+ `SELECT AVG(uptime_7d)::DECIMAL(5,4) as avg_uptime
1783
+ FROM provider_metrics
1784
+ WHERE uptime_7d IS NOT NULL`
1785
+ );
1786
+ const avgUptime = parseFloat(avgUptimeResult.rows[0]?.avg_uptime || '0');
1787
+
1788
+ // Total health checks performed
1789
+ const checksResult = await this.db.query<{ count: string }>(
1790
+ 'SELECT COUNT(*) as count FROM health_checks'
1791
+ );
1792
+ const totalChecks = parseInt(checksResult.rows[0]?.count || '0');
1793
+
1794
+ res.json({
1795
+ totalProviders,
1796
+ onlineProviders,
1797
+ offlineProviders: totalProviders - onlineProviders,
1798
+ avgUptime7d: avgUptime,
1799
+ totalHealthChecks: totalChecks,
1800
+ timestamp: new Date().toISOString(),
1801
+ });
1802
+ } catch (error) {
1803
+ console.error('Error fetching stats:', error);
1804
+ res.status(500).json({ error: 'Failed to fetch stats' });
1805
+ }
1806
+ }
1807
+
1808
+ /**
1809
+ * Start the API server
1810
+ */
1811
+ async start(): Promise<void> {
1812
+ return new Promise((resolve) => {
1813
+ this.app.listen(this.port, () => {
1814
+ console.log(`🚀 API server running on http://localhost:${this.port}`);
1815
+ console.log(` Health: http://localhost:${this.port}/health`);
1816
+ console.log(` Providers: http://localhost:${this.port}/v1/providers`);
1817
+ resolve();
1818
+ });
1819
+ });
1820
+ }
1821
+ }
1822
+ ```
1823
+
1824
+ ### Step 9: Main Application
1825
+
1826
+ Create `src/index.ts`:
1827
+
1828
+ ```typescript
1829
+ import { getDatabaseClient } from './database/client.js';
1830
+ import { BlockchainIndexer } from './indexer/blockchain-indexer.js';
1831
+ import { HealthChecker } from './health/health-checker.js';
1832
+ import { MetricsCalculator } from './metrics/metrics-calculator.js';
1833
+ import { ApiServer } from './api/server.js';
1834
+
1835
+ /**
1836
+ * Configuration from environment variables
1837
+ */
1838
+ const config = {
1839
+ // Database configuration
1840
+ database: {
1841
+ host: process.env.DB_HOST || 'localhost',
1842
+ port: parseInt(process.env.DB_PORT || '5432'),
1843
+ database: process.env.DB_NAME || 'akash_tracker',
1844
+ user: process.env.DB_USER || 'postgres',
1845
+ password: process.env.DB_PASSWORD || 'postgres',
1846
+ },
1847
+
1848
+ // Akash RPC endpoint
1849
+ rpcEndpoint: process.env.AKASH_RPC || 'https://rpc.akashnet.net:443',
1850
+
1851
+ // Intervals (in minutes)
1852
+ indexerInterval: parseInt(process.env.INDEXER_INTERVAL || '30'),
1853
+ healthCheckInterval: parseInt(process.env.HEALTH_CHECK_INTERVAL || '10'),
1854
+ metricsInterval: parseInt(process.env.METRICS_INTERVAL || '60'),
1855
+
1856
+ // API server port
1857
+ apiPort: parseInt(process.env.API_PORT || '3000'),
1858
+
1859
+ // Health check timeout
1860
+ checkTimeout: parseInt(process.env.CHECK_TIMEOUT || '5000'),
1861
+ };
1862
+
1863
+ /**
1864
+ * Main application
1865
+ */
1866
+ async function main() {
1867
+ console.log('🚀 Starting Akash Provider Tracker');
1868
+ console.log('');
1869
+ console.log('Configuration:');
1870
+ console.log(` Database: ${config.database.host}:${config.database.port}/${config.database.database}`);
1871
+ console.log(` RPC: ${config.rpcEndpoint}`);
1872
+ console.log(` Indexer interval: ${config.indexerInterval} minutes`);
1873
+ console.log(` Health check interval: ${config.healthCheckInterval} minutes`);
1874
+ console.log(` Metrics interval: ${config.metricsInterval} minutes`);
1875
+ console.log(` API port: ${config.apiPort}`);
1876
+ console.log('');
1877
+
1878
+ // Initialize database client
1879
+ const db = getDatabaseClient(config.database);
1880
+
1881
+ // Check database connection
1882
+ console.log('📊 Checking database connection...');
1883
+ const isHealthy = await db.healthCheck();
1884
+ if (!isHealthy) {
1885
+ console.error('❌ Database connection failed');
1886
+ process.exit(1);
1887
+ }
1888
+ console.log('✅ Database connection successful');
1889
+ console.log('');
1890
+
1891
+ // Initialize components
1892
+ const indexer = new BlockchainIndexer(config.rpcEndpoint);
1893
+ const healthChecker = new HealthChecker(config.checkTimeout);
1894
+ const metricsCalculator = new MetricsCalculator();
1895
+ const apiServer = new ApiServer(config.apiPort);
1896
+
1897
+ // Start all components
1898
+ await Promise.all([
1899
+ indexer.start(config.indexerInterval),
1900
+ healthChecker.start(config.healthCheckInterval),
1901
+ metricsCalculator.start(config.metricsInterval),
1902
+ apiServer.start(),
1903
+ ]);
1904
+
1905
+ console.log('');
1906
+ console.log('✅ All systems operational');
1907
+ console.log('');
1908
+
1909
+ // Handle graceful shutdown
1910
+ process.on('SIGINT', async () => {
1911
+ console.log('');
1912
+ console.log('🛑 Shutting down...');
1913
+ await db.close();
1914
+ process.exit(0);
1915
+ });
1916
+ }
1917
+
1918
+ // Run the application
1919
+ main().catch((error) => {
1920
+ console.error('❌ Fatal error:', error);
1921
+ process.exit(1);
1922
+ });
1923
+ ```
1924
+
1925
+ ### Step 10: Environment Configuration
1926
+
1927
+ Create `.env.example`:
1928
+
1929
+ ```bash
1930
+ # Database Configuration
1931
+ DB_HOST=localhost
1932
+ DB_PORT=5432
1933
+ DB_NAME=akash_tracker
1934
+ DB_USER=postgres
1935
+ DB_PASSWORD=postgres
1936
+
1937
+ # Akash Configuration
1938
+ AKASH_RPC=https://rpc.akashnet.net:443
1939
+
1940
+ # Intervals (in minutes)
1941
+ INDEXER_INTERVAL=30 # How often to check blockchain for new providers
1942
+ HEALTH_CHECK_INTERVAL=10 # How often to ping providers
1943
+ METRICS_INTERVAL=60 # How often to calculate uptime metrics
1944
+
1945
+ # API Configuration
1946
+ API_PORT=3000
1947
+
1948
+ # Health Check Configuration
1949
+ CHECK_TIMEOUT=5000 # Timeout in milliseconds
1950
+ ```
1951
+
1952
+ ### Step 11: Package Scripts
1953
+
1954
+ Update `package.json`:
1955
+
1956
+ ```json
1957
+ {
1958
+ "name": "akash-provider-tracker",
1959
+ "version": "1.0.0",
1960
+ "type": "module",
1961
+ "scripts": {
1962
+ "build": "tsc",
1963
+ "start": "node dist/index.js",
1964
+ "dev": "tsx watch src/index.ts",
1965
+ "db:setup": "psql -U postgres -f src/database/schema.sql",
1966
+ "db:reset": "dropdb akash_tracker && createdb akash_tracker && npm run db:setup"
1967
+ }
1968
+ }
1969
+ ```
1970
+
1971
+ ---
1972
+
1973
+ ## Deployment
1974
+
1975
+ ### Option 1: Local Development
1976
+
1977
+ ```bash
1978
+ # 1. Install PostgreSQL
1979
+ brew install postgresql # macOS
1980
+ # or
1981
+ sudo apt install postgresql # Ubuntu
1982
+
1983
+ # 2. Create database
1984
+ createdb akash_tracker
1985
+
1986
+ # 3. Initialize schema
1987
+ psql -U postgres -d akash_tracker -f src/database/schema.sql
1988
+
1989
+ # 4. Create .env file
1990
+ cp .env.example .env
1991
+ # Edit .env with your configuration
1992
+
1993
+ # 5. Install dependencies
1994
+ npm install
1995
+
1996
+ # 6. Run in development mode
1997
+ npm run dev
1998
+ ```
1999
+
2000
+ ### Option 2: Docker
2001
+
2002
+ Create `Dockerfile`:
2003
+
2004
+ ```dockerfile
2005
+ FROM node:20-alpine
2006
+
2007
+ WORKDIR /app
2008
+
2009
+ # Install dependencies
2010
+ COPY package*.json ./
2011
+ RUN npm ci --production
2012
+
2013
+ # Copy source
2014
+ COPY . .
2015
+
2016
+ # Build TypeScript
2017
+ RUN npm run build
2018
+
2019
+ # Run application
2020
+ CMD ["npm", "start"]
2021
+ ```
2022
+
2023
+ Create `docker-compose.yml`:
2024
+
2025
+ ```yaml
2026
+ version: '3.8'
2027
+
2028
+ services:
2029
+ postgres:
2030
+ image: postgres:15-alpine
2031
+ environment:
2032
+ POSTGRES_DB: akash_tracker
2033
+ POSTGRES_USER: postgres
2034
+ POSTGRES_PASSWORD: postgres
2035
+ volumes:
2036
+ - postgres_data:/var/lib/postgresql/data
2037
+ - ./src/database/schema.sql:/docker-entrypoint-initdb.d/schema.sql
2038
+ ports:
2039
+ - "5432:5432"
2040
+
2041
+ tracker:
2042
+ build: .
2043
+ depends_on:
2044
+ - postgres
2045
+ environment:
2046
+ DB_HOST: postgres
2047
+ DB_PORT: 5432
2048
+ DB_NAME: akash_tracker
2049
+ DB_USER: postgres
2050
+ DB_PASSWORD: postgres
2051
+ AKASH_RPC: https://rpc.akashnet.net:443
2052
+ INDEXER_INTERVAL: 30
2053
+ HEALTH_CHECK_INTERVAL: 10
2054
+ METRICS_INTERVAL: 60
2055
+ API_PORT: 3000
2056
+ ports:
2057
+ - "3000:3000"
2058
+
2059
+ volumes:
2060
+ postgres_data:
2061
+ ```
2062
+
2063
+ Run with Docker:
2064
+
2065
+ ```bash
2066
+ # Build and start
2067
+ docker-compose up -d
2068
+
2069
+ # View logs
2070
+ docker-compose logs -f tracker
2071
+
2072
+ # Stop
2073
+ docker-compose down
2074
+ ```
2075
+
2076
+ ### Option 3: Deploy to VPS
2077
+
2078
+ ```bash
2079
+ # 1. SSH to your server
2080
+ ssh user@your-server.com
2081
+
2082
+ # 2. Install Node.js and PostgreSQL
2083
+ curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
2084
+ sudo apt install -y nodejs postgresql
2085
+
2086
+ # 3. Clone your repository
2087
+ git clone https://github.com/your-repo/akash-provider-tracker.git
2088
+ cd akash-provider-tracker
2089
+
2090
+ # 4. Setup database
2091
+ sudo -u postgres createdb akash_tracker
2092
+ sudo -u postgres psql -d akash_tracker -f src/database/schema.sql
2093
+
2094
+ # 5. Install dependencies and build
2095
+ npm install
2096
+ npm run build
2097
+
2098
+ # 6. Setup systemd service
2099
+ sudo nano /etc/systemd/system/akash-tracker.service
2100
+ ```
2101
+
2102
+ Create systemd service file:
2103
+
2104
+ ```ini
2105
+ [Unit]
2106
+ Description=Akash Provider Tracker
2107
+ After=network.target postgresql.service
2108
+
2109
+ [Service]
2110
+ Type=simple
2111
+ User=your-user
2112
+ WorkingDirectory=/home/your-user/akash-provider-tracker
2113
+ Environment="NODE_ENV=production"
2114
+ Environment="DB_HOST=localhost"
2115
+ Environment="DB_PORT=5432"
2116
+ Environment="DB_NAME=akash_tracker"
2117
+ Environment="DB_USER=postgres"
2118
+ Environment="DB_PASSWORD=your-password"
2119
+ ExecStart=/usr/bin/node dist/index.js
2120
+ Restart=always
2121
+
2122
+ [Install]
2123
+ WantedBy=multi-user.target
2124
+ ```
2125
+
2126
+ Start the service:
2127
+
2128
+ ```bash
2129
+ # Enable and start
2130
+ sudo systemctl enable akash-tracker
2131
+ sudo systemctl start akash-tracker
2132
+
2133
+ # Check status
2134
+ sudo systemctl status akash-tracker
2135
+
2136
+ # View logs
2137
+ sudo journalctl -u akash-tracker -f
2138
+ ```
2139
+
2140
+ ### Option 4: Deploy to Akash Network
2141
+
2142
+ Create `deploy.yaml` for Akash:
2143
+
2144
+ ```yaml
2145
+ ---
2146
+ version: "2.0"
2147
+
2148
+ services:
2149
+ tracker:
2150
+ image: your-docker-hub/akash-tracker:latest
2151
+ env:
2152
+ - DB_HOST=postgres
2153
+ - DB_NAME=akash_tracker
2154
+ - DB_USER=postgres
2155
+ - DB_PASSWORD=postgres
2156
+ - AKASH_RPC=https://rpc.akashnet.net:443
2157
+ expose:
2158
+ - port: 3000
2159
+ as: 80
2160
+ to:
2161
+ - global: true
2162
+
2163
+ postgres:
2164
+ image: postgres:15-alpine
2165
+ env:
2166
+ - POSTGRES_DB=akash_tracker
2167
+ - POSTGRES_USER=postgres
2168
+ - POSTGRES_PASSWORD=postgres
2169
+ expose:
2170
+ - port: 5432
2171
+ to:
2172
+ - service: tracker
2173
+
2174
+ profiles:
2175
+ compute:
2176
+ tracker:
2177
+ resources:
2178
+ cpu:
2179
+ units: 1
2180
+ memory:
2181
+ size: 2Gi
2182
+ storage:
2183
+ size: 10Gi
2184
+ postgres:
2185
+ resources:
2186
+ cpu:
2187
+ units: 1
2188
+ memory:
2189
+ size: 2Gi
2190
+ storage:
2191
+ size: 50Gi
2192
+
2193
+ placement:
2194
+ akash:
2195
+ pricing:
2196
+ tracker:
2197
+ denom: uakt
2198
+ amount: 1000
2199
+ postgres:
2200
+ denom: uakt
2201
+ amount: 1000
2202
+
2203
+ deployment:
2204
+ tracker:
2205
+ akash:
2206
+ profile: tracker
2207
+ count: 1
2208
+ postgres:
2209
+ akash:
2210
+ profile: postgres
2211
+ count: 1
2212
+ ```
2213
+
2214
+ ---
2215
+
2216
+ ## Maintenance
2217
+
2218
+ ### Daily Tasks
2219
+
2220
+ **Monitor System Health:**
2221
+
2222
+ ```bash
2223
+ # Check if all services are running
2224
+ systemctl status akash-tracker # or docker-compose ps
2225
+
2226
+ # Check recent logs for errors
2227
+ tail -n 100 /var/log/akash-tracker.log
2228
+
2229
+ # Check database size
2230
+ psql -U postgres -d akash_tracker -c "
2231
+ SELECT
2232
+ pg_size_pretty(pg_database_size('akash_tracker')) as size;
2233
+ "
2234
+ ```
2235
+
2236
+ **Monitor Metrics:**
2237
+
2238
+ ```bash
2239
+ # Check API health
2240
+ curl http://localhost:3000/health
2241
+
2242
+ # Get current stats
2243
+ curl http://localhost:3000/v1/stats
2244
+
2245
+ # Check specific provider
2246
+ curl http://localhost:3000/v1/providers/akash1...
2247
+ ```
2248
+
2249
+ ### Weekly Tasks
2250
+
2251
+ **Database Maintenance:**
2252
+
2253
+ ```sql
2254
+ -- Vacuum database (reclaim storage)
2255
+ VACUUM ANALYZE health_checks;
2256
+ VACUUM ANALYZE provider_metrics;
2257
+
2258
+ -- Check table sizes
2259
+ SELECT
2260
+ schemaname,
2261
+ tablename,
2262
+ pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
2263
+ FROM pg_tables
2264
+ WHERE schemaname = 'public'
2265
+ ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
2266
+
2267
+ -- Check index usage
2268
+ SELECT
2269
+ schemaname,
2270
+ tablename,
2271
+ indexname,
2272
+ idx_scan,
2273
+ pg_size_pretty(pg_relation_size(indexrelid)) as size
2274
+ FROM pg_stat_user_indexes
2275
+ ORDER BY idx_scan ASC;
2276
+ ```
2277
+
2278
+ **Backup Database:**
2279
+
2280
+ ```bash
2281
+ # Create backup
2282
+ pg_dump -U postgres akash_tracker > backup_$(date +%Y%m%d).sql
2283
+
2284
+ # Compress backup
2285
+ gzip backup_$(date +%Y%m%d).sql
2286
+
2287
+ # Upload to S3 (optional)
2288
+ aws s3 cp backup_$(date +%Y%m%d).sql.gz s3://your-bucket/backups/
2289
+ ```
2290
+
2291
+ ### Monthly Tasks
2292
+
2293
+ **Archive Old Data:**
2294
+
2295
+ ```sql
2296
+ -- Archive health checks older than 60 days
2297
+ CREATE TABLE health_checks_archive (LIKE health_checks INCLUDING ALL);
2298
+
2299
+ INSERT INTO health_checks_archive
2300
+ SELECT * FROM health_checks
2301
+ WHERE checked_at < NOW() - INTERVAL '60 days';
2302
+
2303
+ DELETE FROM health_checks
2304
+ WHERE checked_at < NOW() - INTERVAL '60 days';
2305
+ ```
2306
+
2307
+ **Review Performance:**
2308
+
2309
+ ```sql
2310
+ -- Find slow queries
2311
+ SELECT
2312
+ calls,
2313
+ total_time,
2314
+ mean_time,
2315
+ query
2316
+ FROM pg_stat_statements
2317
+ ORDER BY mean_time DESC
2318
+ LIMIT 10;
2319
+
2320
+ -- Check for missing indexes
2321
+ SELECT
2322
+ schemaname,
2323
+ tablename,
2324
+ attname,
2325
+ n_distinct,
2326
+ correlation
2327
+ FROM pg_stats
2328
+ WHERE schemaname = 'public'
2329
+ AND n_distinct > 100
2330
+ ORDER BY abs(correlation) ASC
2331
+ LIMIT 20;
2332
+ ```
2333
+
2334
+ ### Troubleshooting
2335
+
2336
+ **Problem: High Memory Usage**
2337
+
2338
+ ```bash
2339
+ # Check Node.js memory usage
2340
+ ps aux | grep node
2341
+
2342
+ # If memory is high, restart service
2343
+ sudo systemctl restart akash-tracker
2344
+
2345
+ # Increase Node.js memory limit (in systemd service file)
2346
+ Environment="NODE_OPTIONS=--max-old-space-size=4096"
2347
+ ```
2348
+
2349
+ **Problem: Database Growing Too Large**
2350
+
2351
+ ```bash
2352
+ # Check table sizes
2353
+ psql -U postgres -d akash_tracker -c "
2354
+ SELECT
2355
+ pg_size_pretty(pg_table_size('health_checks')) as health_checks_size,
2356
+ pg_size_pretty(pg_table_size('provider_metrics')) as metrics_size;
2357
+ "
2358
+
2359
+ # Archive old data (see Monthly Tasks)
2360
+ # Or reduce retention period in health checker
2361
+ ```
2362
+
2363
+ **Problem: Providers Not Being Discovered**
2364
+
2365
+ ```bash
2366
+ # Check indexer logs
2367
+ journalctl -u akash-tracker | grep "Indexer"
2368
+
2369
+ # Manually trigger indexer (if you add a CLI command)
2370
+ node dist/index.js --index-now
2371
+
2372
+ # Check RPC connectivity
2373
+ curl https://rpc.akashnet.net:443/status
2374
+ ```
2375
+
2376
+ **Problem: API Slow Response Times**
2377
+
2378
+ ```sql
2379
+ -- Add missing indexes
2380
+ CREATE INDEX CONCURRENTLY idx_health_checks_provider_time
2381
+ ON health_checks(provider_address, checked_at DESC);
2382
+
2383
+ -- Analyze query performance
2384
+ EXPLAIN ANALYZE
2385
+ SELECT * FROM provider_details;
2386
+
2387
+ -- Update statistics
2388
+ ANALYZE;
2389
+ ```
2390
+
2391
+ ---
2392
+
2393
+ ## Advanced Features
2394
+
2395
+ ### Feature 1: IP Geolocation
2396
+
2397
+ Add IP geolocation to track provider locations:
2398
+
2399
+ ```typescript
2400
+ import axios from 'axios';
2401
+
2402
+ /**
2403
+ * Get IP geolocation using ipapi.co (free tier: 1000 requests/day)
2404
+ */
2405
+ async function getProviderLocation(hostUri: string): Promise<ProviderLocation | null> {
2406
+ try {
2407
+ // Extract hostname from URI
2408
+ const hostname = new URL(hostUri).hostname;
2409
+
2410
+ // Query ipapi.co
2411
+ const response = await axios.get(`https://ipapi.co/${hostname}/json/`);
2412
+ const data = response.data;
2413
+
2414
+ return {
2415
+ providerAddress: '', // Set by caller
2416
+ country: data.country_name,
2417
+ countryCode: data.country_code,
2418
+ region: data.region,
2419
+ regionCode: data.region_code,
2420
+ city: data.city,
2421
+ latitude: data.latitude,
2422
+ longitude: data.longitude,
2423
+ timezone: data.timezone,
2424
+ ipAddress: data.ip,
2425
+ };
2426
+ } catch (error) {
2427
+ console.error('Failed to get geolocation:', error);
2428
+ return null;
2429
+ }
2430
+ }
2431
+
2432
+ // Use in health checker after detecting new provider
2433
+ ```
2434
+
2435
+ ### Feature 2: Alerting System
2436
+
2437
+ Add Slack/Discord alerts for provider downtime:
2438
+
2439
+ ```typescript
2440
+ import axios from 'axios';
2441
+
2442
+ class AlertManager {
2443
+ private webhookUrl: string;
2444
+ private alertedProviders = new Set<string>();
2445
+
2446
+ constructor(webhookUrl: string) {
2447
+ this.webhookUrl = webhookUrl;
2448
+ }
2449
+
2450
+ async sendAlert(provider: string, message: string): Promise<void> {
2451
+ // Avoid duplicate alerts
2452
+ if (this.alertedProviders.has(provider)) return;
2453
+
2454
+ try {
2455
+ await axios.post(this.webhookUrl, {
2456
+ text: `🚨 Provider Alert: ${message}`,
2457
+ blocks: [
2458
+ {
2459
+ type: 'section',
2460
+ text: {
2461
+ type: 'mrkdwn',
2462
+ text: `*Provider:* ${provider}\n*Alert:* ${message}`,
2463
+ },
2464
+ },
2465
+ ],
2466
+ });
2467
+
2468
+ this.alertedProviders.add(provider);
2469
+
2470
+ // Clear alert after 1 hour
2471
+ setTimeout(() => {
2472
+ this.alertedProviders.delete(provider);
2473
+ }, 60 * 60 * 1000);
2474
+
2475
+ } catch (error) {
2476
+ console.error('Failed to send alert:', error);
2477
+ }
2478
+ }
2479
+ }
2480
+
2481
+ // Use in health checker when provider goes down
2482
+ ```
2483
+
2484
+ ### Feature 3: Historical Charts
2485
+
2486
+ Generate uptime charts using Chart.js or similar:
2487
+
2488
+ ```typescript
2489
+ // API endpoint: GET /v1/providers/:address/chart
2490
+ async getProviderChart(req: Request, res: Response): Promise<void> {
2491
+ const { address } = req.params;
2492
+ const days = parseInt(req.query.days as string) || 7;
2493
+
2494
+ const startDate = new Date();
2495
+ startDate.setDate(startDate.getDate() - days);
2496
+
2497
+ // Get hourly uptime data
2498
+ const result = await this.db.query(`
2499
+ SELECT
2500
+ date_trunc('hour', checked_at) as hour,
2501
+ AVG(CASE WHEN is_online THEN 1.0 ELSE 0.0 END) as uptime
2502
+ FROM health_checks
2503
+ WHERE provider_address = $1
2504
+ AND checked_at >= $2
2505
+ GROUP BY hour
2506
+ ORDER BY hour ASC
2507
+ `, [address, startDate]);
2508
+
2509
+ const data = {
2510
+ labels: result.rows.map(r => r.hour),
2511
+ datasets: [{
2512
+ label: 'Uptime',
2513
+ data: result.rows.map(r => (r.uptime * 100).toFixed(2)),
2514
+ }],
2515
+ };
2516
+
2517
+ res.json(data);
2518
+ }
2519
+ ```
2520
+
2521
+ ### Feature 4: Provider Comparison
2522
+
2523
+ Compare multiple providers side-by-side:
2524
+
2525
+ ```typescript
2526
+ // API endpoint: GET /v1/compare?providers=addr1,addr2,addr3
2527
+ async compareProviders(req: Request, res: Response): Promise<void> {
2528
+ const addresses = (req.query.providers as string).split(',');
2529
+
2530
+ const result = await this.db.query(`
2531
+ SELECT * FROM provider_details
2532
+ WHERE address = ANY($1)
2533
+ `, [addresses]);
2534
+
2535
+ // Calculate comparison metrics
2536
+ const comparison = result.rows.map(provider => ({
2537
+ address: provider.address,
2538
+ name: provider.name,
2539
+ uptime7d: provider.uptime_7d,
2540
+ avgResponseTime: provider.avg_response_time_7d,
2541
+ isOnline: provider.is_currently_online,
2542
+ isAudited: provider.is_audited,
2543
+ location: `${provider.city}, ${provider.country}`,
2544
+ }));
2545
+
2546
+ res.json({
2547
+ providers: comparison,
2548
+ winner: comparison.sort((a, b) => b.uptime7d - a.uptime7d)[0],
2549
+ });
2550
+ }
2551
+ ```
2552
+
2553
+ ---
2554
+
2555
+ ## Conclusion
2556
+
2557
+ You now have a complete guide to building your own provider reliability tracker for Akash Network. This system will:
2558
+
2559
+ - ✅ Automatically discover providers from the blockchain
2560
+ - ✅ Continuously monitor provider health
2561
+ - ✅ Calculate uptime percentages over time
2562
+ - ✅ Provide a REST API for your applications
2563
+ - ✅ Track provider metadata and performance
2564
+
2565
+ ### Next Steps
2566
+
2567
+ 1. **Deploy the tracker** to a VPS or Akash itself
2568
+ 2. **Integrate with kadi-deploy** to use reliability data when selecting providers
2569
+ 3. **Add more features** like alerting, geolocation, and historical charts
2570
+ 4. **Monitor and maintain** the system to ensure data quality
2571
+
2572
+ ### Resources
2573
+
2574
+ - **Akash Network Docs:** https://docs.akash.network/
2575
+ - **AkashJS Library:** https://github.com/akash-network/akashjs
2576
+ - **PostgreSQL TimescaleDB:** https://www.timescale.com/ (for time-series optimization)
2577
+ - **KADI Infrastructure:** https://github.com/kadi-build
2578
+
2579
+ ---
2580
+
2581
+ **Happy tracking! 🚀**