Skip to main content

Documentation Index

Fetch the complete documentation index at: https://celonames.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

Use this guide to integrate Celo Names (subname registration) into your application: availability checks, pricing, registration/renewal, and record reads.
This page focuses on contract-level integration (building transactions with the Celo Names registrars and reading back records).

Deployment addresses

Use the correct Celo mainnet contract addresses when you build calls.

Indexer (GraphQL)

Query names/owners/records after you register or update a name.

Agent Skill

See how to wrap these flows into an agent skill.

wagmi/viem integration

A lighter example if you prefer wagmi patterns.

Overview

Celo Names is an ENS-based naming system that lets users register human-readable subnames under the celo.eth parent domain (e.g., alice.celo.eth). The system includes:
  • L2Registrar: handles registration and renewal with CELO or ERC20 tokens.
  • L2SelfRegistrar: lets self-verified users claim one free name.
  • L2Registry: ERC721-based registry that stores names as NFTs with expiration.
  • Indexer: GraphQL API for querying name data.

Contract Addresses (Celo Mainnet)

const CONTRACT_ADDRESSES = {
  // L2 Contracts (Celo)
  L2_REGISTRAR: "0x9Eb22700eFa1558eb2e0E522eB1DECC8025C3127",
  L2_REGISTRY: "0x4d7912779679AFdC592CBd4674b32Fcb189395F7",
  L2_SELF_REGISTRAR: "0x063E9F0bA0061F6C3c6169674c81f43BE21fe8cc",
  REGISTRAR_STORAGE: "0xaAF67A46b99bE9a183580Cd86236cd0c6f2a85cb",
};

const TOKENS = {
  CELO: "0x0000000000000000000000000000000000000000", // Native token
  USDC: "0xcebA9300f2b948710d2653dD7B07f33A8B32118C",
  USDT: "0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e",
  cUSD: "0x765DE816845861e75A25fCA122bb6898B8B1282a",
};

Required ABIs

L2Registrar ABI (Minimal)

