Add ERC 8217: Agent NFT Identity Bindings

,

Summary

Adds a draft ERC for binding ERC-8004 agent identities to external NFT/token contracts.

The draft standardizes:

  • the reserved metadata key agent-binding

  • the binding metadata byte format

  • token standard enum values for ERC-721, ERC-1155, and ERC-6909

  • compact token id encoding with a length byte

  • a minimal verification interface via bindingOf(uint256 agentId)

Verification

Clients verify a binding by:

  1. reading agent-binding from the ERC-8004 metadata

  2. decoding bindingContract, tokenStandard, tokenContract, and tokenId

  3. calling bindingOf(agentId) on bindingContract

  4. checking that the returned binding matches the metadata

Controller semantics are intentionally out of scope.

Update on the draft spec:

agent-binding metadata is now minimal. The registry stores only the 20-byte binding contract address under agent-binding. We no longer duplicate tokenStandard, tokenContract, or token id in that payload.

Why: Those fields are the responsibility of the binding contract. Clients read bindingOf(agentId) on the address from metadata and treat that return value as the canonical Binding. That avoids drift if binding logic or represented token data changes, and keeps a single source of truth onchain.

Spec changes: Verification flow, examples, test cases, and rationale are aligned with this layout. Security section now states explicitly that trust and verifiability sit in the binding contract’s code—bindingOf is only as strong as that contract—and suggests clients assess that contract before relying on its return values.

Indexer feedback on ERC-8217 — three suggestions for robustness

gm gm, I’ve been working on indexing support for ERC-8217 and wanted to share some feedback from the implementer side. These are things that would meaningfully help marketplaces and indexers support the standard correctly. Three concrete suggestions:


1. Mandate a Bound event on IERCAgentBindings

The spec defines a clean verification interface but no required events. From an indexer’s perspective, this means there’s no standard way to detect new bindings reactively. You either know every possible binding contract address upfront, or you poll. This makes general-purpose support fragile.

Adapter8004 happens to emit an event, but a different compliant implementation could be invisible to indexers entirely.

Suggest adding to IERCAgentBindings:

event AgentBound(
  uint256 indexed agentId,
  uint8   indexed standard,
  address indexed tokenContract,
  uint256 tokenId
);

Any compliant implementation MUST emit this on binding registration.


2. Add a reverse lookup: tokenToAgentId

The spec is agent-first: agentId -> bindingOf() -> token. But NFT marketplaces and wallets approach from the token side. Given (chain, tokenContract, tokenId), there is currently no standard way to know if a binding exists. Without a reverse lookup, the only practical path is event log indexing (fine once #1 is resolved, but not sufficient on its own for on-read enrichment or backfills).

Suggest adding to IERCAgentBindings:

/// @notice Returns the agentId bound to a given token, or 0 if none.function tokenToAgentId(
  address tokenContract,
  uint256 tokenId
) external view returns (uint256 agentId);

With both AgentBound and tokenToAgentId, the indexing story is clean in both directions.


3. Declare bindings immutable

The spec is silent on whether bindings are mutable. Adapter8004 is UUPS-upgradeable, so a binding could theoretically change. For an indexer, whether a cached binding can go stale is a correctness question that needs an explicit answer.

The Adapter8004 design already implies immutability (“keeps permanent custody”), so suggest making this normative in the ERC: once agent-binding is written for an agentId, it MUST NOT be updated. This simplifies implementations, eliminates cache invalidation logic for indexers, and makes the trust model cleaner. Clients can verify once and rely on the result.


Happy to elaborate on any of these. Great work overall, the minimal metadata payload design is the right call and makes the verification flow clean.

1 Like

We’ve been running a production ERC-8004 implementation and wanted to
share how we handled binding, because the factory pattern we use may offer a simpler
path than a separate binding contract.


The structural binding approach

Our AgentIdentityRegistryFactory deploys one AgentIdentityRegistry per NFT collection.
The collection address is set at deploy time and never changes. This means binding isn’t a
metadata field that can drift or be set incorrectly, it’s a structural invariant:

If an agent exists in registry X, it is bound to collection X. There is no separate
binding step and no possibility of an agent claiming a binding to a different collection.

The registerWithSource(sourceTokenId) call mints the agent NFT and records the
source token ID in one transaction. The binding is established atomically at registration
and is immutable by construction.

This directly addresses Cody’s third request — immutability is not a policy declaration
but an architectural consequence. There is no setBinding() function to protect against.


On the agent-binding metadata key

The refined design (storing only the 20-byte binding contract address) is cleaner than the
initial approach, but it adds an indirection layer that the factory pattern avoids entirely.
With a per-collection registry, the binding contract address IS the registry address —
clients already have it from the ERC-8004 factory lookup:

factory.lookup(collectionAddress) → registryAddress
registry.bindingOf(agentId)       → (collectionAddress, tokenId)


No separate binding contract needed. The registry already holds this information.


Supporting all three indexer requests

We’d advocate for adding these to the AgentIdentityRegistry interface directly rather
than to a separate binding contract:

1. AgentBound event, agree this is necessary for reactive indexing. In the factory
pattern it fires at registerWithSource() time:

event AgentBound(uint256 indexed agentId, address indexed tokenContract, uint256 tokenId);


2. tokenToAgentId(address tokenContract, uint256 tokenId) — this is a natural fit for
the per-collection registry since every agent’s source token is already stored. A reverse
lookup mapping would make marketplace integrations straightforward without hitting an
off-chain gateway.

3. Immutability — in the factory pattern this is already guaranteed. For implementations
using the agent-binding metadata key, an explicit AgentBound event with no corresponding
AgentUnbound event is probably sufficient to signal intent to indexers.


Proposal

Rather than a separate binding contract per agent, ERC-8217 could recognise the
factory-deployed per-collection registry as a compliant binding implementation, with
the minimal interface requirement being:

interface IAgentBindings {
    event AgentBound(uint256 indexed agentId, address indexed tokenContract, uint256 indexed tokenId);
    function bindingOf(uint256 agentId) external view returns (address tokenContract, uint256 tokenId);
    function tokenToAgentId(address tokenContract, uint256 tokenId) external view returns (uint256 agentId);
}


Implementations that deploy per-collection registries satisfy these by construction.
Implementations that want per-agent flexibility can use the separate binding contract
approach. Both are valid, ERC-8217 defines the interface, not the architecture.

Live on mainnet: AgentIdentityRegistryFactory at 0xc2bb6502a7d8ee3cdb2f96508d6cdf426aa2858f

Thanks for the detailed feedback.

I updated the draft to address two of the three points:

  1. Required binding event

I added an AgentBound event to IERCAgentBindings, with a small adjustment to the shape:

event AgentBound(
    uint256 indexed agentId,
    TokenStandard indexed standard,
    address indexed tokenContract,
    uint256 tokenId,
    address registeredBy <-- added
);

The spec now requires implementations to emit this event when the binding is first written.

  1. Binding immutability

I also added immutability. Once agent-binding is written for an agentId, implementations MUST NOT update the stored binding contract address or the Binding returned by bindingOf(agentId). The draft also calls out that upgradeable implementations MUST preserve this across upgrades.

On the reverse lookup suggestion, I think event indexing is the better fit for the standard. A required tokenToAgentId view would force every implementation to keep a separate onchain reverse mapping, which adds gas/storage cost to the registration path. Since the new AgentBound event includes agentId, standard, tokenContract, and tokenId, indexers can build the reverse lookup offchain without imposing that extra storage requirement on all implementations.

So the current direction is: keep the onchain interface minimal and agent-first, require the event for indexers, and rely on indexed logs for token-to-agent discovery.

1 Like

This contract is not verified, so I can’t see the code.

Are you registering in the standard ERC-8004 registry on Etheruem L1?

0x8004A169FB4a3325136EB29fA0ceB6D2e539a432

