Skip to content

Start typing to search the documentation.

Smart contracts

On this page

Self Agent ID runs on Celo. The core registry implements IERC8004 and the IERC8004ProofOfHuman extension, and the reputation and validation registries are ERC-8004 companion registries. Source: contracts/ (Foundry, MIT).

Contracts

ContractRole
SelfAgentRegistryIdentity registry (UUPS proxy): soulbound ERC-721 per agent, Hub V2 callback, credentials, proof expiry, guardians, sybil checks
SelfHumanProofProviderDescribes Self as a proof provider (providerName() = "self", verificationStrength() = 100)
SelfReputationRegistryERC-8004 Reputation Registry (UUPS proxy): client feedback plus proof-of-human feedback weighted by document type
SelfValidationRegistryERC-8004 Validation Registry (UUPS proxy): validation requests and responses, and freshness validations
CeloAgentVisaTiered soulbound visas (Celo Agent Visa)

Deployed addresses

Public RPC endpoints: https://forno.celo.org (Celo Mainnet) and https://forno.celo-sepolia.celo-testnet.org (Celo Sepolia).

Reading agent state

bytes32 agentKey = bytes32(uint256(uint160(agentAddress))); // Ed25519 agents: the raw public key

uint256 agentId = registry.getAgentId(agentKey);          // 0 if not registered
bool verified   = registry.isVerifiedAgent(agentKey)       // has a human proof...
               && registry.isProofFresh(agentId);          // ...that has not expired
uint256 expires = registry.proofExpiresAt(agentId);        // unix seconds
address provider = registry.getProofProvider(agentId);
uint256 block_  = registry.agentRegisteredAt(agentId);     // block number
SelfAgentRegistry.AgentCredentials memory creds = registry.getAgentCredentials(agentId);

AgentCredentials holds issuingState, name[], idNumber, nationality, dateOfBirth, gender, expiryDate, olderThan and ofac[3]. Undisclosed fields are empty.

Sybil detection

bool same = registry.sameHuman(agentIdA, agentIdB);
uint256 nullifier = registry.getHumanNullifier(agentId);
uint256 count = registry.getAgentCountForHuman(nullifier);
uint256[] memory ids = registry.getAgentsForNullifier(nullifier);

maxAgentsPerHuman (default 1) caps how many active agents one nullifier can hold.

ERC-8004 interfaces

// IERC8004 (base identity registry)
function register() external returns (uint256 agentId);
function register(string calldata agentURI) external returns (uint256 agentId);
function register(string calldata agentURI, string[] calldata keys, bytes[] calldata values) external returns (uint256 agentId);
function setAgentURI(uint256 agentId, string calldata newURI) external;
function getMetadata(uint256 agentId, string memory key) external view returns (bytes memory);
function setMetadata(uint256 agentId, string calldata key, bytes calldata value) external;
function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes calldata signature) external;
function getAgentWallet(uint256 agentId) external view returns (address);
function unsetAgentWallet(uint256 agentId) external;

The IERC8004ProofOfHuman functions (registerWithHumanProof, revokeHumanProof, hasHumanProof, proofExpiresAt, isProofFresh, getHumanNullifier, getProofProvider, isApprovedProvider, sameHuman, getAgentCountForHuman, getAgentsForNullifier) are listed in The extension interface.

The deployed registry requires a human proof (requireHumanProof = true), so the plain register() overloads revert with ProofRequired. Agents are minted through the Self Hub V2 callback (the Self app scan) or registerWithHumanProof. Only the NFT owner can call setAgentURI, setMetadata and updateAgentMetadata. agentWallet is a reserved metadata key, set with setAgentWallet and an EIP-712 signature from the new wallet.

Registration data

The Self app passes userDefinedData as a UTF-8 string, so every position is an ASCII character ('4', not 0x04). Byte 0 is the action, and byte 1 is the verification config digit (0 to 5).

Byte 0Action
K / XRegister with an agent key (linked, privy, smartwallet) / deregister by agent address
W / ERegister wallet-free / Ed25519, each with an optional guardian
R / DRegister with the human’s wallet as the agent key / deregister
F / IRefresh the proof in place / identify (emit the nullifier, no state change)

The SDK builds these strings for you (buildAdvancedRegisterUserDataAscii, buildWalletFreeRegisterUserDataAscii, …). Background: Working with userDefinedData.

Proof expiry and refresh

proofExpiresAt = min(document expiry, registration time + maxProofAge), with maxProofAge set to 365 days. After expiry, isProofFresh() returns false without any transaction, and the NFT stays in place. A refresh (the F action, through the API or requestProofRefresh) moves the expiry forward for the same agent ID.

Deregistration

Every path burns the NFT, frees the human’s nullifier slot and clears the agent’s key, provider, guardian and metadata:

PathCaller
Deregistration proof (X/D)The same human, through a Self app scan (API, SDK or CLI)
selfDeregister(agentId)The NFT owner
guardianRevoke(agentId)The agent’s guardian (W and E registrations can set one)
revokeHumanProof(...)Any caller with a new proof from the same human, through an approved provider

Reputation registry

reputation.giveFeedback(agentId, value, valueDecimals, tag1, tag2, endpoint, feedbackURI, feedbackHash);
reputation.appendResponse(agentId, clientAddress, feedbackIndex, responseURI, responseHash);
(uint64 count, int256 sum, uint8 decimals) = reputation.getSummary(agentId, clientAddresses, tag1, tag2);

An agent’s owner can’t give itself feedback. At registration, the identity registry records proof-of-human feedback weighted by document type:

DocumentWeightTag
E-Passport (NFC)100passport-nfc
EU ID card (NFC)100id-card-nfc
Aadhaar80aadhaar
KYC50kyc-sumsub

Validation registry

validation.validationRequest(validatorAddress, agentId, requestURI, requestHash);                 // agent owner
validation.validationResponse(requestHash, response /* 0-100 */, responseURI, responseHash, tag); // validator
(uint64 count, uint8 avgResponse) = validation.getSummary(agentId, validatorAddresses, tag);
validation.submitFreshnessValidation(agentId);                                                     // anyone
(bool fresh, uint256 lastUpdated) = validation.getLatestFreshness(agentId);

Governance

  • SECURITY_ROLE approves or removes proof providers, sets the Self provider and toggles requireHumanProof.
  • OPERATIONS_ROLE sets maxAgentsPerHuman and maxProofAge, and links the reputation and validation registries.

On Celo Mainnet, multisigs hold these roles (security 3 of 5, operations 2 of 5). The registries are UUPS proxies with ERC-7201 namespaced storage. To build against Hub V2 with Foundry, see Foundry setup.

Was this page helpful?