Skip to content

Operate

Self-hosting & deployment

Everything in Aetheris runs from one repository: Hardhat for the contracts, a self-hosted graph-node for the index, and a Next.js 14 app for the UI and HTTP API. This page walks the whole path from a fresh clone to a public deployment, and ends with the failures people actually hit.

Prerequisites

  • Node.js 18+ and npm.
  • Docker with the compose plugin, for the graph-node stack.
  • A funded Hedera testnet ECDSA account from portal.hedera.com/faucet. The JSON-RPC relay signs EVM transactions, so an ED25519 key cannot deploy or call the contracts.
  • Optional accounts for the integrations you want live: a World ID app with an action and a relying party, a Privy app, a 1inch developer key.

Clone and install

shell
git clone https://github.com/mrnetwork0001/Aetheris.git
cd Aetheris
npm install
cp .env.example .env      # then fill in PRIVATE_KEY at minimum

The repository is Apache-2.0. .env.example already carries the public defaults (relay, mirror node, World ID API base, ENS RPC), so the minimum to compile, test and deploy is a funded PRIVATE_KEY.

Environment variables

Every variable in .env.example, grouped the way the file groups them. Anything prefixed NEXT_PUBLIC_ is bundled into the browser; everything else stays on the server.

Hedera EVM, HTS and HCS

Hedera environment variables
HEDERA_TESTNET_RPCJSON-RPC relay used by Hardhat and the server. Defaults to https://testnet.hashio.io/api (chain id 296).
PRIVATE_KEYFunded Hedera testnet ECDSA key. It deploys the contracts, relays verifyOperator, signs faucet drips and submits HCS frames. Server-only.
HEDERA_OPERATOR_IDHedera account id (0.0.x) of the key above. Required for HCS submits through the Hedera SDK.
HEDERA_OPERATOR_KEYPrivate key used by the Hedera SDK client for HCS. lib/hedera.ts accepts DER, raw ECDSA hex or ED25519 hex and tries ECDSA first for raw hex.
HEDERA_HCS_TOPIC_IDConsensus Service topic the audit log is written to. The deployed app uses 0.0.10518320; its submit key is the operator.
HEDERA_NETWORKHedera network name for the SDK client. testnet.
HEDERA_MIRROR_NODE_URLMirror node the server reads HCS messages from. Default https://testnet.mirrornode.hedera.com.
NEXT_PUBLIC_AETHERIS_AGENCY_ADDRESSDeployed AetherisAgency. Public; shipped to the browser for the client view. Printed by npm run deploy:hedera.
NEXT_PUBLIC_AETHERIS_TREASURY_ADDRESSDeployed AetherisTreasury. Public.
NEXT_PUBLIC_AETHERIS_OPERATOR_ADDRESSFallback operator address shown only when the subgraph has not yet indexed the agency's Operator entity. Unset renders the zero address with a caveat; the live path never shows a fixture.
AETHERIS_HTS_TOKEN_ADDRESSEVM long-zero address of an HTS token (0.0.x mapped to 0x...). If set, scripts/deploy.js associates the treasury with it after deployment. The treasury and every sub-agent must be associated before that token can be escrowed or paid out.
AETHERIS_ENS_NAMEENS name the agency publishes as its identity (constructor argument). Default aetheris.eth.

World ID

World ID environment variables
NEXT_PUBLIC_WORLD_ID_APP_IDApp id from the World ID Developer Portal (app_...; app_staging_... apps accept Simulator proofs). Public: IDKit needs it in the browser. Unset, /api/operator/verify answers 503 WORLD_ID_NOT_CONFIGURED and the UI shows a labelled simulated check.
NEXT_PUBLIC_WORLD_ID_ACTIONAction identifier created under the app's Actions tab. Case-sensitive. The deployed app uses aetheris-operator.
WORLD_ID_RP_IDRelying-party id (rp_...) for IDKit v4. lib/worldid.ts verifies results at POST /api/v4/verify/{rp_id}.
WORLD_ID_RP_SIGNING_KEYHex signing key of the relying party. app/api/worldid/rp-context signs a nonce with it before the widget opens. Server-only secret - never prefix it NEXT_PUBLIC_.
WORLD_ID_API_BASEBase URL of the World ID verifier. Default https://developer.worldcoin.org.
WORLD_ID_ROUTER_ADDRESSWorld ID router passed to the agency constructor. Leave unset to deploy in explicit bypass mode (the contract emits WorldIdBypassActive; replay protection stays on). No router exists on Hedera today.
WORLD_ID_GROUP_IDCredential group id passed to the constructor. 1 is Orb.

Privy, 1inch, The Graph, ENS