1 Like

On the updated spec

The AgentBound event with registeredBy and the immutability mandate are the right
calls. The registeredBy field is a useful addition, adds attribution without changing
the binding semantics.


On tokenToAgentId

Understood on the storage cost reasoning. One practical note: event indexing means a
consumer needs an active indexer to answer “which agent owns this NFT?” — there’s no
synchronous on-chain path for light clients or contracts querying each other. If
on-chain reverse lookup is a non-goal for ERC-8217, that’s a valid scope decision. It
might be worth noting explicitly in the spec that reverse lookups are expected to be
resolved off-chain via event indexing, so integrators know that’s intentional and not
an oversight.

Clarifying our contract

Our factory at 0xc2bb6502a7d8ee3cdb2f96508d6cdf426aa2858f is not the shared ERC-8004
registry at 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432. We took a different
architectural approach: the factory deploys a separate AgentIdentityRegistry per NFT
collection at registration time. The collection address is set as an immutable at deploy
time, binding is structural, not a metadata field.

The agent NFT minted at registerWithSource(tokenId) is inside a registry that is
already scoped to one collection by construction. No setBinding() exists because
there’s nothing to set — the registry’s collection scope is the binding.

This is ERC-8004 compliant (the registry implements the same registerWithSource,
ownerOf, getMetadata interface) but doesn’t share a global registry address.

Whether ERC-8217 wants to accommodate both deployment patterns, shared registry with
metadata-based binding, or per-collection factory with structural binding, is the open
question worth resolving in the spec.

The AgentBound event in our pattern fires from the per-collection registry at
registerWithSource() time, matching the interface you’ve now mandated. Source code is
at github.com/Echo-Merlini/ens-dynamic-kit altough its still in review for Public Release Audit

To answer your question directly, no, we’re not using the shared ERC-8004 registry at
0x8004A169FB4a3325136EB29fA0ceB6D2e539a432. We’ve been running a separate factory
pattern and the contracts weren’t verified yet. They are now.

Factory (deploys one registry per collection):
0xc2bb6502a7d8ee3cdb2f96508d6cdf426aa2858f
https://etherscan.io/address/0xc2bb6502a7d8ee3cdb2f96508d6cdf426aa2858f#code

Deployed registries:

The structural binding is in AgentIdentityRegistry: boundCollection is set as an
immutable in the constructor and never changes. The factory’s deployRegistry() enforces
one registry per collection address — registryOf[sourceCollection] must be zero before
a new one can be deployed.

The AgentBound event and bindingOf() / tokenToAgentId() interface you’ve landed on
maps cleanly onto this pattern. We’d emit AgentBound from registerWithSource() — the
binding is established atomically at that point and is irrevocable by construction.

There is nothing preventing a NFT from being bound to more than one ERC-8004 registration. So it really only works one way, from 8004 token to NFT, and not the other way around.

If you are interested I also wrote ERC-8122, for exactly this purpose. It’s a much more light weight design using ERC-6909 instead of ERC-721. I also have some code here:

1 Like

The one-way concern is valid for a shared singleton registry, if multiple agents can register against the same source NFT, tokenToAgentId has no canonical answer.

The per-collection factory model resolves this by construction. Each registry is bound to exactly one collection via an immutable set at construction, there is no setBinding(). Within that registry, registerWithSource(sourceTokenId) writes to a sourceTokenId → agentId mapping that makes the reverse lookup unambiguous: one source token, one agent, one registry, one collection.

Cross-collection registration is structurally impossible, a second registry cannot claim the same collection address. So tokenToAgentId on our per-collection registry is genuinely 1:1 and bidirectional.

