Indexer and data

The Envio HyperIndex schema, derived entities, metric definitions and the GraphQL queries the app and stats page use.

An Envio HyperIndex indexer turns Plans events on Monad into everything the app reads: plans, member ledgers, live balances, budget bars, the settle-up graph, the activity feed, a fresh-device rebuild and the public traction stats. The app keeps no database of its own.

Pending

Built and tested (21 tests, queries validated against a local Hasura). The Envio Cloud deployment and its GraphQL URL are pending. When it is live, this site's stats page reads from it.

  • envio@3.12.1, TypeScript handlers, Vitest tests.
  • Monad mainnet 143 and testnet 10143, both native HyperSync networks.
  • PlansFactory, KeyRegistry, ClaimEscrow and PlansSend have static addresses; every Pot is registered dynamically from PotCreated.
  • Entities are per chain: filter every query by chainId. Addresses are lowercase. Amounts are AUSD base units (6 decimals).

Schema

32 entities and 13 enums, linked with @derivedFrom. Two layers:

  1. Raw and lifecycle entities, one per onchain object or event: Account, Pot, Member, RulesVersion, AllowlistEntry, Contribution, Spend, SpendShare, Vote, Dispute, DisputeVote, Freeze, RuleChange, RuleChangeVote, KeyWrap, Ack, Payout, Pull, Debt, PotSettlement, Claim, Send, Activity.
  2. Derived entities, maintained by the handlers:
EntityWhat it holdsDrives
MemberBalanceEach member's live net, what they owe and are owed, settledUp"You're owed £18" on every plan
CategorySpendSpend against each category budgetBudget bars and the warning before a spend
SettlementEdgeThe minimal who-owes-whom graphThe Settle up preview
Corridor, PotCorridor, CorridorFlowVolume and count by country pair, with an audit trailCross-border stats
PotDailyA daily series per planPlan summary
DailyStats, GlobalStatsTraction, excluding internal accounts and demo plans/stats

The ledger

The indexer mirrors the contract: net = contributed + personalPaid − share − withdrawn, and tracks each pot's balance independently. The tests assert Σ net == balance after every scenario.

The settle-up graph

Recomputed after every change to any member's net, with a deterministic greedy min-cash-flow: the largest debtor pays the largest creditor min(debt, credit), ties broken by address. Leftover credit is owed by the pot's own balance (PotToMember). Tested on 2,000 random pots: the edges balance exactly, every member is on one side only, there are at most (debtors + creditors − 1) member-to-member edges, and the output doesn't depend on input order.

Corridors

A corridor is an ordered country pair such as GB-IN. Sources: direct sends, send-by-link claims, a plan's LINK claims, settlement pulls and debt payments (split across the debtor's settle-up edges). A Payout alone isn't attributed, because the pot holds pooled money; the cross-member part of a settlement is counted once, on the pull or debt-payment side. Flows with an unknown country aren't attributed; domestic pairs are kept with isCrossBorder = false.

Internal accounts and demo plans

indexer/internal-accounts.json lists the demo members (Ben, Asha, Maya) and team accounts (treasury, deployer, test phones). A demo plan is one created by an internal account or joined by any demo member. If a demo member joins a plan after it was counted, everything that plan contributed to the stats is withdrawn.

Metric definitions

All metrics are per chain and exclude internal accounts and demo plans. They match docs/traction.md and the stats page.

MetricFieldDefinition
Users (headline)usersNon-internal accounts that joined a counted plan, or sent or claimed a non-internal send
AccountsaccountsDistinct non-internal addresses with KeyRegistered
Plans createdpotsCreatedPotCreated, counted plans only
Funded plansfundedPotsCounted plans with at least one Contributed
Members per planmedianMembersPerPotActive members at the end, or now for open plans; median
Countries per planmedianCountriesPerPotDistinct member country codes; median
Time to first funded actionAccount.timeToFirstFundedActionFirst MemberJoined (in a counted plan) to first Contributed, Sent or Claimed; median, seconds
SpendsspendsSpendExecuted
ApprovalsapprovalsVoted with approve = true
Settlements, settled volumesettlements, settledVolumeSettled, and its payouts
Cross-border volumecrossBorderVolume, Corridor.volumeAUSD between different country codes, by pair

Queries

The full set is in indexer/queries/; node queries/run.mjs <endpoint> checks them against a live endpoint.

Plans for an address (myPlans.graphql)
query MyPlans($account: String!, $chainId: Int!) {
  Member(where: { account_id: { _eq: $account }, chainId: { _eq: $chainId } }, order_by: { joinedAt: desc }) {
    id status net contributed share debt
    pot { id meta status startTime endTime balance memberCount activeMemberCount countries frozenUntil isDemo }
  }
  Account(where: { id: { _eq: $account }, chainId: { _eq: $chainId } }) {
    id key country isUser keyWraps(order_by: { timestamp: desc }) { pot_id by_id wrap timestamp }
  }
}
Public stats (stats.graphql, abridged; this site's /stats runs the full query)
query Stats($chainId: Int!) {
  GlobalStats(where: { id: { _eq: "global" }, chainId: { _eq: $chainId } }) {
    users accounts potsCreated fundedPots medianMembersPerPot medianCountriesPerPot
    spends approvals settlements settledVolume crossBorderVolume updatedAt
  }
  DailyStats(where: { chainId: { _eq: $chainId } }, order_by: { day: asc }) { date newUsers spends settledVolume }
  Corridor(where: { chainId: { _eq: $chainId }, isCrossBorder: { _eq: true }, count: { _gt: 0 } },
           order_by: { volume: desc }) { fromCountry toCountry volume count }
  Account(where: { chainId: { _eq: $chainId }, ttffaCounted: { _eq: true } },
          order_by: { timeToFirstFundedAction: asc }) { timeToFirstFundedAction }
}

Run it locally

cd indexer
pnpm install
pnpm codegen
pnpm test              # no network, no Docker
cp .env.example .env   # ENVIO_API_TOKEN and, once deployed, the contract addresses
pnpm dev               # Postgres + Hasura in Docker; GraphQL at http://localhost:8080/v1/graphql
node queries/run.mjs

Edit this page on GitHub

On this page