EIP.tools

EIP.tools

āš ļø DraftStandards Track: ERC

ERC-8160: Primary Agent Registry

A singleton registry mapping each Ethereum address to a single agent identity, so any address can be resolved to the agent it represents.

Authors
Created2026-02-11
Discussion Linkhttps://ethereum-magicians.org/t/erc-8160-primary-agent-registry/27727
Requires

Markdown

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

EIP-GPT summary

Contents
AbstractMotivationUse CasesSpecificationOverviewStorage EncodingRegistry Interfaceregister(address account, address registry, uint256 tokenId)resolveAgentId(address account) → (address registry, uint256 tokenId)agentData(address account) → bytesisRegistered(address account) → boolAccess ControlRegistrar TypesSelf RegistrarERC-1271 RegistrarSigned RegistrarOwnable RegistrarRole-check RegistrarVerification LoopSingleton DeploymentRationaleBackwards CompatibilitySecurity ConsiderationsCopyright

Abstract

This ERC defines a singleton registry that maps each Ethereum address to exactly one agent identity (a registry address and token ID). It is deployed once per chain, providing a single authoritative source for resolving any address to the agent it represents.

Motivation

AI agents are becoming active participants on Ethereum, executing transactions, interacting with protocols, and managing assets. Each agent operates through an Ethereum address, but an Ethereum address by itself says nothing about the agent behind it. Agent registries exist (such as ERC-8004), but given an address, there is no standard way to determine which agent it represents. Wallets cannot display agent identity, protocols cannot verify counterparties, and indexers cannot map on-chain activity back to agents. Without a canonical registry, each application must build its own lookup, leading to inconsistent and unreliable resolution.

The Primary Agent Registry solves this by providing a singleton contract, deployed to every chain, where each Ethereum address can be linked to exactly one agent identity: a specific token ID in a specific agent registry. This creates a single source of truth for address-to-agent resolution. The permissioned registrar architecture supports multiple verification methods, from simple self-attestation for EOAs to cryptographic proof for smart contract wallets.

Use Cases

  • Signed data and transactions: When an agent signs data or submits a transaction, only the Ethereum address is visible. The registry allows anyone to resolve that address to the agent's identity.
  • Application connections: When an agent connects to a dApp, the application can look up the agent identity from the connected address.
  • Address verification: To prevent multiple agents from using the same Ethereum address, the agent's registered address can be reverse resolved to verify it belongs to that agent.

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.

Overview

The Primary Agent Registry is a singleton contract intended to be deployed at a deterministic address on every EVM chain. It stores a single mapping: address → bytes, where the bytes value packs the agent registry address and token ID. Each Ethereum address has at most one agent registration at a time.

Only addresses with the REGISTRAR_ROLE may write to the registry. This enables a flexible system where different registrar contracts implement different verification methods.

Storage Encoding

Each registration is stored as a packed bytes value:

20 bytes: registry address | 1 byte: ID length | N bytes: token ID
  • The registry address is stored as-is (20 bytes)
  • The ID length (1 byte) indicates how many bytes follow for the token ID
  • The token ID is encoded in big-endian with minimal bytes (no leading zeros). Token ID 0 has length 0 with no following bytes.

Storage slot analysis:

Solidity stores bytes values of 31 bytes or fewer inline in a single storage slot. With 20 bytes for the registry address and 1 byte for the length prefix, token IDs of up to 10 bytes (values up to 2^80 - 1) produce a total of 31 bytes, fitting in a single slot. Larger token IDs spill into additional slots but are still fully supported.

Registry Interface

interface IPrimaryAgentRegistry {
    event AgentRegistered(address indexed account, address indexed registry, uint256 tokenId);

    function register(address account, address registry, uint256 tokenId) external;
    function resolveAgentId(address account) external view returns (address registry, uint256 tokenId);
    function agentData(address account) external view returns (bytes memory);
    function isRegistered(address account) external view returns (bool);
}

