Roles
The operator
The operator is the human who runs the agency: the address that deployed both contracts, dispatches work, settles jobs and is the only party who can take margin out of the treasury. On this deployment that is one key, and every step below is what that key actually does.
Who the operator is
scripts/deploy.js passes the deployer as initialOwner of contracts/AetherisAgency.sol and as owner of contracts/AetherisTreasury.sol, so "operator" and "deployer" are the same address. The app learns it from the Operator entity the subgraph indexes from AgencyDeployed, falling back to NEXT_PUBLIC_AETHERIS_OPERATOR_ADDRESS with a caveat when the subgraph has not answered (components/aetheris-server.ts).
| Hedera account | 0.0.10484502 |
|---|---|
| EVM address | 0x69677C85945796066B449c00F90A0582896F1F9b |
| Owns | AetherisAgency (0x16fA…bc81) and AetherisTreasury (0x1036…5598), both OpenZeppelin Ownable. |
| Signs with | PRIVATE_KEY for every EVM call (deploy, relay, faucet) and HEDERA_OPERATOR_KEY for HCS submits. The topic's submit key is this account. |
| Only the operator may | assignSubAgent, cancelTask, settleJob, setWorldId, claimProfit. completeTask is open to the sub-agent or the operator; refundJob to the client or the operator. |
What Mission Control shows
/dashboard is rendered per request by app/(app)/dashboard/page.tsx. The workspace is chosen by components/app/role-context.tsx: if the connected wallet equals the agency's operator the Operator view is shown; if it has funded at least one indexed job the Client view is shown; the Operator | Client toggle in the sidebar overrides either and remembers the choice in localStorage under aetheris.role. Signed out, the Operator view is the default.
The Operator view is, top to bottom:
- Header. The agency's ENS name or short address, the operator address, a "Hedera testnet - 296" pill, the LIVE / DEMO DATA pill and the World ID provenance badge (below).
- Stat row (
components/app/stat-row.tsx): treasury margin retained after payouts, jobs in flight with lifetime and settled counts, total paid to sub-agents with the roster size, and the HCS anchor count with average consensus finality. - Job pipeline (
#jobs): every indexed job with its tasks, filterable by all / in flight / settled, linking to the agency contract on HashScan. - Sub-agent leaderboard (
#agents): earnings and latency indexed fromMicroSettlementevents by the subgraph. - Treasury (
#treasury,#swap): holdings read straight fromAetherisTreasuryover the relay plus the HBAR balance, and the 1inch quote form described in Integrations. - Human operator required (
#operator): the World ID gate and the margin sweep button, both owned bycomponents/treasury-panel.tsx. - HCS audit stream (
#audit): the latest frames from topic 0.0.10518320 with corrections applied.
The World ID badge never overstates
components/operator-verification-badge.tsx combines two on-chain reads from lib/operator-registry.ts - isVerifiedOperator(operator) and worldIdVerificationBypassed() - and shows exactly one of: "World ID verified" (router present, proof checked on-chain), "Verified - bypass mode - no ZK proof on-chain", "Verified - bypass mode - seed nullifier" (the registration came from scripts/seed.js, not from a relayed proof), "Operator unverified" or "World ID status unknown". On Hedera testnet today the honest answer is one of the two bypass labels.
The World ID gate
Sweeping margin is gated on proof of personhood. The browser half lives in components/worldid-gate.tsx, the server half in app/api/worldid/rp-context/route.ts, lib/worldid.ts and app/api/operator/verify/route.ts. The flow is IDKit v4 end to end:
- Request context. "Verify with World ID" POSTs
{action}to/api/worldid/rp-context. The route signs a nonce withWORLD_ID_RP_SIGNING_KEY(signRequestfrom@worldcoin/idkit-core, 300 s TTL) and returns{rp_id, nonce, created_at, expires_at, signature}. The key never reaches the browser. WithoutWORLD_ID_RP_IDand the key the route answers 503. - Prove.
IDKitRequestWidgetopens with the app id, the actionaetheris-operator, thatrp_context, theproofOfHumanpreset committed to the operator address as signal, andallow_legacy_proofs=false. The user scans with World App. - Relay. The raw IDKit 4.0 result is POSTed as
{result, signal, ensName?}to/api/operator/verify. The route rate-limits per IP (5 per minute), validates the shape and extracts the RP-scoped nullifier fromresponses[0].nullifier. - Verify server-side.
verifyWorldIdV4forwards the result unchanged toPOST https://developer.worldcoin.org/api/v4/verify/{rp_id}(base overridable withWORLD_ID_API_BASE). A 4xx becomes401 PROOF_REJECTED; nothing is sent on-chain. - Check the nullifier.
nullifierHashUsed(nullifier)is read from the agency over the relay (ethers withbatchMaxCount: 1, because Hashio rejects batched calls). If it is already burned the route answers409 NULLIFIER_ALREADY_USED. - Burn it on-chain. The deployer key sends
verifyOperator(signal, 0, nullifier, [0 x 8], ensName ?? "aetheris.eth"). The contract revertsZeroAddress/InvalidNullifier/NullifierAlreadyUsedfirst, then setsnullifierHashUsedbefore any external call, then marksisVerifiedOperator[signal], stores the nullifier and ENS name, and emitsOperatorVerified. - Show the receipt. The gate renders the HashScan link for the
verifyOperatortransaction, the nullifier, and the on-chain note. A second proof from the same human is shown as "Nullifier already burned on-chain" from the 409.
200 | Relayed. Body carries txHash, hashscan, nullifierHash, ensName, blockNumber and worldIdBypassed. |
|---|---|
400 | Malformed body: no result or legacy proof, a zero nullifier, a signal that is not an EVM address, an invalid ensName, or an unexpected field. |
401 PROOF_REJECTED | World ID's verifier rejected the proof. Nothing was sent on-chain. |
409 NULLIFIER_ALREADY_USED | The nullifier is already burned in the agency - checked with nullifierHashUsed before sending, and again if the transaction reverts with NullifierAlreadyUsed. |
429 RATE_LIMITED | More than 5 relays per minute from one IP. The relay spends the deployer's HBAR. |
502 | The verifier or the Hedera relay was unreachable, or the transaction reverted for another reason. |
503 WORLD_ID_NOT_CONFIGURED | NEXT_PUBLIC_WORLD_ID_APP_ID is unset. The route never pretends success. |
503 RELAYER_NOT_CONFIGURED | PRIVATE_KEY or NEXT_PUBLIC_AETHERIS_AGENCY_ADDRESS is missing or malformed. |
Announced bypass mode
There is no World ID router contract on Hedera, so the agency was deployed with WORLD_ID_ROUTER_ADDRESS unset. The constructor emits WorldIdBypassActive(agency, "WORLD_ID_ROUTER_UNSET: zero-knowledge proofs are NOT verified on this deployment"), and inside verifyOperator the branch that would call router.verifyProof instead emits OperatorVerifiedWithoutProof(signal, nullifierHash). Only the ZK check is skipped: the nullifier is burned exactly as it would be with a router, so replay protection holds. The view worldIdVerificationBypassed() reports the mode and the relay echoes it as worldIdBypassed: true. setWorldId(router, groupId) lets the owner attach a router later; clearing it emits WorldIdBypassActive again.
Claiming margin
AetherisTreasury.claimProfit(token, amount, to) is onlyOwner and additionally requires IAetherisOperatorRegistry(agency).isVerifiedOperator(msg.sender), otherwise it reverts OperatorNotVerified. It checks retainedMargin[token] (InsufficientMargin if short), debits it and totalObligations, pays through the same HTS-or-ERC-20 _payout used for sub-agents, and emits ProfitClaimed(operator, token, amount, nullifierHash) with the nullifier that verified the operator, so the subgraph can tie the payout to the proof. Escrow is untouchable by this path; only closed-job margin is claimable.
The one real claim
scripts/claim-margin.js performed a single claim of 0.5 aUSD (500000 base units of 0x0000…fBC1) to the operator in transaction 0x8304…0c50 at block 40453585. The script then anchored frame #26 to the topic after the receipt - for a claim the chain is the source of truth, so the frame quotes the transaction hash it describes - and appended Correction #27 voiding frame #25, a test frame that had no matching on-chain event. Readers (lib/hedera.ts) hide #25 and keep the correction visible. The frame links open the raw mirror-node record for each sequence number; HashScan lists the same frames under the topic's Messages tab.
Scripts the operator runs
npm run compile # hardhat compile npm run test:contracts # 37 tests in test/aetheris.test.js npm run deploy:hedera # scripts/deploy.js - prints .env + subgraph.yaml blocks npx hardhat run scripts/seed.js --network hederaTestnet npx hardhat run scripts/claim-margin.js --network hederaTestnet
scripts/seed.jsdrives a full lifecycle against the deployed contracts: Job A settled in aUSD over HTS with three tasks to the three Hedera-native sub-agents, Job B settled in aUSDC over ERC-20 with two tasks, and Job C left Dispatched with one task completed and one outstanding. ForJobCreated,SubAgentAssignedandTaskCompletedthe HCS frame is written first and its sequence number passed intocompleteTask; settlement frames are written from the receipt's events. If the HTS stage fails the ERC-20 stages still run.scripts/claim-margin.jsaudits everyProfitClaimedframe against the treasury's events since block 40396776, claimsmin(0.5 aUSD, retainedMargin(aUSD))with a 1,000,000 gas limit, anchors the truthful frame, and appends aCorrectionfor any frame with no on-chain event.scripts/hcs.jsis the shared helper both use to submit frames withHEDERA_OPERATOR_ID/HEDERA_OPERATOR_KEY; the app'sPOST /api/hcsdoes the same from the server.
