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
| Address | 0x16fA9CC838Ab5380F0Ebe3C261a2F57E0FBAbc81 |
|---|---|
| startBlock | 40,396,781 |
| Mapping | subgraph/src/agency.ts |
| Handlers (7) | handleAgencyDeployed, handleOperatorVerified, handleJobCreated, handleSubAgentAssigned, handleTaskCompleted, handleJobSettled, handleHcsLogAnchored |
AetherisTreasury
| Address | 0x10360383a6b43Fd22BE257bE334E9A9ad83B5598 |
|---|---|
| startBlock | 40,396,776 |
| Mapping | subgraph/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,
netMarginandmarginRate, 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;
seenViaHtsflips once an HTS settlement used it. - Job - a client-funded unit of work: client, token, deposit,
specURI,status,totalTaskFeesagainstdepositfor a live margin view, timestamps per status. - Task - one sub-agent assignment,
id = {jobId}-{taskId}: role, fee, status,resultHash,hcsTopicId,hcsSequenceNumber, and thesettlementthat paid it. - JobSettlement - immutable close-out record per
JobSettled: gross deposit, paid to sub-agents,netMargin,marginRate. - Settlement - one
MicroSettlement,id = {txHash}-{logIndex}: amount,viaHtsand the derivedrail(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.
{
settlements(where: { viaHts: true }, orderBy: timestamp, orderDirection: desc) {
id
amount
rail
viaHts
token { id }
subAgent { id }
job { jobId }
transactionHash
}
}Sub-agent leaderboard
{
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
{
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.
# 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.
# 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
- 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). - With the tunnel up,
localhost:8020andlocalhost:5001on your machine are the VPS ports, so the deploy command is the local one with5001in place of5101. - Point
NEXT_PUBLIC_SUBGRAPH_URLathttp://VPS:8000/subgraphs/name/aetherisand 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
ethereumsetting. - 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.
