531 lines
14 KiB
Markdown
531 lines
14 KiB
Markdown
|
|
# Explorer Freshness And Diagnostics Contract
|
||
|
|
|
||
|
|
This document defines the minimum public freshness and diagnostics payloads the SolaceScan frontend needs in order to present chain activity, transaction visibility, and snapshot posture without relying on frontend inference.
|
||
|
|
|
||
|
|
It is intended to close the remaining gap between:
|
||
|
|
|
||
|
|
- a frontend that now renders and explains state honestly, and
|
||
|
|
- upstream APIs that still omit authoritative freshness metadata for several critical surfaces.
|
||
|
|
|
||
|
|
## Goal
|
||
|
|
|
||
|
|
The frontend should be able to answer these questions directly from public API fields:
|
||
|
|
|
||
|
|
1. Is the chain head current?
|
||
|
|
2. When was the latest visible transaction indexed?
|
||
|
|
3. What is the latest non-empty block?
|
||
|
|
4. Is the homepage using a live feed, a snapshot, or mixed evidence?
|
||
|
|
5. Which subsystem is stale: RPC, indexing, relay monitoring, or stats?
|
||
|
|
6. Which values are reported directly vs inferred vs unavailable?
|
||
|
|
|
||
|
|
The frontend should not have to infer these from a combination of:
|
||
|
|
|
||
|
|
- `/api/v2/stats`
|
||
|
|
- `/api/v2/main-page/blocks`
|
||
|
|
- `/api/v2/main-page/transactions`
|
||
|
|
- `/explorer-api/v1/track1/bridge/status`
|
||
|
|
|
||
|
|
unless there is no backend alternative.
|
||
|
|
|
||
|
|
## Design Principles
|
||
|
|
|
||
|
|
- Prefer explicit freshness fields over derived heuristics.
|
||
|
|
- Separate chain freshness from indexed-transaction freshness.
|
||
|
|
- Distinguish reported facts from inferred or partial facts.
|
||
|
|
- Make incompleteness first-class.
|
||
|
|
- Keep the contract calm and operational, not alarmist.
|
||
|
|
|
||
|
|
## Proposed Public Endpoints
|
||
|
|
|
||
|
|
Two additions are recommended.
|
||
|
|
|
||
|
|
### 1. Extend `GET /api/v2/stats`
|
||
|
|
|
||
|
|
This endpoint already feeds the homepage summary cards. It should become the authoritative public summary for chain freshness and indexed activity freshness.
|
||
|
|
|
||
|
|
### 2. Extend `GET /explorer-api/v1/track1/bridge/status`
|
||
|
|
|
||
|
|
This endpoint already powers Mission Control. It should expose snapshot/feed posture and subsystem freshness more directly.
|
||
|
|
|
||
|
|
If backend implementation prefers separation, these fields may instead be exposed from a new endpoint:
|
||
|
|
|
||
|
|
`GET /explorer-api/v1/track1/observability/freshness`
|
||
|
|
|
||
|
|
The frontend does not require a separate endpoint as long as the fields below are available from a stable public contract.
|
||
|
|
|
||
|
|
## Required Additions To `/api/v2/stats`
|
||
|
|
|
||
|
|
### Current gaps
|
||
|
|
|
||
|
|
The current `stats` payload gives totals, but it does not reliably expose:
|
||
|
|
|
||
|
|
- latest indexed transaction timestamp
|
||
|
|
- latest non-empty block
|
||
|
|
- authoritative utilization freshness
|
||
|
|
- confidence/completeness metadata
|
||
|
|
|
||
|
|
### Required fields
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"total_blocks": 3873353,
|
||
|
|
"total_transactions": 52391,
|
||
|
|
"total_addresses": 10294,
|
||
|
|
"latest_block": 3873353,
|
||
|
|
"average_block_time": 2000,
|
||
|
|
"gas_prices": {
|
||
|
|
"slow": 0.02,
|
||
|
|
"average": 0.03,
|
||
|
|
"fast": 0.05
|
||
|
|
},
|
||
|
|
"network_utilization_percentage": 0,
|
||
|
|
"transactions_today": 18,
|
||
|
|
|
||
|
|
"freshness": {
|
||
|
|
"chain_head": {
|
||
|
|
"block_number": 3873353,
|
||
|
|
"timestamp": "2026-04-10T21:42:15Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_indexed_transaction": {
|
||
|
|
"hash": "0x...",
|
||
|
|
"block_number": 3858013,
|
||
|
|
"timestamp": "2026-04-10T12:31:05Z",
|
||
|
|
"age_seconds": 33070,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_non_empty_block": {
|
||
|
|
"block_number": 3858013,
|
||
|
|
"timestamp": "2026-04-10T12:31:05Z",
|
||
|
|
"age_seconds": 33070,
|
||
|
|
"distance_from_head": 15340,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_indexed_block": {
|
||
|
|
"block_number": 3873353,
|
||
|
|
"timestamp": "2026-04-10T21:42:15Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
|
||
|
|
"completeness": {
|
||
|
|
"transactions_feed": "complete",
|
||
|
|
"blocks_feed": "complete",
|
||
|
|
"gas_metrics": "partial",
|
||
|
|
"utilization_metrics": "partial"
|
||
|
|
},
|
||
|
|
|
||
|
|
"sampling": {
|
||
|
|
"stats_generated_at": "2026-04-10T21:42:16Z",
|
||
|
|
"stats_window_seconds": 300,
|
||
|
|
"rpc_probe_at": "2026-04-10T21:42:15Z"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Field Semantics
|
||
|
|
|
||
|
|
### `freshness.chain_head`
|
||
|
|
|
||
|
|
The latest chain head known from the authoritative public RPC or canonical head source.
|
||
|
|
|
||
|
|
This is the answer to:
|
||
|
|
|
||
|
|
- "Is the chain alive?"
|
||
|
|
- "Is head visibility current?"
|
||
|
|
|
||
|
|
### `freshness.latest_indexed_transaction`
|
||
|
|
|
||
|
|
The most recent transaction currently visible in the public indexed transaction feed.
|
||
|
|
|
||
|
|
This is the answer to:
|
||
|
|
|
||
|
|
- "How recent is the latest visible transaction?"
|
||
|
|
|
||
|
|
### `freshness.latest_non_empty_block`
|
||
|
|
|
||
|
|
The most recent indexed block containing one or more transactions.
|
||
|
|
|
||
|
|
This is the answer to:
|
||
|
|
|
||
|
|
- "Are head blocks empty because the chain is quiet?"
|
||
|
|
- "What is the last block with visible activity?"
|
||
|
|
|
||
|
|
### `freshness.latest_indexed_block`
|
||
|
|
|
||
|
|
The latest block successfully indexed into the explorer's public block dataset.
|
||
|
|
|
||
|
|
This disambiguates:
|
||
|
|
|
||
|
|
- current chain head
|
||
|
|
- current explorer indexed head
|
||
|
|
|
||
|
|
### `completeness.*`
|
||
|
|
|
||
|
|
Simple public-facing availability states for each summary subsystem:
|
||
|
|
|
||
|
|
- `complete`
|
||
|
|
- `partial`
|
||
|
|
- `stale`
|
||
|
|
- `unavailable`
|
||
|
|
|
||
|
|
These should not be interpreted as outage severity; they describe data completeness only.
|
||
|
|
|
||
|
|
### `sampling.*`
|
||
|
|
|
||
|
|
Metadata for when the summary itself was generated and what freshness window it represents.
|
||
|
|
|
||
|
|
## Required Additions To Mission Control Payload
|
||
|
|
|
||
|
|
Mission Control currently provides useful relay detail, but the homepage still infers snapshot scope and partial feed posture from surrounding evidence.
|
||
|
|
|
||
|
|
### Required fields
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"status": "degraded",
|
||
|
|
"checked_at": "2026-04-10T21:42:16Z",
|
||
|
|
"mode": {
|
||
|
|
"kind": "snapshot",
|
||
|
|
"updated_at": "2026-04-10T21:42:16Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"reason": "live_homepage_stream_not_attached",
|
||
|
|
"scope": "relay_monitoring_homepage_card_only",
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"subsystems": {
|
||
|
|
"rpc_head": {
|
||
|
|
"status": "operational",
|
||
|
|
"updated_at": "2026-04-10T21:42:15Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"tx_index": {
|
||
|
|
"status": "stale",
|
||
|
|
"updated_at": "2026-04-10T12:31:05Z",
|
||
|
|
"age_seconds": 33070,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"bridge_relay_monitoring": {
|
||
|
|
"status": "degraded",
|
||
|
|
"updated_at": "2026-04-10T21:42:16Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"stats_summary": {
|
||
|
|
"status": "partial",
|
||
|
|
"updated_at": "2026-04-10T21:42:16Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "medium"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Required Enumerations
|
||
|
|
|
||
|
|
These enums should be consistent across public surfaces.
|
||
|
|
|
||
|
|
### Activity interpretation
|
||
|
|
|
||
|
|
- `active`
|
||
|
|
- `quiet`
|
||
|
|
- `sparse_activity`
|
||
|
|
- `fresh_head_stale_tx_visibility`
|
||
|
|
- `limited_observability`
|
||
|
|
|
||
|
|
This value should be emitted only when the backend can support it directly. Otherwise the frontend may continue to derive it as a presentation layer.
|
||
|
|
|
||
|
|
### Data source confidence
|
||
|
|
|
||
|
|
- `high`
|
||
|
|
- `medium`
|
||
|
|
- `low`
|
||
|
|
- `unknown`
|
||
|
|
|
||
|
|
### Data origin
|
||
|
|
|
||
|
|
- `reported`
|
||
|
|
- `inferred`
|
||
|
|
- `sampled`
|
||
|
|
- `unavailable`
|
||
|
|
|
||
|
|
### Completeness
|
||
|
|
|
||
|
|
- `complete`
|
||
|
|
- `partial`
|
||
|
|
- `stale`
|
||
|
|
- `unavailable`
|
||
|
|
|
||
|
|
## Example Payloads
|
||
|
|
|
||
|
|
These examples are intended to accelerate frontend/backend alignment by showing how the contract should represent common live states.
|
||
|
|
|
||
|
|
### Example A: Healthy Live State
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"freshness": {
|
||
|
|
"chain_head": {
|
||
|
|
"block_number": 3874000,
|
||
|
|
"timestamp": "2026-04-10T22:10:14Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_indexed_block": {
|
||
|
|
"block_number": 3874000,
|
||
|
|
"timestamp": "2026-04-10T22:10:14Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_indexed_transaction": {
|
||
|
|
"hash": "0x...",
|
||
|
|
"block_number": 3873998,
|
||
|
|
"timestamp": "2026-04-10T22:10:10Z",
|
||
|
|
"age_seconds": 5,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_non_empty_block": {
|
||
|
|
"block_number": 3873998,
|
||
|
|
"timestamp": "2026-04-10T22:10:10Z",
|
||
|
|
"age_seconds": 5,
|
||
|
|
"distance_from_head": 2,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"completeness": {
|
||
|
|
"transactions_feed": "complete",
|
||
|
|
"blocks_feed": "complete",
|
||
|
|
"gas_metrics": "complete",
|
||
|
|
"utilization_metrics": "complete"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Example B: Quiet Chain But Current
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"freshness": {
|
||
|
|
"chain_head": {
|
||
|
|
"block_number": 3875000,
|
||
|
|
"timestamp": "2026-04-10T23:10:14Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_indexed_block": {
|
||
|
|
"block_number": 3875000,
|
||
|
|
"timestamp": "2026-04-10T23:10:14Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_indexed_transaction": {
|
||
|
|
"hash": "0x...",
|
||
|
|
"block_number": 3874902,
|
||
|
|
"timestamp": "2026-04-10T23:01:42Z",
|
||
|
|
"age_seconds": 512,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_non_empty_block": {
|
||
|
|
"block_number": 3874902,
|
||
|
|
"timestamp": "2026-04-10T23:01:42Z",
|
||
|
|
"age_seconds": 512,
|
||
|
|
"distance_from_head": 98,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"activity_interpretation": "quiet"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Example C: Fresh Head, Stale Transaction Visibility
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"freshness": {
|
||
|
|
"chain_head": {
|
||
|
|
"block_number": 3876000,
|
||
|
|
"timestamp": "2026-04-11T00:10:14Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_indexed_block": {
|
||
|
|
"block_number": 3875999,
|
||
|
|
"timestamp": "2026-04-11T00:10:12Z",
|
||
|
|
"age_seconds": 3,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_indexed_transaction": {
|
||
|
|
"hash": "0x...",
|
||
|
|
"block_number": 3860660,
|
||
|
|
"timestamp": "2026-04-10T15:02:10Z",
|
||
|
|
"age_seconds": 32900,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"latest_non_empty_block": {
|
||
|
|
"block_number": 3860660,
|
||
|
|
"timestamp": "2026-04-10T15:02:10Z",
|
||
|
|
"age_seconds": 32900,
|
||
|
|
"distance_from_head": 15340,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
}
|
||
|
|
},
|
||
|
|
"activity_interpretation": "fresh_head_stale_tx_visibility",
|
||
|
|
"completeness": {
|
||
|
|
"transactions_feed": "stale",
|
||
|
|
"blocks_feed": "complete",
|
||
|
|
"gas_metrics": "partial",
|
||
|
|
"utilization_metrics": "partial"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Example D: Snapshot Mode State
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"data": {
|
||
|
|
"status": "degraded",
|
||
|
|
"checked_at": "2026-04-11T00:10:15Z",
|
||
|
|
"mode": {
|
||
|
|
"kind": "snapshot",
|
||
|
|
"updated_at": "2026-04-11T00:10:15Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"reason": "live_homepage_stream_not_attached",
|
||
|
|
"scope": "relay_monitoring_homepage_card_only",
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"subsystems": {
|
||
|
|
"rpc_head": {
|
||
|
|
"status": "operational",
|
||
|
|
"updated_at": "2026-04-11T00:10:14Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"tx_index": {
|
||
|
|
"status": "stale",
|
||
|
|
"updated_at": "2026-04-10T15:02:10Z",
|
||
|
|
"age_seconds": 32900,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
},
|
||
|
|
"bridge_relay_monitoring": {
|
||
|
|
"status": "degraded",
|
||
|
|
"updated_at": "2026-04-11T00:10:15Z",
|
||
|
|
"age_seconds": 1,
|
||
|
|
"source": "reported",
|
||
|
|
"confidence": "high"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Frontend Usage Rules
|
||
|
|
|
||
|
|
Once the fields above exist, the frontend should follow these rules:
|
||
|
|
|
||
|
|
1. Use backend freshness fields directly where present.
|
||
|
|
2. Stop deriving latest transaction age from the transactions page feed when `freshness.latest_indexed_transaction` is available.
|
||
|
|
3. Stop deriving last non-empty block from recent block scanning when `freshness.latest_non_empty_block` is available.
|
||
|
|
4. Use `mode.kind`, `mode.reason`, and `mode.scope` directly for homepage snapshot messaging.
|
||
|
|
5. Use `source` and `confidence` badges only where they improve trust and do not clutter.
|
||
|
|
|
||
|
|
## Backward-Compatible Rollout Plan
|
||
|
|
|
||
|
|
### Phase A
|
||
|
|
|
||
|
|
Add fields without removing any current keys:
|
||
|
|
|
||
|
|
- extend `/api/v2/stats`
|
||
|
|
- extend bridge status payload with `mode` and `subsystems`
|
||
|
|
|
||
|
|
### Phase B
|
||
|
|
|
||
|
|
Frontend prefers new fields when available and falls back to inference when absent.
|
||
|
|
|
||
|
|
### Phase C
|
||
|
|
|
||
|
|
Once fields are consistently present in production:
|
||
|
|
|
||
|
|
- reduce frontend inference paths
|
||
|
|
- remove duplicate explanatory fallback logic where it is no longer needed
|
||
|
|
|
||
|
|
## Minimum Viable Backend Implementation
|
||
|
|
|
||
|
|
If full rollout is not possible immediately, the minimum high-leverage addition is:
|
||
|
|
|
||
|
|
### `/api/v2/stats`
|
||
|
|
|
||
|
|
- `freshness.chain_head`
|
||
|
|
- `freshness.latest_indexed_transaction`
|
||
|
|
- `freshness.latest_non_empty_block`
|
||
|
|
- `sampling.stats_generated_at`
|
||
|
|
|
||
|
|
### `/explorer-api/v1/track1/bridge/status`
|
||
|
|
|
||
|
|
- `mode.kind`
|
||
|
|
- `mode.updated_at`
|
||
|
|
- `mode.reason`
|
||
|
|
- `mode.scope`
|
||
|
|
|
||
|
|
That alone would materially reduce frontend ambiguity.
|
||
|
|
|
||
|
|
## Why This Contract Matters
|
||
|
|
|
||
|
|
The frontend now presents state honestly enough that the remaining ambiguity is no longer visual. It is contractual.
|
||
|
|
|
||
|
|
Without these fields, the UI must keep inferring:
|
||
|
|
|
||
|
|
- whether the chain is quiet or stale
|
||
|
|
- whether the homepage is in snapshot mode because of relay posture or indexing posture
|
||
|
|
- whether low activity is real or a visibility gap
|
||
|
|
|
||
|
|
With these fields, the product becomes:
|
||
|
|
|
||
|
|
- more trustworthy
|
||
|
|
- easier to evaluate externally
|
||
|
|
- less likely to be misread as broken
|
||
|
|
|
||
|
|
## Summary
|
||
|
|
|
||
|
|
The next backend milestone is not broad API expansion. It is a targeted public freshness contract.
|
||
|
|
|
||
|
|
The public explorer needs explicit answers for:
|
||
|
|
|
||
|
|
- current chain head
|
||
|
|
- current indexed head
|
||
|
|
- latest visible transaction
|
||
|
|
- last non-empty block
|
||
|
|
- snapshot/feed mode
|
||
|
|
- subsystem freshness/completeness
|
||
|
|
|
||
|
|
That is the smallest backend addition with the highest frontend trust impact.
|