Assay

Reputation and payment rail for agent-to-agent services. ENS + Hedera, claims checked on The Graph.

Assay

Created At

ETHGlobal Lisbon 2026

Project Description

As agents start paying other agents for services, three things are missing: a way to discover who offers what, a way to decide whether a stranger is competent and honest before paying them, and a cheap micro-payment rail. Star ratings do not solve the trust problem, because they are opinions. I wanted reputation that reflects whether a result actually held up, not whether someone liked it.

Assay is a generic rail over capabilities that can prove themselves. A provider agent publishes what it does (capability id, price, endpoint) and its track record as ENS text records, and posts a bond on-chain. A requester agent resolves the ENS name, reads the manifest and the reputation, and decides for itself whether to pay. Nothing forces the decision either way. Payment is per call, on Hedera testnet.

The result carries factual, block-stamped claims: a token's liquidity, its age in blocks, its transaction count, all pinned to a specific block number. Anyone can challenge a claim, and a verifier re-derives it from The Graph at that exact block. If the provider lied, its bond is slashed and its ENS reputation is written down. Reputation here is not a star rating, it is the outcome of a check, and bad answers get punished in a way anyone can audit afterward.

The loop, across three independent networks with no bridge between them:

discover (ENS) -> read reputation (ENS) -> pay (Hedera) -> serve (The Graph) -> challenge -> verifier re-derives from The Graph -> slash + reputation update (ENS)

The demo instantiates one concrete capability, rug-pull risk scoring for an ERC-20 token, because it has the right shape: expensive to compute well, cheap to spot-check. The product itself is an MCP server, so the requester side is a real Claude Code session driving real tools rather than an app I wrote to narrate itself. The demo prompt sets a goal and a budget and never names the provider to distrust, the claim to check, or when to challenge, so the decision belongs to the agent.

What is real: every Hedera transaction (pay, bond, slash), every ENS read and write on Sepolia, every Graph query against live mainnet data, and the verifier's comparison logic. All of it has run end to end in both directions, an honest provider that keeps its bond and a provider that lies and gets slashed. The lying provider is a deliberately tampered test harness that runs the real capability and alters exactly one claim before returning it, and it says so on screen when it runs. The gaps are listed in the repo rather than hidden: there is no challenger deposit yet, settlement is not atomic across the three networks, and the payment gate confirms that the transaction settled without yet comparing the amount and memo in code.

How it's Made

pnpm monorepo, TypeScript ESM, run straight from source under tsx with no emit step. The shared contracts live in packages/core: Claim, Capability, Manifest, Reputation, Job, plus three ports (RegistryPort, PaymentsPort, GraphPort). Each network is an adapter behind one of those ports, so core orchestrates the loop and knows nothing about rug-score, and the one concrete capability knows nothing about how it gets paid.

ENS (Sepolia), via ethers. Each provider gets a subname under a parent I own, carrying two text records: assay:manifest (capability id, price, endpoint, bond reference, and a verifierHash committing to the verifier's source) and assay:rep (score, jobs, slashes, bond). Subnames resolve and accept writes through a wildcard resolver, so rugscore.assay.eth has no entry of its own in the ENS registry (its registry owner reads as the zero address) yet its text records read and write correctly. That is one transaction to set up the parent name and zero transactions per provider onboarded, which is the property that makes per-agent identity practical at all. One gotcha: the .eth BaseRegistrar reports the name expired while resolution works fine, so do not gate on it.

Hedera (testnet), via @hashgraph/sdk. Payments are HBAR transfers whose memo carries the request hash, confirmed by polling the mirror node before the provider serves. postBond and slash move real HBAR on the same rail. Two things bit me here. First, the portal issues ECDSA accounts, and PrivateKey.fromString silently reads a bare hex key as ED25519 and hands back a valid key that is not yours, which you find out at the network as INVALID_SIGNATURE. Second, and this one only showed up on a live run with every unit test green: the mirror node reports a self-transfer as only the fee movement, so with a single funded account the payment never appears in the transfer list and no amount check can pass on it. The fix was a second funded account, not a weaker check.

The audit trail is the Hedera Consensus Service, and it is the non-obvious use of the second service. Every other claim this project makes is re-derivable by a stranger (the Graph queries are block-pinned, the ENS records are public, the transfers are on a public mirror node) but the loop's event log was a file on my disk that I could have rewritten after the run. So the sink folds every line it writes into a SHA-256 chain and submits the head of that chain at each turning point of the loop. Six roughly 100-byte messages cover a run of any length, they leak no job data, and a match proves strictly more than a copy would: a copy shows those events existed, a chain head shows that no line anywhere before it was added, removed, reordered or edited. Topic 0.0.9753542 holds two live runs, and a committed log in the repo reproduces all 12 anchors with one command on a fresh clone, with Hedera's consensus timestamps attesting when that state existed.

The Graph, through the gateway against a Uniswap v3 subgraph. getTokenSignals(token, atBlock) returns liquidity in the deepest pool, age in blocks, transaction count, volume and top-pool concentration, always read at a specific block and never at current state, because the verifier has to query the same block the claim was stamped at. A block outside the subgraph's indexed range fails loudly rather than silently returning live data, which is the property a verifier needs: a silent fallback to whatever is true now would slash honest providers every time the chain moves.

The requester side is an MCP server (nine tools) registered in .mcp.json, so a real Claude Code session is the client. I built two custom demo runners and deleted both. The reason is worth keeping: a renderer I write is less credible than the tool the audience already uses, because in Claude Code a judge sees the MCP server badge, the real tool names, the arguments and the raw JSON that came back, and none of it has to be taken on trust.

The hacky part worth mentioning is the lying-provider harness. Rather than a second, less-real code path, it wraps the real capability and tampers exactly one claim before returning it, so the verifier gets tested against the real output shape rather than a fixture. That test is also the demo climax.

background image mobile

Join the mailing list

Get the latest news and updates