> ## Documentation Index
> Fetch the complete documentation index at: https://celonames.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Integration: Celo Names

> Minimum requirements for integrating Celo Names (subname registration) into your application

Use this guide to integrate Celo Names (subname registration) into your application: availability checks, pricing, registration/renewal, and record reads.

<Info>
  This page focuses on contract-level integration (building transactions with the Celo Names registrars and reading back records).
</Info>

<Columns cols={2}>
  <Card title="Deployment addresses" icon="location-dot" href="/developer/deployments">
    Use the correct Celo mainnet contract addresses when you build calls.
  </Card>

  <Card title="Indexer (GraphQL)" icon="database" href="/developer/indexer-graphql">
    Query names/owners/records after you register or update a name.
  </Card>
</Columns>

<Columns cols={2}>
  <Card title="Agent Skill" icon="robot" href="/developer/agent-skill">
    See how to wrap these flows into an agent skill.
  </Card>

  <Card title="wagmi/viem integration" icon="code" href="/developer/integration-wagmi">
    A lighter example if you prefer wagmi patterns.
  </Card>
</Columns>

## 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)

```typescript theme={null}
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)

```typescript theme={null}
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)

```typescript theme={null}
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)

```typescript theme={null}
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)

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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).

```typescript theme={null}
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)

```typescript theme={null}
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)

<Warning>
  ERC20 registration needs a permit signature so the contract can pull tokens on the user's behalf.
</Warning>

```typescript theme={null}
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:

```typescript theme={null}
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

<Tabs>
  <Tab title="Renew with CELO">
    Calls `renew(label, durationInYears)` and sends the native CELO rent amount.
  </Tab>

  <Tab title="Renew with ERC20">
    Calls `renewERC20(...)` and includes an ERC20 permit so the registrar can pull tokens.
  </Tab>
</Tabs>

```typescript theme={null}
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

<Check>
  These samples assume you're using `viem` and the ENS encoding helpers used to build resolver data.
</Check>

Install the required packages:

```bash theme={null}
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:

```typescript theme={null}
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

<Note>
  Use these event signatures to confirm transaction outcomes and to drive UI updates.
</Note>

<AccordionGroup>
  <Accordion title="Event signatures">
    ```typescript theme={null}
    // 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"],
    };
    ```
  </Accordion>
</AccordionGroup>

## Error Handling

<Tip>
  Convert these error keys (or revert messages) into clear UI states, and keep the transaction hash around for debugging.
</Tip>

Common errors to handle:

<AccordionGroup>
  <Accordion title="Contract error keys">
    ```typescript theme={null}
    // 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",
    };
    ```
  </Accordion>
</AccordionGroup>

## 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)`

```typescript theme={null}
// 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],
  });
}
```