Integration environment variables
NEXT_PUBLIC_PRIVY_APP_IDPrivy app id. Required for login and embedded wallets (lib/privy.ts). The app's URL must be in the Privy app's allowed origins.
PRIVY_APP_SECRETPrivy server secret. Server-only.
ONEINCH_API_KEY1inch Swap API v6.0 key. Used only by the proxy routes /api/swap/quote and /api/swap/build; it never reaches the browser. Without it the routes return a typed 502.
NEXT_PUBLIC_ONEINCH_BASE_URL1inch API base. Default https://api.1inch.dev.
NEXT_PUBLIC_SUBGRAPH_URLGraphQL endpoint of your graph-node, for example http://localhost:8100/subgraphs/name/aetheris locally or http://VPS:8000/subgraphs/name/aetheris in production. Panels that depend on it show DEMO DATA when it is unset.
GRAPH_DEPLOY_KEYReserved for The Graph's hosted deploy flow. Not used by the self-hosted path, which deploys over the admin port instead.
NEXT_PUBLIC_ENS_RPC_URLEthereum mainnet RPC for ENS resolution. lib/ens.ts falls back to publicnode / drpc / ankr / cloudflare when this is unset or failing. Default https://ethereum-rpc.publicnode.com.

Compile and test the contracts

shell
npm run compile           # hardhat compile (Solidity 0.8.24)
npm run test:contracts    # hardhat test - 37 tests in test/aetheris.test.js

The tests cover both settlement rails. hardhat.config.js defines a single network, hederaTestnet, pointed at HEDERA_TESTNET_RPC with chain id 296 and PRIVATE_KEY as its only account.

Deploy to Hedera testnet

shell
npm run deploy:hedera     # hardhat run scripts/deploy.js --network hederaTestnet

scripts/deploy.js deploys AetherisTreasury then AetherisAgency, wires the agency into the treasury, associates the treasury with AETHERIS_HTS_TOKEN_ADDRESS when it is set, and prints paste-ready blocks for .env and subgraph/subgraph.yaml (addresses and start blocks). With WORLD_ID_ROUTER_ADDRESS unset the agency is constructed with a zero router and emits WorldIdBypassActive in the deployment transaction.

To skip deployment and use the existing contracts, the reference deployment is 0x16fA9CC838Ab5380F0Ebe3C261a2F57E0FBAbc81 (agency, block 40396781) and 0x10360383a6b43Fd22BE257bE334E9A9ad83B5598 (treasury, block 40396776).

Seed a job lifecycle

shell
npx hardhat run scripts/seed.js --network hederaTestnet

scripts/seed.js reuses the pre-created aUSD token (0.0.10484673) and the three Hedera-native sub-agents listed in its KNOWN_SUB_AGENTS roster (see Settlement rails), checks the treasury's token association through ensureTreasuryAssociated, then runs jobs through every status. Each step is anchored to the HCS topic before the contract call, so the sequence numbers written into completeTask match what the mirror node returns. It needs HEDERA_OPERATOR_ID, HEDERA_OPERATOR_KEY and HEDERA_HCS_TOPIC_ID in addition to the deployer key. Without a seed the subgraph indexes an empty agency and the dashboard has nothing to show.

Subgraph: local graph-node

Hedera is not in The Graph's hosted network registry, so the index runs on a graph-node you operate, pointed at the Hedera JSON-RPC relay. subgraph/docker-compose.yml brings up graph-node, IPFS (kubo) and Postgres 14.

shell
docker compose -f subgraph/docker-compose.yml up -d
npm run codegen            # graph codegen subgraph/subgraph.yaml
npm run build:subgraph     # graph build subgraph/subgraph.yaml
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
Local graph-node ports
GraphQLhttp://localhost:8100/subgraphs/name/aetheris
Admin (create / deploy)http://localhost:8020
Indexing statushttp://localhost:8030
IPFS API (host side)http://localhost:5101
Postgreslocalhost:5432

Two values must agree: the label before the colon in the compose file's ethereum: "hedera-testnet:..." and network: in subgraph/subgraph.yaml. Both say hedera-testnet. The compose file also sets GRAPH_ETHEREUM_GENESIS_BLOCK_NUMBER: 1 because the relay has no block 0 and graph-node's chain-head probe fails on boot without it. Set NEXT_PUBLIC_SUBGRAPH_URL to the GraphQL URL once the deploy finishes.

Subgraph: production on a VPS

deploy/vps-subgraph.sh is a one-shot installer for a Linux host. Run it as root:

shell (on the VPS)
curl -fsSL https://raw.githubusercontent.com/mrnetwork0001/Aetheris/main/deploy/vps-subgraph.sh | sudo bash
ufw allow 8000/tcp

What the script does, in order:

  1. Installs Docker through get.docker.com if it is missing and checks the compose plugin.
  2. Writes /opt/aetheris-subgraph/docker-compose.yml: graph-node, ipfs/kubo:v0.29.0 and postgres:14, with ethereum: hedera-testnet:$HEDERA_TESTNET_RPC (default Hashio) and GRAPH_ETHEREUM_GENESIS_BLOCK_NUMBER: 1.
  3. Publishes only GraphQL (8000) on all interfaces. The admin port 8020, the status port 8030 and IPFS 5001 are bound to 127.0.0.1 so nobody can redeploy over your subgraph from the internet.
  4. Starts the stack and waits up to three minutes for the status port to answer.

Because the admin and IPFS ports are loopback-only, you deploy from your laptop through an SSH tunnel:

