This dashboard is built to monitor indexers participating in The Graph Protocol's Rewards Eligibility Oracle system.
The Rewards Eligibility Oracle links eligibility for indexing rewards to the provision of quality service to consumers. Only indexers meeting minimum performance standards receive rewards, ensuring incentives align with providing value to the network.
Key Features:
- Off-chain Oracle Nodes: Assess indexer performance against published criteria
- On-chain Oracle Contract: Tracks which indexers are eligible for rewards
- Service Quality Metrics: Indexers are evaluated on actual service provision
- 14-Day Eligibility Period: Qualifying indexers have their eligibility renewed for 14 days
- Performance-Based: Ineligible indexers are denied rewards until they improve service quality
This system ensures that rewards are distributed only to indexers who actively serve queries and maintain quality service, rather than simply allocating tokens based on stake alone.
Learn More:
Version 0.0.15 (Nov 7, 2025):
- 🌐 Public Access: REO dashboard is now publicly accessible without authentication
- ⚡ Improved Performance: Direct static file serving for faster load times + instant transaction fetching via Arbiscan API
- 🧭 Breadcrumb Navigation: Added navigation bar at top with CSS-styled home icon
- 🔗 Transaction Links: Last Renewed dates now link to Arbiscan transaction details
- 📊 Transaction Hash Tracking: New
last_renewed_on_txfield stores renewal transaction hash - 🚀 Fast Transaction Retrieval: Replaced slow RPC block scanning with Etherscan V2 API (< 1 second vs. 50,000 block scan)
Version 0.0.13 (Nov 3, 2025):
- 🎯 Watch Specific Indexers: New
/watch,/unwatch, and/watchlistcommands let subscribers monitor specific indexers - 📧 Personalized Notifications: Daily summaries filtered based on each user's watched indexers
- 📋 Detailed Status Changes: Notifications now show individual indexer addresses with their status transitions
- 📢 Announcement Tool: New script to notify existing subscribers about feature updates
- 📝 Daily Summary Messaging: Clarified that bot sends daily summary messages, not real-time notifications
- 🔗 GIP-0079 Reference: Added direct link to proposal in help message
Version 0.0.12 (Nov 3, 2025):
- 📅 Last Renewed Column: New column showing when each indexer's eligibility was last renewed
- 🎨 Hover Tooltips: CSS-based tooltips show full timestamps on date hover for faster loading
- 📏 Short Date Format: Dates display as "2-Nov-2025" by default, full timestamp on hover
Version 0.0.11 (Nov 3, 2025):
- 🔧 Provider-Agnostic Configuration: Renamed
QUICK_NODEtoRPC_ENDPOINT- now works with any Ethereum RPC provider (Alchemy, Infura, QuickNode, Ankr, etc.)
See CHANGELOG.md for complete version history.
This project includes comprehensive documentation:
-
README.md - Main documentation (this file)
- Dashboard features and functionality
- How the oracle system works
- Installation and usage instructions
- File structure and configuration
-
README_TelegramBOT.md - Telegram Bot Setup Guide
- Complete bot deployment instructions
- VPS setup and configuration
- Systemd service setup for automatic startup
- Bot commands and user management
- Troubleshooting guide
-
LOGGING.md - Logging System Documentation
- Overview of all log files
- How to monitor bot activity
- Subscriber activity tracking
- Error log analysis
- Log rotation and maintenance
-
UTILS_COMMANDS.md - Utility Commands Reference
- Telegram bot service commands
- Nginx configuration and restart commands
- Log viewing and troubleshooting
- Emergency recovery procedures
-
activity_log_indexers_status_changes.json - Activity Log File
- Cumulative history of all indexer status changes
- Tracks status transitions over time (eligible ↔ grace ↔ ineligible)
- Automatically updated by
generate_dashboard.py - Used by Telegram notifier to send status change alerts
- See Activity Log section for detailed structure
- 📊 Smart Sorting: Automatically sorts by eligibility status (eligible → grace → ineligible), then by ENS name
- 🏷️ Three Status Types:
- Eligible: Indexers with current eligibility (green badge)
- Grace Period: Indexers in 14-day grace period with expiration date (yellow badge)
- Ineligible: Indexers who lost eligibility (red badge)
- 🔍 Real-time Search: Filter indexers by address or ENS name
- 🔗 Blockchain Integration: Fetches live data from Arbitrum Sepolia via RPC endpoint
- ⏰ Oracle Tracking: Displays last oracle update time and 14-day eligibility period from contract
- ⏳ Grace Period Monitoring: Shows when grace period expires for indexers in transition
- 📱 Responsive Design: Mobile-friendly dark theme UI with collapsible sections
- 💾 Offline Fallback: Can work with cached transaction data from JSON file
- 🔗 Transaction Links: Last Renewed dates link to Arbiscan transaction details for eligible indexers
- 🧭 Breadcrumb Navigation: Easy navigation back to home page with styled breadcrumb bar
The dashboard tracks three distinct eligibility states for indexers:
An indexer is eligible when:
- Their
eligibility_renewal_timematches the contract'slast_oracle_update_time - This means they met the performance criteria in the most recent oracle update
- They will receive rewards for this period
An indexer enters the grace period when:
- They were previously eligible (have an
eligibility_renewal_timefrom a past oracle update) - Their
eligibility_renewal_timedoes NOT match the currentlast_oracle_update_time(they weren't renewed in the latest update) - Current time is still within:
eligibility_renewal_time + eligibility_period(typically 14 days) - During grace period: Indexers can still receive rewards and have time to improve their service quality
- After grace period expires: They become ineligible
Example Grace Period Scenario:
- Day 0: Indexer meets criteria, gets renewed (
eligibility_renewal_time= Day 0) - Day 7: Oracle runs again, indexer doesn't meet criteria
- Day 7-21: Indexer is in grace period (14 days from Day 0)
- Day 22: If still not renewed, indexer becomes ineligible
An indexer is ineligible when:
- They have no
eligibility_renewal_time, OR - Their grace period has expired (current time >
eligibility_renewal_time + eligibility_period) - They will not receive rewards until they improve service quality and get renewed by the oracle
┌───────────────────┐
│ Network Subgraph │ ──┐
│ (Active Indexers)│ │
└───────────────────┘ │
│
┌───────────────────┐ │
│ ENS Subgraph │ ──┤
│ (Name Resolution)│ ├──► retrieveActiveIndexers()
└───────────────────┘ │ │
(or cached) │ ▼
│ active_indexers.json
┌───────────────────┐ │ +
│ ens_resolution │ ──┤ ens_resolution.json (cached ENS)
│ .json │ │ │
└───────────────────┘ │ ▼
│ checkEligibility()
┌───────────────────┐ │ │
│ Contract: Oracle │ ──┤ ▼
│ (Eligibility) │ │ renderIndexerTable() (merges ENS data)
└───────────────────┘ │ │
│ ▼
┌───────────────────┐ │ HTML Dashboard (only eligible indexers)
│last_transaction │ ──┘
│ .json │
└───────────────────┘
retrieveActiveIndexers(): Queries The Graph's network subgraph to get indexers with self stake > 0- Queries network subgraph (deployment ID:
DZz4kDTdmzWLWsV373w2bSmoar3umKKH9y82SUKr5qmp) - Fetches contract metadata:
- Calls
getLastOracleUpdateTime()(function selector:0xbe626dd2) - Calls
getEligibilityPeriod()(function selector:0xd0a5379e) - Stores both values in metadata section of JSON
- Calls
- ENS resolution strategy (controlled by
USE_CACHED_ENSenvironment variable):- If
USE_CACHED_ENS=Y: Loads ENS names fromens_resolution.jsoncache file - If
USE_CACHED_ENS=N: Queries ENS subgraph (deployment ID:5XqPmWe6gjyrJtFn9cLy237i4cWw2j9HcUJEXsP5qGtH) and updates cache
- If
- Writes results to
active_indexers.jsonwith fields:address,is_eligible,status,eligible_until,eligible_until_readable,eligibility_renewal_time(ENS stored separately) - ENS data saved to
ens_resolution.jsonfor caching
- Queries network subgraph (deployment ID:
checkEligibility(): Checks eligibility status for all active indexers- Pass 1: Calls
isEligible(address)on contract for all indexers, stores result inis_eligiblefield - Pass 2: Only for eligible indexers, calls
getEligibilityRenewalTime(address)(function selector:0xd353402d) and updateseligibility_renewal_time - Pass 3: Determines final status based on eligibility renewal time and grace period:
- "eligible":
eligibility_renewal_time == last_oracle_update_time - "grace":
eligibility_renewal_time != last_oracle_update_timeANDcurrent_time < eligibility_renewal_time + eligibility_period- Sets
eligible_until(Unix timestamp) andeligible_until_readable(human-readable format)
- Sets
- "ineligible": Grace period has expired or no eligibility renewal time
- "eligible":
- Updates
active_indexers.jsonwith complete eligibility data including status
- Pass 1: Calls
updateStatusChangeDates(): Detects and tracks status changes between runs- Before generating new data: Backs up current
active_indexers.jsontoactive_indexers_previous_run.json - After eligibility check: Compares current run with previous run
- Comparison logic (by indexer address):
- Status changed (e.g., "eligible" → "grace"): Sets
last_status_change_dateto current date (format:21/Oct/2025) - Status unchanged: Keeps the previous
last_status_change_datevalue (could be empty or a date) - New indexer: Leaves
last_status_change_dateempty (no previous status to compare)
- Status changed (e.g., "eligible" → "grace"): Sets
- Date only appears when status actually changes - stays empty until first change occurs
- Updates
active_indexers.jsonwith status change dates
- Before generating new data: Backs up current
logStatusChanges(): Maintains a cumulative activity log of all status changes- Creates/updates
activity_log_indexers_status_changes.json - Metadata section (overwritten each run):
last_check: Timestamp when script last ranlast_oracle_update_time: Latest oracle update from contract
- Status changes section (appended):
- Logs each status transition with: address, previous_status, new_status, date_status_change
- Only logs actual status changes (not new indexers or unchanged statuses)
- Preserves complete historical record of all transitions
- Runs after
updateStatusChangeDates()to capture all changes - Provides audit trail for monitoring indexer status evolution over time
- Creates/updates
renderIndexerTable(): Readsactive_indexers.jsonand returns all indexers- Loads ENS names from
ens_resolution.jsoncache - Merges ENS data with indexer eligibility data
- Returns list of all indexers with ENS names to display on the dashboard
- Both eligible and ineligible indexers are displayed with appropriate status badges
- Loads ENS names from
The script tries multiple methods to fetch the last transaction data (in priority order):
-
get_last_transaction_from_json():- Reads from local
last_transaction.jsonfile (fastest, offline-capable)
- Reads from local
-
get_last_transaction_via_rpc():- Scans recent blocks via RPC endpoint
- Finds the most recent transaction to the contract address
-
get_last_transaction():- Falls back to Arbiscan API
- Returns
Noneif API is unavailable (no mock data used)
get_oracle_update_time():- Calls the contract's
getLastOracleUpdateTime()function via RPC - Function selector:
0xbe626dd2 - Returns Unix timestamp of the last oracle update
- Calls the contract's
get_eligibility_period():- Calls the contract's
getEligibilityPeriod()function via RPC - Function selector:
0xd0a5379e - Returns eligibility period in seconds
- Calls the contract's
generate_html_dashboard():- Loads all indexers using
renderIndexerTable() - Creates a complete, self-contained HTML file
- Embeds all CSS styling
- Includes JavaScript for search and sort functionality
- Displays all indexers with status badges (eligible/ineligible) in the main table
- Formats timestamps to human-readable dates
- Loads all indexers using
telegram_bot.py:- Telegram bot that runs 24/7 to handle user subscriptions
- Manages subscriber database in
subscribers_telegram.json - Logs all activity to
logs/directory - Provides commands:
/start,/subscribe,/unsubscribe,/watch,/unwatch,/watchlist,/status,/stats,/help,/test - Supports indexer-specific subscriptions with personalized notifications
telegram_notifier.py:- Called by
generate_dashboard.pyafter status changes are logged - Reads subscriber list and activity log
- Filters notifications based on each subscriber's watched indexers
- Shows individual indexer addresses with status transitions
- Sends formatted daily summary messages (once per day maximum)
- Handles rate limiting and error recovery
- Called by
announce_update.py:- Tool to notify all existing subscribers about new features
- Includes confirmation prompt and success/failure statistics
The dashboard now supports daily summary notifications via Telegram bot for oracle updates and indexer status changes. Users can subscribe to receive daily summaries and optionally watch specific indexers.
- 📧 Daily Summary Messages: Once-per-day notification when the oracle runs and updates eligibility
- 🎯 Watch Specific Indexers: Subscribe to updates for specific indexers using
/watch <address>command - 📝 Detailed Status Changes: Shows individual indexer addresses with their status transitions
⚠️ Grace Period Monitoring: Alerts when indexers enter/exit grace period- ❌ Ineligibility Notifications: Track when indexers become ineligible
- 📊 Summary Statistics: Total counts of eligible, grace, and ineligible indexers
- 👥 Self-Service Subscription: Users can subscribe/unsubscribe anytime via bot commands
- 📋 Watch List Management:
/watchlistcommand to view watched indexers - 🔗 GIP-0079 Reference: Direct link to proposal in help message
Daily Summary Message (with status changes):
🔔 Oracle Update Detected!
━━━━━━━━━━━━━━━━━━━
Update Time: 2025-11-03 14:30:45 UTC
📊 Dashboard Stats:
• Total Indexers: 150
• Eligible: 142 ✅
• Grace Period: 5 ⚠️
• Ineligible: 3 ❌
📝 Status Changes Detected:
0x1234567890abcdef1234567890abcdef12345678
ineligible → eligible ✅
0xabcdef1234567890abcdef1234567890abcdef12
eligible → grace period ⚠️
0x5555666677778888999900001111222233334444
grace → ineligible ❌
🔍 [View Full Dashboard](http://dashboards.thegraph.foundation/reo/)
If watching specific indexers:
When you use /watch <address>, you only receive notifications about your watched indexers. Example:
🔔 Oracle Update Detected!
━━━━━━━━━━━━━━━━━━━
Update Time: 2025-11-03 14:30:45 UTC
📊 Dashboard Stats:
• Total Indexers: 150
• Eligible: 142 ✅
• Grace Period: 5 ⚠️
• Ineligible: 3 ❌
📝 Status Changes Detected:
0x1234567890abcdef1234567890abcdef12345678
eligible → grace period ⚠️
🔍 [View Full Dashboard](http://dashboards.thegraph.foundation/reo/)
Only the indexers you're watching appear in status changes.
- Open Telegram and search for
@BotFather - Send
/newbotcommand - Choose a name (e.g., "REO Dashboard Alerts")
- Choose a username (must end in 'bot', e.g., "reo_dashboard_bot")
- BotFather will give you a BOT TOKEN (looks like:
123456789:ABCdef...) - Save this token securely
Add the bot token to your .env file:
# Telegram Bot Configuration (optional)
TELEGRAM_BOT_TOKEN=your_bot_token_herepip3 install python-telegram-bot==20.7Or use the requirements file:
pip3 install -r requirements.txtThe bot needs to run continuously to accept user subscriptions. Choose one method:
Option A: Using screen (Recommended for simplicity)
cd /home/graph/ftpbox/reo
screen -S telegram_bot
python3 telegram_bot.py
# Press Ctrl+A then D to detachTo manage the screen session:
# List running screens
screen -ls
# Reattach to view logs
screen -r telegram_bot
# Kill the bot
screen -X -S telegram_bot quitOption B: Using systemd (Recommended for production)
# Copy service file
sudo cp telegram_bot_service.service /etc/systemd/system/telegram_bot.service
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable telegram_bot.service
sudo systemctl start telegram_bot.service
# Check status
sudo systemctl status telegram_bot.service
# View logs
sudo journalctl -u telegram_bot.service -f
# Restart if needed
sudo systemctl restart telegram_bot.serviceOption C: Using nohup (Simple background process)
cd /home/graph/ftpbox/reo
nohup python3 telegram_bot.py > telegram_bot.log 2>&1 &
# Check if running
ps aux | grep telegram_bot.py
# View logs
tail -f telegram_bot.log
# Kill if needed
pkill -f telegram_bot.pySet up a cron job to run the dashboard script periodically:
crontab -eAdd one of these lines:
# Run every hour
0 * * * * cd /home/graph/ftpbox/reo && /usr/bin/python3 generate_dashboard.py >> /home/graph/ftpbox/reo/cron.log 2>&1
# Run every 30 minutes
*/30 * * * * cd /home/graph/ftpbox/reo && /usr/bin/python3 generate_dashboard.py >> /home/graph/ftpbox/reo/cron.log 2>&1View cron logs:
tail -f /home/graph/ftpbox/reo/cron.logUsers can interact with the bot using these commands:
| Command | Description |
|---|---|
/start |
Welcome message with bot introduction |
/subscribe |
Subscribe to receive notifications |
/unsubscribe |
Stop receiving notifications |
/status |
Check your subscription status |
/stats |
View bot statistics (total subscribers, notifications sent) |
/help |
Show available commands and help |
/test |
Send a test notification (for subscribed users) |
- Search for your bot on Telegram (using the username you chose)
- Send
/startor/subscribeto the bot - Receive confirmation message
- Start getting notifications when the dashboard script runs!
The bot automatically manages subscribers in subscribers_telegram.json:
{
"subscribers": [
{
"chat_id": 123456789,
"username": "user123",
"subscribed_at": "2025-10-22 14:30:00",
"active": true
}
],
"stats": {
"total_subscribers": 1,
"total_notifications_sent": 45
}
}Bot not responding:
- Check if bot is running:
ps aux | grep telegram_bot.pyorscreen -ls - Check bot logs:
tail -f telegram_bot.logorsudo journalctl -u telegram_bot.service -f - Verify
TELEGRAM_BOT_TOKENis set correctly in.env
Notifications not sending:
- Check cron is running:
crontab -l - View cron logs:
tail -f cron.log - Verify bot token is valid
- Check for errors in dashboard script output
Rate limiting errors:
- Telegram has rate limits (30 messages/second)
- The notifier includes small delays between messages
- For large subscriber lists (100+), messages are automatically paced
.
├── generate_dashboard.py # Main script
├── indexers.txt # Legacy file (still read for backwards compatibility)
├── active_indexers.json # Active indexers with eligibility data (generated)
├── active_indexers_previous_run.json # Backup of previous run for status change tracking (generated)
├── activity_log_indexers_status_changes.json # Activity log tracking all status changes (generated)
├── activity_log_indexers_status_changes.json.example # Example format for activity log
├── ens_resolution.json # ENS name cache (generated)
├── last_transaction.json # Cached transaction data (generated)
├── grt.png # Logo image for the dashboard
├── index.html # Generated dashboard (output)
├── .env # Environment variables (create from env.example)
├── env.example # Template for environment variables
├── requirements.txt # Python dependencies
├── README.md # This file
│
├── telegram_bot.py # Telegram bot for user subscriptions (optional)
├── telegram_notifier.py # Notification sender module (optional)
├── telegram_bot_service.service # Systemd service file for bot (optional)
├── subscribers_telegram.json # Subscriber database (generated by bot)
├── subscribers_telegram.json.example # Example subscriber structure
├── logs/ # Telegram bot logs directory (generated)
│ ├── telegram_bot.log # Bot technical logs
│ └── telegram_bot_activity.log # User activity logs
└── cron.log # Cron job logs (generated)
pip install -r requirements.txtOr install manually:
pip install requests python-dotenv pycryptodomeThe script reads configuration from a .env file in the project root.
Setup:
-
Copy the example file:
cp env.example .env
-
Edit
.envand add your actual values:CONTRACT_ADDRESS=0x9BED32d2b562043a426376b99d289fE821f5b04E ARBISCAN_API_KEY=your_arbiscan_api_key RPC_ENDPOINT=your_rpc_endpoint_url GRAPH_API_KEY=your_graph_api_key USE_CACHED_ENS=N
The script will automatically load these variables. The first four variables are required for the script to run.
python3 generate_dashboard.pyThis will:
- Backup previous run: Copy
active_indexers.jsontoactive_indexers_previous_run.json(if it exists) - Retrieve active indexers from The Graph's network subgraph (with self stake > 0)
- Fetch contract metadata:
- Call
getLastOracleUpdateTime()to get the latest oracle update timestamp - Call
getEligibilityPeriod()to get the grace period duration (14 days)
- Call
- Resolve ENS names:
- If
USE_CACHED_ENS=Y: Load ENS names fromens_resolution.jsoncache - If
USE_CACHED_ENS=N: Query ENS subgraph and updateens_resolution.jsoncache
- If
- Check eligibility (Three-Pass Approach):
- Pass 1: Call contract's
isEligible()function for all indexers - Pass 2: Call
getEligibilityRenewalTime()for eligible indexers - Pass 3: Determine status based on renewal time comparison and grace period:
- Set status to "eligible", "grace", or "ineligible"
- Calculate
eligible_untilfor grace period indexers
- Pass 1: Call contract's
- Track status changes: Compare with previous run to detect status changes
- If status changed: Set
last_status_change_dateto current date - If status unchanged: Keep previous date (or empty if no previous change)
- If new indexer: Leave date empty
- If status changed: Set
- Log status changes to activity log: Append status transitions to cumulative log
- Update metadata (last_check, last_oracle_update_time)
- Append new status change entries to historical record
- Save complete indexer data to
active_indexers.json(without ENS names) - Render dashboard showing all indexers with status badges (eligible/grace/ineligible) merged with ENS names from cache
- Fetch the latest transaction data
- Generate
index.htmlwith sorted table and interactive features
open index.htmlOr simply double-click index.html to open it in your browser.
All configuration is managed through environment variables in the .env file:
- Variable:
CONTRACT_ADDRESS - Default:
0x9BED32d2b562043a426376b99d289fE821f5b04E(Arbitrum Sepolia) - Purpose: The Rewards Eligibility Oracle contract address
- Variable:
ARBISCAN_API_KEY - Purpose: Fetches transaction data from Arbiscan API
- Get yours: Arbiscan API Keys
- Variable:
RPC_ENDPOINT - Purpose: Connects to Arbitrum Sepolia for real-time blockchain data
- Supported Providers: Any Ethereum RPC provider (Alchemy, Infura, QuickNode, Ankr, etc.)
- Get yours:
- Variable:
GRAPH_API_KEY - Purpose: Queries The Graph's network and ENS subgraphs
- Get yours: The Graph Studio
- Variable:
USE_CACHED_ENS - Values:
YorN - Purpose: Controls whether to use cached ENS data or fetch from subgraph
Y: Use cached ENS names fromens_resolution.json(faster, saves API calls)N: Query ENS subgraph and update cache (required for first run or to refresh ENS data)
- Default:
N - Note: On first run or when cache doesn't exist, ENS data will be fetched regardless of this setting
This file is automatically generated by the script and contains all active indexers with their eligibility data (without ENS names).
Structure:
{
"metadata": {
"retrieved": "2025-10-21 08:00:00 UTC",
"total_count": 99,
"last_oracle_update_time": 1760956267,
"eligibility_period": 1209600
},
"indexers": [
{
"address": "0x0058223c6617cca7ce76fc929ec9724cd43d4542",
"is_eligible": true,
"status": "eligible",
"eligible_until": "",
"eligible_until_readable": "",
"eligibility_renewal_time": 1760956267,
"last_status_change_date": ""
},
{
"address": "0x51637a35f7f054c98ed51904de939b9561d37885",
"is_eligible": true,
"status": "grace",
"eligible_until": 1762111555,
"eligible_until_readable": "2025-11-02 19:25:55 UTC",
"eligibility_renewal_time": 1760901955,
"last_status_change_date": "21/Oct/2025"
}
]
}Metadata Fields:
retrieved: Timestamp when the data was fetchedtotal_count: Total number of active indexerslast_oracle_update_time: Unix timestamp from contract'sgetLastOracleUpdateTime()eligibility_period: Duration in seconds (14 days = 1209600 seconds) from contract'sgetEligibilityPeriod()
Indexer Fields:
is_eligible: Boolean indicating if the indexer returnedtruefrom contract'sisEligible()eligibility_renewal_time: Unix timestamp of eligibility renewal (from contract'sgetEligibilityRenewalTime())status: Current eligibility status - one of three values:- "eligible": Renewal time matches oracle update time
- "grace": Within grace period (renewal time + eligibility period > current time)
- "ineligible": Grace period expired or never eligible
eligible_until: Unix timestamp when grace period expires (only set for "grace" status)eligible_until_readable: Human-readable expiration date (only set for "grace" status)last_status_change_date: Date when the status last changed (format:21/Oct/2025)- Empty string (
"") if status has never changed or for new indexers - Date string (e.g.,
"21/Oct/2025") if status changed compared to previous run - Tracks status transitions between any states (eligible ↔ grace ↔ ineligible)
- Empty string (
- All indexers are displayed on the dashboard with status badges
This file is automatically generated by the script and contains cached ENS name resolutions.
Structure:
{
"metadata": {
"retrieved": "2025-10-17 20:00:00 UTC",
"total_count": 100,
"ens_resolved": 74
},
"ens_resolutions": {
"0x0058223c6617cca7ce76fc929ec9724cd43d4542": "grassets-tech-2.eth",
"0x01f17c392614c7ea586e7272ed348efee21b90a3": "oraclegen-indexer.eth",
"0x0874e792462406dc12ee96b75e52a3bdbba3a123": "posthuman-validator.eth"
}
}Key Fields:
ens_resolutions: Dictionary mapping lowercase addresses to ENS namestotal_count: Total number of addresses in the cacheens_resolved: Number of addresses with resolved ENS names- This cache is used during dashboard rendering to merge ENS names with indexer data
Example:
{
"hash": "0x4401f694b80775533566053f88026220e1eab4c84f771e5b600df76f89a768bc",
"blockNumber": "205536308",
"timeStamp": "1760696509",
"status": "Success",
"readable_time": "Oct-17-2025 10:21:49"
}This file is automatically created as a backup of the previous run's active_indexers.json.
Purpose:
- Enables status change tracking between script runs
- Contains the exact same structure as
active_indexers.json - Automatically created/overwritten before each new run
- Only the most recent previous run is kept (no historical archive)
Usage in Status Change Detection: Each time the script runs, it:
- Backs up current
active_indexers.json→active_indexers_previous_run.json - Generates new
active_indexers.jsonwith fresh data - Compares the new file with the backup to detect status changes
- Updates
last_status_change_datein the new file based on comparison
Example Scenario:
- Run 1: Indexer X has status "eligible" →
last_status_change_date="" - Run 2: Indexer X still has status "eligible" →
last_status_change_date=""(no change) - Run 3: Indexer X changes to status "grace" →
last_status_change_date="21/Oct/2025"(change detected!) - Run 4: Indexer X still has status "grace" →
last_status_change_date="21/Oct/2025"(keeps previous date)
This file maintains a cumulative historical record of all indexer status changes.
Purpose:
- Creates an audit trail of status transitions over time
- Preserves complete history of all status changes (not just the most recent)
- Enables analysis of indexer behavior patterns
- Provides accountability and transparency for status evolution
Structure:
{
"metadata": {
"last_check": "2025-10-21 11:14:06 UTC",
"last_oracle_update_time": 1761040822
},
"status_changes": [
{
"address": "0x0874e792462406dc12ee96b75e52a3bdbba3a123",
"previous_status": "grace",
"new_status": "eligible",
"date_status_change": "2025-10-21"
},
{
"address": "0x1234567890abcdef1234567890abcdef12345678",
"previous_status": "eligible",
"new_status": "grace",
"date_status_change": "2025-10-22"
}
]
}How It Works: Each time the script runs, it:
- Updates metadata (overwrites):
last_check: Current timestamp when script ranlast_oracle_update_time: Latest oracle update from contract
- Appends status changes:
- Detects any indexer status transitions
- Adds new entries with address, previous_status, new_status, and date
- Never removes or modifies existing entries
- Preserves history:
- File grows over time as status changes accumulate
- Complete historical record of all transitions
- Can track patterns (e.g., indexer cycling between statuses)
Key Fields:
address: Indexer Ethereum addressprevious_status: Status before the change (eligible/grace/ineligible)new_status: Status after the change (eligible/grace/ineligible)date_status_change: Date when the change was detected (YYYY-MM-DD format)
Example Usage:
- Monitor which indexers frequently lose eligibility
- Track grace period usage patterns
- Identify indexers that maintain consistent eligible status
- Generate reports on overall network eligibility trends
Collapsible debug section (click header to expand/collapse):
- Sepolia Contract on Arbitrum: Contract address with link to Arbiscan
- Last Oracle Update Time: Latest oracle update timestamp from the contract
- Last Transaction ID: Transaction hash with link to Arbiscan
- Block Number: Block where the transaction occurred
- Eligibility Period: 14-day grace period duration (1209600 seconds)
Combined search and filter section (search left-aligned, filters right-aligned):
- Search Box: Real-time filtering by indexer address or ENS name
- Status Filter Buttons: Click to filter by status (eligible, grace, ineligible)
- Active State: Selected filters invert colors (solid background, dark text)
- Toggle Behavior: Click again to remove filter
- Reset Button: Clears both search and status filter
- Helpful Tooltips: Hover over any filter button to see instant descriptions (custom CSS tooltips with 0.2s fade-in):
- eligible: "Indexers that are eligible for rewards"
- grace: "Grace period is XX days" (dynamically shows actual period from contract)
- ineligible: "Indexers that are NOT eligible for rewards"
- Reset: "Show All"
- Combined Filtering: Search and status filters work together
- All Indexers Displayed: Shows all active indexers (eligible, grace period, and ineligible)
- Status Badges:
- eligible (green): Indexer's renewal time matches oracle update time
- grace (yellow): Indexer is in grace period - eligible until expiration date shown
- ineligible (red): Indexer's grace period has expired or never eligible
- Eligible Until Column: Shows grace period expiration date in format:
2-Nov-2025 at 19:25:55 UTC - Smart Sorting: Automatically sorted by eligibility status first (eligible → grace → ineligible), then alphabetically by ENS name
- Sortable columns: Click any header to sort by that column (while maintaining eligibility priority)
- Statistics: Shows total indexers and filtered count
- Color-coded: ENS names highlighted, missing ENS shown in gray
- Clickable addresses: Each indexer address links to their profile on The Graph Explorer
The script uses a three-tier status system:
if eligibility_renewal_time == last_oracle_update_time:
status = "eligible"
elif eligibility_renewal_time != last_oracle_update_time:
grace_period_end = eligibility_renewal_time + eligibility_period
if current_time < grace_period_end:
status = "grace"
eligible_until = grace_period_end
else:
status = "ineligible"
else:
status = "ineligible"All Unix timestamps are converted to UTC format:
datetime.fromtimestamp(timestamp_int, tz=timezone.utc)
.strftime("%Y-%m-%d %H:%M:%S UTC")The script calls multiple contract functions using RPC:
# getLastOracleUpdateTime()
function_selector = '0xbe626dd2'
# getEligibilityPeriod()
function_selector = '0xd0a5379e'
# getEligibilityRenewalTime(address)
function_selector = '0xd353402d'When fetching transactions, the script scans the last 100 blocks to find the most recent transaction to the contract address.
The script includes robust error handling with no mock data:
- Falls back through multiple data sources (JSON → RPC endpoint → Arbiscan)
- Prints informative console messages for debugging
- Gracefully handles missing data without using fake/mock values
- Displays clear error messages in the dashboard UI when data is unavailable:
- "Unable to fetch transaction data" - when all transaction fetch methods fail
- "Unable to fetch oracle update time" - when contract call fails
- All errors are displayed in styled red text (
.error-messageclass) in the dashboard
The dashboard uses a modern dark theme:
- Background:
#0C0A1D(dark purple) - Text:
#F8F6FF(off-white) - Accent:
#494755(medium gray) - Font: Poppins (from Google Fonts)
Works in all modern browsers:
- Chrome/Edge (Chromium)
- Firefox
- Safari
- Mobile browsers
MIT License
Copyright (c) 2025
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
X (Twitter): @pdiomede