Development

Building with onchain identity on Base (Basenames, Farcaster, XMTP)

Integrate Basenames for human-readable addresses, Farcaster for social graphs, and XMTP for encrypted messaging. Copy-paste code for each.

15 minUpdated 2026-03-01

Why onchain identity matters

Hex addresses like 0x1a2b...3c4d are unreadable and impossible to verify at a glance. Identity layers turn them into human-readable names, social profiles, and messaging endpoints. Base has the most complete identity stack in crypto: Basenames for naming, Farcaster for social graphs, XMTP for messaging, and OnchainKit to render it all in React.

The Base identity stack

LayerProtocolWhat it does
NamesBasenamesENS subdomains on Base — yourname.base.eth
SocialFarcasterDecentralized social network with onchain identity (FIDs)
MessagingXMTPE2E encrypted wallet-to-wallet messaging
UIOnchainKitReact components that resolve and display all of the above

Basenames — human-readable addresses

Basenames are ENS subdomains on Base L2. They resolve exactly like ENS names but live onchain on Base, making them fast and cheap.

Contract addresses (Base mainnet)

ContractAddress
Registry0xb94704422c2a1e396835a571837aa5ae53285a95
RegistrarController0x4cCb0BB02FCABA27e82a56646E81d8c5bC4119a5
L2Resolver0xC6d566A56A1aFf6508b41f6c90ff131615583BCD
ReverseRegistrar0x79ea96012eea67a83431f1701b3dff7e37f9e282

Pricing (per year)

LengthPrice
3 characters0.1 ETH
4 characters0.01 ETH
5–9 characters0.001 ETH
10+ characters0.0001 ETH

Resolve a Basename to an address

typescript
// resolve-basename.ts — Forward-resolve a .base.eth name to an address
import { createPublicClient, http, namehash } from "viem";
import { base } from "viem/chains";

const L2_RESOLVER = "0xC6d566A56A1aFf6508b41f6c90ff131615583BCD" as const;

const L2_RESOLVER_ABI = [
  {
    name: "addr",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "node", type: "bytes32" }],
    outputs: [{ name: "", type: "address" }],
  },
] as const;

const client = createPublicClient({ chain: base, transport: http() });

export async function resolveBasename(name: string): Promise<string | null> {
  try {
    const node = namehash(name); // e.g. "alice.base.eth"
    const address = await client.readContract({
      address: L2_RESOLVER,
      abi: L2_RESOLVER_ABI,
      functionName: "addr",
      args: [node],
    });
    return address === "0x0000000000000000000000000000000000000000"
      ? null
      : address;
  } catch {
    return null;
  }
}

// Usage
const addr = await resolveBasename("alice.base.eth");
console.log(addr); // 0x1234...abcd or null

Reverse-resolve an address to a Basename

typescript
// reverse-resolve.ts — Get the Basename for a given address
import { createPublicClient, http, namehash, encodePacked, keccak256 } from "viem";
import { base } from "viem/chains";

const L2_RESOLVER = "0xC6d566A56A1aFf6508b41f6c90ff131615583BCD" as const;
const REVERSE_REGISTRAR = "0x79ea96012eea67a83431f1701b3dff7e37f9e282" as const;

const RESOLVER_ABI = [
  {
    name: "name",
    type: "function",
    stateMutability: "view",
    inputs: [{ name: "node", type: "bytes32" }],
    outputs: [{ name: "", type: "string" }],
  },
] as const;

const client = createPublicClient({ chain: base, transport: http() });

export async function reverseResolve(address: string): Promise<string | null> {
  try {
    // Reverse node: addr.reverse
    const reverseNode = namehash(
      `${address.toLowerCase().slice(2)}.addr.reverse`
    );
    const name = await client.readContract({
      address: L2_RESOLVER,
      abi: RESOLVER_ABI,
      functionName: "name",
      args: [reverseNode],
    });
    return name || null;
  } catch {
    return null;
  }
}

