Skip to content

Data

The Graph subgraph

The subgraph turns the event stream from both contracts into queryable history: jobs, tasks, per-sub-agent earnings, every micro-settlement with the rail it used, every HCS anchor, and daily roll-ups. It runs on a self-hosted graph-node pointed at the Hedera JSON-RPC relay.

Source lives in subgraph/: the manifest subgraph/subgraph.yaml, the schema subgraph/schema.graphql, and the mappings subgraph/src/agency.ts and subgraph/src/treasury.ts. The app reads the endpoint from NEXT_PUBLIC_SUBGRAPH_URL.

Why self-hosted

Hedera is not in The Graph's hosted network registry. The comment at the top of subgraph/docker-compose.yml records the check: @pinax/graph-networks-registry ships 156 networks and none of them is Hedera, so there is no Subgraph Studio or decentralised network target to deploy to. The supported path - and the one Hedera's own subgraph guide prescribes - is a graph-node you run yourself, with its ethereum setting pointed at a Hedera JSON-RPC relay. Aetheris uses Hashio, https://testnet.hashio.io/api, under the network label hedera-testnet.

Two things must agree or indexing silently never starts: the label before the colon in the compose file's ethereum: "hedera-testnet:https://..." and network: hedera-testnet in the manifest. Hedera's stock example calls it testnet; either works as long as both sides match.

The manifest

subgraph/subgraph.yaml is specVersion: 1.2.0, apiVersion: 0.0.9, indexerHints.prune: auto, and declares two data sources. The split follows what the contracts actually emit.

AetherisAgency

AetherisAgency data source
Address0x16fA9CC838Ab5380F0Ebe3C261a2F57E0FBAbc81
startBlock40,396,781
Mappingsubgraph/src/agency.ts
Handlers (7)handleAgencyDeployed, handleOperatorVerified, handleJobCreated, handleSubAgentAssigned, handleTaskCompleted, handleJobSettled, handleHcsLogAnchored

AetherisTreasury

AetherisTreasury data source
Address0x10360383a6b43Fd22BE257bE334E9A9ad83B5598
startBlock40,396,776
Mappingsubgraph/src/treasury.ts
Handlers (3)handleMicroSettlement, handleTreasuryRebalanced, handleProfitClaimed

