Skip to main content

Adding Farcaster to Your App

Learn how to integrate Farcaster profiles, casts, and social features into your application using the Web3 Identity API.

What You'll Build

  • Farcaster profile lookup by username, FID, or connected wallet
  • Display user's recent casts
  • Show follower/following counts
  • Cross-platform identity linking (Farcaster ↔ ENS)

Prerequisites

  • Node.js 18+
  • React or Next.js project
  • Basic understanding of React hooks

Step 1: Understanding Farcaster Identifiers

Farcaster users can be looked up by multiple identifiers:

IdentifierExampleUse Case
FID3Unique numeric ID
Usernamedwr.ethHuman-readable handle
Address0x6b0b...Connected wallet
ENSvitalik.ethIf wallet has ENS

Step 2: Create the Farcaster Service

Create src/services/farcaster.ts:

const API_BASE = 'https://api.web3identity.com/api/farcaster';

export interface FarcasterUser {
fid: number;
username: string;
displayName: string;
bio: string;
pfp: string;
followers: number;
following: number;
verifications: string[];
}

export interface Cast {
hash: string;
text: string;
timestamp: string;
likes: number;
recasts: number;
replies: number;
}

export async function getFarcasterUser(identifier: string): Promise<FarcasterUser> {
const response = await fetch(`${API_BASE}/${encodeURIComponent(identifier)}`);

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'User not found');
}

return response.json();
}

export async function getUserCasts(
fid: number,
options: { limit?: number; cursor?: string } = {}
): Promise<{ casts: Cast[]; nextCursor?: string }> {
const params = new URLSearchParams();
if (options.limit) params.set('limit', options.limit.toString());
if (options.cursor) params.set('cursor', options.cursor);

const response = await fetch(`${API_BASE}/${fid}/casts?${params}`);

if (!response.ok) {
throw new Error('Failed to fetch casts');
}

return response.json();
}

export async function searchUsers(query: string): Promise<FarcasterUser[]> {
const response = await fetch(
`${API_BASE}/search?q=${encodeURIComponent(query)}&type=users`
);

if (!response.ok) {
throw new Error('Search failed');
}

const data = await response.json();
return data.results;
}

Step 3: Create React Hooks

Create src/hooks/useFarcaster.ts:

import { useState, useEffect, useCallback } from 'react';
import {
getFarcasterUser,
getUserCasts,
FarcasterUser,
Cast
} from '../services/farcaster';

interface FarcasterState {
user: FarcasterUser | null;
casts: Cast[];
loading: boolean;
error: string | null;
hasMore: boolean;
}

export function useFarcasterProfile(identifier: string | null) {
const [state, setState] = useState<FarcasterState>({
user: null,
casts: [],
loading: false,
error: null,
hasMore: false,
});

const [cursor, setCursor] = useState<string | undefined>();

useEffect(() => {
if (!identifier) {
setState({ user: null, casts: [], loading: false, error: null, hasMore: false });
return;
}

const fetchUser = async () => {
setState(prev => ({ ...prev, loading: true, error: null }));

try {
const user = await getFarcasterUser(identifier);
const { casts, nextCursor } = await getUserCasts(user.fid, { limit: 10 });

setState({
user,
casts,
loading: false,
error: null,
hasMore: !!nextCursor,
});
setCursor(nextCursor);
} catch (error) {
setState(prev => ({
...prev,
loading: false,
error: error instanceof Error ? error.message : 'Failed to load profile',
}));
}
};

fetchUser();
}, [identifier]);

const loadMore = useCallback(async () => {
if (!state.user || !cursor) return;

try {
const { casts, nextCursor } = await getUserCasts(state.user.fid, {
limit: 10,
cursor,
});

setState(prev => ({
...prev,
casts: [...prev.casts, ...casts],
hasMore: !!nextCursor,
}));
setCursor(nextCursor);
} catch (error) {
console.error('Failed to load more casts:', error);
}
}, [state.user, cursor]);

return { ...state, loadMore };
}

Step 4: Build the Profile Component

Create src/components/FarcasterProfile.tsx:

import React from 'react';
import { useFarcasterProfile } from '../hooks/useFarcaster';
import { CastCard } from './CastCard';
import './FarcasterProfile.css';

interface Props {
identifier: string;
}

export function FarcasterProfile({ identifier }: Props) {
const { user, casts, loading, error, hasMore, loadMore } = useFarcasterProfile(identifier);

if (loading && !user) {
return <ProfileSkeleton />;
}

if (error) {
return (
<div className="farcaster-error">
<span>⚠️</span>
<p>{error}</p>
</div>
);
}

if (!user) {
return null;
}

return (
<div className="farcaster-profile">
{/* Header */}
<div className="profile-header">
<img src={user.pfp} alt={user.username} className="pfp" />

<div className="profile-info">
<h2 className="display-name">{user.displayName}</h2>
<p className="username">@{user.username}</p>
</div>
</div>

{/* Bio */}
{user.bio && <p className="bio">{user.bio}</p>}

{/* Stats */}
<div className="stats">
<div className="stat">
<span className="stat-value">{formatNumber(user.followers)}</span>
<span className="stat-label">Followers</span>
</div>
<div className="stat">
<span className="stat-value">{formatNumber(user.following)}</span>
<span className="stat-label">Following</span>
</div>
</div>

{/* Verified Addresses */}
{user.verifications.length > 0 && (
<div className="verifications">
<h3>Verified Addresses</h3>
{user.verifications.map(addr => (
<code key={addr} className="address">
{addr.slice(0, 6)}...{addr.slice(-4)}
</code>
))}
</div>
)}

{/* Casts */}
<div className="casts-section">
<h3>Recent Casts</h3>
{casts.map(cast => (
<CastCard key={cast.hash} cast={cast} author={user} />
))}

{hasMore && (
<button onClick={loadMore} className="load-more">
Load More
</button>
)}
</div>
</div>
);
}

function ProfileSkeleton() {
return (
<div className="farcaster-profile skeleton">
<div className="profile-header">
<div className="pfp-skeleton" />
<div className="info-skeleton">
<div className="name-skeleton" />
<div className="username-skeleton" />
</div>
</div>
</div>
);
}

function formatNumber(num: number): string {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
}

Step 5: Create the Cast Card Component

Create src/components/CastCard.tsx:

import React from 'react';
import { Cast, FarcasterUser } from '../services/farcaster';
import './CastCard.css';

interface Props {
cast: Cast;
author: FarcasterUser;
}

export function CastCard({ cast, author }: Props) {
const timestamp = new Date(cast.timestamp);
const timeAgo = getTimeAgo(timestamp);

return (
<div className="cast-card">
<div className="cast-header">
<img src={author.pfp} alt="" className="author-pfp" />
<div className="author-info">
<span className="author-name">{author.displayName}</span>
<span className="author-username">@{author.username}</span>
<span className="separator">·</span>
<span className="timestamp">{timeAgo}</span>
</div>
</div>

<p className="cast-text">{cast.text}</p>

<div className="cast-actions">
<button className="action">
<ReplyIcon />
<span>{cast.replies}</span>
</button>
<button className="action recast">
<RecastIcon />
<span>{cast.recasts}</span>
</button>
<button className="action like">
<LikeIcon />
<span>{cast.likes}</span>
</button>
</div>
</div>
);
}

function getTimeAgo(date: Date): string {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);

if (seconds < 60) return `${seconds}s`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h`;
if (seconds < 604800) return `${Math.floor(seconds / 86400)}d`;
return date.toLocaleDateString();
}

// Icon components
const ReplyIcon = () => (
<svg viewBox="0 0 24 24" width="18" height="18">
<path fill="currentColor" d="M1.751 10c0-4.42 3.584-8 8.005-8h4.366c4.49 0 8.129 3.64 8.129 8.13 0 2.96-1.607 5.68-4.196 7.11l-8.054 4.46v-3.69h-.067c-4.49.1-8.183-3.51-8.183-8.01z" />
</svg>
);

const RecastIcon = () => (
<svg viewBox="0 0 24 24" width="18" height="18">
<path fill="currentColor" d="M4.5 3.88l4.432 4.14-1.364 1.46L5.5 7.55V16c0 1.1.896 2 2 2H13v2H7.5c-2.209 0-4-1.79-4-4V7.55L1.432 9.48.068 8.02 4.5 3.88zM16.5 6H11V4h5.5c2.209 0 4 1.79 4 4v8.45l2.068-1.93 1.364 1.46-4.432 4.14-4.432-4.14 1.364-1.46 2.068 1.93V8c0-1.1-.896-2-2-2z" />
</svg>
);

const LikeIcon = () => (
<svg viewBox="0 0 24 24" width="18" height="18">
<path fill="currentColor" d="M16.697 5.5c-1.222-.06-2.679.51-3.89 2.16l-.805 1.09-.806-1.09C9.984 6.01 8.526 5.44 7.304 5.5c-1.243.07-2.349.78-2.91 1.91-.552 1.12-.633 2.78.479 4.82 1.074 1.97 3.257 4.27 7.129 6.61 3.87-2.34 6.052-4.64 7.126-6.61 1.111-2.04 1.03-3.7.477-4.82-.561-1.13-1.666-1.84-2.908-1.91z" />
</svg>
);

Step 6: Cross-Platform Identity

Link Farcaster with ENS profiles:

// Get connected identities
async function getConnectedIdentities(farcasterUsername: string) {
const response = await fetch(
`https://api.web3identity.com/api/farcaster/${farcasterUsername}`
);
const user = await response.json();

// Get ENS names for verified addresses
const ensNames = await Promise.all(
user.verifications.map(async (address: string) => {
const res = await fetch(
`https://api.web3identity.com/api/ens/reverse/${address}`
);
if (res.ok) {
const data = await res.json();
return { address, ens: data.name };
}
return { address, ens: null };
})
);

return {
farcaster: user,
wallets: ensNames,
};
}

Example Component

function CrossPlatformProfile({ identifier }: { identifier: string }) {
const [identities, setIdentities] = useState(null);

useEffect(() => {
getConnectedIdentities(identifier).then(setIdentities);
}, [identifier]);

if (!identities) return <Loading />;

return (
<div className="cross-platform">
<FarcasterProfile data={identities.farcaster} />

<h3>Connected Wallets</h3>
{identities.wallets.map(({ address, ens }) => (
<div key={address} className="wallet">
<code>{address.slice(0, 6)}...{address.slice(-4)}</code>
{ens && <span className="ens">{ens}</span>}
</div>
))}
</div>
);
}

Step 7: Search Implementation

Add user search functionality:

import { useState, useEffect } from 'react';
import { searchUsers, FarcasterUser } from '../services/farcaster';
import { useDebounce } from '../hooks/useDebounce';

export function FarcasterSearch() {
const [query, setQuery] = useState('');
const [results, setResults] = useState<FarcasterUser[]>([]);
const [loading, setLoading] = useState(false);

const debouncedQuery = useDebounce(query, 300);

useEffect(() => {
if (!debouncedQuery) {
setResults([]);
return;
}

setLoading(true);
searchUsers(debouncedQuery)
.then(setResults)
.finally(() => setLoading(false));
}, [debouncedQuery]);

return (
<div className="farcaster-search">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search Farcaster users..."
className="search-input"
/>

{loading && <div className="loading">Searching...</div>}

<div className="results">
{results.map(user => (
<div key={user.fid} className="result-item">
<img src={user.pfp} alt="" />
<div>
<strong>{user.displayName}</strong>
<span>@{user.username}</span>
</div>
</div>
))}
</div>
</div>
);
}

Error Handling Best Practices

async function safeFetch<T>(url: string): Promise<T | null> {
try {
const response = await fetch(url);

// Handle rate limiting
if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || '60';
console.warn(`Rate limited. Retry after ${retryAfter}s`);
return null;
}

// Handle x402 payment required
if (response.status === 402) {
console.warn('Payment required for this endpoint');
// Implement payment flow
return null;
}

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

return response.json();
} catch (error) {
console.error('Fetch error:', error);
return null;
}
}

Next Steps