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
| Layer | Protocol | What it does |
|---|---|---|
| Names | Basenames | ENS subdomains on Base — yourname.base.eth |
| Social | Farcaster | Decentralized social network with onchain identity (FIDs) |
| Messaging | XMTP | E2E encrypted wallet-to-wallet messaging |
| UI | OnchainKit | React 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)
| Contract | Address |
|---|---|
| Registry | 0xb94704422c2a1e396835a571837aa5ae53285a95 |
| RegistrarController | 0x4cCb0BB02FCABA27e82a56646E81d8c5bC4119a5 |
| L2Resolver | 0xC6d566A56A1aFf6508b41f6c90ff131615583BCD |
| ReverseRegistrar | 0x79ea96012eea67a83431f1701b3dff7e37f9e282 |
Pricing (per year)
| Length | Price |
|---|---|
| 3 characters | 0.1 ETH |
| 4 characters | 0.01 ETH |
| 5–9 characters | 0.001 ETH |
| 10+ characters | 0.0001 ETH |
Resolve a Basename to an address
// 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
// 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
// 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:
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
// 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:
// 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:
npm install @coinbase/onchainkit
OnchainKit provides ready-made identity components that auto-resolve Basenames, avatars, and Coinbase attestations.
Components
| Component | Purpose |
|---|---|
<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
// 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
npm install @farcaster/auth-kit
Full sign-in component
// 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
| Field | Type | Description |
|---|---|---|
fid | number | Farcaster ID — unique user identifier |
username | string | Farcaster username |
displayName | string | Display name |
pfpUrl | string | Profile picture URL |
custody | string | Address that owns this FID |
verifications | string[] | 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
// 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
// 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
// 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
// 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
// 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!");
}
}
User consent
XMTP has a built-in consent system for spam filtering. Every conversation starts as unknown. Your app should let users allow or deny:
| State | Meaning |
|---|---|
unknown | New conversation, not yet triaged |
allowed | User accepted — show in inbox |
denied | User 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:
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
// 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 case | Tool |
|---|---|
| Display a readable name | Basenames |
| Show social context (bio, followers, casts) | Farcaster |
| Send direct messages to a wallet | XMTP |
| Render identity in React | OnchainKit |
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...000as a name. -
Farcaster FID is not a wallet address. One FID can have multiple verified addresses. Use the
verificationsarray, notcustody, 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>withchain={base}for components to resolve correctly.
Resources
| Resource | Link |
|---|---|
| Basenames docs | docs.base.org/identity/basenames |
| OnchainKit docs | onchainkit.xyz |
| Farcaster docs | docs.farcaster.xyz |
| Farcaster AuthKit | docs.farcaster.xyz/auth-kit |
| Neynar API | docs.neynar.com |
| XMTP docs | docs.xmtp.org |
| XMTP Node SDK | github.com/xmtp/xmtp-node-js-sdk |
| Base Sepolia faucet | docs.base.org/tools/network-faucets |
Related guides
- Dev environment setup — Foundry, Hardhat, and RPC configuration
- Deploy a smart contract on Base — Contract deployment walkthrough
- Coinbase Smart Wallet guide — Wallet integration with OnchainKit
- Launch an AI agent on Base — Autonomous agents with onchain identity