MicroSettlement is emitted by the treasury, not the agency - it fires inside AetherisTreasury.settleSubAgent. The handler resolves the owning agency from the job (falling back to the treasury's agency() view) so agency aggregates stay correct. Every event signature is copied verbatim from the frozen contracts/interfaces/IAetherisEvents.sol; see the event list.

Entities

subgraph/schema.graphql defines 17 entities and three enums (JobStatus, TaskStatus, SettlementRail) that mirror the Solidity enums member for member. Addresses and hashes are Bytes; amounts are raw base-unit BigInt (decimals are resolved in the UI, since HTS tokens do not expose ERC-20 decimals() uniformly); ratios are BigDecimal. Reverse relations use @derivedFrom, and every counter is a running aggregate - no handler ever recomputes a total by scanning children.

  • Protocol - singleton roll-up (id = "aetheris"): counts of agencies, operators, sub-agents, jobs, settlements (and how many went via HTS), revenue, margin, profit claimed.
  • Operator - a World-ID-gated human: verified, nullifierHash, ENS name, agencies run, profit claimed.
  • Agency - the agency contract: job and task counters, gross revenue, paid to sub-agents, netMargin and marginRate, HTS vs ERC-20 settlement counts, unique sub-agents, HCS anchor count.
  • SubAgent - a worker address. This entity is the leaderboard: totalEarned, tasksCompleted, completionRate, averageFee, roles.
  • AgentRoleStat - per-(sub-agent, role) breakdown, id = {subAgent}-{role}.
  • Token - any token seen in a deposit, fee, settlement, claim or swap; seenViaHts flips once an HTS settlement used it.
  • Job - a client-funded unit of work: client, token, deposit, specURI, status, totalTaskFees against deposit for a live margin view, timestamps per status.
  • Task - one sub-agent assignment, id = {jobId}-{taskId}: role, fee, status, resultHash, hcsTopicId, hcsSequenceNumber, and the settlement that paid it.
  • JobSettlement - immutable close-out record per JobSettled: gross deposit, paid to sub-agents, netMargin, marginRate.
  • Settlement - one MicroSettlement, id = {txHash}-{logIndex}: amount, viaHts and the derived rail (HTS or ERC20), linked to job, task, sub-agent and token.
  • Rebalance - one TreasuryRebalanced: from/to token, amounts, dstChainId, crossChain, the 1inch tx hash, executionRate.
  • ProfitClaim - one ProfitClaimed: operator, token, amount, and the nullifier that authorised it.
  • HcsAnchor - one HcsLogAnchored: messageHash, topicId, sequenceNumber - the join key to the mirror node.
  • AgencyDayData - daily roll-up per agency keyed on timestamp / 86400: jobs, tasks, settlements by rail, volume, revenue, margin, unique sub-agents, cumulative totals.
  • ProtocolDayData - the same daily roll-up protocol-wide, plus rebalances and active agencies.
  • ActiveSubAgentMarker, ActiveAgencyMarker - internal de-duplication markers so unique counts can be maintained incrementally. Never queried by the UI.

Example queries

POST these to the GraphQL endpoint as {"query": "..."}. Amounts come back in base units; aUSD and aUSDC both have 6 decimals.

Settlements that went through HTS

The core Hedera claim, queryable directly. The live audit log shows job 6 settling over ERC-20 (viaHts: false) because it was funded in the test ERC-20 aUSDC; jobs funded in the HTS token aUSD are paid through the HTS system contract and carry viaHts: true.

GraphQL
{
  settlements(where: { viaHts: true }, orderBy: timestamp, orderDirection: desc) {
    id
    amount
    rail
    viaHts
    token { id }
    subAgent { id }
    job { jobId }
    transactionHash
  }
}

Sub-agent leaderboard

GraphQL
{
  subAgents(first: 10, orderBy: totalEarned, orderDirection: desc) {
    id
    tasksAssigned
    tasksCompleted
    tasksPaid
    totalEarned
    completionRate
    averageFee
    roles
    roleStats { role tasksCompleted totalEarned }
  }
}

Daily revenue and throughput for the agency

GraphQL
{
  agencyDayDatas(
    where: { agency: "0x16fa9cc838ab5380f0ebe3c261a2f57e0fbabc81" }
    orderBy: date
    orderDirection: asc
  ) {
    date
    jobsCreated
    jobsSettled
    microSettlements
    htsMicroSettlements
    erc20MicroSettlements
    microSettledVolume
    revenue
    netMargin
    marginRate
    cumulativeNetMargin
  }
}

Running it locally

The local stack is subgraph/docker-compose.yml: graph-node, IPFS (kubo) and Postgres 14. Deploy with the Graph CLI through the admin port.

shell
# 1. start graph-node + IPFS + Postgres (ports 8100, 8001, 8020, 8030, 8040, 5101, 5432)
docker compose -f subgraph/docker-compose.yml up -d

# 2. generate AssemblyScript bindings and compile the mappings
npm run codegen
npm run build:subgraph

# 3. register the name, then deploy through the admin port
npx graph create --node http://localhost:8020/ aetheris
npx graph deploy --node http://localhost:8020/ --ipfs http://localhost:5101 \
  aetheris subgraph/subgraph.yaml --output-dir subgraph/build

# 4. query
curl -s http://localhost:8100/subgraphs/name/aetheris \
  -H 'content-type: application/json' \
  -d '{"query":"{ settlements(where:{viaHts:true}) { amount subAgent { id } } }"}'

Production on a VPS

deploy/vps-subgraph.sh stands up the same three services on a server, but exposes only the GraphQL port publicly. The admin port 8020, the indexing-status port 8030 and IPFS 5001 are bound to 127.0.0.1, so nobody can redeploy over the subgraph from the internet; deployment happens through an SSH tunnel from your laptop.

shell
# on the VPS: brings up the stack with only :8000 public
bash deploy/vps-subgraph.sh

# from your laptop: tunnel the admin and IPFS ports, then deploy as if local
ssh -N -L 8020:127.0.0.1:8020 -L 5001:127.0.0.1:5001 user@VPS &
npx graph create --node http://localhost:8020/ aetheris
npx graph deploy --node http://localhost:8020/ --ipfs http://localhost:5001 \
  aetheris subgraph/subgraph.yaml --output-dir subgraph/build

# GraphQL afterwards
http://VPS:8000/subgraphs/name/aetheris
  1. The script waits for http://127.0.0.1:8030/ to answer, then reminds you to open port 8000 in the firewall (ufw allow 8000/tcp).
  2. With the tunnel up, localhost:8020 and localhost:5001 on your machine are the VPS ports, so the deploy command is the local one with 5001 in place of 5101.
  3. Point NEXT_PUBLIC_SUBGRAPH_URL at http://VPS:8000/subgraphs/name/aetheris and redeploy the app.

After a contract redeploy, npm run deploy:hedera (scripts/deploy.js) prints the new addresses and block numbers as a ready-to-paste subgraph.yaml block.

Honest limits

  • Self-hosted means single-indexer. There is no decentralised network of indexers attesting to this data, no curation, and no dispute mechanism - just a graph-node you (or we) operate. Anyone can verify it by running their own from the same manifest against the same relay.
  • The relay is the data source. graph-node reads blocks and logs from Hashio, Hedera's public relay. If you are rate-limited, swap in your own relay or mirror-node RPC in the compose file's ethereum setting.
  • Amounts are raw. The schema stores base units and leaves decimals to the reader, because HTS tokens do not expose decimals() uniformly through the EVM.
  • Panels say where their data came from. Anything in the app that reads the subgraph carries a LIVE or DEMO DATA pill; when the endpoint is unreachable the panel says so rather than presenting seed data as indexed history.