shell (on your laptop)
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 \
  --version-label v0.0.1 aetheris subgraph/subgraph.yaml --output-dir subgraph/build

GraphQL is then public at http://VPS:8000/subgraphs/name/aetheris; put that into NEXT_PUBLIC_SUBGRAPH_URL on the frontend. Note the tunnel maps IPFS to local port 5001, not 5101 as in the local compose file - stop any local stack first or pick another local port and pass it to --ipfs.

Frontend on Vercel

The app is a standard Next.js 14 project with no custom build step: import the repository, keep the default next build, and set the environment below. The API routes run as serverless functions, so every server-only secret must be present in the project's environment, not just locally.

env (Vercel project settings)
HEDERA_TESTNET_RPC
PRIVATE_KEY
HEDERA_OPERATOR_ID
HEDERA_OPERATOR_KEY
HEDERA_HCS_TOPIC_ID
HEDERA_NETWORK
HEDERA_MIRROR_NODE_URL
NEXT_PUBLIC_AETHERIS_AGENCY_ADDRESS
NEXT_PUBLIC_AETHERIS_TREASURY_ADDRESS
NEXT_PUBLIC_AETHERIS_OPERATOR_ADDRESS
AETHERIS_HTS_TOKEN_ADDRESS
AETHERIS_ENS_NAME
NEXT_PUBLIC_WORLD_ID_APP_ID
NEXT_PUBLIC_WORLD_ID_ACTION
WORLD_ID_RP_ID
WORLD_ID_RP_SIGNING_KEY
WORLD_ID_API_BASE
NEXT_PUBLIC_PRIVY_APP_ID
PRIVY_APP_SECRET
ONEINCH_API_KEY
NEXT_PUBLIC_ONEINCH_BASE_URL
NEXT_PUBLIC_SUBGRAPH_URL
NEXT_PUBLIC_ENS_RPC_URL
  • WORLD_ID_ROUTER_ADDRESS and WORLD_ID_GROUP_ID are deploy-time inputs to scripts/deploy.js; the frontend does not read them. GRAPH_DEPLOY_KEY is unused on the self-hosted path.
  • Add the Vercel URL (and any preview URLs you use) to the Privy app's allowed origins, and to the World ID app if the portal asks for one.
  • The faucet (POST /api/faucet) and the World ID relay (POST /api/operator/verify) keep their rate limits in process memory (app/api/faucet/route.ts: 3 drips per address per hour, 30 per hour overall). On serverless, each warm instance counts separately.
  • The subgraph URL must be reachable from the browser and from the functions. A plain http://VPS:8000 endpoint will be blocked as mixed content on an https page - put it behind TLS (a reverse proxy on the VPS) before pointing a production frontend at it.

Troubleshooting

Ports 5001 and 8000 are already taken

On a development machine 5001 (macOS AirPlay, a local uvicorn or Flask server) and 8000 (local API servers, ssh -L tunnels) are frequently occupied. The local compose file therefore publishes IPFS on 5101 and GraphQL on 8100. The failures when you get this wrong are silent and misleading: graph deploy reports Failed to upload to IPFS: Not Found, and GraphQL queries return another server's 404 body. Always pass --ipfs http://localhost:5101 locally and query 8100.

Hashio rejects eth_getLogs in batches

The public relay refuses eth_getLogs inside a JSON-RPC batch. ethers v6 batches by default, so the server providers are created with batchMaxCount: 1 (lib/treasury.ts, lib/operator-registry.ts). If you add a new provider and see log queries fail only in the app but not in Hardhat, this is why. graph-node issues its own single requests and is unaffected; if it is rate-limited, swap in your own relay through HEDERA_TESTNET_RPC.

ECDSA versus ED25519 keys

Hedera accounts can be ED25519 or ECDSA, but only ECDSA keys have an EVM address and can sign JSON-RPC transactions. Deploy, seed, the faucet and the relay all need an ECDSA PRIVATE_KEY. For the HCS client, lib/hedera.ts parses raw hex as ECDSA first: the SDK's fromStringDer silently accepts a raw 32-byte hex string and returns an ED25519 key for it, which signs as the wrong account and fails with INVALID_SIGNATURE. If your HCS submits fail that way, check the key type before anything else.

Privy: login opens and immediately fails

Privy checks the page origin against the allowed origins configured in its dashboard. Add every origin you serve the app from - http://localhost:3000, the Vercel production URL and any preview URL - or the modal rejects the login. Also confirm NEXT_PUBLIC_PRIVY_APP_ID is set at build time; it is a public variable and is baked into the bundle.

World ID: "Action not found" on the v2 verifier

Actions created in the current Developer Portal are World ID 4.0 actions scoped to the app's relying party. The legacy /api/v2/verify/{app_id} endpoint answers Action not found for them. Aetheris verifies IDKit v4 results at /api/v4/verify/{rp_id} instead (lib/worldid.ts, verifyWorldIdV4), which needs WORLD_ID_RP_ID and a server-signed request context from POST /api/worldid/rp-context (WORLD_ID_RP_SIGNING_KEY). If you see the v2 error, you are missing the relying-party values, not the action.