register(address account, address registry, uint256 tokenId)

Registers, overwrites, or clears an account's agent identity. MUST only be callable by addresses with REGISTRAR_ROLE. MUST revert if account is the zero address. If registry is the zero address, the registration MUST be deleted (clearing the account's agent identity). Otherwise, the packed encoding MUST be stored as the value for the account key.

resolveAgentId(address account) → (address registry, uint256 tokenId)

Decodes and returns the agent identity for an account. If not registered, MUST return (address(0), 0).

agentData(address account) → bytes

Returns the raw packed bytes for an account. Returns empty bytes if not registered.

isRegistered(address account) → bool

Returns whether the account has a registration. Implementations MUST check that the stored bytes length is greater than zero.

Access Control

The registry MUST enforce role-based access as follows. Two roles are required; their identifiers are left to the implementation (e.g. bytes32 constants).

  • Admin role: The holder(s) of this role MAY grant and revoke the registrar role. There MUST be at least one address with the admin role at all times (e.g. set at deployment).
  • Registrar role: Only addresses with this role MAY call register. The admin role MAY grant or revoke the registrar role.

Implementations MAY use OpenZeppelin Contracts AccessControl or any equivalent mechanism that provides the above semantics. The registry MUST revert if register is called by an address that does not have the registrar role.

Registrar Types

Implementations SHOULD provide one or more of the following registrar contracts, each granted REGISTRAR_ROLE:

Self Registrar

Allows any msg.sender to register their own address. This is the simplest method suitable for EOAs.

function register(address registry, uint256 tokenId) external;

The registrar calls primaryAgentRegistry.register(msg.sender, registry, tokenId).

ERC-1271 Registrar

Allows smart contract wallets to register by providing a valid ERC-1271 signature over the registration data. To clear a registration, pass registry = address(0). The registrar MUST maintain a per-account nonce to prevent signature replay attacks.

mapping(address => uint256) public nonces;

function register(address account, address registry, uint256 tokenId, bytes calldata signature) external;

The registrar MUST verify the signature by calling isValidSignature(hash, signature) on the account contract, where hash is computed as:

keccak256(abi.encodePacked(account, registry, tokenId, block.chainid, address(this), nonces[account]))

The nonce MUST be incremented after each successful register call to prevent replay of previously-approved signatures.

Signed Registrar

Allows an EOA to sign a message off-chain, and then anyone can submit that signature on-chain to register on the EOA's behalf. To clear a registration, pass registry = address(0). The registrar MUST maintain a per-account nonce to prevent signature replay and MUST support an optional deadline for signature expiry.

mapping(address => uint256) public nonces;

function register(address account, address registry, uint256 tokenId, uint256 deadline, bytes calldata signature) external;

If deadline is non-zero, the registrar MUST revert if block.timestamp > deadline. The registrar MUST verify the signature by recovering the signer using ECDSA over an EIP-191 personal_sign hash, where the message hash is computed as:

keccak256(abi.encodePacked(account, registry, tokenId, deadline, block.chainid, address(this), nonces[account]))

The recovered signer MUST equal account. The nonce MUST be incremented after each successful register call to prevent replay of previously-signed messages.

Ownable Registrar

Allows the owner of a contract implementing owner() or getOwner() to register the contract's agent identity.

function register(address contractAddress, address registry, uint256 tokenId) external;

The registrar MUST verify that msg.sender matches the return value of either owner() or getOwner() on the target contract.

Role-check Registrar

Allows the admin or authorized role holder of a contract that exposes a role check to register the contract's agent identity.

function register(address contractAddress, address registry, uint256 tokenId) external;

The registrar MUST verify that msg.sender is authorized to act for the contract (e.g. by calling a function such as hasRole(adminRole, msg.sender) or owner() == msg.sender on the target contract, when that contract implements such an interface). The exact role or method is implementation-defined; the requirement is that only an address the target contract considers its admin or equivalent may register on its behalf.

Verification Loop

The Primary Agent Registry provides one direction of a bidirectional identity link. To fully verify an agent's identity, consumers SHOULD complete the verification loop by checking that the agent registry confirms the same address.

Agent registries that conform to ERC-8004 (Trustless Agents) store an agentWallet for each agent token. Agent registries that conform to ERC-8122 (Minimal Agent Registry) store an agent_account metadata field. The verification loop uses these fields to create a bidirectional proof of identity:

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│                      Verification Loop                           │
│                                                                  │
│   Ethereum Address                                               │
│        │                                                         │
│        │  1. resolveAgentId()                                    │
│        ā–¼                                                         │
│   PrimaryAgentRegistry ──→ (registry, tokenId)                   │
│                   │                                              │
│                   │  2. getAgentWallet(tokenId)                  │
│                   │     or getMetadata(tokenId, "agent_account") │
│                   ā–¼                                              │
│                Agent Registry ──→ wallet address                 │
│                                        │                         │
│                                        │  3. match?              │
│                                        ā–¼                         │
│                                 Ethereum Address                 │
│                                                                  │
│   āœ“ Verified if 1. address == 3. address                         │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Forward resolution (address → agent):

  1. Call primaryAgentRegistry.resolveAgentId(address) to get (registry, tokenId)
  2. Query the agent registry for the agent's linked wallet address:
    • ERC-8004: getAgentWallet(tokenId) returns the agent's wallet address
    • ERC-8122: getMetadata(tokenId, "agent_account") returns the agent's account address
  3. Verify that the returned wallet/account address matches the original Ethereum address

Reverse resolution (agent → address):

  1. Query the agent registry for the agent's wallet address (via agentWallet or agent_account)
  2. Call primaryAgentRegistry.resolveAgentId(walletAddress) to get (registry, tokenId)
  3. Verify that the returned (registry, tokenId) matches the original agent identity

A registration is considered verified when both directions of the loop agree: the Primary Agent Registry points to an agent token whose registry points back to the same address. Consumers SHOULD treat unverified registrations (where the loop does not complete) as unconfirmed claims.

Singleton Deployment

The Primary Agent Registry SHOULD be deployed at a deterministic address on every EVM chain using CREATE2 or a similar mechanism. This ensures a consistent entry point for reverse resolution across all chains.

Rationale

This design is inspired by ENS primary names, which solve the same reverse resolution problem for human-readable names: given an address, determine the single name it represents. The Primary Agent Registry applies this pattern to agent identities, mapping each address to one (registry, tokenId) pair. A packed bytes encoding keeps the common case (token IDs under 2^80) in a single storage slot. Verification logic is delegated to separate registrar contracts so that new verification methods can be added without modifying the registry.

Backwards Compatibility

This ERC introduces a new singleton contract and does not modify existing standards. Existing Agent registry formats ERC-8004 and ERC-8122 are supported, however, any agent registry design MAY be supported that that uses a contract address and number id.

This ERC is designed to work alongside:

  • ERC-8004 (Trustless Agents): The getAgentWallet function provides the forward direction of the verification loop
  • ERC-8122 (Minimal Agent Registry): The agent_account metadata field provides the forward direction of the verification loop

Security Considerations

The security of the system depends on the correctness of registrar contracts granted REGISTRAR_ROLE. The Self Registrar allows any EOA to claim any (registry, tokenId) pair without verifying token ownership, so consumers MUST NOT trust a registration at face value and SHOULD complete the Verification Loop to confirm the agent registry points back to the same address. The ERC-1271 Registrar and Signed Registrar both include block.chainid, address(this), and a per-account nonce in the signed hash to prevent cross-chain, cross-contract, and same-contract replay. The Signed Registrar additionally supports a deadline parameter; signers SHOULD set a finite deadline to limit the window during which an unused signature can be submitted. Because the registry is a singleton, compromise of the admin role could affect all registrations on that chain, so implementations SHOULD use a multisig or governance mechanism for the admin role.

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