// Usage
const name = await reverseResolve("0x1234567890abcdef1234567890abcdef12345678");
console.log(name); // "alice.base.eth" or null

Register a Basename with Foundry

solidity
// script/RegisterBasename.s.sol — Register a Basename programmatically
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;

import "forge-std/Script.sol";

interface IRegistrarController {
    struct RegisterRequest {
        string name;
        address owner;
        uint256 duration;
        address resolver;
        bytes[] data;
        bool reverseRecord;
    }
    function register(RegisterRequest calldata request) external payable;
}

contract RegisterBasename is Script {
    address constant REGISTRAR = 0x4cCb0BB02FCABA27e82a56646E81d8c5bC4119a5;
    address constant RESOLVER = 0xC6d566A56A1aFf6508b41f6c90ff131615583BCD;

    function run() external {
        uint256 pk = vm.envUint("PRIVATE_KEY");
        address owner = vm.addr(pk);

        vm.startBroadcast(pk);

        IRegistrarController.RegisterRequest memory req = IRegistrarController.RegisterRequest({
            name: "myname",           // registers myname.base.eth
            owner: owner,
            duration: 365 days,
            resolver: RESOLVER,
            data: new bytes[](0),
            reverseRecord: true       // set as primary name
        });

        // 10+ chars = 0.0001 ETH/year. Adjust value for shorter names.
        IRegistrarController(REGISTRAR).register{value: 0.001 ether}(req);

        vm.stopBroadcast();
    }
}

Run with:

bash
forge script script/RegisterBasename.s.sol --rpc-url https://mainnet.base.org --broadcast

See deploy smart contracts on Base for Foundry setup.

OnchainKit Name and Avatar components

tsx
// BasenameDisplay.tsx — Render a Basename + avatar in React
import { Name, Avatar } from "@coinbase/onchainkit/identity";
import { base } from "viem/chains";

export function BasenameDisplay({ address }: { address: `0x${string}` }) {
  return (
    <div className="flex items-center gap-3">
      <Avatar address={address} chain={base} className="w-10 h-10 rounded-full" />
      <Name address={address} chain={base} className="text-lg font-semibold" />
    </div>
  );
}

Setting text records

Text records let you attach metadata to your Basename — Twitter handle, GitHub, website, avatar URL. Set them via the L2Resolver:

typescript
// set-text-record.ts — Attach social metadata to a Basename
import { createWalletClient, http, namehash } from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

const L2_RESOLVER = "0xC6d566A56A1aFf6508b41f6c90ff131615583BCD" as const;

const RESOLVER_ABI = [
  {
    name: "setText",
    type: "function",
    stateMutability: "nonpayable",
    inputs: [
      { name: "node", type: "bytes32" },
      { name: "key", type: "string" },
      { name: "value", type: "string" },
    ],
    outputs: [],
  },
] as const;

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const wallet = createWalletClient({ account, chain: base, transport: http() });

const node = namehash("myname.base.eth");

// Supported keys: com.twitter, com.github, url, avatar, description
await wallet.writeContract({
  address: L2_RESOLVER,
  abi: RESOLVER_ABI,
  functionName: "setText",
  args: [node, "com.twitter", "myhandle"],
});

OnchainKit Identity components

Install OnchainKit:

bash
npm install @coinbase/onchainkit

OnchainKit provides ready-made identity components that auto-resolve Basenames, avatars, and Coinbase attestations.

Components

ComponentPurpose
<Identity />Wrapper that provides identity context
<Name />Resolves and displays Basename or ENS name
<Avatar />Resolves and displays avatar from name records
<Badge />Shows Coinbase verification badge (attestation)
<Address />Displays truncated address with copy

Full profile card example

tsx
// IdentityCard.tsx — Full user profile card with Basename, avatar, and attestation badge
import {
  Identity,
  Name,
  Avatar,
  Badge,
  Address,
} from "@coinbase/onchainkit/identity";
import { base } from "viem/chains";

