Whitelabel Communities Registration Flow

How to implement communities registration on your application with Clusters API v1

For projects that want to deploy a registration page quickly, we provide a simple settings panel where you configure some basic settings and be on your way. More information on that here.

For projects that want to implement the community name registration directly into their app, this guide's for you!


What's the Difference?

If you deploy a community name registration page through our settings panel, we'll host the page where you redirect users to when they need/want to claim a community name. In this flow, users register community names by signing a message.

If you implement the community name registration into your hosted app, the flow requires the Cluster owner's Authentication Key to claim a name on behalf of the user.

This simply means the owner's wallet needs to generate a secret key and manage it in your application.

The below guide walks through a NextJS app that enables users to register a name in a Clusters community, using the Clusters API v1 - Communities. The guide uses direct HTTP calls only, making it easy to extend or port to other stacks.

We will reference the claim-community-cluster-demo repo.


Prerequisites

  1. Register a Cluster through our GUI at https://clusters.xyz/register, or programmatically through API v1 - Registration.

  2. Enabling the communities feature on your Clusters requires manual activation. Please contact us to have it enabled for you.

  3. Get the Authentication Key of the owner (the wallet used to register the Cluster).

Test Clusters can be registered on https://testnet.clusters.xyz/register. When testing, add the testnet=true query param to each endpoint.


Authenticate Owner

The owner's Authentication Key is required to register a community name on behalf of a user's wallet address. You only need to generate this secret key once, and can re-use it. This token is used to register a name on behalf of a user's wallet address.

To generate one quickly, a function called getAuthKey has been included in the useAuthKey hook in the claim-community-cluster-demo repo for your convenience.

The following endpoints are in API v1 - Authentication:

Get Signing Message

  1. GET /v1/auth/message

  2. Returns a message and a signingDate

  3. Sign the message with the owner's wallet

curl -X GET 'https://api.clusters.xyz/v1/auth/message'

Response

{
  "message": "clusters.xyz verification\n\nBefore interacting with certain functionality, we require a wallet signature for verification.\n\n2024-05-14T19:08:27.985Z",
  "signingDate": "2024-05-14T19:08:27.985Z"
}

Get Authentication Key

  1. POST /v1/auth/token

  2. Send in body, the signature, signingDate, wallet as owner's wallet, and type as "evm"

  3. Save this token, you will need it every time to register a name on behalf of a user

curl --request POST \
  --url https://api.clusters.xyz/v1/auth/token \
  --header 'Content-Type: application/json' \
  --data '{
    "signature": "0x68b3eaa1fd6...",
    "signingDate": "2024-05-14T19:08:27.985Z",
    "type": "evm",
    "wallet": "0x0000000000000000000000000000000000000000"
  }'

Response

{
  "token": "RmUyNi4yKjEqZjkyOGU3OWNjMmY5NDlkODZmM2I4...."
}

Endpoints Used in Registration Process

  1. GET v1/names/owner/address/:address

  2. Check if a wallet address is part of your Clusters community

  3. Returns all Clusters and community names registered to a single wallet address

curl -X GET 'https://api.clusters.xyz/v1/names/owner/address/0x5755d1dcea21caa687339c305d143e6e78f96adf

Response

[
    {
        "name": "cypherpunks/satoshi",
        "owner": "0x5755d1dcea21caa687339c305d143e6e78f96adf",
        "totalWeiAmount": "0",
        "createdAt": "2025-04-28 18:10:05.805+00",
        "updatedAt": "2025-04-28 18:10:05.805+00",
        "updatedBy": "0x5755d1dcea21caa687339c305d143e6e78f96adf",
        "isTestnet": false,
        "clusterId": "0x...",
        "expiresAt": null
    },
    {
        "name": "mclovin/satoshi",
        "owner": "0x5755d1dcea21caa687339c305d143e6e78f96adf",
        "totalWeiAmount": "0",
        "createdAt": "2025-04-28 18:10:05.805+00",
        "updatedAt": "2025-04-28 18:10:05.805+00",
        "updatedBy": "0x5755d1dcea21caa687339c305d143e6e78f96adf",
        "isTestnet": false,
        "clusterId": "0x...",
        "expiresAt": null
    }
]

  1. GET /v1/names/community/:clusterName/check/:name

  2. Check the availability of a community name before calling the register endpoint.

curl -X GET 'https://api.clusters.xyz/v1/names/community/cypherpunks/check/desiredName'

Response

{
  "name": "cypherpunks/desiredName",
  "isAvailable": true
}

  1. POST /v1/names/community/:clusterName/register

  2. This call should only be made from your server because the header includes the AUTHKEY, which you generated by authenticating the owner's wallet.

  3. The body of the call includes the user's desired community name and their associated wallet address.

