One signed intent, carried from the payment through the enclave into the verdict.
Payment, execution and reputation are each solved for agents; nothing connects them. What follows is the whole connection — every preimage, every recovery, and the one link we could not close. It is written to be checked against the code, not to be believed.
What gets signed
Alice commits to the job before anyone is paid and before any work starts. The commitment is a single hash over the brief, the data, the constraints, the price and a nonce — so a job differing in any one of those is a different hash.
intentHash = keccak256( abi.encode(briefHash, dataHash, constraints, price, nonce) )
She signs that hash with EIP-712. The contract later recovers her own signer and compares it against the registered client — it never trusts an intent-and-output pair that Bob hands it, because Bob has every reason to hand it a matching pair.
The brief and the data themselves travel ECIES-encrypted to Bob's registered public key. What is public is the commitment; what is private is the job.
The two signatures
Both are ecrecover-compatible, and they prove different things. Confusing them is the easiest way to overstate this project, so they are stated separately.
A · 0G's TEE signature — a genuine enclave produced this output
Measured, not assumed: the signed message is not the output text. It is a colon-joined tuple in which the output appears as the sha256 of the raw response body.
"<h1>:<sha256(raw response body)>:<ProviderType>:<ProviderIdentity>:<h3>" verify: recoverAddress(hashMessage(text), signature) === teeSignerAddress
So the output is covered — as the fingerprint of the body containing it, not as plain text. Same guarantee against tampering; a different sentence, and we use the accurate one. A one-byte change to the body drops out of the tuple. Hash the raw bytes: a JSON re-stringify depends on key order and would break silently the day the provider reorders a field.
Two further conditions the code enforces rather than assumes: the chat id comes from the ZG-Res-Key response header, and teeSignerAcknowledged must be true — a TeeML provider whose signer the contract owner has not vouched for is the provider's own claim, and we refuse to verify against it.
B · The binding signature — this code checked the match
preimage = keccak256("agentId|sealId|timestamp|hex(sha256(body))")
signature = 64-byte R‖S, no EIP-191 prefix — v is discarded by the wrapper
→ brute-force v ∈ {27,28} off-chain, pass v into the contractThe body it signs must be byte-stable, because raw bytes are what get hashed; nothing is re-stringified before verification.
The on-chain verdict
Verifier.sol on Base Sepolia is the only thing that decides. It does three checks and refuses the job unless all three pass:
1. reconstruct the binding preimage, recover the signer
require signer == enclaveSignerOf[agentId]
2. recover Alice's EIP-712 signer
require signer == registeredClient
3. require match == true
→ emit JobVerified(intentHash, outputHash)A job that fails is not silently dropped — it emits a rejection with a reason, and the subgraph indexes those too, so a cheating attempt stays visible exactly where hiring decisions are made. The four ways Bob can cheat, and what each one trips:
substitute he answers a different job → MatchFalse tamper he edits the input → MatchFalse forge he rewrites the signed body → BadEnclaveSig selfintent he supplies his own intent → BadClientSig
The binding we shipped
This is the section to read if you only read one. Bob's binding — the code that recomputes the hash and decides match — runs on an ordinary host, not in an attested enclave. We had no TDX machine and 0G does not host that execution for us.
Rather than fake an attestation, we moved one end of the binding inside 0G's real enclave:
Alice signs intentHash → it is placed at the TOP of the prompt → the model copies it verbatim into its answer → 0G's TEE signs sha256(response body), which CONTAINS that answer ⇒ a genuine 0G enclave has attested: "a response carrying THIS intentHash was produced in here"
Bob cannot forge that first link on his own machine — it comes from 0G hardware. The echo is checked against the raw output by our own code, not by the compute backend, because a backend could simply claim it inserted the commitment. Verification is exact-match on the full 64-hex value: one shifted character breaks it, and "close enough" is not a category. Measured 5/5 verbatim.
What that does not give you: the match check itself is computed by unattested code. Alice can verify it independently; a stranger cannot. attestation: 'none' and imageHash: null stay in the code and in the UI until that changes. The honest sentence is: 0G attests the compute and carries the intent through it; the match check is client-verifiable, not yet third-party-verifiable.
Money, and its ordering
Bob answers HTTP 402 until he is paid, and does no work before the authorisation exists. But the authorisation is not the payment: money moves only after the contract has ruled.
Alice authorizes x402 → nothing has moved yet
Verifier emits JobVerified → the job is ruled valid
Bob calls /settle → the guard RE-READS that receipt on chain
for THIS intentHash, then releasesThe guard is structural rather than a convention each backend is trusted to follow. Checking merely that a transaction hash was supplied would let Bob invent one; reading the event makes that impossible. A rejected job never reaches the settle line, and the signed authorisation is never submitted.
Two rails, and the receipt reports which one you got rather than asserting it:
base-stealth paidTo is a fresh ERC-5564 address, different every job
paidTo ≠ agentIdentity — the payment record does not name Bob
hedera-x402 paidTo == agentIdentity, the account published in his agent card
this rail buys autonomy, not privacy, and says soTwo addresses a reader can compare beat a private: true flag they would have to take our word for. The limit, stated before someone works it out unaided: on this testnet there is one client and one agent, and JobVerified names the agentId, so timing correlation still links a payout to Bob. What the stealth address hides is the payment record, not the whole system.
The timeline
Every stage — quote, intent, enclave, output, settlement — is committed to a Hedera Consensus Service topic, including the rejections. The timestamps shown in the demo are Hedera's own consensus timestamps, read back from the public mirror node; a timeline whose times come from the machine that wrote it proves nothing.
What goes on the topic is commitments only — hashes, stage names, flags. The brief, the data and the output never go near it, and the code scans its own outgoing messages for those secrets before they reach the network.
Check it yourself
Nothing here needs our word for it. Each of these is a public address on a public testnet.
| Verifier · Base Sepolia | 0x3B116D648B710f551e37223c4c4d39879AFEEb96 |
| ERC-8004 registry · Base Sepolia | 0x8004A818BFB912233c491871b3d84c89A494BD9e |
| Agent ids | Bob 8429 · Alice 8431 |
| 0G provider · Galileo, chainId 16602 | 0xa48f01287233509FD694a22Bf840225062E67836 |
| Model · attestation | qwen/qwen2.5-omni-7b · TeeML |
| 0G TEE signer | 0x83df4B8EbA7c0B3B740019b8c9a77ffF77D508cF |
| Hedera timeline topic | 0.0.9738448 |
| Subgraph | confidential-agents (The Graph Studio) |
Or run it. Every phase has a gate that proves it against live networks, and the recorded evidence is checked in so the gates cost no faucet credit.
pnpm demo:base a full job, no payment rail
pnpm demo:base -- --fraud substitute
rejected on chain, nothing settles
pnpm gate:P3-D an honest run verifies; three fraud modes are
rejected ON CHAIN — MatchFalse, BadEnclaveSig, BadClientSig
pnpm gate:P1-D the fourth, tamper, which shares the MatchFalse path
pnpm measure:e2e re-measure end-to-end latencyMeasured end to end over 5 runs with no payment rail: p50 21.0 s, p95 22.7 s against a 60 s budget. The dominant term is the 0G inference call, about 12.4 s.
What we do not claim
A spec that only lists what works is a brochure. These are the limits we hold ourselves to, and we would rather say them than have them found.
- Not Sybil-proof reputationFeedback anchored to paid, verified jobs. Inflating it is expensive, not impossible.
- Not a solution to prompt injectionThe binding catches task substitution and input tampering. That is the whole claim.
- Not two TEEsOne, and 0G built it. Bob's binding runs on an ordinary host — see §04.
- Not decentralized computeThe provider we pinned reports ProviderType: centralized, ProviderIdentity: aliyun. The TEE seal is real Intel TDX; the operator is a single cloud.
- Not private on HederaThe Hedera run buys autonomy. Recipient privacy is the Base run's stealth rail.
- Not firstConfidential agent payments shipped before us. The intent binding is what is ours.
- Not “the AI decided, therefore it is trustworthy”The agents' brain is unattested and outside every guarantee here. It can refuse to trade; it cannot overpay, invent a counterparty, or make an invalid job verify. If the brain and the contract disagree, the contract is the one that decided.
- Not “Claude runs the analysis”Claude decides; 0G Sealed Inference produces the deliverable. Swapping the brain cannot change one byte of what the enclave signed.