const L2_REGISTRAR_ABI = [
  // Read functions
  {
    inputs: [{ name: "label", type: "string" }],
    name: "available",
    outputs: [{ name: "", type: "bool" }],
    stateMutability: "view",
    type: "function",
  },
  {
    inputs: [
      { name: "label", type: "string" },
      { name: "durationInYears", type: "uint64" },
      { name: "paymentToken", type: "address" },
    ],
    name: "rentPrice",
    outputs: [{ name: "", type: "uint256" }],
    stateMutability: "view",
    type: "function",
  },
  // Write functions
  {
    inputs: [
      { name: "label", type: "string" },
      { name: "durationInYears", type: "uint64" },
      { name: "owner", type: "address" },
      { name: "resolverData", type: "bytes[]" },
    ],
    name: "register",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
  {
    inputs: [
      { name: "label", type: "string" },
      { name: "durationInYears", type: "uint64" },
      { name: "owner", type: "address" },
      { name: "resolverData", type: "bytes[]" },
      { name: "paymentToken", type: "address" },
      {
        name: "permit",
        type: "tuple",
        components: [
          { name: "value", type: "uint256" },
          { name: "deadline", type: "uint256" },
          { name: "v", type: "uint8" },
          { name: "r", type: "bytes32" },
          { name: "s", type: "bytes32" },
        ],
      },
    ],
    name: "registerERC20",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [
      { name: "label", type: "string" },
      { name: "durationInYears", type: "uint64" },
    ],
    name: "renew",
    outputs: [],
    stateMutability: "payable",
    type: "function",
  },
  {
    inputs: [
      { name: "label", type: "string" },
      { name: "durationInYears", type: "uint64" },
      { name: "paymentToken", type: "address" },
      {
        name: "permit",
        type: "tuple",
        components: [
          { name: "value", type: "uint256" },
          { name: "deadline", type: "uint256" },
          { name: "v", type: "uint8" },
          { name: "r", type: "bytes32" },
          { name: "s", type: "bytes32" },
        ],
      },
    ],
    name: "renewERC20",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
];

L2SelfRegistrar ABI (Minimal)

const L2_SELF_REGISTRAR_ABI = [
  {
    inputs: [
      { name: "label", type: "string" },
      { name: "owner", type: "address" },
      { name: "resolverData", type: "bytes[]" },
    ],
    name: "claim",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
];

L2Registry ABI (Minimal)

const L2_REGISTRY_ABI = [
  {
    inputs: [{ name: "data", type: "bytes[]" }],
    name: "multicall",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
  {
    inputs: [
      { name: "from", type: "address" },
      { name: "to", type: "address" },
      { name: "tokenId", type: "uint256" },
    ],
    name: "safeTransferFrom",
    outputs: [],
    stateMutability: "nonpayable",
    type: "function",
  },
];

ERC20 Permit ABI (Minimal)

const ERC20_PERMIT_ABI = [
  {
    inputs: [{ name: "owner", type: "address" }],
    name: "nonces",
    outputs: [{ name: "", type: "uint256" }],
    stateMutability: "view",
    type: "function",
  },
];

Core Integration Functions

1. Check Name Availability

import { createPublicClient, http } from "viem";
import { celo } from "viem/chains";

const publicClient = createPublicClient({
  chain: celo,
  transport: http(),
});

async function isNameAvailable(label: string): Promise<boolean> {
  const available = await publicClient.readContract({
    address: CONTRACT_ADDRESSES.L2_REGISTRAR,
    abi: L2_REGISTRAR_ABI,
    functionName: "available",
    args: [label],
  });
  return available;
}

2. Get Registration Price

async function getRentPrice(
  label: string,
  durationInYears: number,
  tokenAddress: string = "0x0000000000000000000000000000000000000000", // CELO
): Promise<bigint> {
  const price = await publicClient.readContract({
    address: CONTRACT_ADDRESSES.L2_REGISTRAR,
    abi: L2_REGISTRAR_ABI,
    functionName: "rentPrice",
    args: [label, durationInYears, tokenAddress],
  });
  return price;
}

3. Encode Resolver Data

Resolver data sets the initial records at registration time (text records, address records, contenthash).
import { encodeFunctionData, namehash, parseAbi, toHex } from "viem";
import { getCoderByCoinType } from "@ensdomains/address-encoder";

const SET_TEXT_FUNC =
  "function setText(bytes32 node, string key, string value)";
const SET_ADDRESS_FUNC =
  "function setAddr(bytes32 node, uint256 coin, bytes value)";
const SET_CONTENTHASH_FUNC =
  "function setContenthash(bytes32 node, bytes value)";

interface EnsRecords {
  texts: { key: string; value: string }[];
  addresses: { coinType: number; value: string }[];
  contenthash?: { protocol: string; value: string };
}

function convertToResolverData(
  fullName: string,
  records: EnsRecords,
): `0x${string}`[] {
  const node = namehash(fullName);
  const resolverData: `0x${string}`[] = [];

  // Encode text records
  records.texts
    .filter((text) => text.value.length > 0)
    .forEach((text) => {
      const data = encodeFunctionData({
        functionName: "setText",
        abi: parseAbi([SET_TEXT_FUNC]),
        args: [node, text.key, text.value],
      });
      resolverData.push(data);
    });

  // Encode address records
  records.addresses.forEach((addr) => {
    const coinEncoder = getCoderByCoinType(addr.coinType);
    if (!coinEncoder) return;

    const decoded = coinEncoder.decode(addr.value);
    const hexValue = toHex(decoded);
    const data = encodeFunctionData({
      functionName: "setAddr",
      abi: parseAbi([SET_ADDRESS_FUNC]),
      args: [node, BigInt(addr.coinType), hexValue],
    });
    resolverData.push(data);
  });

  return resolverData;
}

4. Register with Native Token (CELO)

import { createWalletClient, custom } from "viem";

const walletClient = createWalletClient({
  chain: celo,
  transport: custom(window.ethereum),
});

async function registerWithCELO(
  label: string,
  durationInYears: number,
  owner: string,
  records: EnsRecords,
) {
  // Get price
  const price = await getRentPrice(label, durationInYears);

  // Prepare resolver data
  const resolverData = convertToResolverData(`${label}.celo.eth`, records);

  // Simulate and write transaction
  const { request } = await publicClient.simulateContract({
    address: CONTRACT_ADDRESSES.L2_REGISTRAR,
    abi: L2_REGISTRAR_ABI,
    functionName: "register",
    args: [label, durationInYears, owner, resolverData],
    value: price,
    account: owner,
  });

  return await walletClient.writeContract(request);
}

5. Register with ERC20 Token (USDC/USDT/cUSD)

ERC20 registration needs a permit signature so the contract can pull tokens on the user’s behalf.
const PERMIT_TYPES = {
  Permit: [
    { name: "owner", type: "address" },
    { name: "spender", type: "address" },
    { name: "value", type: "uint256" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" },
  ],
};

async function createERC20Permit(
  token: { address: string; name: string; version: string; decimals: number },
  spender: string,
  value: bigint,
  owner: string,
) {
  // Get nonce
  const nonce = await publicClient.readContract({
    address: token.address,
    abi: ERC20_PERMIT_ABI,
    functionName: "nonces",
    args: [owner],
  });

  const deadline = BigInt(Math.floor(Date.now() / 1000) + 300); // 5 minutes

  // Sign permit
  const signature = await walletClient.signTypedData({
    domain: {
      name: token.name,
      version: token.version,
      chainId: celo.id,
      verifyingContract: token.address,
    },
    types: PERMIT_TYPES,
    primaryType: "Permit",
    message: {
      owner,
      spender,
      value,
      nonce,
      deadline,
    },
  });

  // Serialize signature
  const sig = signature.slice(2);
  return {
    value,
    deadline,
    v: parseInt(sig.slice(128, 130), 16),
    r: `0x${sig.slice(0, 64)}` as `0x${string}`,
    s: `0x${sig.slice(64, 128)}` as `0x${string}`,
  };
}

async function registerWithERC20(
  label: string,
  durationInYears: number,
  owner: string,
  records: EnsRecords,
  paymentToken: { address: string; name: string; version: string },
) {
  // Get price in token
  const price = await getRentPrice(
    label,
    durationInYears,
    paymentToken.address,
  );

  // Create permit
  const permit = await createERC20Permit(
    paymentToken,
    CONTRACT_ADDRESSES.L2_REGISTRAR,
    price,
    owner,
  );

  // Prepare resolver data
  const resolverData = convertToResolverData(`${label}.celo.eth`, records);

  // Register
  const { request } = await publicClient.simulateContract({
    address: CONTRACT_ADDRESSES.L2_REGISTRAR,
    abi: L2_REGISTRAR_ABI,
    functionName: "registerERC20",
    args: [
      label,
      durationInYears,
      owner,
      resolverData,
      paymentToken.address,
      permit,
    ],
    account: owner,
  });

  return await walletClient.writeContract(request);
}

6. Claim with Self Verification (Free)

Self-verified users can claim one free name:
async function claimWithSelf(
  label: string,
  owner: string,
  records: EnsRecords,
) {
  const resolverData = convertToResolverData(`${label}.celo.eth`, records);

  const { request } = await publicClient.simulateContract({
    address: CONTRACT_ADDRESSES.L2_SELF_REGISTRAR,
    abi: L2_SELF_REGISTRAR_ABI,
    functionName: "claim",
    args: [label, owner, resolverData],
    account: owner,
  });

  return await walletClient.writeContract(request);
}

7. Renew a Name

Calls renew(label, durationInYears) and sends the native CELO rent amount.
async function renewName(
  label: string,
  durationInYears: number,
  paymentToken: string = "0x0000000000000000000000000000000000000000",
) {
  const isNative =
    paymentToken === "0x0000000000000000000000000000000000000000";

  if (isNative) {
    const price = await getRentPrice(label, durationInYears);
    const { request } = await publicClient.simulateContract({
      address: CONTRACT_ADDRESSES.L2_REGISTRAR,
      abi: L2_REGISTRAR_ABI,
      functionName: "renew",
      args: [label, durationInYears],
      value: price,
      account: owner,
    });
    return await walletClient.writeContract(request);
  } else {
    // ERC20 renewal requires permit
    const price = await getRentPrice(label, durationInYears, paymentToken);
    const permit = await createERC20Permit(
      { address: paymentToken, name: "USDC", version: "2", decimals: 6 },
      CONTRACT_ADDRESSES.L2_REGISTRAR,
      price,
      owner,
    );

    const { request } = await publicClient.simulateContract({
      address: CONTRACT_ADDRESSES.L2_REGISTRAR,
      abi: L2_REGISTRAR_ABI,
      functionName: "renewERC20",
      args: [label, durationInYears, paymentToken, permit],
      account: owner,
    });
    return await walletClient.writeContract(request);
  }
}

Dependencies

These samples assume you’re using viem and the ENS encoding helpers used to build resolver data.
Install the required packages:
npm i viem @ensdomains/address-encoder @ensdomains/content-hash

Querying Name Data (Indexer)

The Celo Names indexer provides a GraphQL API for querying name data:
const INDEXER_URL = "https://celo-indexer-reader.namespace.ninja/graphql";

// Get names by owner
async function getNamesByOwner(owner: string) {
  const query = `
    query GetOwnerNames($where: nameFilter) {
      names(where: $where, orderBy: "created_at", orderDirection: "desc", limit: 100) {
        items {
          id
          label
          full_name
          expiry
          owner
          records {
            addresses
            texts
            contenthash
          }
        }
      }
    }
  `;

  const response = await fetch(INDEXER_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      query,
      variables: { where: { owner: owner.toLowerCase() } },
    }),
  });

  const result = await response.json();
  return result.data.names.items;
}

Key Constraints

  1. Label length: Minimum 3 characters
  2. Duration: 1 year and above
  3. Payment options:
    • CELO (native token)
    • USDC, USDT, cUSD (ERC20 with permit)
  4. Self claim: one free name per verified user; expires after 1 year
  5. Renewal: can be done before or after expiration (grace period considerations)

Events to Monitor

Use these event signatures to confirm transaction outcomes and to drive UI updates.
// Name registration event
const NameRegisteredEvent = {
  name: "NameRegistered",
  signature: "NameRegistered(string,bytes32,address,uint64,address,uint256)",
  params: ["label", "node", "owner", "durationInYears", "token", "price"],
};

// Name renewal event
const NameRenewedEvent = {
  name: "NameRenewed",
  signature: "NameRenewed(string,bytes32,uint64,address,uint256)",
  params: ["label", "node", "durationInYears", "token", "price"],
};

// Name claimed via Self
const NameClaimedEvent = {
  name: "NameClaimed",
  signature: "NameClaimed(string,bytes32,address)",
  params: ["label", "node", "owner"],
};

Error Handling

Convert these error keys (or revert messages) into clear UI states, and keep the transaction hash around for debugging.
Common errors to handle:
// Contract errors
const ERRORS = {
  SubnameDoesNotExist: "Subname does not exist",
  InvalidDuration: "Duration must be between 1-10000 years",
  InvalidLabelLength: "Label must be 3-64 characters",
  InsufficientFunds: "Insufficient payment amount",
  TokenNotAllowed: "Payment token not supported",
  BlacklistedName: "Name is blacklisted",
  NotWhitelisted: "Address not whitelisted",
  NotSelfVerified: "User not verified with Self",
  MaximumNamesClaimed: "Maximum free names claimed",
};

Self Verification Integration

To integrate Self verification for free name claiming:
  1. Install the Self QR code component (from @selfxyz/qrcode)
  2. Use the scope seed: "celo-names"
  3. After successful verification, call claimWithSelf()
  4. Check verification status via RegistrarStorage.isVerified(user)
// Check if user is Self verified
async function isSelfVerified(user: string): Promise<boolean> {
  return publicClient.readContract({
    address: CONTRACT_ADDRESSES.REGISTRAR_STORAGE,
    abi: [
      {
        inputs: [{ name: "user", type: "address" }],
        name: "isVerified",
        outputs: [{ name: "", type: "bool" }],
        stateMutability: "view",
        type: "function",
      },
    ],
    functionName: "isVerified",
    args: [user],
  });
}