EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8274: AI Inference Proof Verification Interfaces

Two-layer interface for on-chain AI inference proof verification, separating IAgentVerifier (application) from IProofVerifier (algorithm)

Authors
Created2026-05-26
Discussion Linkhttps://ethereum-magicians.org/t/draft-erc-universal-ai-inference-verification-registry/28083
Requires
Referenced by

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1771PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationIProofVerifier β€” Inner Algorithm Layerverify()proofSystem()proofProfile()IAgentVerifier β€” Outer Application LayerStateful DesignVerification CommitmentIAgentVerifiable β€” Declaration LayerInterface DetectionRationalePrimitive: OCP as the Unifying ModelArchitecture: Two Composable, Opaque LayersQuery: Input Provenance (WYRIWE)Result: Proof AnchoringBackwards CompatibilityReference ImplementationCoding in PracticeLayer 1 β€” IProofVerifier: Proof System BackendLayer 2 β€” IAgentVerifier: Wraps IProofVerifierLayer 3 β€” Settlement Contract (ERC-8183): Wraps IAgentVerifierComposability in PracticeERC-8183 β€” Agentic CommerceWYRIWE β€” Input ProvenanceJudgment Validator β€” attestation/judgmentSecurity ConsiderationsCopyright

Abstract

On-chain AI inference can be verified through multiple proof paradigms β€” zkML, opML, TEE, oracle, multisig, and others β€” but no common interface exists between them. Every protocol that needs verified AI inference today must write a separate adapter per proof system, resulting in NΓ—M integration complexity and vendor lock-in.

This ERC defines three minimal base interfaces organized into two layers:

  • IProofVerifier β€” the inner algorithm layer, implemented by proof system providers (ZK teams, TEE vendors, oracle networks). Stateless. Answers: "is this proof cryptographically valid for this input and output?"
  • IAgentVerifier β€” the outer application layer, implemented by application developers. Stateful. Wraps an IProofVerifier. Answers: "for this task, was this agent authorized to make this claim, and does the proof confirm it?"
  • IAgentVerifiable β€” implemented by settlement contracts to declare which IAgentVerifier they use.

It also defines an optional IAgentProofProfile query extension for deployments whose active verifier configuration can be identified before a call.

The separation allows proof system providers and application developers to work independently: neither needs to know the other's internals.

Motivation

Verification infrastructure already exists, but it is highly fragmented.

Verification paradigms already live:

ParadigmDescription
zkMLCryptographic proof that a specific model produced a given output from a given input
opMLOptimistic AI inference execution with a challenge window for fraud proofs
TEEAI inference run inside a hardware-isolated enclave with a verifiable attestation report
AttestationOff-chain inference result certified by one or more authorized signers β€” covers oracle gateways (e.g. Chainlink), multisig / AVS validator networks (e.g. EigenLayer), and judgment validators

ERCs that need inference verification:

The problem: these two sides cannot talk to each other. Every protocol that needs verified AI inference today must write a separate adapter per proof system β€” resulting in vendor lock-in and NΓ—M integration complexity. What's missing is a standard verifier interface that sits in the middle. This ERC provides exactly that:

Verification Backends   This ERC                ERCs

                   ╔══════════════════╗
   zkML        ─┐  β•‘  AI Inference    β•‘  β”Œβ”€ ERC-8183 (Agentic Commerce)
   opML        ──  β•‘  Proof           β•‘  β”œβ”€ ERC-8004 (Trustless Agents)
   TEE         ─┼─►║  Verification    ║◄─┼─ ERC-8001 (Agent Coordination)
   Attestation β”€β”˜  β•‘  Interfaces      β•‘  └─ ERC-7007 (AIGC Token)
                   β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.


IProofVerifier β€” Inner Algorithm Layer

IProofVerifier is the inner algorithm layer. Three defining properties:

  • Stateless β€” holds no configuration about which agent, model, or session is being verified; all context is passed through parameters on every call
  • Verification algorithm specific β€” encapsulates the cryptographic verification logic of one proof system (ZK, TEE, optimistic, or attestation)
  • Implemented by proof system providers β€” the parties who implement cryptographic verification for a specific proof system (e.g. ZK teams, TEE vendors, oracle networks, attestation gateway operators); called by agent developers from within IAgentVerifier, with no knowledge of the agent context above
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

/// @title IProofVerifier
/// @notice Stateless cryptographic verification interface for AI inference proof backends.
/// Implemented by proof system providers independently of any calling context.
/// Answers: "is this proof cryptographically valid for this input and output?"
interface IProofVerifier {
    /// @notice Verify an AI inference proof.
    /// @param inputHash  Commitment to the model input (hash scheme is backend-specific).
    /// @param outputHash Commitment to the model output (hash scheme is backend-specific).
    /// @param metadata   Invariant deployment configuration β€” fields stable across calls
    ///                   for this verifier deployment (e.g. ZK circuit key, TEE enclave hash,
    ///                   signer registry address). Stored by IAgentVerifier; callers never
    ///                   encode or see this field.
    /// @param proof      Per-inference cryptographic evidence (e.g. ZK proof bytes,
    ///                   EIP-712 signature, TEE attestation report).
    /// @return True if the proof is cryptographically valid for the given input and output.
    function verify(
        bytes32 inputHash,
        bytes32 outputHash,
        bytes calldata metadata,
        bytes calldata proof
    ) external returns (bool);

