Last updated: 2026-03-05
The Mento v3 monitoring system provides real-time visibility into Mento's on-chain FX protocol on Celo. It indexes on-chain events from FPMM pools, oracle contracts, and factory contracts, then exposes them through a GraphQL API and a public dashboard.
- Operational awareness — pool health, oracle liveness, trading limit pressure, rebalancer liveness
- Trade analytics — volume, TVL, fee revenue per pool over time
- Alerting (Phase 2) — proactive alerts when KPIs breach thresholds
- Data access (Phase 3) — Streamlit sandbox for quantitative team
| What | URL |
|---|---|
| Dashboard | https://monitoring.mento.org |
| Mainnet GraphQL | https://indexer.hyperindex.xyz/60ff18c/v1/graphql |
┌───────────────────────────────────────────────────────────────────────┐
│ Celo Chain │
│ FPMMFactory · FPMM pools (×4) · SortedOracles · VPFactory │
└──────────────────────────────┬────────────────────────────────────────┘
│ Events (GRPC/RPC)
▼
┌──────────────────────┐
│ Envio HyperIndex │
│ (Hosted, free tier) │
│ EventHandlers.ts │
└──────────┬───────────┘
│ writes
▼
┌──────────────────────┐
│ Postgres + Hasura │
│ (managed by Envio) │
└──────────┬───────────┘
│ GraphQL
┌────────────────┴────────────────┐
▼ ▼
┌──────────────────────┐ ┌──────────────────────┐
│ Next.js Dashboard │ │ Streamlit Sandbox │
│ (Vercel) │ │ (Phase 3, Python) │
│ monitoring.mento.org│ │ │
└──────────────────────┘ └──────────────────────┘
│
▼ (Phase 2)
┌──────────────────────┐
│ Aegis / Grafana │
│ Alerting │
└──────────────────────┘
| Component | Technology | Hosting | Repo Path |
|---|---|---|---|
| Indexer | Envio HyperIndex | Envio hosted | indexer-envio/ |
| GraphQL API | Hasura (auto-managed) | Envio hosted | — |
| Dashboard | Next.js 16 + Plotly | Vercel | ui-dashboard/ |
| Alerting (future) | Aegis / Grafana | TBD | — |
| Network | Chain ID | Status | Start Block |
|---|---|---|---|
| Celo Mainnet | 42220 | ✅ Live | 60664513 |
| Celo Sepolia | 44787 | ✅ Live | — |
| Monad Mainnet | — | ⏳ Blocked on contract deploy | — |
| Contract | Address |
|---|---|
| FPMMFactory | 0xa849b475FE5a4B5C9C3280152c7a1945b907613b |
| Router | 0x4861840C2EfB2b98312B0aE34d86fD73E8f9B6f6 |
| OracleAdapter | 0xa472fBBF4b890A54381977ac392BdF82EeC4383a |
| SortedOracles | 0xefB84935239dAcdecF7c5bA76d8dE40b077B7b33 |
| Address | Pair |
|---|---|
0x8c0014afe032e4574481d8934504100bf23fcb56 |
USDm / GBPm |
0xb285d4c7133d6f27bfb29224fb0d22e7ec3ddd2d |
USDm / axlUSDC |
0x462fe04b4fd719cbd04c0310365d421d02aaa19e |
USDm / USDC |
0x0feba760d93423d127de1b6abecdb60e5253228d |
USDT / USDm |
What: Is the oracle reporting fresh prices? Measures the ratio of current timestamp to oracle expiry.
Fields: oracleOk (bool), oracleTimestamp, oracleExpiry, oracleNumReporters
Applies to: FPMM pools only (VirtualPools → N/A)
| Status | Condition |
|---|---|
| OK | oracleOk == true |
| WARN | liveness ratio > 0.8 |
| CRITICAL | liveness ratio ≥ 1.0 (oracle expired) |
What: How far is the oracle price from the market rate? Measures priceDifference / rebalanceThreshold.
Fields: priceDifference, rebalanceThreshold, healthStatus
Applies to: FPMM pools only
| Status | Condition |
|---|---|
| OK | priceDifference / rebalanceThreshold < 0.8 |
| WARN | ratio ≥ 0.8 sustained for > 15 min |
| CRITICAL | ratio ≥ 0.8 sustained for > 60 min |
The healthStatus field on Pool encodes the current status: "OK" | "WARN" | "CRITICAL" | "N/A".
What: How close are net flows to their configured trading limits? Prevents runaway one-directional flows.
Fields: limitPressure0, limitPressure1 (string, e.g. "0.1230"), limitStatus, netflow0, netflow1, limit0, limit1
Entity: TradingLimit (one per pool per token)
Applies to: FPMM pools only
| Status | Condition |
|---|---|
| OK | max pressure < 0.8 |
| WARN | max pressure > 0.8 |
| CRITICAL | max pressure ≥ 1.0 (limit hit) |
limitPressure = |netflow| / limit — stored as decimal string for precision.
What: Is the rebalancer bot actively rebalancing pools when needed?
Fields: rebalancerAddress, rebalanceLivenessStatus on Pool; effectivenessRatio on RebalanceEvent
Applies to: FPMM pools only
| Status | Condition |
|---|---|
| ACTIVE | Rebalancer has rebalanced recently |
| N/A | Pool is a VirtualPool |
effectivenessRatio per RebalanceEvent = (priceDifferenceBefore - priceDifferenceAfter) / priceDifferenceBefore — how much of the deviation was corrected.
What: Is there enough collateral in the Stability Pool to absorb liquidations?
Fields: TBD (requires Liquity v2 indexing — Phase 2)
Applies to: Liquity v2 CDP pools (GBPm TroveManager / StabilityPool)
| Status | Condition |
|---|---|
| OK | headroom > 0 |
| CRITICAL | headroom ≤ 0 (undercollateralized) |
Full schema: indexer-envio/schema.graphql
Mutable per-pool state. Updated on every relevant event.
type Pool {
id: ID! # pool address (lowercase)
token0: String # token0 address
token1: String # token1 address
source: String! # "fpmm" | "virtual"
reserves0: BigInt! # current reserve0
reserves1: BigInt! # current reserve1
swapCount: Int! # cumulative swap count
notionalVolume0: BigInt! # cumulative notional volume in token0
notionalVolume1: BigInt! # cumulative notional volume in token1
rebalanceCount: Int! # cumulative rebalance count
# Oracle state (FPMM only; defaults to zero/false for VirtualPools)
oracleOk: Boolean!
oraclePrice: BigInt!
oraclePriceDenom: BigInt!
oracleTimestamp: BigInt!
oracleExpiry: BigInt!
oracleNumReporters: Int!
referenceRateFeedID: String!
priceDifference: BigInt!
rebalanceThreshold: Int!
lastRebalancedAt: BigInt!
healthStatus: String! # "OK" | "WARN" | "CRITICAL" | "N/A"
# Trading limits (FPMM only)
limitStatus: String! # "OK" | "WARN" | "CRITICAL" | "N/A"
limitPressure0: String!
limitPressure1: String!
# Rebalancer
rebalancerAddress: String!
rebalanceLivenessStatus: String! # "ACTIVE" | "N/A"
createdAtBlock: BigInt!
createdAtTimestamp: BigInt!
updatedAtBlock: BigInt!
updatedAtTimestamp: BigInt!
}Hourly pre-aggregated activity per pool. Industry standard (Uniswap/Balancer pattern).
type PoolSnapshot {
id: ID! # "{poolId}-{hourTimestamp}"
poolId: String! @index
timestamp: BigInt! @index # unix timestamp truncated to hour
# Point-in-time state at end of this hour
reserves0: BigInt!
reserves1: BigInt!
# Per-hour activity
swapCount: Int!
swapVolume0: BigInt!
swapVolume1: BigInt!
rebalanceCount: Int!
mintCount: Int!
burnCount: Int!
# Running cumulative totals
cumulativeSwapCount: Int!
cumulativeVolume0: BigInt!
cumulativeVolume1: BigInt!
blockNumber: BigInt!
}Gap-filling (missing hours with no activity) is handled by the dashboard layer via forward-fill — not block handlers (Envio onBlock lacks block timestamps).
Per-oracle-event health snapshot. Powers the dual y-axis oracle chart.
type OracleSnapshot {
id: ID!
poolId: String! @index
timestamp: BigInt! @index
oraclePrice: BigInt!
oraclePriceDenom: BigInt!
oracleOk: Boolean!
numReporters: Int!
priceDifference: BigInt!
rebalanceThreshold: Int!
source: String!
blockNumber: BigInt!
}Per-pool per-token trading limit state.
type TradingLimit {
id: ID! # "{poolId}-{tokenAddress}"
poolId: String!
token: String!
limit0: BigInt!
limit1: BigInt!
decimals: Int!
netflow0: BigInt!
netflow1: BigInt!
lastUpdated0: BigInt!
lastUpdated1: BigInt!
limitPressure0: String!
limitPressure1: String!
limitStatus: String! # "OK" | "WARN" | "CRITICAL"
updatedAtBlock: BigInt!
updatedAtTimestamp: BigInt!
}Per-rebalance event with effectiveness measurement.
type RebalanceEvent {
id: ID!
poolId: String! @index
sender: String!
priceDifferenceBefore: BigInt!
priceDifferenceAfter: BigInt!
improvement: BigInt! # priceDifferenceBefore - priceDifferenceAfter
effectivenessRatio: String! # improvement / priceDifferenceBefore, e.g. "0.5000"
txHash: String!
blockNumber: BigInt!
blockTimestamp: BigInt! @index
}Protocol-wide metrics dashboard.
Tiles:
- Total pools (FPMM + VirtualPool count)
- Active pools (with swap activity in last 24h)
- Health breakdown (OK / WARN / CRITICAL counts)
- Total swap count
Components:
PoolsTable— all pools with HealthBadge, last swap, volume- Activity ranking by swap count
Status: ✅ Live
Per-pool deep-dive.
Tabs:
- Overview — reserve chart (Plotly), recent swaps table
- Analytics — PoolSnapshot charts (hourly volume bars + cumulative count), OracleChart (FPMM only)
- (Future) Limits — TradingLimit pressure panel
- (Future) Rebalancer — RebalanceEvent timeline
Status: ✅ Live (tabs 1 + 2)
| Component | File | Status |
|---|---|---|
PoolsTable |
components/pools-table.tsx |
✅ Live |
HealthBadge |
components/health-badge.tsx |
✅ Live |
HealthPanel |
components/health-panel.tsx |
✅ Live |
OracleChart |
components/oracle-chart.tsx |
✅ Live |
ReserveChart |
components/reserve-chart.tsx |
✅ Live |
SnapshotChart |
components/snapshot-chart.tsx |
✅ Live |
NetworkSwitcher |
components/network-switcher.tsx |
✅ Live |
LimitBadge |
— | 🔜 Stream C |
LimitPanel |
— | 🔜 Stream C |
LivenessBadge |
— | 🔜 Stream C |
RebalancerPanel |
— | 🔜 Stream C |
isFpmm(pool) in ui-dashboard/src/lib/tokens.ts is the single source of truth for FPMM vs VirtualPool detection. Used to conditionally render oracle health, trading limit, and rebalancer components (shows "N/A" badges for VirtualPools).
The dashboard supports multiple network targets via a network switcher. Each network target has:
- A Hasura/GraphQL endpoint URL
- A Hasura admin secret
- A block explorer base URL
| Target | Description |
|---|---|
CELO_MAINNET_HOSTED |
Celo Mainnet (Envio hosted) |
CELO_SEPOLIA_HOSTED |
Celo Sepolia (Envio hosted) |
CELO_MAINNET |
Celo Mainnet (local dev) |
CELO_SEPOLIA |
Celo Sepolia (local dev) |
The live dashboard (monitoring.mento.org) uses CELO_MAINNET_HOSTED by default.
| Limitation | Details |
|---|---|
| Endpoint hash changes on each deploy | Envio free tier generates new URL per deploy; requires Vercel env var update |
| No authentication on dashboard | Google Auth deferred; Envio endpoints are public (no admin secret) |
| Cannot run two indexers locally | Port 9898 hardcoded in Envio |
| SortedOracles on Sepolia | Contracts return zero address; oracle indexing mainnet-only |
| Gap-fill not yet implemented | PoolSnapshot charts may show gaps for periods with no activity |
| Monad blocked | Awaiting contract deployment to Monad |
| No Liquity v2 indexing | TroveManager / StabilityPool — Phase 2 |
| Dashboard component tests | Zero component-level tests; only lib utils covered |
- Dashboard components for trading limits and rebalancer liveness
- TVL display on global page
- Gap-fill logic for snapshot charts
- Liquity v2 CDP indexing (TroveManager, StabilityPool, Trove entities)
- Aegis/Grafana alerting with the 5 KPI thresholds
- Monad indexing (once contracts are deployed)
- Roman's Streamlit sandbox (Python, reads from same Hasura backend)
- Google Auth (NextAuth.js —
@mentolabs.xyzonly) - ClickHouse sink for heavy analytics
pnpm --filter @mento-protocol/ui-dashboard test
pnpm --filter @mento-protocol/ui-dashboard typecheck
pnpm --filter @mento-protocol/ui-dashboard lintGitHub Actions (.github/workflows/):
ui-dashboard.yml— lint + typecheck + test + Codecovindexer-envio.yml— typecheck + lintnotify-envio-deploy.yml— Discord notification ondeploy/*push
Branch protection: "Quality Checks" required on main.