// Coinbase account verification schema
const CB_ATTESTATION_SCHEMA =
  "0xf8b05c79f090979bf4a80270aba232dff11a10d9ca55c4f88de95317970f0de9";

export function IdentityCard({ address }: { address: `0x${string}` }) {
  return (
    <Identity
      address={address}
      chain={base}
      schemaId={CB_ATTESTATION_SCHEMA}
      className="rounded-xl border border-gray-800 bg-gray-900 p-4"
    >
      <div className="flex items-center gap-4">
        <Avatar className="w-14 h-14 rounded-full" />
        <div>
          <div className="flex items-center gap-2">
            <Name className="text-lg font-bold text-white" />
            <Badge className="w-4 h-4" />
          </div>
          <Address className="text-sm text-gray-400" />
        </div>
      </div>
    </Identity>
  );
}

The <Badge /> renders automatically when the address has a Coinbase verification attestation (EAS). See the Coinbase Smart Wallet guide for wallet integration.

Sign In with Farcaster (SIWF)

Sign In with Farcaster gives your app OAuth-like authentication backed by onchain identity. Users scan a QR code or approve in Warpcast — no email, no password.

Install

bash
npm install @farcaster/auth-kit

Full sign-in component

tsx
// FarcasterSignIn.tsx — Complete SIWF integration
import "@farcaster/auth-kit/styles.css";
import { AuthKitProvider, SignInButton, useProfile } from "@farcaster/auth-kit";

const authConfig = {
  rpcUrl: "https://mainnet.optimism.io", // Farcaster ID registry is on OP Mainnet
  domain: "yourdomain.com",              // Your app's domain
  siweUri: "https://yourdomain.com",     // SIWE callback URI
};

function SignInWithFarcaster() {
  return (
    <AuthKitProvider config={authConfig}>
      <SignInButton
        onSuccess={({ fid, username, displayName, pfpUrl, custody, verifications }) => {
          console.log("Signed in:", {
            fid,           // Farcaster ID (number)
            username,      // e.g. "alice"
            displayName,   // e.g. "Alice"
            pfpUrl,        // Profile picture URL
            custody,       // Custody address (owns the FID)
            verifications, // Array of verified ETH addresses
          });
        }}
      />
    </AuthKitProvider>
  );
}

export default SignInWithFarcaster;

What you get back

FieldTypeDescription
fidnumberFarcaster ID — unique user identifier
usernamestringFarcaster username
displayNamestringDisplay name
pfpUrlstringProfile picture URL
custodystringAddress that owns this FID
verificationsstring[]Verified Ethereum addresses

This gives you instant access to a user's social graph. No registration flow needed.

Farcaster data for your app

The fastest way to read Farcaster data (profiles, casts, followers) is the Neynar API. Free tier gives you 300 requests/minute.

Fetch a user profile by Ethereum address

typescript
// farcaster-profile.ts — Look up a Farcaster user by their verified address
const NEYNAR_API_KEY = process.env.NEYNAR_API_KEY!;

interface FarcasterUser {
  fid: number;
  username: string;
  display_name: string;
  pfp_url: string;
  follower_count: number;
  following_count: number;
  verifications: string[];
}

export async function getFarcasterUserByAddress(
  address: string
): Promise<FarcasterUser | null> {
  const res = await fetch(
    `https://api.neynar.com/v2/farcaster/user/bulk-by-address?addresses=${address}`,
    { headers: { accept: "application/json", api_key: NEYNAR_API_KEY } }
  );
  const data = await res.json();
  const users = data[address.toLowerCase()];
  return users?.[0] ?? null;
}

// Usage
const user = await getFarcasterUserByAddress("0x1234...abcd");
console.log(user?.username); // "alice"

Fetch a user's recent casts

