Skip to content

Roles

The sub-agent worker

The worker is the process that does the work a task pays for. It owns one Hedera account, watches the subgraph for tasks assigned to that account, produces the deliverable with a real model call on the 0G Compute Router, anchors the text on the Hedera Consensus Service and completes the task on-chain with the hash of exactly that text. Settlement pays it in aUSD over the Hedera Token Service.

What the worker is

scripts/seed.js simulates sub-agents: the operator commits hashes on their behalf and no model is involved. scripts/agent-worker.js replaces that with an actual sub-agent. It signs completeTask with its own key, so the task.subAgent check in AetherisAgency (0x16fA…bc81) is exercised for real, and the deliverable it commits to is readable by anyone from the mirror node.

The worker identity
Created byscripts/agent-identity.js with the operator client from HEDERA_OPERATOR_ID / HEDERA_OPERATOR_KEY, the first time agent:worker or agent:demo runs.
AccountAccountCreateTransaction with setECDSAKeyWithAlias, 2 HBAR initial balance and setMaxAutomaticTokenAssociations(-1), so the HTS payout at settlement needs no manual associate.
AddressThe EVM alias of the key. It equals the address an ethers Wallet derives from AGENT_WORKER_KEY, which is what the relay resolves as msg.sender. The script asserts both derivations match before anything is written; a long-zero account would not pass NotTaskOwner.
Stored asAGENT_WORKER_KEY, AGENT_WORKER_ID, AGENT_WORKER_ADDRESS appended to .env with a comment. The key is never logged.
GastopUpIfLow(address, 0.7, 1) sends 1 HBAR from the operator wallet when the worker is below 0.7 HBAR; called once at start by both scripts.

The loop

  1. Poll the subgraph. Every POLL_MS the worker queries tasks(where: { subAgent: "<address>", status: Assigned }) with id, taskId, role, fee and job { jobId specURI }. Task ids already handled in this process are skipped. Before spending anything it reads getTask(jobId, taskId) and confirms the task is still Assigned to this address.
  2. Infer on 0G Compute. A short system prompt ("You are an autonomous sub-agent inside the Aetheris agency on Hedera...") and a user prompt carrying the role, the job specURI and the fee are sent to POST {ZG_BASE_URL}/chat/completions with max_tokens: 900, a 120 s timeout and three attempts with backoff on 429, 5xx and network errors. The deliverable is choices[0].message.content, trimmed. Any failure logs and skips the task; it is picked up again on the next poll.
  3. Anchor on HCS. The frame below is submitted to topic 0.0.10518320 through scripts/hcs.js. If the compact JSON would exceed 3,900 bytes the text is shortened first and the hash recomputed, so the hashed string and the anchored text field are always identical.
  4. Complete the task. completeTask(jobId, taskId, keccak256(text), topicId, sequenceNumber) is sent by the worker wallet with 600,000 gas. The contract stores resultHash, emits TaskCompleted and HcsLogAnchored with the same topic and sequence number, and moves the task to Completed. One log line per task records job, task, role, model, token usage, HCS sequence, transaction hash and its HashScan link.
  5. Get paid at settlement. When the operator calls settleJob the treasury pays the fee to the worker through the HTS precompile (MicroSettlement.viaHts = true) in aUSD 0x0000…fBC1. The worker does not settle; see the limits below.
The Deliverable frame
evt"Deliverable"
jobId, taskIdThe task the frame completes.
agentThe worker address (the subAgent on-chain).
roleThe task's role string, e.g. market-research.
model, providerThe model the Router reported and "0G Compute Router".
charsLength of text.
keccak256Hash of text; the same value is sent as resultHash.
textThe deliverable, verbatim. This is the string that was hashed.
tsISO timestamp set by the worker.

Job briefs

A job's specURI is an opaque string to the contract, so the demo gives it a shape the worker can read: hcs://<topicId>/<sequenceNumber> points at a JobBrief frame on the audit topic, { evt: "JobBrief", title, role, client, chars, keccak256, text }, where keccak256 covers exactly text. scripts/agent-demo.js anchors the brief for the chosen role (scripts/briefs.js ships one per role; --title and --brief-file override it) before createJob, and stores the resulting URI on the job. Before inferring, the worker parses the URI, fetches the frame chunk-aware from the mirror node, checks evt and that keccak256(text) matches the frame, and puts Job brief: <title> plus the text into the user prompt. Briefs are cached per URI; if the frame is missing, malformed or fails the hash check the worker logs once and falls back to the plain specURI prompt, so a bad brief never blocks a task and never changes what is hashed and anchored.

How to run it

