CIS-8004: Agent Registry¶
Created |
May 21, 2026 |
|---|---|
Draft |
May 25, 2026 |
Supported versions |
Smart contract version 1 or newer
(Protocol version 4 or newer)
|
Standard identifier |
|
Requires |
Abstract¶
CIS-8004 is the Concordium implementation of ERC-8004 Trustless Agents (Identity Registry). It defines a smart contract interface for registering autonomous software agents as CIS-2 non-fungible tokens on the Concordium blockchain.
Each agent is minted as a CIS-2 NFT and identified by an AgentTokenId.
A registration may optionally attach an external reference pointing to a CIS-8 cryptographic external-key binding.
The interface defines the following:
Data structures for agent records, external references, and agent status.
Entrypoints for registering, updating, revoking, and querying agents.
Logged events for each mutating operation.
Standard rejection error codes.
Specification¶
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
General types and serialization¶
Note
Integers are encoded in little-endian unless stated otherwise.
String¶
A variable-length UTF-8 encoded string.
It is serialized as: 2 bytes for the length (n) in little-endian, followed by n bytes for the UTF-8 encoding of the string:
String ::= (n: Byte²) (value: Byteⁿ)
Bytestring¶
A variable-length byte array.
It is serialized as: 2 bytes for the length (n) in little-endian, followed by n bytes:
Bytestring ::= (n: Byte²) (value: Byteⁿ)
AccountAddress¶
An address of a Concordium account.
It is serialized as 32 bytes:
AccountAddress ::= (address: Byte³²)
ContractAddress¶
An address of a Concordium smart contract instance. It consists of an index and a subindex, both unsigned 64-bit integers.
It is serialized as: 8 bytes for the index (index) followed by 8 bytes for the subindex (subindex), both little-endian:
ContractAddress ::= (index: Byte⁸) (subindex: Byte⁸)
BlockHash¶
A 32-byte block hash identifying a Concordium block. Used in the setAgentWallet signed message to prevent cross-network replay.
It is serialized as 32 bytes:
BlockHash ::= (hash: Byte³²)
Timestamp¶
A point in time given in milliseconds since Unix epoch, represented as an unsigned 64-bit integer.
It is serialized as 8 bytes in little-endian:
Timestamp ::= (milliseconds: Byte⁸)
AgentTokenId¶
A token identifier for an agent NFT, using the CIS-2 TokenIdU64 wire form.
It is serialized as 1 byte with value 8 (the length prefix), followed by 8 bytes for the token id in little-endian:
AgentTokenId ::= (8: Byte) (id: Byte⁸)
Schema-driven tooling renders an AgentTokenId as a 16-character lowercase hex string
(e.g. "0a00000000000000" for token id 10).
ExternalKeyId¶
As defined in CIS-8.
ExternalRefKind¶
Discriminates between supported external reference types.
Currently the only defined kind is Cis8, which carries a ExternalKeyId.
Additional kinds may be defined in future revisions of this standard.
It is serialized as 1 byte with value 0 for Cis8, followed by the ExternalKeyId:
ExternalRefKind ::= (0: Byte) (key: ExternalKeyId) // Cis8
ExternalReference¶
A reference to an active entry in a CIS-8 external key registry.
The contract_address field identifies the registry contract instance.
The kind field carries the external reference type and identifier.
It is serialized as a ContractAddress (contract_address) followed by an
ExternalRefKind (kind):
ExternalReference ::= (contract_address: ContractAddress) (kind: ExternalRefKind)
AgentStatus¶
The lifecycle status of a registered agent.
It is serialized as 1 byte with value 0 for Active and 1 for Revoked:
AgentStatus ::= (0: Byte) // Active
| (1: Byte) // Revoked
MetadataEntry¶
A key-value pair for on-chain metadata associated with an agent. The key is a string and the value is an arbitrary byte array.
It is serialized as a String (key) followed by a Bytestring (value):
MetadataEntry ::= (key: String) (value: Bytestring)
AgentView¶
The full observable record for a registered agent, as returned by agentOf and agentByExternalReference.
The owner_account is derived from the CIS-2 ownership state and is not stored in the agent record itself.
Optional fields are each prefixed by 1 byte: value 0 means absent (no further bytes follow for that field), value 1 means present (followed by the field value).
The following helper types are used:
OptionalString ::= (0: Byte) | (1: Byte) (s: String)
OptionalHash256 ::= (0: Byte) | (1: Byte) (h: Byte³²)
OptionalExtRef ::= (0: Byte) | (1: Byte) (r: ExternalReference)
OptionalAddress ::= (0: Byte) | (1: Byte) (a: AccountAddress)
OptionalTimestamp ::= (0: Byte) | (1: Byte) (t: Timestamp)
It is serialized as:
AgentView ::= (token_id: AgentTokenId) (owner_account: AccountAddress)
(agent_uri: OptionalString) (metadata_hash: OptionalHash256)
(external_reference: OptionalExtRef) (agent_wallet: OptionalAddress)
(status: AgentStatus) (registered_at: Timestamp)
(revoked_at: OptionalTimestamp) (revocation_reason: OptionalString)
setAgentWallet signed message¶
The setAgentWallet entrypoint requires the new wallet account to prove control by providing a
Concordium account signature over a canonical signed message.
The message is a fixed 123-byte sequence constructed as follows.
The contract MUST derive contract_address from its own address and genesis_hash from the chain
metadata; neither value may be supplied as a parameter.
It is serialized as the 26 raw ASCII bytes of the domain separation tag CIS-8004/v1/setAgentWallet,
followed by an AgentTokenId (token_id),
an AccountAddress (new_wallet),
a Timestamp (deadline),
a ContractAddress (contract_address),
and a BlockHash (genesis_hash):
SetAgentWalletSigningData ::= ("CIS-8004/v1/setAgentWallet": Byte²⁶)
(token_id: AgentTokenId)
(new_wallet: AccountAddress)
(deadline: Timestamp)
(contract_address: ContractAddress)
(genesis_hash: BlockHash)
Cross-contract verification¶
When register or
setExternalReference is called with a present
external_reference, the contract MUST perform the following steps.
Validate the registry address. The
external_reference.contract_addressMUST match the configured CIS-8 dependency address. A mismatch MUST cause the contract to reject withInvalidExternalReference.Verify ownership. Call
ownerOfKeyon the CIS-8 registry with the supplied ExternalKeyId. The response MUST be a presentRegistrationwithstatus = Activeandownerequal to the transaction sender. Any other response MUST cause the contract to reject withInvalidExternalReference.Check uniqueness. No other active agent MUST already hold the same external reference. The contract MUST reject with
ExternalReferenceTakenif a conflict is found.
Logged events¶
CIS-8004-specific events use tags 240–245. A custom event SHOULD NOT have a first byte colliding with any of the events defined by this specification. CIS-2 inherited events are emitted directly as specified in CIS-2.
Registered¶
A Registered event MUST be logged whenever a new agent is successfully registered.
It is serialized as: first a byte with the value of 240, followed by the
AgentTokenId (token_id), the AccountAddress (owner),
an OptionalString (agent_uri), and an OptionalExtRef (external_reference):
Registered ::= (240: Byte) (token_id: AgentTokenId) (owner: AccountAddress)
(agent_uri: OptionalString) (external_reference: OptionalExtRef)
URIUpdated¶
A URIUpdated event MUST be logged whenever the agent URI of an existing agent is updated.
It is serialized as: first a byte with the value of 241, followed by the
AgentTokenId (token_id) and an OptionalString (agent_uri):
URIUpdated ::= (241: Byte) (token_id: AgentTokenId) (agent_uri: OptionalString)
ExternalReferenceSet¶
An ExternalReferenceSet event MUST be logged whenever the external reference of an agent is
set, updated, or cleared.
This includes both explicit setExternalReference
calls and transfer-induced clears.
It is serialized as: first a byte with the value of 242, followed by the
AgentTokenId (token_id) and an OptionalExtRef (external_reference):
ExternalReferenceSet ::= (242: Byte) (token_id: AgentTokenId)
(external_reference: OptionalExtRef)
MetadataSet¶
A MetadataSet event MUST be logged whenever an on-chain metadata value is set or updated.
It is serialized as: first a byte with the value of 243, followed by the
AgentTokenId (token_id), a String (key),
and a Bytestring (value):
MetadataSet ::= (243: Byte) (token_id: AgentTokenId) (key: String) (value: Bytestring)
Revoked¶
A Revoked event MUST be logged whenever an agent is revoked.
It is serialized as: first a byte with the value of 244, followed by the
AgentTokenId (token_id), the AccountAddress (owner),
and an OptionalString (reason):
Revoked ::= (244: Byte) (token_id: AgentTokenId) (owner: AccountAddress)
(reason: OptionalString)
AgentWalletSet¶
An AgentWalletSet event MUST be logged whenever the agent wallet is set, updated, or cleared.
This includes both explicit setAgentWallet calls and
transfer-induced resets.
It is serialized as: first a byte with the value of 245, followed by the
AgentTokenId (token_id) and an OptionalAddress (new_wallet):
AgentWalletSet ::= (245: Byte) (token_id: AgentTokenId) (new_wallet: OptionalAddress)
Contract functions¶
A smart contract implementing this standard MUST export the following functions:
A contract implementing this standard MUST also export the complete CIS-0 interface
(supports) and the complete CIS-2 interface
(transfer, balanceOf, operatorOf, updateOperator, tokenMetadata).
register¶
Mint a new agent NFT and optionally attach a URI, a metadata hash, an external reference,
and initial on-chain metadata.
The sender MUST be an account; contract callers MUST be rejected with Unauthorized.
Parameter¶
The parameter consists of an OptionalString (agent_uri), an OptionalHash256
(metadata_hash), an OptionalExtRef (external_reference), a 2-byte little-endian
count (m) of initial metadata entries, and then m MetadataEntry
records (initial_metadata):
RegisterParam ::= (agent_uri: OptionalString) (metadata_hash: OptionalHash256)
(external_reference: OptionalExtRef)
(m: Byte²) (initial_metadata: MetadataEntryᵐ)
Requirements¶
The
agent_uri, if present, MUST NOT exceed 4096 bytes.If
external_referenceis present, the contract MUST perform cross-contract verification.The
initial_metadataMUST NOT contain the reserved keyagentWallet; the contract MUST reject withReservedKeyif it does.On success the contract MUST assign the next available
AgentTokenId, mint a CIS-2 NFT to the sender, setagent_wallet = Some(sender), and emit Registered.
setAgentURI¶
Update the agent URI for an existing agent. The caller MUST be the current CIS-2 owner of the token.
Parameter¶
The parameter consists of an AgentTokenId (token_id) and an
OptionalString (agent_uri):
SetAgentURIParam ::= (token_id: AgentTokenId) (agent_uri: OptionalString)
Requirements¶
The contract MUST reject with
AgentNotFoundif no agent exists fortoken_id.The contract MUST reject with
AgentRevokedif the agent’s status isRevoked.The contract MUST reject with
Unauthorizedif the caller is not the CIS-2 owner.The
agent_uri, if present, MUST NOT exceed 4096 bytes.On success the contract MUST emit URIUpdated.
setExternalReference¶
Set or update the external reference for an existing agent.
The caller MUST be the current CIS-2 owner of the token.
Supplying an absent external_reference clears any existing reference.
Parameter¶
The parameter consists of an AgentTokenId (token_id) and an
OptionalExtRef (external_reference):
SetExternalReferenceParam ::= (token_id: AgentTokenId)
(external_reference: OptionalExtRef)
Requirements¶
The contract MUST reject with
AgentNotFoundif no agent exists fortoken_id.The contract MUST reject with
AgentRevokedif the agent’s status isRevoked.The contract MUST reject with
Unauthorizedif the caller is not the CIS-2 owner.If
external_referenceis present, the contract MUST perform cross-contract verification.On success the contract MUST emit ExternalReferenceSet.
setAgentWallet¶
Change the payment address associated with an agent. The caller MUST be the current CIS-2 owner of the token. The new wallet MUST prove control by providing a Concordium account signature over the setAgentWallet signed message.
Parameter¶
The parameter consists of an AgentTokenId (token_id), an
AccountAddress (new_wallet), a Timestamp (deadline),
and the serialized Concordium account signature of new_wallet over the canonical signed
message (signature):
SetAgentWalletParam ::= (token_id: AgentTokenId) (new_wallet: AccountAddress)
(deadline: Timestamp) (signature: AccountSignature)
where AccountSignature is the standard Concordium account signature serialization.
Requirements¶
The contract MUST reject with
AgentNotFoundif no agent exists fortoken_id.The contract MUST reject with
AgentRevokedif the agent’s status isRevoked.The contract MUST reject with
Unauthorizedif the caller is not the CIS-2 owner.The contract MUST reject with
AgentWalletDeadlinePassedifdeadlineis in the past.The contract MUST verify the signature against the setAgentWallet signed message using
new_wallet’s account credentials. The contract MUST reject withInvalidAgentWalletProofif verification fails.On success the contract MUST emit AgentWalletSet.
getAgentWallet¶
Return the current payment address associated with an agent.
Parameter¶
The parameter consists of the AgentTokenId (token_id) to query:
GetAgentWalletParam ::= (token_id: AgentTokenId)
Response¶
The response is an OptionalAddress:
GetAgentWalletResponse ::= (0: Byte) // not set
| (1: Byte) (wallet: AccountAddress) // current wallet
setMetadata¶
Set an on-chain metadata value for an existing agent. The caller MUST be the current CIS-2 owner of the token.
Parameter¶
The parameter consists of an AgentTokenId (token_id), a
String (key), and a Bytestring (value):
SetMetadataParam ::= (token_id: AgentTokenId) (key: String) (value: Bytestring)
Requirements¶
The contract MUST reject with
AgentNotFoundif no agent exists fortoken_id.The contract MUST reject with
Unauthorizedif the caller is not the CIS-2 owner.The contract MUST reject with
ReservedKeyifkeyisagentWallet.If
valueviolates implementation-defined limits the contract MUST reject withInvalidMetadata.On success the contract MUST emit MetadataSet.
getMetadata¶
Return the on-chain metadata value for a given agent and key.
Parameter¶
The parameter consists of an AgentTokenId (token_id) and a
String (key):
GetMetadataParam ::= (token_id: AgentTokenId) (key: String)
Response¶
The response is an OptionalBytestring:
OptionalBytestring ::= (0: Byte) | (1: Byte) (v: Bytestring)
GetMetadataResponse ::= (0: Byte) // key not set
| (1: Byte) (value: Bytestring) // current value
agentOf¶
Return the full agent record for a given token id.
Parameter¶
The parameter consists of the AgentTokenId (token_id) to query:
AgentOfParam ::= (token_id: AgentTokenId)
Response¶
Returns the AgentView for the agent.
The contract MUST reject with AgentNotFound if no agent exists for token_id.
agentByExternalReference¶
Return the agent record that holds a given external reference.
Parameter¶
The parameter consists of the ExternalReference (external_reference) to
resolve:
AgentByExternalReferenceParam ::= (external_reference: ExternalReference)
Response¶
Returns the AgentView for the matching agent.
The contract MUST reject with AgentNotFound if no active agent holds the supplied
external reference.
isActive¶
Return whether an agent exists and has Active status.
Parameter¶
The parameter consists of the AgentTokenId (token_id) to query:
IsActiveParam ::= (token_id: AgentTokenId)
Response¶
The response is 1 byte with value 0 for false and 1 for true:
IsActiveResponse ::= (0: Byte) // false
| (1: Byte) // true
revoke¶
Mark an agent’s status as Revoked.
The caller MUST be the current CIS-2 owner of the token.
Parameter¶
The parameter consists of an AgentTokenId (token_id) and an
OptionalString (reason):
RevokeParam ::= (token_id: AgentTokenId) (reason: OptionalString)
Requirements¶
The contract MUST reject with
AgentNotFoundif no agent exists fortoken_id.The contract MUST reject with
Unauthorizedif the caller is not the CIS-2 owner.The contract MUST reject with
AgentAlreadyRevokedif the agent’s status is alreadyRevoked.On success the contract MUST set the agent’s status to
Revoked, clear theexternal_referenceif present, and emit Revoked.
Transfer semantics¶
A CIS-2 transfer of a CIS-8004 agent NFT MUST perform the following additional steps atomically with the transfer:
Clear the external reference. The agent’s
external_referenceMUST be set to absent. The contract MUST emit ExternalReferenceSet withexternal_referenceabsent.Reset the agent wallet. The agent’s
agent_walletMUST be set to absent. The contract MUST emit AgentWalletSet withnew_walletabsent.
All other fields of the agent record (agent_uri, metadata_hash, on_chain_metadata,
status, registered_at) are preserved across transfers.
The new owner MUST call setAgentWallet to re-establish a payment address.
Rejection errors¶
A smart contract following this specification MUST use the following error codes to reject under the described conditions:
Name |
Error code |
Description |
|---|---|---|
|
-7200 |
No agent exists for the supplied |
|
-7201 |
The sender is not authorized for the operation. |
|
-7202 |
The agent is |
|
-7204 |
An active agent already holds the supplied |
|
-7206 |
The supplied |
|
-7208 |
The metadata value violates implementation-defined limits or encoding requirements. |
|
-7209 |
|
|
-7211 |
|
|
-7212 |
The signature in |
|
-7213 |
The |
Rejecting using an error code from the table above MUST only occur in a situation as described in the corresponding error description.
The smart contract implementing this specification MAY introduce custom error codes other than the ones specified in the table above.
Departures from ERC-8004¶
CIS-8004 departs from ERC-8004 Trustless Agents in the following ways:
NFT base standard. ERC-721 is replaced by CIS-2.
Signature verification. EIP-712 / ERC-1271 is replaced by Concordium ed25519 account signatures verified via the host
check_account_signaturefunction.Identifier convention. CAIP-19 canonical form is used:
ccd:<network>/cis-2:<token_address>.External reference. A new on-chain
external_referencefield links the agent to a CIS-8 cryptographic external-key binding.Receive hook. The CIS-2 receive hook replaces ERC-721
safeTransferFromandonERC721Received.Native revocation. A dedicated
revokeentrypoint is provided.ID-backed accountability. Owner identity is backed by the Concordium protocol invariant that accounts are associated with verified real-world identities.
Reputation and Validation Registries. These are out of scope.
CAIP-19 Resolution¶
An agent NFT is identified in CAIP-19 notation as:
ccd:<network>/cis-2:<token_address>
where token_address is the Base58Check encoding of: version byte 0x02, followed by the
ULEB128 encoding of the contract index, the ULEB128 encoding of the contract subindex, and the
8 bytes of the AgentTokenId payload (the token id without its length prefix).
Registration File Schema¶
The off-chain JSON document referenced by agent_uri MUST conform to the ERC-8004 v2
registration type with the following CIS-8004 additions and constraints.
When an externalReference is present, the kind.variant field MUST be "Cis8" and
the kind.payload MUST be a CIS-8 ExternalKeyId object.
Support for additional reference kinds may be added in future revisions of this standard.
The agent_uri length limit is 4096 bytes to accommodate small data: URIs.
{
"type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
"name": "<agent display name>",
"externalReference": {
"contract_address": "<CIS-8 contract address>",
"kind": {
"variant": "Cis8",
"payload": { }
}
},
"supportedTrust": [
"concordium-id-backed",
"cis8-ownership-proof"
]
}