typescript
// farcaster-casts.ts — Get recent casts from a Farcaster user
export async function getRecentCasts(fid: number, limit = 10) {
  const res = await fetch(
    `https://api.neynar.com/v2/farcaster/feed/user/${fid}/casts?limit=${limit}`,
    { headers: { accept: "application/json", api_key: NEYNAR_API_KEY } }
  );
  const data = await res.json();
  return data.casts as Array<{
    hash: string;
    text: string;
    timestamp: string;
    reactions: { likes_count: number; recasts_count: number };
  }>;
}

// Usage
const casts = await getRecentCasts(12345);
casts.forEach((c) => console.log(c.text));

Farcaster also supports Mini Apps (formerly Frames) — interactive apps embedded directly in the feed. See the Farcaster Mini Apps docs for details.

XMTP — encrypted wallet-to-wallet messaging

XMTP is a decentralized messaging protocol with end-to-end encryption. Version 3 (MLS-based) supports 1:1 and group chats. Every wallet can have an XMTP inbox — no signup needed.

SDKs available: Node.js (@xmtp/node-sdk), React (@xmtp/react-sdk), React Native, Kotlin, Swift.

Initialize an XMTP client

typescript
// xmtp-client.ts — Create an XMTP v3 client with a wallet signer
import { Client } from "@xmtp/node-sdk";
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import { toBytes } from "viem";

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

// XMTP expects a signer with getAddress() and signMessage()
const signer = {
  getAddress: () => account.address,
  signMessage: async (message: string) => {
    const wallet = createWalletClient({
      account,
      chain: base,
      transport: http(),
    });
    return wallet.signMessage({ message });
  },
};

// Initialize the XMTP client
const xmtp = await Client.create(signer, { env: "production" });
console.log("XMTP inbox:", xmtp.address);

Send a message

typescript
// xmtp-send.ts — Send a message to another wallet address
// Check if the recipient has XMTP enabled first
const canMessage = await xmtp.canMessage("0xRecipientAddress");
if (!canMessage) {
  console.log("Recipient has not enabled XMTP");
  process.exit(1);
}

// Create or load a 1:1 conversation
const conversation = await xmtp.conversations.newDm("0xRecipientAddress");

// Send a text message
await conversation.send("gm from Base!");
console.log("Message sent");

Listen for incoming messages

typescript
// xmtp-stream.ts — Stream incoming messages in real-time
const stream = await xmtp.conversations.streamAllMessages();

for await (const message of stream) {
  // Skip messages sent by us
  if (message.senderAddress === xmtp.address) continue;

  console.log(`[${message.senderAddress}]: ${message.content}`);

  // Auto-reply example
  const convo = await xmtp.conversations.getConversationById(
    message.conversationId
  );
  if (convo) {
    await convo.send("Thanks for your message!");
  }
}

XMTP has a built-in consent system for spam filtering. Every conversation starts as unknown. Your app should let users allow or deny:

StateMeaning
unknownNew conversation, not yet triaged
allowedUser accepted — show in inbox
deniedUser rejected — hide or filter

Content types

XMTP supports more than text: attachments, reactions, replies, read receipts, and onchain transaction references. Install content type packages as needed:

bash
npm install @xmtp/content-type-reaction @xmtp/content-type-reply

Putting it all together — identity-aware app

Architecture

User connects wallet
       │
       ▼
Resolve Basename (viem)
       │
       ▼
Fetch Farcaster profile (Neynar API)
       │
       ▼
Check XMTP status (canMessage)
       │
       ▼
Display full identity + enable messaging

Full identity component

tsx
// FullIdentity.tsx — Display Basename + Farcaster + XMTP status for any address
import { useEffect, useState } from "react";
import { Name, Avatar, Identity } from "@coinbase/onchainkit/identity";
import { base } from "viem/chains";

interface FarcasterProfile {
  username: string;
  display_name: string;
  follower_count: number;
}

