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
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_TESTNET_RPC | JSON-RPC relay used by Hardhat and the server. Defaults to https://testnet.hashio.io/api (chain id 296). |
|---|---|
PRIVATE_KEY | Funded Hedera testnet ECDSA key. It deploys the contracts, relays verifyOperator, signs faucet drips and submits HCS frames. Server-only. |
HEDERA_OPERATOR_ID | Hedera account id (0.0.x) of the key above. Required for HCS submits through the Hedera SDK. |
HEDERA_OPERATOR_KEY | Private 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_ID | Consensus Service topic the audit log is written to. The deployed app uses 0.0.10518320; its submit key is the operator. |
HEDERA_NETWORK | Hedera network name for the SDK client. testnet. |
HEDERA_MIRROR_NODE_URL | Mirror node the server reads HCS messages from. Default https://testnet.mirrornode.hedera.com. |
NEXT_PUBLIC_AETHERIS_AGENCY_ADDRESS | Deployed AetherisAgency. Public; shipped to the browser for the client view. Printed by npm run deploy:hedera. |
NEXT_PUBLIC_AETHERIS_TREASURY_ADDRESS | Deployed AetherisTreasury. Public. |
NEXT_PUBLIC_AETHERIS_OPERATOR_ADDRESS | Fallback 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_ADDRESS | EVM 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_NAME | ENS name the agency publishes as its identity (constructor argument). Default aetheris.eth. |
World ID
NEXT_PUBLIC_WORLD_ID_APP_ID | App 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_ACTION | Action identifier created under the app's Actions tab. Case-sensitive. The deployed app uses aetheris-operator. |
WORLD_ID_RP_ID | Relying-party id (rp_...) for IDKit v4. lib/worldid.ts verifies results at POST /api/v4/verify/{rp_id}. |
WORLD_ID_RP_SIGNING_KEY | Hex 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_BASE | Base URL of the World ID verifier. Default https://developer.worldcoin.org. |
WORLD_ID_ROUTER_ADDRESS | World 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_ID | Credential group id passed to the constructor. 1 is Orb. |
Privy, 1inch, The Graph, ENS
NEXT_PUBLIC_PRIVY_APP_ID | Privy 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_SECRET | Privy server secret. Server-only. |
ONEINCH_API_KEY | 1inch 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_URL | 1inch API base. Default https://api.1inch.dev. |
NEXT_PUBLIC_SUBGRAPH_URL | GraphQL 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_KEY | Reserved 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_URL | Ethereum 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
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
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
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.
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
| GraphQL | http://localhost:8100/subgraphs/name/aetheris |
|---|---|
| Admin (create / deploy) | http://localhost:8020 |
| Indexing status | http://localhost:8030 |
| IPFS API (host side) | http://localhost:5101 |
| Postgres | localhost: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:
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:
- Installs Docker through
get.docker.comif it is missing and checks the compose plugin. - Writes
/opt/aetheris-subgraph/docker-compose.yml: graph-node,ipfs/kubo:v0.29.0andpostgres:14, withethereum: hedera-testnet:$HEDERA_TESTNET_RPC(default Hashio) andGRAPH_ETHEREUM_GENESIS_BLOCK_NUMBER: 1. - Publishes only GraphQL (8000) on all interfaces. The admin port 8020, the status port 8030 and IPFS 5001 are bound to
127.0.0.1so nobody can redeploy over your subgraph from the internet. - 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:
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.
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_ADDRESSandWORLD_ID_GROUP_IDare deploy-time inputs toscripts/deploy.js; the frontend does not read them.GRAPH_DEPLOY_KEYis 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:8000endpoint 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.
