Proofs and the registry
Status. The engine signs every score today, as the EIP-712 proof below with the Merkle root of every fill behind it, and anyone can verify the signature and the fills. The three registry contracts go live on Elysium mainnet (chain id 99901) next. Until then proofs are signed but not posted, and the on-chain step of a check is skipped.
Contracts
Three small ownerless contracts in contracts/src. No admin, no owner, no upgrade path, no fees.
| Contract | What it does |
|---|---|
QuaestorIdentity |
ERC-8004 compatible identity registry. Each agent is an ERC-721 token (register, setAgentURI, setMetadata, getMetadata, tokenURI). Whoever registers owns the token. The hl.master metadata key holds the Hyperliquid master address |
QuaestorReputation |
ERC-8004 compatible reputation registry (giveFeedback, revokeFeedback, appendResponse, readFeedback, getSummary, getClients, getLastIndex), plus signed score proofs (postScore, scoreRecord, latestScore, verifyFill) |
QuaestorMandates |
Agent rules: setMandate(agentId, maxDrawdownBps, maxLeverageX100, markets), owner or approved operator only, versioned and logged |
The agent's owner cannot rate its own agent: feedback from an owner or approved operator reverts. Readers choose which attesters they trust by passing them to getSummary, as ERC-8004 intends.
The score proof
A proof is an EIP-712 message:
domain { name: "Quaestor", version: "1", chainId, verifyingContract: QuaestorReputation }
ScoreProof(
uint256 agentId, identity token id (0 for example accounts, which are never posted)
address subject, Hyperliquid master account
bytes32 formula, keccak256("q-score/1.0.0")
uint16 score, 0..1000
bytes32 metricsHash, keccak256 of the canonical metrics JSON
bytes32 fillsRoot, Merkle root of every fill behind the score
uint32 fillCount,
uint64 fromTime, first fill, ms
uint64 toTime, last fill, ms
uint64 issuedAt ms
)
The engine's attester key signs it. Anyone can relay it with postScore(proof, "q-score/1.0.0", signature); the contract recovers the signer and records the proof as ERC-8004 feedback from that signer (tag1 = "q-score", tag2 = formula version, feedbackHash = digest). A proof can be posted once.
The fills fingerprint
Each fill becomes a leaf:
leaf = keccak256(keccak256(abi.encode(
address account, uint64 tid, uint64 time, string coin, bool isBuy,
string px, string sz, string fee, string closedPnl, bytes32 hash)))
Prices and sizes stay exactly as Hyperliquid's decimal strings. Leaves are ordered by time, then trade id, then account, and folded with sorted-pair hashing (an odd node is carried up). verifyFill(digest, leaf, proof) on the registry checks any single fill against a posted root. The engine serves the Merkle path at GET /v1/agents/:id/fills/:tid/proof.
Change, drop or add one fill and the root no longer matches.
Verifying
The verify page does all of this in the browser: recovers the signer, compares it to the published attester, reads scoreRecord(digest) straight from the chain, and re-hashes every fill in the window to rebuild the root. The SDK's verifyProof and the MCP tool verify_score_proof do the same.
Gas (Foundry gas report, via-IR, 2,000 optimizer runs)
| Call | Gas |
|---|---|
register(uri, metadata) |
~190k |
postScore, first proof for an agent |
~349k |
postScore, later proofs |
lower (client already known) |
setMandate with two markets |
~124k |
giveFeedback |
~97k |
Runtime sizes: identity 5.4 KB, reputation 9.1 KB, mandates 2.4 KB. A full deploy of all three simulates at about 4.93M gas on Elysium testnet; at the testnet's 0.2 gwei max fee that is about 0.001 HYPE.
Deploying
npm run deploy:local # starts anvil if needed, deploys, writes contracts/deployments/anvil.json
npm run deploy:testnet # asks for the deployer key at the prompt, deploys to Elysium testnet
The Elysium mainnet deploy is the next step for these contracts; the engine picks the registry up from its deployment file as soon as it exists.
The engine reads contracts/deployments/<chain>.json for the chain set in QUAESTOR_CHAIN.