interface IdentityState {
  farcaster: FarcasterProfile | null;
  xmtpEnabled: boolean;
  loading: boolean;
}

export function FullIdentity({ address }: { address: `0x${string}` }) {
  const [state, setState] = useState<IdentityState>({
    farcaster: null,
    xmtpEnabled: false,
    loading: true,
  });

  useEffect(() => {
    async function load() {
      // Fetch Farcaster profile
      const fcRes = await fetch(`/api/farcaster/user?address=${address}`);
      const fcData = await fcRes.json();

      // Check XMTP reachability
      const xmtpRes = await fetch(`/api/xmtp/can-message?address=${address}`);
      const xmtpData = await xmtpRes.json();

      setState({
        farcaster: fcData.user ?? null,
        xmtpEnabled: xmtpData.canMessage ?? false,
        loading: false,
      });
    }
    load();
  }, [address]);

  if (state.loading) return <div>Loading identity...</div>;

  return (
    <div className="rounded-xl border border-gray-800 bg-gray-900 p-6 max-w-sm">
      {/* Basename + Avatar via OnchainKit */}
      <Identity address={address} chain={base}>
        <div className="flex items-center gap-4 mb-4">
          <Avatar className="w-16 h-16 rounded-full" />
          <div>
            <Name className="text-xl font-bold text-white" />
            <p className="text-sm text-gray-500 font-mono">
              {address.slice(0, 6)}...{address.slice(-4)}
            </p>
          </div>
        </div>
      </Identity>

      {/* Farcaster */}
      {state.farcaster && (
        <div className="border-t border-gray-800 pt-3 mt-3">
          <p className="text-sm text-gray-400">Farcaster</p>
          <p className="text-white font-semibold">@{state.farcaster.username}</p>
          <p className="text-gray-500 text-xs">
            {state.farcaster.follower_count.toLocaleString()} followers
          </p>
        </div>
      )}

      {/* XMTP */}
      <div className="border-t border-gray-800 pt-3 mt-3 flex items-center gap-2">
        <span
          className={`w-2 h-2 rounded-full ${
            state.xmtpEnabled ? "bg-green-500" : "bg-gray-600"
          }`}
        />
        <span className="text-sm text-gray-400">
          {state.xmtpEnabled ? "XMTP enabled — can message" : "XMTP not enabled"}
        </span>
      </div>
    </div>
  );
}

When to use what

Use caseTool
Display a readable nameBasenames
Show social context (bio, followers, casts)Farcaster
Send direct messages to a walletXMTP
Render identity in ReactOnchainKit

Common patterns and gotchas

  • Basename resolution returns zero address if no name is set. Always check for the null/zero case — don't display 0x000...000 as a name.

  • Farcaster FID is not a wallet address. One FID can have multiple verified addresses. Use the verifications array, not custody, to match wallets.

  • Not every wallet has XMTP enabled. Always call canMessage() before trying to send. Show a clear UI state for unreachable addresses.

  • Neynar free tier: 300 req/min. Cache aggressively. User profiles rarely change — a 5-minute cache is fine.

  • XMTP network rate limits exist. Batch message sends and use streaming instead of polling.

  • Base Sepolia Basenames use different contracts. Don't hardcode mainnet addresses — use a config object that switches by chain ID. Check Base docs for testnet addresses.

  • OnchainKit needs a provider. Wrap your app in <OnchainKitProvider> with chain={base} for components to resolve correctly.

Resources

ResourceLink
Basenames docsdocs.base.org/identity/basenames
OnchainKit docsonchainkit.xyz
Farcaster docsdocs.farcaster.xyz
Farcaster AuthKitdocs.farcaster.xyz/auth-kit
Neynar APIdocs.neynar.com
XMTP docsdocs.xmtp.org
XMTP Node SDKgithub.com/xmtp/xmtp-node-js-sdk
Base Sepolia faucetdocs.base.org/tools/network-faucets