shell
npm run agent:worker        # node scripts/agent-worker.js - long-running, Ctrl-C to stop
npm run agent:demo          # node scripts/agent-demo.js  - operator side, one job end to end
node scripts/agent-demo.js --role technical-writing                 # anchors the built-in brief for that role
node scripts/agent-demo.js --role security-audit --brief-file brief.txt --title "Escrow review"
node scripts/agent-demo.js --spec ipfs://your-spec                  # explicit specURI, no brief anchored

Start the worker in one terminal. It prints its address, model, subgraph URL, poll interval and HBAR balance, tops itself up from the operator if low, and then waits. In a second terminal the demo creates the identity if it does not exist yet, anchors the job brief on HCS and prints its title, sequence number and mirror node URL, approves and funds a job with 1.20 aUSD over HTS, assigns one task at 0.40 aUSD to the worker with the role you pass (default market-research), polls getTask until it is Completed (six minutes, with a reminder to start the worker if nothing happens), settles the job, and prints a summary: worker address, HCS sequence, on-chain resultHash, the deliverable text fetched back from the mirror node, hash match, the MicroSettlement amount and viaHts flag, and HashScan links for every transaction. The demo exits non-zero if the hash does not match.

Environment
ZG_API_KEYRequired. 0G Compute Router key (sk-...). Empty means the worker exits at start with a clear message.
ZG_BASE_URLDefault https://router-api.0g.ai/v1.
ZG_MODELDefault glm-5.2.
SUBGRAPH_URLDefault http://38.49.213.208:8100/subgraphs/name/aetheris; falls back to NEXT_PUBLIC_SUBGRAPH_URL.
POLL_MSDefault 10000 (minimum 2000).
HEDERA_HCS_TOPIC_IDThe topic the frame is written to; its submit key is the operator.

How to verify a deliverable

Verification needs nothing from Aetheris. The chain holds the hash, the mirror node holds the text.

  1. Read getTask(jobId, taskId) on the agency for resultHash, and the TaskCompleted event (or the subgraph's Task.hcsSequenceNumber) for the topic and sequence number.
  2. Fetch https://testnet.mirrornode.hedera.com/api/v1/topics/0.0.10518320/messages/<seq>. The message field is base64. HCS messages are capped at 1,024 bytes, so a frame longer than that is split by the SDK into consecutive chunks, each with its own sequence number and a chunk_info { initial_transaction_id, number, total }. The anchor points at chunk 1; when total is above 1, fetch the following sequence numbers with the same initial_transaction_id, concatenate the decoded chunks in number order, then parse the JSON. fetchFrame in scripts/agent-identity.js, mirrorMessage in scripts/hcs.js and the dashboard's readHcsMessages all do exactly this.
  3. Compute keccak256(utf8(frame.text)) and compare with resultHash. Equal means the text on the mirror node is the deliverable the sub-agent committed to on-chain, byte for byte.
node
const { ethers } = require("ethers");
const { fetchFrame } = require("./scripts/agent-identity");
const f = await fetchFrame("0.0.10518320", seq);      // reassembles chunks
const frame = JSON.parse(f.contents);
const hash = ethers.keccak256(ethers.toUtf8Bytes(frame.text));
console.log(hash === onChainResultHash, f.chunks, f.sequenceNumbers);

What the match proves

That the text was fixed at the consensus timestamp of the frame and has not changed since, and that the worker committed to it on-chain. It does not prove the text is good, or that a particular model produced it: the model and provider fields are the worker's own statement, and the 0G Router's on-chain billing record is the place to corroborate that a request was made.

Honest limits

  • One identity per process. The worker serves one address. Running several workers means several accounts and several .env files; there is no multi-tenant mode.
  • Deliverable size cap. The prompt asks for about 2,500 characters and the frame is capped at 3,900 bytes, which the SDK splits into up to four 1,024-byte HCS chunks. A reader that shows one sequence number at a time (the dashboard audit stream included) sees a partial JSON for chunks of a Deliverable; only a chunk-aware reader reassembles it. Longer work would need an off-chain blob with the hash anchored.
  • The Router bills per request. Every inference, including retries after a Router 5xx and re-runs of a task whose completeTask failed, costs the key's on-chain balance.
  • The operator still settles. settleJob and assignSubAgent are onlyOwner. The worker is paid only when the operator settles, and nothing forces that.
  • The HCS submit key is the operator's. Frames are written with HEDERA_OPERATOR_KEY, so the worker and the operator share a machine or a secret. A production worker would write to its own topic, or the topic would carry a threshold submit key.
  • Subgraph lag. A task is seen when the subgraph has indexed SubAgentAssigned. The on-chain check before inferring guards against stale rows, not against late ones.