    /// @notice Proof system identifier in "{paradigm}/{variant}" format.
    function proofSystem() external view returns (string memory);

    /// @notice Collision-resistant description of this verifier's verification
    /// type and configuration version. Meaning is scoped to this verifier address.
    /// SHOULD equal keccak256(abi.encode(proofSystem(), deploymentSalt)) where
    /// deploymentSalt is a backend-specific constant (e.g. verifier runtime bytecode
    /// hash, circuit commitment hash, enclave measurement).
    function proofProfile() external view returns (bytes32);
}

verify()

verify() takes four parameters with distinct roles:

  • inputHash β€” commitment to the model input. The hash scheme is backend-specific and MUST be documented by each implementation.
  • outputHash β€” commitment to the model output. The hash scheme is backend-specific and MUST be documented by each implementation.
  • metadata β€” invariant configuration that does not change between calls for a given deployment: ZK circuit verification key, TEE enclave hash, signer registry address, etc. This is deployment-time configuration owned by IAgentVerifier; the settlement contract never encodes or sees it.
  • proof β€” per-inference cryptographic evidence specific to this inference event: ZK proof bytes, EIP-712 signature, TEE attestation report, or multisig aggregate.

inputHash and outputHash are interpreted within the concrete verifier configuration that processes the call. Consumers MUST NOT assume commitments produced under different verifier configurations are comparable unless those configurations explicitly declare the same canonical encoding and hash scheme. Matching proofProfile() values from different verifier addresses is not, by itself, such a declaration.

proofSystem()

Identifies what kind of proof system this verifier implements. Returns a {paradigm}/{variant} string:

  • paradigm β€” the verification paradigm, determining the trust model and security assumptions (see table below)
  • variant β€” the specific implementation within that paradigm
ParadigmTrust modelSecurity assumptionsproofSystem() examples
zkMathematical: ZK circuit proves a specific model produced a given output from a given inputSoundness of the ZK proving system and correctness of the circuitzk/sp1, zk/ezkl
opEconomic: execution is assumed correct within a challenge window; fraud proofs slash the submitterAt least one honest challenger is watching within the challenge windowop/ora
teeHardware: a trusted enclave attests that execution occurred in an isolated environmentTrustworthiness of the hardware manufacturer; no side-channel or supply-chain compromisetee/nitro, tee/marlin
attestationAuthoritative: one or more authorized signers certify the result β€” covers oracle gateways, multisig validator networks, and judgment validatorsAuthorized signer set is not compromised; accountability is reputational or stake-based depending on the variantattestation/multisig, attestation/wyriwe, attestation/judgment

proofProfile()

Returns a collision-resistant bytes32 description of the verification type and configuration version reported by the verifier. For example, it can distinguish circuit versions within zk/sp1 or enclave measurements within tee/nitro.

proofProfile() is self-reported and MUST be interpreted in the scope of the concrete IProofVerifier address. Two addresses returning the same value MAY implement different verification behavior. A consumer MUST NOT trust an unknown verifier address solely because it reports a recognized proofProfile(). The trust and review target is the verifier address, its deployed code, and any upgrade authority; the profile describes the verification type/version used by that address.

A conformant implementation MUST change proofProfile() whenever verification semantics or any behavior-affecting configuration not supplied through metadata changes. For a proxy or otherwise upgradeable verifier, consumers MUST evaluate the active implementation and upgrade governance; the verifier address is a stable trust handle, not evidence that its code is immutable.

Using the verifier runtime bytecode hash as deploymentSalt is one reference derivation for an immutable on-chain verifier because consumers can independently recompute it. It is not the only conformant derivation: a ZK verifier MAY use a circuit verifying-key commitment, a TEE verifier MAY use an enclave measurement, and other backends MAY use an equivalent independently reviewable component appropriate to their trust model.


IAgentVerifier β€” Outer Application Layer

IAgentVerifier is the outer application layer. Three defining properties:

  • Stateful β€” stores deployment-time configuration: which IProofVerifier(s) to use, agent authorization rules, metadata to pass to the inner verifier, and optionally task tracking state
  • Application layer β€” orchestrates agent-specific authorization and verification logic; shields settlement contracts from proof-system differences so callers invoke the same verify() interface regardless of whether the backend is ZK, TEE, or attestation
  • Implemented by agent developers β€” the parties who deploy verification services for their agents, configuring authorization rules and IProofVerifier backends; called by settlement protocols (e.g. ERC-8183) via IAgentVerifiable, with the cryptographic internals remaining the exclusive concern of proof system providers
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IProofVerifier.sol";