curl --request POST \
  --url https://api.clusters.xyz/v1/names/community/cypherpunks/register \
  --header 'Authorization: Bearer AUTHKEY'
  --header 'Content-Type: application/json' \
  --data '{ 
    "name": "desiredName",
    "walletAddress": "0x0000000000000000000000000000000000000000"
  }'

Response

{
  "clusterName": "cypherpunks/desiredName",
  "owner": "0x0000000000000000000000000000000000000000"
}

How it Comes Together in the Demo repo

Below is a breakdown of how claim-community-cluster-demo app uses the Cluster API v1 to create the flow for a user to claim a community name.

Fetch Community Name

When user connects wallet, wagmi useAccount hook defines address of connected wallet, triggering tanstack useQuery hook to call the Get Names by Owner endpoint.

Look for whether this address already has a registered to your Clusters community as the response can contain many unrelated entries (e.g. other communities or personally owned Clusters).

// src/hooks/useCommunityNameQuery.ts
import { fetchCommunityName } from '../lib/api/clusters';
import { useQuery } from '@tanstack/react-query';
import { useAccount } from 'wagmi';
import { CommunityName } from '../types/cluster';

export const COMMUNITY_NAME_QUERY_KEY = 'communityName';

export function useCommunityNameQuery() {
  const { address } = useAccount();

  return useQuery({
    queryKey: [COMMUNITY_NAME_QUERY_KEY, address],
    queryFn: async () => {
      if (!address) return null;
      const data: CommunityName[] = await fetchCommunityName(address);
      const communityMember = data.find(member => member.name.startsWith('yourCommunityCluster/'));
      return communityMember?.name ?? null;
    },
    enabled: !!address,
  });
}

The root of the application reacts to the returned loading state or clusterName from the useCommunityNameQuery hook.

// src/app/page.tsx
import { useCommunityNameQuery } from "../hooks/useCommunityNameQuery";

const { data: communityName, isLoading } = useCommunityNameQuery();

// UI displayed conditionally

Claiming Community Name

When a user types their desired community name, the useCommunityNameAvailability hook triggers a debounced check for its availability via call to Check Community Name Availability endpoint, after the user pauses typing.

// src/hooks/useCommunityNameAvailability
import { useState, useCallback, useEffect } from 'react';
import { checkNameAvailability } from '../lib/api/clusters';
import useDebounce from './useDebounce';

export function useCommunityNameAvailability(delay = 500) {
  const [isAvailable, setIsAvailable] = useState<boolean | null>(null);
  const [isChecking, setIsChecking] = useState(false);
  const [desiredName, setDesiredName] = useState("");
  
  const debouncedDesiredName = useDebounce(desiredName, delay);

  const checkAvailability = useCallback(async (name: string) => {
    if (!name) {
      setIsAvailable(null);
      return;
    }

    setIsChecking(true);
    
    try {
      const data = await checkNameAvailability(name);
      setIsAvailable(data.isAvailable);
    } catch (error) {
      console.error('Error checking name availability:', error);
      setIsAvailable(null);
    } finally {
      setIsChecking(false);
    }
  }, []);

  useEffect(() => {
    checkAvailability(debouncedDesiredName);
  }, [debouncedDesiredName, checkAvailability]);

  return {
    desiredName,
    setDesiredName,
    isAvailable,
    isChecking
  };
}

The ClaimModal component uses the hook's returned state to update UI.

// src/components/ClaimModal.tsx
const { desiredName, setDesiredName, isAvailable, isChecking } = useCommunityNameAvailability();
const { claimName, isClaiming } = useCommunityNameClaim();

const handleClaimName = useCallback(async () => {
  const success = await claimName(desiredName);
  if (success) {
    setDesiredName("");
  }
}, [desiredName, claimName, setDesiredName]);

When user triggers claimName function from useCommunityNameClaim hook, it communicates with the api/cluster/register_community_name/route.ts NextJS API route to call Register a Community Name endpoint.

// src/lib/server/clusters.ts
const { name, walletAddress, communityName, apiKey, authKey } = params;

try {
  const response = await fetch(
    `https://api.clusters.xyz/v1/names/community/${communityName}/register?testnet=true`, 
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-Key": apiKey, // optional
        "Authorization": `Bearer ${authKey}` // Community Cluster owner's Authentication Key
      },
      body: JSON.stringify({
        name, // user's desired community name
        walletAddress // the user's wallet address to be associated to community name
      })
    }
  );
  
  const data = await response.json();
  return { ...data, success: response.ok };

If successful, useCommunityNameQuery hook will trigger a refetch and the updated community name will display on the UI. Your user is now a bona-fide community member!

Last updated