We’ve been running ERC-8004 in production since April 2026 and wanted to open a
discussion on an architectural decision that the current draft leaves unresolved: should
the reference deployment model be a per-chain singleton or a per-collection
factory?
The draft currently describes registries as per-chain singletons. We shipped the
opposite, and want to explain why, what the tradeoffs are, and whether the spec should
accommodate both patterns explicitly.
What we built
AgentIdentityRegistryFactory deploys one AgentIdentityRegistry per NFT collection.
The collection address is set as an immutable in the registry constructor and never
changes. Three collections are live on mainnet, each with its own independently-owned
registry.
The full factory and registry source is readable on Etherscan (links at the bottom).
The key surface area:
// Factory — one deployment per collection, owner-controlled
contract AgentIdentityRegistryFactory is Ownable2Step {
mapping(address => address) public registryOf;
function deployRegistry(
address sourceCollection,
RegistryConfig calldata cfg // name, symbol, mintPrice, treasury, royalties
) external onlyOwner returns (address registry);
function lookup(address sourceCollection)
external view
returns (address registry, bool isDelisted);
}
// Registry — per-collection, bound at construction
contract AgentIdentityRegistry is ERC721, EIP712 {
address public immutable boundCollection; // set once, never changes
// mints agent NFT after live ownership check of sourceTokenId in boundCollection
function registerWithSource(uint256 sourceTokenId) external payable;
// agent hot wallet — separate from NFT owner, EIP-712 + IERC1271
function getAgentWallet(uint256 agentId) external view returns (address);
// arbitrary key/value metadata per agent (capabilities, endpoints, services)
function getMetadata(uint256 agentId, string calldata key)
external view returns (bytes memory);
}
Consumer lookup (two hops, but cacheable):
factory.lookup(collectionAddress) → (registryAddress, isDelisted)
registry.bindingOf(agentId) → (collectionAddress, tokenId)
The boundCollection immutable is the binding. There is no setBinding() —
the architectural constraint is the guarantee.
The core architectural argument
Per-chain singleton pros:
-
Single canonical address - consumers always know where to look
-
Indexers watch one contract
-
Simpler cross-collection queries
Per-chain singleton cons:
-
A single registry owner controls agent identity for the entire chain
-
One vulnerability or admin key compromise affects every collection
-
Collections can’t have independent economics (mint price, royalties, treasury)
-
Delisting one bad collection requires touching the global state
Per-collection factory pros:
-
Binding is a structural invariant - there is no
setBinding()because there’s nothing
to set. If an agent exists in registry X, it is bound to collection X, always. -
Registry ownership is scoped to the collection admin. A collection’s registry operator
cannot affect another collection’s agents. -
Independent economics per collection: mint price, treasury, royalty receiver, credits.
-
Isolation: a compromised or misbehaving collection can be delisted from the factory
frontend without affecting other registries - on-chain agents remain valid. -
tokenToAgentIdreverse lookup is trivial - each registry already owns exactly one
collection, so a mapping from sourceTokenId to agentId is straightforward without
cross-collection ambiguity.
Per-collection factory cons:
-
No canonical single address - consumers need a factory lookup step
-
Indexers must watch the factory for
RegistryDeployedevents to discover all
registries -
Cross-collection agent queries require aggregation across multiple contracts
The ERC-8217 connection
This debate is directly relevant to ERC-8217,
which is standardising agent-NFT identity bindings. ERC-8217 currently assumes a
per-agent binding contract (one binding contract per agent). The factory pattern suggests
a third model: per-collection binding, where the registry itself is the binding contract.
In our implementation, ERC-8217’s IAgentBindings interface is satisfied by the
per-collection registry:
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);
}
Binding is established atomically at registerWithSource(tokenId) and is irrevocable by
construction. No separate binding step, no separate binding contract per agent.
What we’re asking
-
Should ERC-8004 explicitly define both deployment patterns (singleton and factory) as
valid implementations, with a minimal interface requirement that both satisfy? -
Should the factory address be the canonical discovery mechanism — i.e.,
factory.lookup(collection)as the standard consumer entrypoint rather than a
hardcoded registry address? -
Is the isolation model (per-collection ownership, independent economics) worth the
indexer complexity cost?
We’re not arguing the singleton model is wrong - there are valid use cases for a shared
registry. But the spec should acknowledge both patterns rather than implying singletons
are the only valid deployment.
Open for pushback.
Contracts (all verified on Etherscan — source readable there):
-
Registry (Pixel Goblins):
0xe0454dfa17a57a84c3e0e2dbfda5318cbbe91e2c -
Registry (Goblinarinos):
0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d -
Registry (Sproto Gremlins):
0x608520f2725c312092cc837a05a5a1c504ab26f4
Live gateway: https://gateway.ensub.org
Live attestations: https://gateway.ensub.org/admin/attestations
Agent manifest (example): https://gateway.ensub.org/agent/0xe61f5a6783ae09949b9a1b6821b68f89c0d7bb2d/5/.well-known/agent.json
Related threads: