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

CIS-8004

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: 4 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: 4 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 162-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. The owner is the current CIS-2 owner of token_id at the time of the call. The wallet_set_nonce is the agent’s current nonce, which the contract increments after each successful setAgentWallet call and on every CIS-2 transfer of the token; clients MUST read the current value from getAgentWalletNonce before constructing the message.

Note that token_id is serialized here as a raw 8-byte little-endian unsigned integer, not using the length-prefixed AgentTokenId wire form.

It is serialized as the 26 raw ASCII bytes of the domain separation tag CIS-8004/v1/setAgentWallet, followed by the token id as a raw 8-byte little-endian unsigned integer (token_id), an AccountAddress (owner), an AccountAddress (new_wallet), the wallet-set nonce as a raw 8-byte little-endian unsigned integer (wallet_set_nonce), a Timestamp (deadline), a ContractAddress (contract_address), and a BlockHash (genesis_hash):

SetAgentWalletSigningData ::= ("CIS-8004/v1/setAgentWallet": Byte²⁶)
                               (token_id: Byte⁸)
                               (owner: AccountAddress)
                               (new_wallet: AccountAddress)
                               (wallet_set_nonce: Byte⁸)
                               (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.

  1. Validate the registry address. The external_reference.contract_address MUST match the configured CIS-8 dependency address. A mismatch MUST cause the contract to reject with InvalidExternalReference.

  2. Verify ownership. Call ownerOfKey on the CIS-8 registry with the supplied ExternalKeyId. The response MUST be a present Registration with status = Active and owner equal to the transaction sender. Any other response MUST cause the contract to reject with InvalidExternalReference.

  3. Check uniqueness. No other active agent MUST already hold the same external reference. The contract MUST reject with ExternalReferenceTaken if 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), an OptionalHash256 (metadata_hash), and an OptionalExtRef (external_reference):

Registered ::= (240: Byte) (token_id: AgentTokenId) (owner: AccountAddress)
               (agent_uri: OptionalString) (metadata_hash: OptionalHash256)
               (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), an OptionalString (agent_uri), and an OptionalHash256 (metadata_hash):

URIUpdated ::= (241: Byte) (token_id: AgentTokenId) (agent_uri: OptionalString)
               (metadata_hash: OptionalHash256)

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, transfer-induced clears, and revoke-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_reference is present, the contract MUST perform cross-contract verification.

  • The initial_metadata MUST NOT contain the reserved key agentWallet; the contract MUST reject with ReservedKey if it does.

  • On success the contract MUST mint a new CIS-2 NFT to the sender, emit Registered, and emit AgentWalletSet with new_wallet set to the sender’s address.

setAgentURI

Update the agent URI and metadata hash for an existing agent. The caller MUST be the current CIS-2 owner of the token. Either field may be set to absent to clear its current value.

Parameter

The parameter consists of an AgentTokenId (token_id), an OptionalString (agent_uri), and an OptionalHash256 (metadata_hash):

SetAgentURIParam ::= (token_id: AgentTokenId) (agent_uri: OptionalString)
                      (metadata_hash: OptionalHash256)
Requirements
  • The contract MUST reject with AgentNotFound if no agent exists for token_id.

  • The contract MUST reject with AgentRevoked if the agent’s status is Revoked.

  • The contract MUST reject with Unauthorized if the caller is not the CIS-2 owner.

  • The agent_uri, if present, MUST NOT exceed 4096 bytes.

  • On success the contract MUST set agent_uri and metadata_hash to the supplied values (replacing any previous values, including clearing them if absent) and 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 AgentNotFound if no agent exists for token_id.

  • The contract MUST reject with AgentRevoked if the agent’s status is Revoked.

  • The contract MUST reject with Unauthorized if the caller is not the CIS-2 owner.

  • If external_reference is 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 AgentNotFound if no agent exists for token_id.

  • The contract MUST reject with AgentRevoked if the agent’s status is Revoked.

  • The contract MUST reject with Unauthorized if the caller is not the CIS-2 owner.

  • The contract MUST reject with AgentWalletDeadlinePassed if deadline is in the past.

  • The contract MUST verify the signature against the setAgentWallet signed message using new_wallet’s account credentials. The contract MUST reject with InvalidAgentWalletProof if 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)
Requirements
  • The contract MUST reject with AgentNotFound if no agent exists for token_id.

Response

The response is an OptionalAddress:

GetAgentWalletResponse ::= (0: Byte)                         // wallet not set
                          | (1: Byte) (wallet: AccountAddress) // current wallet

getAgentWalletNonce

Return the current wallet-set nonce for an agent. Clients MUST read this value before constructing the setAgentWallet signed message.

Parameter

The parameter consists of the AgentTokenId (token_id) to query:

GetAgentWalletNonceParam ::= (token_id: AgentTokenId)
Requirements
  • The contract MUST reject with AgentNotFound if no agent exists for token_id.

Response

The response is the current nonce as a raw 8-byte little-endian unsigned integer:

GetAgentWalletNonceResponse ::= (nonce: Byte⁸)

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 AgentNotFound if no agent exists for token_id.

  • The contract MUST reject with AgentRevoked if the agent’s status is Revoked.

  • The contract MUST reject with Unauthorized if the caller is not the CIS-2 owner.

  • The contract MUST reject with ReservedKey if key is agentWallet.

  • If value violates implementation-defined limits the contract MUST reject with InvalidMetadata.

  • 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
Requirements
  • The contract MUST reject with AgentNotFound if no agent exists for token_id.

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.

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 AgentNotFound if no agent exists for token_id.

  • The contract MUST reject with Unauthorized if the caller is not the CIS-2 owner.

  • The contract MUST reject with AgentAlreadyRevoked if the agent’s status is already Revoked.

  • On success the contract MUST set the agent’s status to Revoked, clear the external_reference if present, and emit Revoked. If external_reference was present, the contract MUST also emit ExternalReferenceSet with external_reference absent, before emitting Revoked.

Transfer semantics

A CIS-2 transfer of a CIS-8004 agent NFT MUST perform the following additional steps atomically with the transfer:

  1. Clear the external reference. The agent’s external_reference MUST be set to absent. The contract MUST emit ExternalReferenceSet with external_reference absent.

  2. Reset the agent wallet. The agent’s agent_wallet MUST be set to absent. The contract MUST emit AgentWalletSet with new_wallet absent.

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.

The to field of a CIS-2 transfer MUST be an account address. The contract MUST reject with InvalidReceiver if to is a contract 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

AgentNotFound

-7200

No agent exists for the supplied token_id or external_reference.

Unauthorized

-7201

The sender is not authorized for the operation.

AgentRevoked

-7202

The agent is Revoked; the operation requires Active status.

InvalidReceiver

-7203

The to field of a transfer call is a contract address; agent NFT ownership is restricted to accounts.

ExternalReferenceTaken

-7204

An active agent already holds the supplied external_reference.

InvalidExternalReference

-7206

The supplied external_reference does not resolve to an active CIS-8 entry owned by the sender, or the registry address does not match the configured dependency.

InvalidMetadata

-7208

The metadata value violates implementation-defined limits or encoding requirements.

AgentAlreadyRevoked

-7209

revoke was called on an agent that is already Revoked.

ReservedKey

-7211

setMetadata was called with the reserved key agentWallet.

InvalidAgentWalletProof

-7212

The signature in setAgentWallet does not verify against new_wallet’s account credentials.

AgentWalletDeadlinePassed

-7213

The deadline in setAgentWallet has passed.

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:

  1. NFT base standard. ERC-721 is replaced by CIS-2.

  2. Signature verification. EIP-712 / ERC-1271 is replaced by Concordium ed25519 account signatures verified via the host check_account_signature function.

  3. Identifier convention. CAIP-19 canonical form is used: ccd:<network>/cis-2:<token_address>.

  4. External reference. A new on-chain external_reference field links the agent to a CIS-8 cryptographic external-key binding.

  5. Receive hook. The CIS-2 receive hook replaces ERC-721 safeTransferFrom and onERC721Received.

  6. Native revocation. A dedicated revoke entrypoint is provided.

  7. ID-backed accountability. Owner identity is backed by the Concordium protocol invariant that accounts are associated with verified real-world identities.

  8. 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"
  ]
}