This is exactly the argument we opened in the ERC-8004 factory thread (https://ethereum-magicians.org/t/erc-8004-in-production-per-collection-factory-vs-chain-singleton-which-should-be-the-reference-architecture/ ) the singleton vs. factory debate determines whether bidirectional lookup is even possible at spec level. Worth linking the two discussions.

On ERC-8122 / ERC-6909 - noted, will look at the lighter-weight approach. The ERC-721 choice on our end was driven by wanting the agent NFT itself to be tradeable and ownable, but the binding mechanism is separable from that.

1 Like

Also there is ERC-8048 which turns your NFT into an agent NFT. How is having a factory registry better than ERC-8048?

1 Like

The distinction is where the agent identity lives.

ERC-8048 extends the source token contract with a metadata store — token.metadata(tokenId, “endpoint[mcp]”). That requires the original collection to implement or upgrade to the interface. If the collection contract is already deployed and not upgradeable, ERC-8048 can’t help it.

ERC-8004/8217 creates a separate registry that binds to the source NFT at mint time. The source collection doesn’t implement anything, it doesn’t know the registry exists. Pixel Goblins, Goblinarinos, Sproto Gremlins all have live agent registries on mainnet today and none of those original contracts were touched.

The structural difference: ERC-8048 is metadata-on-token. ERC-8004 is identity-bound-to-token. One extends the NFT, the other creates an independent agent NFT that proves it owns a relationship to the source, without requiring the source to cooperate.

For new collections that control their contract, ERC-8048 is a reasonable metadata layer. For retroactive compatibility with any existing collection, the separate registry model is the only option that doesn’t require touching deployed contracts.

Actually, ERC-8048 also has the ability for the metadata to be stored outside the NFT contract, exactly for this reason.

> Onchain Metadata Contract Reference (Optional Extension)

We will see if this option gets adoption or not, but it’s basically the same thing as your factory idea, in the sense that the sidecar contract has the metadata and is bound to the token in a one-to-one way.

The optional extension narrows the gap, agreed. But there’s still a meaningful difference in where the binding authority lives.

ERC-8048’s external metadata contract is referenced from the token, the tokenURI JSON has to point to it. That means the source contract either needs to support a mutable tokenURI or have been authored with that hook in mind.

For fully on-chain collections this is a hard blocker. Pixel Goblins are 100% on-chain ETHscriptions , the pixel art is the calldata, there is no tokenURI to update. We have a live ERC-8004 agent registry running on that collection today, with zero changes to the original contract. ERC-8048’s external extension can’t do that, it still requires the token to point outward.

ERC-8004 goes the other direction, the registry declares the binding unilaterally, after verifying live ownership of the source token. The source contract is completely unaware. That’s what makes it genuinely broad: ETHscriptions, immutable collections, legacy contracts with no upgrade path, all supported without touching the original.

The other distinction is what the sidecar IS. ERC-8048’s external contract is a key-value metadata store. ERC-8004’s registry is a full agent identity, its own NFT, its own owner, a hot wallet, credits, A2A trust scope. The binding is structural, but so is everything built on top of it.

1 Like

Adapter8004 could have additional types for exotic token types, etc. In my view, the value of ERC-8004 is mostly in the fact that it is a singleton and provides discovery.

The fact that Adapter8004 is also a singleton means that it can have the same properties of providing discoverability.

1 Like

Worth clarifying, our implementation is explicitly not a singleton. That’s the core of the architectural debate we opened in the ERC-8004 factory thread (https://ethereum-magicians.org/t/erc-8004-in-production-per-collection-factory-vs-chain-singleton-which-should-be-the-reference-architecture/).

Discoverability in the factory model doesn’t require a singleton, it requires a factory:

factory.lookup(collectionAddress) → (registryAddress, isDelisted)

One canonical factory address, arbitrary number of collection registries. Consumers always know where to look. Indexers watch the factory for RegistryDeployed events to discover all registries. Discoverability is preserved without concentrating admin control.

The singleton pattern’s discoverability benefit comes at a cost we weren’t willing to accept: one admin key controls agent identity for the entire chain, one vulnerability affects every collection, collections can’t have independent economics or ownership. If Adapter8004 is a singleton, it inherits all of those tradeoffs too.

The interesting question isn’t singleton vs. factory for discoverability, it’s whether the spec should define a standard factory interface so that both patterns are interoperable. That’s what we’re asking in the factory thread. Would value your take there.

I see a tradeoff here. I think the goals of onchain agents are slightly different than NFT tokens themselves. In the end, if the onchain agent concept is successful, it will need to be adopted generally and adapt to a changing technology landscape.

While there is admin risk, the risk is not with the token itself using the “binding” idea. I do think, however, that it makes sense to put the key metadata of the token in the collection NFT token, i.e. ERC-8048. In that way, if anything happens to the ERC-8004 registry, the key metadata about your agent NFT is safe, and it is basically like having something happen to a marketplace. Then you can just use a different marketplace.

Adapter8004 needs to also be upgradable because ERC-8004 is upgradable and could add functions or features that we want to mirror.

1 Like

The resilience argument for on-NFT metadata makes sense for static data, name,
description, bound collection. But it raises a question worth thinking through:
If the agent runtime reads on-chain metadata as trusted context (system prompt,
capability config, endpoint declarations), that data becomes an attack surface.

On-chain metadata is publicly readable. An adversary who can read the tokenURI
can craft inputs designed to exploit what they know about the agent’s
configuration. We saw this pattern cause real damage today, a separate production agent system
(not ours) got exploited via input poisoning precisely because the input it
acted on wasn’t committed or verified.

Our model treats every input as untrusted regardless of source — on-chain or
off. The three-hash commitment (raw_input_hash → sanitization_pipeline_hash → input_hash) makes any modification between receipt and execution detectable.
That provenance chain doesn’t exist if metadata lives on the NFT and gets
ingested directly.

Not arguing against on-NFT metadata for durability, but the runtime that reads
it still needs an input provenance layer. Where does that live in the ERC-8048
model?


You’ve been pulling on exactly the right threads throughout this discussion,
the singleton vs. factory tradeoff, bidirectional lookup, the ERC-8048
comparison, these are the questions the spec needs answered before it
stabilises. Genuinely useful pushback.

Which is why it feels like the right moment to mention: we filed two ENSIPs
today that are a direct response to everything this thread has surfaced.

ENSIP-27 — Agent Card Schema (/.well-known/agent.json): standardises the
document at the end of the discovery chain, including on-chain identity
anchoring via ERC-8004, input provenance fields, and A2A trust scope.
PR: https://github.com/ensdomains/ensips/pull/75

ENSIP-26 contribution — adds erc8004 identity field and
agent-endpoint[card] as a defined protocol value to the existing agent text
records spec.
PR: https://github.com/ensdomains/ensips/pull/76

Both backed by a live production gateway. The questions you’ve been asking here
are exactly the ones the spec design needs to answer, would value your eyes on
the PRs.

1 Like

ERC-8048 is a general solution for onchain metadata for NFTs. The only thing about it related to agents is specified in the example. So the goal is not to tell projects how they should use onchain metadata, but instead to provide a standard for saving it, retrieving it, and indexing it.

Thanks for the heads up! I will check them out and comment on the PRs.

2 Likes

On the question of where the input provenance layer lives in the ERC-8048 model — that’s exactly what OCP addresses at L3.

Regardless of where metadata lives (on-NFT, off-chain, ENS record), the runtime that ingests it still needs to answer: was this the input that actually reached the model, and can that be verified independently?

OCP’s answer is a single committed digest:

raw_input_hash → sanitization_pipeline_hash → input_hash → record(input_hash) on-chain

The on-chain commitment is the anchor. Any modification between receipt and execution produces a different input_hash — detectable by anyone with access to the raw transaction, no trusted party required.

This is chain-agnostic and metadata-source-agnostic. It doesn’t matter whether the agent’s configuration came from an NFT tokenURI, an ENS record, or an off-chain endpoint. Whatever reaches the model gets committed. That provenance chain is what makes the exploit Tiago described detectable after the fact — and preventable with a verifier in the execution path.

The L3 commitment spec and reference implementation are live:

Damon / OCP