ERC-8273: Attestation-Gated Agentic Actions

Summary

ERC-8273 defines a standard interface for an on-chain Agent Attestation Registry — a per-operation, transaction-scoped attestation primitive designed for AI agent systems.

The core idea: instead of asking “which agent is this?” (identity), we ask “does the agent executing this on-chain operation have the required qualification?” (per-operation attestation). Identity and per-operation attestation are complementary layers that should not be conflated.

Problem

On-chain agent systems today lack a standardized way to gate operations based on per-action attestation. An identity NFT (e.g. ERC-8004) can tell you that “this address claims to be an agent,” but cannot tell you anything about a particular operation — whether the agent is authorized, whether its runtime environment is trustworthy, or whether an action was autonomous vs. human-directed.

These questions all share the same structural need: an attestation record that is bound to a concrete operation and queryable on-chain within the issuing transaction.

Design

  • Atomic execution model: An authorized Attestor calls attestAndCall, which writes the attestation to transient storage (EIP-1153), executes the action through an execution profile, and relies on the EVM to automatically clear authorization at transaction end. No persistent active authorization ever exists.

  • Two authorization axes: capability (coarse-grained class, e.g. keccak256("DEFI_ACCESS_V1")) + actionDigest (fine-grained action fingerprint binding target contract, selector, arguments, and nonce). actionDigest = 0 means capability-only mode.

  • Execution profiles: Abstracts how the Registry causes the target call to originate from the agent wallet. Two profiles are defined:

  • Direct Wallet (AGENT_EXECUTE_V1): for ERC-7702 / AA wallets implementing IAgentExecute

  • ERC-4337 UserOperation (ERC4337_USEROP_V1): for existing 4337 wallets via EntryPoint.handleOps

  • Gating primitives: getActiveAttestationByWallet(msg.sender, capability, actionDigest) reverts when no active attestation exists — structurally eliminating the common integration bug of forgetting to check a bool return.

  • ERC-8004 integration: Implementations claiming ERC-8004 support set subjectId = agentId and subjectType = keccak256("ERC8004_AGENT").

Relationship to Existing Standards

  • Atomic execution model: An authorized Attestor calls attestAndCall, which writes the attestation to transient storage (EIP-1153), executes the action through an execution profile, and relies on the EVM to automatically clear authorization at transaction end. No persistent active authorization ever exists.

  • Two authorization axes: capability (coarse-grained class, e.g. keccak256("DEFI_ACCESS_V1")) + actionDigest (fine-grained action fingerprint binding target contract, selector, arguments, and nonce). actionDigest = 0 means capability-only mode.

  • Execution profiles: Abstracts how the Registry causes the target call to originate from the agent wallet. Two profiles are defined:

    • Direct Wallet (AGENT_EXECUTE_V1): for ERC-7702 / AA wallets implementing IAgentExecute
    • ERC-4337 UserOperation (ERC4337_USEROP_V1): for existing 4337 wallets via EntryPoint.handleOps
  • Gating primitives: getActiveAttestationByWallet(msg.sender, capability, actionDigest) reverts when no active attestation exists — structurally eliminating the common integration bug of forgetting to check a bool return.

  • ERC-8004 integration: Implementations claiming ERC-8004 support set subjectId = agentId and subjectType = keccak256("ERC8004_AGENT").

Open Questions & Feedback Welcome

  1. Execution profiles: Are the two defined profiles (Direct Wallet + ERC-4337) sufficient for the current ecosystem, or should we define additional ones (e.g. ERC-7579 module execution)?

  2. Capability namespace: Should there be a registry or convention for well-known capability identifiers, or is the current flat namespace sufficient?

  3. Cross-registry composability: The current design allows multiple independent Registry deployments. Should the standard define any mechanism for cross-registry recognition?

  4. actionDigest derivation: The derivation rule is agreed between Attestor and DApp. Should the standard be more prescriptive about the derivation scheme?

We look forward to your feedback and discussion.


PR: Add ERC: Attestation-Gated Agentic Actions by xrqin · Pull Request #1770 · ethereum/ERCs · GitHub

3 Likes

Good timing on this, we’ve been building a post-execution attestation stack that sits naturally on top of exactly what ERC-8300 is doing, and a few of the open questions you raised have concrete answers from that work.

On the pre/post execution pairing

ERC-8300 answers “can this agent do this?” before execution. What’s missing after the transaction clears is “did this agent do this, and can anyone verify it?”, the transient storage model is elegant for authorization but leaves no permanent audit trail by design. That gap is what ERC-8263 (anchorProof(agentId, proofHash)) and OCP (record(input_hash)) address on the write and read sides respectively. The two approaches stack cleanly:

ERC-8300   attestAndCall → capability + actionDigest gate (pre-execution, transient)
ERC-8263   anchorProof   → proofHash committed on-chain (post-execution, permanent)
OCP        record        → input_hash anchored on L2 (post-execution, independently verifiable)


On actionDigest derivation (your open question)

We’ve been using SHA-256 of the sanitized input as the operation fingerprint — input_hash in our attestation schema. The “sanitized” qualifier matters: if a sanitization pipeline ran between the user and the agent, the digest is over the post-pipeline input, not the raw input. A separate raw_input_hash captures the original. When no sanitization applies, both are equal and a sentinel hash (sanitization_pipeline_hash = IDENTITY_SENTINEL) confirms the pass-through provably.

This maps directly to your actionDigest: actionDigest = SHA-256(sanitized_input). When actionDigest = 0 (capability-only), that corresponds to our action_type-level classification without a specific input fingerprint.

On cross-registry composability

ERC-8004 is the identity anchor in our stack — registry.getAgentWallet(agentId) gives the canonical signing key for any registered agent. ERC-8300 already references ERC-8004 which is the right call. One encoding note if it’s useful: ERC-8263 uses bytes32 agentId, ERC-8004 uses uint256 — bridge is bytes32(uint256(agentId)), worth being explicit in the spec so verifiers don’t have to guess.

Live reference implementation with input_hash, l4_signature, l3_tx all wired:

GET https://gateway.ensub.org/agent/0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d/5/attestations?limit=1


https://github.com/Echo-Merlini/hbs-attestation-poc

dinamic.eth / TMerlini

1 Like

Thanks @TMerlini, this is very useful — especially the concrete implementation experience.

On the pre/post pairing

Agreed on the layering. One clarification: ERC-8300 does persist an immutable AttestationRecord (with status = Recorded) for every attestAndCall invocation — what’s transient is only the active authorization, not the audit trail. So after the transaction clears, you still have an on-chain record with attestor, capability, actionDigest, evidenceHash, wallet, and issuedAt, queryable via getAttestation(attestationId). The Attested event is also indexable.

That said, this audit record is intentionally minimal — it records that an attestation was issued, not what happened during execution. Your ERC-8263 / OCP stack fills exactly that gap (post-execution proof of what the agent actually did). The two layers are complementary:

ERC-8300   "was the agent authorized?"     (persistent record + transient gate)
ERC-8263   "what did the agent produce?"    (permanent proof commitment)
OCP        "can anyone independently verify?" (L2 anchoring)

On actionDigest derivation

Interesting approach with SHA-256 over sanitized input. A few observations:

  • ERC-8300’s reference implementation uses keccak256 to stay native to the EVM (no precompile needed for on-chain recomputation by the DApp). If a DApp needs to recompute actionDigest at the gating point to call getActiveAttestationByWallet(msg.sender, capability, recomputedDigest), keccak256 is significantly cheaper on-chain than SHA-256.
  • The sanitized vs. raw distinction is a good pattern. In ERC-8300’s framing this could be expressed by encoding the sanitization semantics into the capability namespace — e.g. keccak256("DEFI_SWAP_V1:scheme=SANITIZED_INPUT_V1") — so the DApp and Attestor are explicitly aligned on which derivation rule applies.
  • We’ve intentionally left the derivation scheme open (your open question #4) rather than mandating one formula. Your experience suggests that different use cases genuinely need different schemes, which validates the current design of encoding the scheme into capability.

On the uint256 vs bytes32 encoding

Good catch. ERC-8300’s SubjectRef uses uint256 subjectId + bytes32 subjectType, which is directly aligned with ERC-8004’s uint256 agentId. For standards using bytes32 agentId (like ERC-8263), the bridge uint256(bytes32(agentId)) works as you noted. We could add a brief note in the spec clarifying this mapping — or alternatively, this is exactly the kind of thing a future namespace registration mechanism could standardize.

Will check out the reference implementation. Thanks for sharing.

2 Likes

@xrqin thanks for the correction on the audit trail.

The permanent AttestationRecord per invocation changes the picture meaningfully, the stack is actually cleaner than I described: ERC-8300 gives you both the persistent authorization record and the transient gate, ERC-8263 gives you the execution proof. No gap, fully complementary.

On the keccak256 vs SHA-256 distinction, this is fine architecturally, they’re operating at different layers on different data. ERC-8300’s actionDigest = keccak256(action data) is a pre-execution authorization hash optimized for on-chain cost. Our post-execution stack uses input_hash = SHA-256(sanitized_input) as the commitment hash in ERC-8263 and OCP. They were never meant to be the same value.

The one place worth being explicit in the spec: when an implementation bridges ERC-8300 pre-execution gating with ERC-8263 post-execution commitment, verifiers need to know they’re comparing keccak256(action data) against SHA-256(sanitized_input) , not the same hash of the same input. A brief note in the Rationale that actionDigest and input_hash are distinct primitives serving different layers would prevent silent mismatches in cross-standard implementations.

dinamic.eth / TMerlini

@xrqin this is a clean primitive — per-operation attestation sits at exactly the right abstraction level between identity and execution.

Two thoughts from the ERC-8275 side (Agent Service Discovery + Escrow):

1. Discovery → Attestation pipeline
ERC-8275 lets a client discover a provider agent, fund an escrow, and receive service output. The missing piece is pre-execution qualification — once the client finds a provider via 8275 discovery, how do they verify the provider can actually do the job? ERC-8300’s AttestationRecord fills that gap cleanly: after discovery (8275), before escrow funding (8275), query the attestation registry (8300) for the required qualification.

2. Post-execution attestation as reputation input
@TMerlini mentioned the post-execution attestation stack — this is where it gets interesting. After a job completes through 8275 escrow, the evaluator’s attestation (or the provider’s self-attestation with counter-signing) becomes a permanent record via 8300. That record feeds back into the discovery layer: next client queries 8275 → sees this provider has N verified completions via 8300 → funds with higher confidence.

The layering is natural:

One question on scope: the current spec defines AttestationRecord per invocation. For batch operations (e.g., a provider executing 50 inferences across 50 escrows), does each get its own AttestationRecord, or is there a batched attestation model? This matters for gas efficiency when 8275 + 8300 are composed at scale.

1 Like

The batch question is the right one to solve early, and the answer is Merkle batching.

Each action in a batch gets a full L4 EIP-712 attestation off-chain (one per action, individually verifiable via /verify/:inputHash). On-chain, only the Merkle root of all action hashes is posted to AttestationIndex one transaction regardless of batch size.

Batch of N actions:
  action_1 → input_hash_1, l4_sig_1
  action_2 → input_hash_2, l4_sig_2
  ...
  action_N → input_hash_N, l4_sig_N

  Merkle root = hash(input_hash_1, input_hash_2, ..., input_hash_N)
  → one on-chain record


Any individual action can be proven against the root with a Merkle inclusion proof. A dispute on action_3 only requires the proof path for action_3 - no need to re-execute or re-attest the whole batch.

For the ERC-8275 + ERC-8273 + ERC-8300 composition:

  • Discovery (8275): find the node

  • Execution (8300): run the batch

  • Attestation (8273): L4 signs each action off-chain, Merkle root goes on-chain

  • Reputation (8275 feedback): each individual action’s off-chain attestation is queryable for future discovery. The on-chain root is just the gas-efficient anchor.

The pre-execution qualification check you describe maps cleanly here too, a client queries the attestation registry for the node’s historical Merkle roots, spot-checks inclusion proofs on past actions, funds the escrow. No need to trust the node’s self-reported reputation.

Live reference, individual action attestation already running:
https://gateway.ensub.org/agent/verify/758d61f26a44448384e5c4468a0dcb7a2abe456067b0f7b505bc28b9411fe931

This is not yet specified in ERC-8273 v0.1, worth adding as a batch profile.

@TMerlini — Merkle batching with EIP-712 per-action attestations is the right primitive. This maps cleanly onto 8275’s architecture in two places:

1. Escrow settlement at scale
A provider fulfilling N escrows in a batch posts one Merkle root to the AttestationIndex. The 8275 escrow contract’s hook accepts a Merkle inclusion proof against that root — verifying one escrow without touching the other N-1. Gas-efficient and dispute-friendly.

2. Discovery ranking from verified history
The 8275 registry queries AttestationIndex for a provider’s Merkle roots → resolves inclusion proofs for individual completions → derives a reputation signal. Same root, two consumers: escrow settlement (8275) and discovery ranking (8275).

One implementation question: would the Merkle root’s on-chain record include a that links back to the individual escrow IDs on 8275? That cross-reference is needed for the hook to know which Merkle root to verify against.

1 Like

Thanks @Panini for the question on batch operations.

ERC-8273’s current design is one attestAndCall = one AttestationRecord. This is intentional — atomicity and simplicity are core to the transient storage model.

That said, batching is already supported within a single attestAndCall via the execution profile. The Direct Wallet profile decodes exec.data as (AgentCall[] calls, bytes authData) — that calls array can contain multiple target operations. So a single attestation can gate a batch of actions executed in one transaction:

Attestor -> attestAndCall(capability, actionDigest, ...)
  -> wallet.executeFromRelayer([call1, call2, call3], authData)
      -> DApp1.action()
      -> DApp2.action()
      -> DApp3.action()

The actionDigest binds the entire batch: keccak256(abi.encode(calls, attestationId)). If any call in the batch fails, the whole transaction reverts.

This keeps the registry simple (one record, one transient slot, one event) while still supporting multi-action batches at the execution layer. Gas cost scales with the number of target calls, not the number of attestation records.

xrqin —

The actionDigest binding the full batch is the right primitive. One question worth considering for implementers: once the attestation executes and the transient slot clears, what’s the independently verifiable record that this specific batch ran against this specific digest?

OCP’s proof envelope can anchor that — the actionDigest committed on-chain, verifiable from raw ledger data without trusting the attesting system. Pairs naturally with the ERC-8263 commitment surface if the profile needs portable verification above the gate layer.

Worth a note in the spec for implementers who need audit trails.

— Damon

1 Like

@Panini the cross-reference belongs in the EIP-712 leaf, not in a separate on-chain index.

Each leaf encodes the escrow ID as a typed field:

keccak256(abi.encode(
  escrowId,       // ERC-8275 escrow reference
  actionDigest,   // keccak256 of the completed action
  agentId,        // ERC-8004 identity anchor
  timestamp
))


The settlement hook flow is then:

  1. Hook receives escrowId at claim time

  2. Provider submits (merkleRoot, proof, leaf) alongside the claim

  3. Hook reconstructs the expected leaf from the known escrowId + submitted fields

  4. Verifies inclusion against the root posted to AttestationIndex

  5. Match → release funds; no match → revert

No separate escrowId → root mapping needed on-chain. The binding is structural, a proof valid for escrow A cannot be reused for escrow B because the leaf hash changes with the escrowId field. The root is the anchor; the leaf is the claim.

One thing worth specifying in 8273 v0.1: the leaf schema for the batch profile should be a first-class typed struct (EIP-712 domain + struct hash), not left to implementation. That way ERC-8275 settlement hooks have a stable surface to verify against without trusting the provider’s serialization.

Thanks @Damonzwicker.

The on-chain audit trail is already there. Every attestAndCall emits an Attested event with attestationId, wallet, capability, actionDigest, subjectHash, attestor, and evidenceHash — all indexed and permanently queryable from raw ledger data. The persistent AttestationRecord is also stored on-chain and retrievable via getAttestation(attestationId).

So after the transient slot clears, you can still independently verify: which wallet was attested, under what capability, with what actionDigest, by which attestor, at what time. No trust in the attesting system required — just read the event logs or call the view function.

That said, ERC-8273 records that an attestation was issued and what it authorized, not what the execution produced. If the use case requires anchoring execution outputs or results independently, that’s where post-execution commitment layers like OCP or ERC-8263 add value on top.

2 Likes

xrqin —

That’s the right boundary. ERC-8273 covers the attestation record — what was authorized and by whom. OCP covers the execution output — what was produced and that it matches the committed digest.

The two compose cleanly: gate the action with ERC-8273, anchor the result with OCP. The full audit trail covers both sides.

— Damon

2 Likes