Counterfactual
Emit-only ERC-8004 adapter registration for indexer-keyed identities that can settle onchain later.
What counterfactual registration is#
Counterfactual registration lets the current controller of an external token
publish ERC-8004-style identity events without minting an ERC-8004 token. It is
an indexable, low-gas alternative when an agent needs a stable identity before
it needs a full onchain registration. Anyone who controls the same external
token can later promote that identity path by calling the normal register
function with the same token coordinates.
The emitted events are the record. Indexers key that record by a deterministic
registrationHash, then reconstruct the current off-chain identity from the
latest counterfactual events for the underlying token.
Every counterfactual function still checks current bound-token control. ERC-721
uses ownerOf(tokenId) == msg.sender; ERC-1155 and ERC-6909 use
balanceOf(msg.sender, tokenId) > 0. Shared-control token standards therefore
allow any current holder to emit the latest claim.
Why use it#
Counterfactual registration is useful when an agent needs a stable identifier before it needs a minted ERC-8004 identity. The caller pays only event-log gas for the claim and can defer the onchain mint until the identity needs registry state.
The model also lets a master NFT carry multiple counterfactual identities. Each
distinct (tokenContract, tokenId) maps to a distinct registrationHash; the
same token always maps to the same hash on a given chain and adapter.
When you later need an onchain ERC-8004 token, call register with the same
external token coordinates. There is no settle function today; settle-later
promotion is future work, and the current promotion path is a normal onchain
registration.
The registrationHash#
The registrationHash is the canonical indexer key for a counterfactual
identity:
registrationHash = keccak256(
abi.encode(block.chainid, address(adapter), tokenContract, tokenId)
)
The adapter exposes the same computation as a public view:
function registrationHash(address tokenContract, uint256 tokenId) external view returns (bytes32)
Off-chain consumers should call that view when possible instead of re-implementing the encoding.
The hash includes:
| Field | Why it is included |
|---|---|
block.chainid | Prevents replay across chains. |
address(adapter) | Prevents replay across adapter deployments or proxies. |
tokenContract | Binds the claim to one external token contract. |
tokenId | Binds the claim to one token id inside that contract. |
TokenStandard is intentionally excluded. A hybrid token contract that exposes
the same tokenId through multiple standards can therefore collide on the same
hash, but the indexer resolves the standard from the originating
CounterfactualAgentRegistered event and uses token coordinates as the
off-chain binding key.
Function reference#
Every counterfactual write:
- requires
tokenContract != address(0)(InvalidTokenContract()otherwise); - requires the caller to currently control the external token
(
NotController(account, type(uint256).max)otherwise); - computes
registrationHashusing the canonical encoding above; - emits one event on
IERC8004AdapterCounterfactual; - uses
nonReentrant; - writes no adapter storage and makes no ERC-8004 registry call.
counterfactualRegister (with metadata)#
function counterfactualRegister(
TokenStandard standard,
address tokenContract,
uint256 tokenId,
string calldata agentURI,
MetadataEntry[] memory metadata
) public returns (bytes32 registrationHash)
What it does
Emits a counterfactual identity claim for an external token. No ERC-8004
identity is minted. The returned registrationHash is the canonical indexer
key for this off-chain claim.
Parameters
standard-ERC721,ERC1155, orERC6909.tokenContract- external token contract address.tokenId- id withintokenContract.agentURI- initial off-chain URI.metadata- array ofMetadataEntry { string metadataKey; bytes metadataValue; }.
Returns
registrationHash- deterministic, chain-and-adapter-scoped id for this counterfactual claim.
Events
CounterfactualAgentRegistered(registrationHash, tokenContract, tokenId, standard, agentURI, metadata, emitter).
Reverts
InvalidTokenContract()iftokenContractis zero.NotController(account, type(uint256).max)if the caller does not currently control the external token.ReservedMetadataKey("agent-binding")if the metadata array contains the canonical binding key.
counterfactualRegister (no metadata)#
function counterfactualRegister(
TokenStandard standard,
address tokenContract,
uint256 tokenId,
string calldata agentURI
) external returns (bytes32 registrationHash)
What it does
Convenience overload equivalent to the metadata-array form with an empty
MetadataEntry[]. Same event and revert behavior.
Returns
registrationHash- deterministic, chain-and-adapter-scoped id for this counterfactual claim.
counterfactualSetAgentURI#
function counterfactualSetAgentURI(
TokenStandard standard,
address tokenContract,
uint256 tokenId,
string calldata newURI
) external
What it does
Emits a counterfactual URI update for the token's off-chain identity.
Parameters
standard,tokenContract,tokenId- external token coordinates.newURI- new off-chain URI.
Events
CounterfactualAgentURISet(registrationHash, tokenContract, tokenId, newURI, emitter).
Reverts
InvalidTokenContract()iftokenContractis zero.NotController(account, type(uint256).max)if the caller does not currently control the external token.
counterfactualSetMetadata#
function counterfactualSetMetadata(
TokenStandard standard,
address tokenContract,
uint256 tokenId,
string calldata metadataKey,
bytes calldata metadataValue
) external
What it does
Emits a counterfactual single-key metadata write.
Parameters
standard,tokenContract,tokenId- external token coordinates.metadataKey,metadataValue- metadata pair.
Events
CounterfactualMetadataSet(registrationHash, tokenContract, tokenId, metadataKey, metadataValue, emitter).
Reverts
InvalidTokenContract()iftokenContractis zero.NotController(account, type(uint256).max)if the caller does not currently control the external token.ReservedMetadataKey("agent-binding")ifmetadataKeyis the canonical binding key.
counterfactualSetMetadataBatch#
function counterfactualSetMetadataBatch(
TokenStandard standard,
address tokenContract,
uint256 tokenId,
MetadataEntry[] calldata metadata
) external
What it does
Emits a counterfactual batch metadata write in one transaction.
Parameters
standard,tokenContract,tokenId- external token coordinates.metadata- array ofMetadataEntry { string metadataKey; bytes metadataValue; }.
Events
CounterfactualMetadataBatchSet(registrationHash, tokenContract, tokenId, metadata, emitter).
Reverts
InvalidTokenContract()iftokenContractis zero.NotController(account, type(uint256).max)if the caller does not currently control the external token.ReservedMetadataKey("agent-binding")if any entry uses the canonical binding key.
counterfactualSetAgentWallet#
function counterfactualSetAgentWallet(
TokenStandard standard,
address tokenContract,
uint256 tokenId,
address newWallet
) external
What it does
Emits a counterfactual agent-wallet assignment. This function deliberately accepts no signature and no deadline because no ERC-8004 wallet binding is being created. The event is purely an off-chain claim gated by current bound-token control.
Parameters
standard,tokenContract,tokenId- external token coordinates.newWallet- wallet address being claimed off-chain.
Events
CounterfactualAgentWalletSet(registrationHash, tokenContract, tokenId, newWallet, emitter).
Reverts
InvalidTokenContract()iftokenContractis zero.NotController(account, type(uint256).max)if the caller does not currently control the external token.
counterfactualUnsetAgentWallet#
function counterfactualUnsetAgentWallet(
TokenStandard standard,
address tokenContract,
uint256 tokenId
) external
What it does
Emits a counterfactual agent-wallet clear.
Parameters
standard,tokenContract,tokenId- external token coordinates.
Events
CounterfactualAgentWalletUnset(registrationHash, tokenContract, tokenId, emitter).
Reverts
InvalidTokenContract()iftokenContractis zero.NotController(account, type(uint256).max)if the caller does not currently control the external token.
registrationHash#
function registrationHash(address tokenContract, uint256 tokenId) external view returns (bytes32)
What it does
Returns the canonical counterfactual hash for the given external token, scoped to the current chain and this adapter proxy.
Behavior
return keccak256(abi.encode(block.chainid, address(this), tokenContract, tokenId));
Reverts
- Does not perform token-control checks and does not revert for a zero
tokenContract; it only computes the hash.
Event reference#
All counterfactual events are declared on IERC8004AdapterCounterfactual.
registrationHash, tokenContract, and tokenId are indexed on every event,
so indexers can filter by any of those three topics.
CounterfactualAgentRegistered#
event CounterfactualAgentRegistered(
bytes32 indexed registrationHash,
address indexed tokenContract,
uint256 indexed tokenId,
TokenStandard standard,
string agentURI,
MetadataEntry[] metadata,
address emitter
)
Fires from counterfactualRegister. The payload establishes the initial
counterfactual identity claim: token standard, initial URI, initial metadata,
and the controller address that emitted the claim.
CounterfactualAgentURISet#
event CounterfactualAgentURISet(
bytes32 indexed registrationHash,
address indexed tokenContract,
uint256 indexed tokenId,
string newURI,
address emitter
)
Fires from counterfactualSetAgentURI. newURI replaces the current
counterfactual URI for the token under the latest-event policy.
CounterfactualMetadataSet#
event CounterfactualMetadataSet(
bytes32 indexed registrationHash,
address indexed tokenContract,
uint256 indexed tokenId,
string metadataKey,
bytes metadataValue,
address emitter
)
Fires from counterfactualSetMetadata. The key/value pair updates one
metadata key in the counterfactual record.
CounterfactualMetadataBatchSet#
event CounterfactualMetadataBatchSet(
bytes32 indexed registrationHash,
address indexed tokenContract,
uint256 indexed tokenId,
MetadataEntry[] metadata,
address emitter
)
Fires from counterfactualSetMetadataBatch. The payload carries multiple
metadata entries emitted as one update.
CounterfactualAgentWalletSet#
event CounterfactualAgentWalletSet(
bytes32 indexed registrationHash,
address indexed tokenContract,
uint256 indexed tokenId,
address newWallet,
address emitter
)
Fires from counterfactualSetAgentWallet. newWallet is the runtime wallet
claimed for the off-chain identity. This is not an ERC-8004 registry wallet
binding and has no signature proof.
CounterfactualAgentWalletUnset#
event CounterfactualAgentWalletUnset(
bytes32 indexed registrationHash,
address indexed tokenContract,
uint256 indexed tokenId,
address emitter
)
Fires from counterfactualUnsetAgentWallet. Indexers should clear the current
counterfactual wallet value for the token.
Indexer guidance#
Use (tokenContract, tokenId) as the authoritative conflict domain and
registrationHash as the stable lookup key. The chain does not dedupe or
enforce uniqueness; it only verifies current token control at emission time.
The canonical policy is latest event per (tokenContract, tokenId) wins. If
the same controller, or another current controller on a shared-control token
standard, re-emits a claim, the most recent log is the current record.
Deduplicate exact re-emissions by transaction hash and log index first. For semantic duplicates across transactions, keep the latest log and treat older logs as history. Do not assume identical payloads mean the event can be skipped unless your indexer has already processed that exact log.
If you observe an update event before the corresponding
CounterfactualAgentRegistered event, you can back-resolve the hash by scanning
for CounterfactualAgentRegistered with the same indexed registrationHash or
the same (tokenContract, tokenId). If no registration event is available in
your indexed range, keep the update as an incomplete record and continue
watching; the update still came from a controller-gated adapter call.
Relationship to the on-chain register surface#
Counterfactual functions mirror the onchain registration and setter shapes by
taking (TokenStandard, tokenContract, tokenId, ...), then applying the same
bound-token control rule used by register.
They do not mint an ERC-8004 token. There is no agentId, no ownerOf record
in the ERC-8004 registry, no registry metadata, and no adapter bindingOf
storage row. Any identity state exists only in event logs and indexer state.
Use counterfactual registration when you want a cheap, indexer-keyed identity
now and can defer the registry mint. Use Contract Reference
register when you need a real ERC-8004 token and registry-backed metadata or
wallet state. A future settle path may link the two more directly; today,
settling means calling the normal onchain register function with the same
external token.