/// @title IAgentVerifier
/// @notice Stateful application-layer verification interface.
/// Implemented by application developers. Wraps one or more IProofVerifier(s).
/// Answers: "for this task, was this agent authorized to make this claim,
/// and does the proof confirm it?"
interface IAgentVerifier {
    /// @notice SHOULD be emitted on every verify() call β€” both passing and failing.
    /// Carries the raw preimage fields of verificationDigest, enabling any observer
    /// to independently recompute the digest and confirm inclusion per OCP
    /// (recompute β†’ compare β†’ confirm).
    event VerificationCompleted(
        bytes32 indexed taskId,
        bytes32 indexed agentId,
        bytes32 indexed verificationDigest,
        bool    valid,
        bytes32 inputHash,
        bytes32 outputHash,
        bytes32 agentProofProfile,
        uint256 timestamp,
        bytes   instanceId,          // identifier of this verifier deployment; see Verification Commitment
        bytes   chainId               // identifier of the chain; see Verification Commitment
    );

    /// @notice Verify an agent's inference claim for a specific task.
    /// @param taskId     Task identifier. Binds verification to a single task.
    ///                   Callers SHOULD encode uint256 IDs as bytes32
    ///                   (e.g. ERC-8183 jobId, settlement periodId).
    /// @param agentId    Identity of the agent that performed the inference.
    ///                   Checked against the implementation's stored authorization config.
    /// @param inputHash  Commitment to the model input.
    /// @param outputHash Commitment to the model output.
    /// @param proof      Cryptographic evidence, passed unchanged to the inner IProofVerifier(s).
    /// @return valid              Whether the agent is authorized and the proof is valid.
    /// @return verificationDigest Domain-bound semantic commitment to this verification event.
    ///                            Computed from taskId, agentId, inputHash, outputHash, valid,
    ///                            agentProofProfile, block.timestamp, instanceId, and chainId.
    ///                            Returned regardless of outcome. MAY be used as an on-chain
    ///                            audit anchor (e.g. ERC-8183 job.reason).
    /// @dev NOT view β€” implementations MAY write state (e.g. single-use taskId tracking).
    function verify(
        bytes32 taskId,
        bytes32 agentId,
        bytes32 inputHash,
        bytes32 outputHash,
        bytes calldata proof
    ) external returns (bool valid, bytes32 verificationDigest);

}

Implementations with a single active verifier configuration MAY expose the following optional extension for callers that need to query agentProofProfile before invoking verify(). It is separate from IAgentVerifier so the base interface and its ERC-165 interface ID remain unchanged. Implementations whose selected verifier or metadata depends on call inputs SHOULD define a query that accepts the corresponding routing inputs instead.

interface IAgentProofProfile {
    function agentProofProfile() external view returns (bytes32);
}

Stateful Design

IAgentVerifier is stateful by design. Its state falls into two categories:

Application state β€” deployment-time application rules specific to the agent. What to store here is left to the implementation; typical examples include agent authorization configuration (allowlist, registry reference, on-chain role), task state for single-use enforcement, model or policy requirements, and accepted time windows.

IProofVerifier reference(s) and metadata β€” which proof backend(s) to call and the invariant metadata to pass each one. Typical examples include a single IProofVerifier address with its corresponding circuit key or enclave hash encoded as metadata, or a keyed mapping of verifiers for multi-paradigm deployments. Both are set at deployment and SHOULD be immutable; metadata encoding is specific to each IProofVerifier and opaque to callers above.

This ERC does not mandate the internal structure of IAgentVerifier implementations. Implementations MAY use a single IProofVerifier, an array, a mapping, or any other arrangement. The verify() signature above and the behavioral and commitment requirements below apply regardless of that arrangement.

For a verification attempt using a single inner verifier, IAgentVerifier MUST derive agentProofProfile from the concrete inner verifier configuration selected for that attempt:

agentProofProfile = keccak256(abi.encode(
    verifierAddress,
    IProofVerifier(verifierAddress).proofProfile(),
    keccak256(metadata)
));

Here metadata is the exact invariant byte string configured for, and when invoked passed to, that verifier. This address-scoped composition prevents equal self-reported proofProfile() values at different addresses from being treated as equivalent verification behavior.

If multiple inner verifiers contribute to one attempt, the implementation MUST document and use a deterministic composition that commits to every participating (verifierAddress, proofProfile, keccak256(metadata)) tuple and to the aggregation or routing policy version. It MUST NOT treat a shared self-reported profile as evidence that different addresses behave equivalently. The resulting agentProofProfile MUST be included in VerificationCompleted and verificationDigest as specified below.

If application checks reject an attempt before invoking an inner verifier, agentProofProfile MUST commit to the configuration selected or that would have been selected for that attempt. If no verifier configuration can be selected, the implementation MUST define and document a deterministic sentinel profile. The reference implementation below has one configured backend and therefore commits to that backend on both passing and failing attempts.

Verification Commitment

verify() returns (bool valid, bytes32 verificationDigest). The verificationDigest is a domain-bound semantic commitment to the full verification event β€” it identifies what was verified, where (which chain and which deployment), and when β€” but does not guarantee uniqueness across distinct calls with identical parameters within the same block. Exact occurrence identity is supplied by the event's inclusion proof (the transaction and log index), not by the digest.

The preimage is:

keccak256(abi.encode(
    taskId, agentId, inputHash, outputHash,
    valid, agentProofProfile, block.timestamp,
    instanceId, chainId
))
  • instanceId: bytes β€” identifies the verifier deployment. On Ethereum: abi.encodePacked(address(this)). Non-EVM deployments use their own canonical deployment identifier.
  • chainId: bytes β€” identifies the chain. On Ethereum: abi.encode(block.chainid).

Including valid, the address-scoped agentProofProfile, block.timestamp, instanceId, and chainId ensures that the same inputs verified by different inner verifier configurations, on different chains, by different outer deployments, at different times, or with different outcomes, produce different digests. block.timestamp makes the digest self-contained β€” the verification time is encoded directly rather than requiring block lookup. instanceId and chainId prevent cross-deployment and cross-chain collisions when the digest is used as a detached audit anchor.

VerificationCompleted SHOULD be emitted on every call β€” passing and failing β€” carrying the same fields as the preimage. This follows OCP's commitment model: any external observer can independently verify a VerificationCompleted record by applying the recompute β†’ compare β†’ confirm inclusion procedure, without calling any contract and trusting any intermediary.

verificationDigest MAY be stored as an on-chain audit anchor by the settlement contract (e.g. as ERC-8183's job.reason).


IAgentVerifiable β€” Declaration Layer

Settlement contracts MUST implement IAgentVerifiable to declare which IAgentVerifier they use and MUST emit an AgentVerifierUpdated(bytes data) event on any change.

The data field is a neutral envelope whose encoding is chosen by the deployment. Two recommended encodings are:

  • Simplest (address tracking): abi.encode(previousVerifier, newVerifier) β€” sufficient for deployments that only need to record which verifier address was active before and after the switch.
  • Extended (versioned lineage): a richer encoding that carries structured identifiers for the upgrade β€” for example, a namespace identifier and a transition identifier within that namespace, enabling consumers to reconstruct a full verifier upgrade history from an off-chain state chain.

Deployments MAY use other encodings. The event provides the bytes slot; the deployment defines the encoding, and consumers decode accordingly.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IAgentVerifier.sol";

/// @title IAgentVerifiable
/// @notice Interface for settlement contracts that declare which agent verifier they use.
interface IAgentVerifiable {
    /// @notice Returns the agent verifier used by this contract.
    function getAgentVerifier() external view returns (IAgentVerifier);

    /// @notice MUST be emitted when the agent verifier changes.
    /// @param data Encoding of the verifier change, chosen by the deployment.
    ///        Recommended: abi.encode(previousVerifier, newVerifier).
    event AgentVerifierUpdated(bytes data);
}

Interface Detection

All implementations SHOULD support ERC-165:

// IProofVerifier
function supportsInterface(bytes4 interfaceId) external view returns (bool) {
    return interfaceId == type(IProofVerifier).interfaceId
        || interfaceId == type(IERC165).interfaceId;
}

// IAgentVerifier
function supportsInterface(bytes4 interfaceId) external view returns (bool) {
    return interfaceId == type(IAgentVerifier).interfaceId
        || interfaceId == type(IERC165).interfaceId;
}

An implementation of the optional IAgentProofProfile extension MUST additionally return true for type(IAgentProofProfile).interfaceId.


Rationale

Primitive: OCP as the Unifying Model

OCP (Observation Commitment Protocol) is a system-agnostic verification primitive: given the emitted event data, any observer can independently verify a commitment without trusting the system that produced it or relying on any contract. It defines the minimum procedure any proof system must satisfy:

recompute β†’ compare β†’ confirm inclusion

This ERC adopts OCP as the unifying model for IAgentVerifier events β€” not only because every proof backend satisfies the primitive, but because VerificationCompleted maps directly onto it: the event is the observation, verificationDigest is the commitment, and the emitted fields are the preimage.

verificationDigest is computed from (taskId, agentId, inputHash, outputHash, valid, agentProofProfile, block.timestamp, instanceId, chainId). The event carries those same fields alongside the digest, making every record self-describing and independently auditable:

  • On-chain: recompute the digest from held parameters and compare against what was emitted
  • Off-chain: apply all three steps β€” recompute from emitted fields, compare, confirm event inclusion

confirm inclusion requires reading event logs, which is off-chain only. IProofVerifier.verify() handles the first two steps on-chain; external observers handle the third via VerificationCompleted records. The same primitive thus covers both contexts without requiring contracts to fetch their own logs.

agentProofProfile in the digest commits to the selected inner verifier address, its self-reported profile, and the configured metadata, ensuring that the same inputs verified by different inner configurations produce different digests.

Architecture: Two Composable, Opaque Layers

The split between IAgentVerifier (outer application layer) and IProofVerifier (inner algorithm layer) exists because two independent parties must implement the standard:

  • Proof system providers implement IProofVerifier. They know their cryptographic details but not the calling application context. IProofVerifier is stateless β€” one deployed instance serves many IAgentVerifier deployments without redeployment; metadata carries all invariant configuration and is owned by IAgentVerifier.
  • Agent developers implement IAgentVerifier. They know their application rules but should not be coupled to a specific proof system. IAgentVerifier is stateful β€” authorization rules, model requirements, and task tracking belong in state.

Settlement contracts MUST NOT call IProofVerifier directly β€” doing so would require knowing the backend's metadata encoding. The two-layer boundary keeps each party isolated:

IAgentVerifier.verify(taskId, agentId, inputHash, outputHash, proof)
  β†’ check taskId (if single-use)
  β†’ check agentId authorization (if needed)
  β†’ delegate to IProofVerifier(s) internally
  β†’ compute verificationDigest
  β†’ return (valid, verificationDigest)

Opacity. The two-layer design achieves opacity at every level:

  • Settlement contracts are opaque to the proof backend β€” they call IAgentVerifier.verify() the same way regardless of whether the backend is ZK, TEE, or attestation.
  • IAgentVerifier is opaque to the settlement contract β€” its internal verifier configuration, metadata encoding, and authorization rules are not exposed to callers.

Query: Input Provenance (WYRIWE)

inputHash commits to whatever input was provided but says nothing about its origin. A verifier confirms "this model produced this output from this input" β€” not whether the input was legitimate.

WYRIWE fills this gap with a three-commitment scheme:

rawInputHash             = keccak256(raw_user_input)
sanitizationPipelineHash = keccak256(sanitization_spec_cid || rawInputHash)
inputHash                = keccak256(sanitized_input)

IProofVerifier.verify()'s inputHash maps directly to WYRIWE's input_hash β€” same field, same construction. WyriweProofVerifier is already deployed on mainnet, implements IProofVerifier, and can be passed directly to any IAgentVerifier.

Consumers requiring input provenance SHOULD adopt WYRIWE alongside verify(). This ERC does not prescribe how inputHash is derived.

Result: Proof Anchoring

verify() confirms cryptographic validity but not when the proof was generated. A consumer can confirm "this proof checks out" but not "the proof was committed before any action was taken."

Proof anchoring fills this gap: agents anchor a proofHash on-chain at inference time via an on-chain anchor registry. Consumers requiring proof binding SHOULD verify the anchor alongside verify().

The anchor check is an application-layer concern β€” it belongs in IAgentVerifier, not IProofVerifier. Implementations can accept a Merkle inclusion proof inside proof to confirm proofHash exists in the anchor registry.

Purpose
proofHash (verdict anchor)"This verdict was committed at time T" β€” verdict commitment
verificationDigest"This verification event occurred with these parameters"
OCP inputHash (input anchor)"This task input was committed at time T" β€” input commitment

The two anchors serve distinct commitment targets at different points in the chain and MUST NOT be conflated. committed_at references the verdict anchor (verdict commitment style), not the OCP input anchor.


Backwards Compatibility

No backwards compatibility issues. This ERC introduces new interfaces and does not modify any existing standard.


Reference Implementation

Coding in Practice

The following three examples form a complete composable stack. Each layer is independently deployable; Layer 2 wraps Layer 1 and Layer 3 wraps Layer 2.

Layer 1 β€” IProofVerifier: Proof System Backend

SP1ProofVerifier implements the ZK paradigm. proofSystem() is declared public so proofProfile() can call it internally. This immutable reference derives its profile from the deployed runtime bytecode hash; the outer AgentVerifier separately commits to the concrete address and programVKey metadata.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IProofVerifier.sol";

/// @title SP1ProofVerifier
/// @notice IProofVerifier implementation for SP1 zkVM AI inference proofs.
///
/// metadata: abi.encode(bytes32 programVKey)
///   programVKey β€” SP1 verification key identifying the ZK circuit for this model
/// proof: SP1 proof bytes (Plonk or Groth16 format)
contract SP1ProofVerifier is IProofVerifier {

    function verify(
        bytes32 inputHash,
        bytes32 outputHash,
        bytes calldata metadata,
        bytes calldata proof
    ) external returns (bool) {
        bytes32 programVKey = abi.decode(metadata, (bytes32));
        // Verify the SP1 ZK proof: circuit identified by programVKey produced
        // outputHash from inputHash. Delegates to SP1 Plonk verifier contract.
        // Full implementation omitted; see SP1 documentation.
        return true;
    }

    function proofSystem() public pure returns (string memory) {
        return "zk/sp1";
    }

    function proofProfile() external view returns (bytes32) {
        return keccak256(abi.encode(proofSystem(), address(this).codehash));
    }
}

Layer 2 β€” IAgentVerifier: Wraps IProofVerifier

AgentVerifier is constructed with an IProofVerifier instance (e.g. SP1ProofVerifier from Layer 1), deployment metadata, and an authorized agent set. The inner verifier configuration is held in contract state β€” the settlement contract in Layer 3 never sees metadata or the verifier address. This example chooses an application policy in which a successful verification consumes taskId, while a failed verification may be retried; other replay policies remain conformant.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IAgentVerifier.sol";
import "./IProofVerifier.sol";
import "./IERC165.sol";

/// @title AgentVerifier
/// @notice IAgentVerifier implementation wrapping a single IProofVerifier backend.
///
/// metadata is stored at deployment and passed to the inner verifier on every call.
contract AgentVerifier is IAgentVerifier, IAgentProofProfile, IERC165 {

    IProofVerifier private immutable _verifier;
    bytes private _metadata;

    mapping(bytes32 => bool) private _authorizedAgents;
    mapping(bytes32 => bool) private _usedTasks;

    constructor(
        IProofVerifier verifier_,
        bytes memory metadata_,
        bytes32[] memory authorizedAgents_
    ) {
        _verifier = verifier_;
        _metadata = metadata_;
        for (uint256 i = 0; i < authorizedAgents_.length; i++) {
            _authorizedAgents[authorizedAgents_[i]] = true;
        }
    }

    function agentProofProfile() public view override returns (bytes32) {
        return keccak256(abi.encode(
            address(_verifier),
            _verifier.proofProfile(),
            keccak256(_metadata)
        ));
    }

    function supportsInterface(bytes4 interfaceId) external pure override returns (bool) {
        return interfaceId == type(IAgentVerifier).interfaceId
            || interfaceId == type(IAgentProofProfile).interfaceId
            || interfaceId == type(IERC165).interfaceId;
    }

    function verify(
        bytes32 taskId,
        bytes32 agentId,
        bytes32 inputHash,
        bytes32 outputHash,
        bytes calldata proof
    ) external override returns (bool valid, bytes32 verificationDigest) {
        if (!_usedTasks[taskId] && _authorizedAgents[agentId]) {
            valid = _verifier.verify(inputHash, outputHash, _metadata, proof);
            if (valid) _usedTasks[taskId] = true;
        }

        bytes32 profile = agentProofProfile();
        uint256 ts = block.timestamp;
        bytes memory instanceId = abi.encodePacked(address(this));
        bytes memory chainId = abi.encode(block.chainid);
        verificationDigest = keccak256(abi.encode(
            taskId, agentId, inputHash, outputHash, valid, profile, ts,
            instanceId, chainId
        ));

        emit VerificationCompleted(
            taskId, agentId, verificationDigest, valid, inputHash, outputHash,
            profile, ts, instanceId, chainId
        );
    }
}

Layer 3 β€” Settlement Contract (ERC-8183): Wraps IAgentVerifier

ProofEvaluator implements IAgentVerifiable, declares its IAgentVerifier via getAgentVerifier(), and calls verify() at settlement time. verificationDigest is forwarded as job.reason to the ERC-8183 ACP protocol.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./IAgentVerifiable.sol";
import "./IAgentVerifier.sol";

interface IACP {
    function complete(uint256 jobId, bytes32 reason, bytes calldata data) external;
    function reject(uint256 jobId, bytes32 reason, bytes calldata data) external;
}

contract ProofEvaluator is IAgentVerifiable {
    IAgentVerifier private _agentVerifier;
    IACP private immutable _acp;

    constructor(IAgentVerifier agentVerifier_, IACP acp_) {
        _agentVerifier = agentVerifier_;
        _acp = acp_;
        emit AgentVerifierUpdated(address(0), address(agentVerifier_));
    }

    function getAgentVerifier() external view returns (IAgentVerifier) {
        return _agentVerifier;
    }

    function settle(
        uint256 jobId,
        bytes32 agentId,
        bytes32 inputHash,
        bytes32 outputHash,
        bytes calldata proof
    ) external {
        bytes32 taskId = bytes32(jobId);
        (bool valid, bytes32 verificationDigest) =
            _agentVerifier.verify(taskId, agentId, inputHash, outputHash, proof);

        if (valid) _acp.complete(jobId, verificationDigest, "");
        else       _acp.reject(jobId, verificationDigest, "");
    }
}

Composability in Practice

ERC-8183 β€” Agentic Commerce

ERC-8183 defines a job lifecycle where an evaluator calls complete() or reject() after assessing a submission. Four decisions apply directly:

complete() signature. The evaluator implements IAgentVerifiable; the verifier is resolved at settlement via getAgentVerifier() β€” no pre-registration required. The caller supplies only agent-level claims:

complete(jobId, agentId, inputHash, outputHash, proof)

metadata and the verifier address are owned by IAgentVerifier internally.

job.reason = verificationDigest. ERC-8183's bytes32 reason field is the natural anchor for verificationDigest β€” a domain-bound semantic commitment, OCP-verifiable by any observer. Exact occurrence identity is supplied by the event's inclusion proof, not by the digest.

verify() returns bool. No reconstruction needed:

(bool valid, bytes32 verificationDigest) =
    getAgentVerifier().verify(bytes32(jobId), agentId, inputHash, outputHash, proof);

if (valid) ACP.complete(jobId, verificationDigest, "");
else       ACP.reject(jobId, verificationDigest, "");

inputHash follows WYRIWE β€” keccak256(sanitized_input), binding verification to the full provenance chain.

WYRIWE β€” Input Provenance

WYRIWE makes IProofVerifier's inputHash auditable. The base interface commits to a value β€” WYRIWE commits to how that value was derived, binding the signed inference receipt to the provenance chain that produced its input.

Triple-hash chain (normative in WYRIWE):

rawInputHash             = keccak256(raw_user_input)
sanitizationPipelineHash = keccak256(sanitization_spec_cid || rawInputHash)
inputHash                = keccak256(sanitized_input)

sanitizationPipelineHash binds the public pipeline spec to this specific raw input (a pipeline commitment cannot be replayed against a different input), and inputHash commits to the exact bytes the model received. The verification invariant: given the three hashes and the public spec at sanitization_spec_cid, any verifier can recompute the pipeline over the raw input and confirm inputHash β€” no party is trusted to assert it. The sentinel case (no transformation) is a provable claim, not an assumption: sanitizationPipelineHash = keccak256(IDENTITY_SENTINEL_CID || rawInputHash).

Two commitments, two auditor questions (they are complementary, not contradictory): sanitizationPipelineHash proves a specific pipeline was declared and bound to a specific raw input β€” faithful application is the prover's commitment, not something the hash alone enforces. inputHash proves what the model actually received. A conformant implementation produces both; the verification invariant (recompute the pipeline over the raw input, confirm inputHash) is what ties declaration to delivery.

Inner layer. proofSystem() returns "attestation/wyriwe". verify() recomputes the commitment binding {agentId, modelHash, inputHash, outputHash, timestamp}, recovers the signer from the EIP-712 WyriweAttestation signature, and checks it against the ERC-8004 registry declared in agentData β€” pure recomputation, no external state reads beyond the registry check. Layer separation holds: an operator running only the inner layer gets receipt verification; adding the outer layer gets identity-bound accountability.

Judgment as provenance one layer up. The same chain-of-custody shape maps slot-for-slot onto judgment (WYRIWE Β§L4): rawProposalHash ↔ rawInputHash, verdictHash ↔ sanitizationPipelineHash (the verdict is the public spec transforming proposed β†’ permitted), executedActionHash ↔ inputHash. The binding rule carries over unchanged β€” verdictHash = keccak256(verdict_artifact_ref || rawProposalHash) β€” so a verdict cannot be shopped against a different proposal, exactly as a pipeline commitment cannot be replayed against a different input. This is the JudgmentExecutionAttestation used by the Judgment Validator subsection above; the two attestations compose without sharing fields.

Confidence and aggregation. WYRIWE attestations do not carry a verdict score β€” they carry a tamper-evident receipt of what ran. Confidence derivation follows the Type A model established above: a pure function of settled commit-reveal records, no trusted updater, no mutable on-chain score. A bare signed receipt without a settled outcome does not satisfy the Type A invariant; Type B (usage-based) weight carries the same self-dealing risk noted above. Aggregation and weighting live in IAgentVerifier, above the inner layer.

Live proof. Ledger entry 23 (api.babyblueviper.com/ledger/23) is the first production instance of this composition:

StepDetail
IdentityPGA #14, tokenId 14 in the dinamic AgentIdentityRegistry (ERC-8004-style) β€” mainnet ownerOf checkable
ProvenanceNon-sentinel: rawProposalHash β†’ verdictHash β†’ executedActionHash
Verdictapprove_with_concerns, confidence 0.85 β€” concerns: identity-binding, impersonation
TimingVerdict timestamp strictly before execution timestamp β€” ordering invariant independently verifiable
AnchorEntry 23 verdict event id β†’ TruthAnchorV1 (proof anchoring) β€” anchor leg pending

Judgment Validator β€” attestation/judgment

A judgment claim asserts that a validator examined a proposed action and issued a verdict on its soundness β€” in settings where the right answer is not computable at decision time (pre-trade review, contribution-reward evaluation, compliance sign-off). Verification establishes authenticity, never soundness: verify() answers "is this authentically the named validator's verdict over exactly this input?" It MUST NOT be read as "the action is sound."

Inner-layer wiring. proofSystem() returns "attestation/judgment". verify() checks the validator's signature over the canonical payload binding {verdict object, inputHash, validator identity}; the scheme is described by proofProfile() and address-scoped by the outer agentProofProfile commitment (the production reference uses schnorr over a Nostr event). outputHash commits to the verdict object:

{
  "verdict": "approve | approve_with_concerns | reject",
  "confidence": 0.85,
  "issues": ["<machine-readable concern>", "..."]
}

Judgment verdicts are trinary, not binary. codeMeasurement MUST be absent for judgment claims β€” absent, not zero-filled.

committed_at and judgment_type (REQUIRED for attestation/*). Two fields make the commit-before-outcome guarantee checkable rather than assumed:

  • committed_at β€” the timestamp at which the verdict was committed, sourced from the proof anchoring proofHash leg (where proofHash is the verdict event id). This is a distinct commitment target from the OCP anchor, which commits the input (the prompt/task β€” when it was issued). The two anchor different things at different points in the chain β€” input-commitment (OCP) -> verdict-commitment (proof anchoring) -> settlement β€” and a verifier MUST treat them as separate, never substituting one for the other. committed_at MUST be provably prior to the outcome the verdict is graded against.
  • judgment_type β€” discriminates what committed_at is checked against:
    • outcome_verifiable (Type A): committed_at MUST predate the realized outcome's settlement.
    • consensus_weighted (Type B): committed_at MUST predate the reveal.

The field is universal across attestation/*; the judgment_type discriminator carries the per-type reference point β€” which is why the two are specified together.

How the deterministic bool emerges. Judgment is non-deterministic; the interface output is not. Aggregation lives in IAgentVerifier (never the inner layer), by claim type:

  • Type A β€” outcome-verifiable. Validator confidence weights are derived as a pure function of settled commit-reveal records (verdicts committed on-chain before outcomes, revealed after) β€” recomputable by any party from chain state, with no updater-trust problem. Outcome-oracle dependency, stated explicitly: this recomputation is trustless only where outcomes settle on-chain; off-chain-settled outcomes (an exchange account, a counterparty system) require an outcome oracle, and the aggregation inherits that oracle's trust assumptions. Consumers MUST be able to distinguish on-chain-settled from oracle-attested records.
  • Type B β€” non-verifiable. Usage-based weight MUST count distinct counterparties with their own at-stake identity, not call volume, and SHOULD be capped relative to outcome-derived weight (the self-dealing guard).

The resulting bool gates settlement exactly like any other backend (see the ERC-8183 wiring above): authenticity from the inner layer, threshold over derived weights at the outer.

recordPointer accountability invariants (normative for judgment claims; the record format is reference data):

  1. Losses MUST be present β€” a record showing only wins is marketing, not accountability.
  2. Outcomes MUST settle somewhere the validator cannot edit.
  3. The pre-outcome timestamp (committed_at, above) MUST be anchored at issue time to a system the validator does not control. For the verdict this is the proof anchoring proofHash leg β€” NOT the OCP input anchor (which commits the input, not the verdict; see committed_at above). It is the same kind of primitive as OCP's observation commitment, applied one layer up to the verdict.

Commitment and outcome evidence MUST be separately addressable: {recordPointer}/commitment and {recordPointer}/outcome.

Execution binding (optional). Where the judged action is subsequently executed, the reviewed→executed chain of custody binds with the WYRIWE L4 attestation — JudgmentExecutionAttestation(... rawProposalHash, verdictHash, executedActionHash ...) — committed at verdict time, revealed post-execution.

Production reference. https://api.babyblueviper.com/ledger β€” a judgment validator running against real capital: 26 signed entries, 10 wins / 9 losses across 19 settled outcomes (losses published by design); every verdict relay-anchored at issue time with per-entry retention status; evidence-separability sub-paths live; first on-chain commit-reveal settlement of a judgment attestation on Sepolia (GenericCommitRevealSettler, entry 19 β€” committedAt < revealedAt checkable on-chain); and a live cross-stack trace (entry 23) binding a registry identity through a signed pre-action verdict to an executed action via the triple-hash. Offered as reference data, not a required format.


Security Considerations

IProofVerifier trust boundary

This ERC defines the interface but does not vet IProofVerifier implementations. Agent developers MUST NOT assume a given IProofVerifier is secure simply because it satisfies the interface. Before configuring a backend, agent developers SHOULD independently assess its cryptographic assumptions, audit status, and known attack surface. A backend that always returns true, uses a broken circuit, or accepts replayed attestations will silently undermine the entire verification chain.

Trust chain

settlement contract β†’ IAgentVerifier β†’ IProofVerifier[]

Each link SHOULD be audited independently. The same trust boundary applies at every layer: settlement contracts MUST NOT assume a given IAgentVerifier is secure simply because it satisfies the interface, just as agent developers MUST NOT assume the same of IProofVerifier. IAgentVerifier does not expose its internal verifier set; consumers requiring auditability SHOULD verify the full deployment configuration at setup time.

Replay and repeated execution policy

IProofVerifier does not define replay protection. A valid proof over (inputHash, outputHash) may replay across contexts if the underlying proof system does not bind to a chain ID, contract address, or nonce.

This ERC does not impose universal single-use semantics. An IAgentVerifier MAY reject every duplicate attempt, allow retry after a failed attempt, allow repeated successful verification, or apply another application-defined policy. The implementation MUST document its selected policy. Where single-use semantics are required, they SHOULD be enforced by IAgentVerifier with a stable application-defined key such as taskId or (taskId, agentId).

verificationDigest is an audit commitment, not a replay nullifier: because it includes block.timestamp, repeated calls can produce different digests. Callers MUST NOT infer single-use behavior from the interface or from digest uniqueness.

Verification commitment self-containment

VerificationCompleted is designed to be independently verifiable without querying any contract state. On-chain state is mutable β€” contracts may be upgraded, storage may change, and authorization rules may evolve. If reconstructing or confirming a verificationDigest required reading live contract state, historical records would become unverifiable as that state changed. The event carries all preimage fields alongside the digest precisely so that any observer can verify a past event against the immutable log alone, independent of current contract state.


Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth