Display Ads Component
Create visual display ad components (banners, popups, videos) for your chat interface.
Basic Usage
import { useDisplayAd } from '@earnlayer/sdk/react';
import { useEffect, useRef } from 'react';
function DisplayAdComponent({ shouldRefetch }: { shouldRefetch: boolean }) {
const { ad, isLoading, refetch, refetchWithRefresh } = useDisplayAd({
adType: 'banner',
autoFetch: false
});
const prevShouldRefetch = useRef(false);
useEffect(() => {
if (shouldRefetch && !prevShouldRefetch.current) {
prevShouldRefetch.current = true;
refetchWithRefresh(); // Use refresh to exclude last ad
} else if (!shouldRefetch) {
prevShouldRefetch.current = false;
}
}, [shouldRefetch, refetchWithRefresh]);
if (isLoading) return <div>Loading ad...</div>;
if (!ad) return null;
return (
<a
href={ad.url}
target="_blank"
rel="noopener noreferrer"
className="block p-4 border rounded-lg hover:shadow-lg transition"
>
{ad.imageUrl && (
<img
src={ad.imageUrl}
alt={ad.title}
className="w-full h-auto rounded mb-2"
/>
)}
<h3 className="font-bold text-lg">{ad.title}</h3>
{ad.description && (
<p className="text-sm text-gray-600 mt-1">{ad.description}</p>
)}
<span className="text-xs text-blue-600 mt-2 inline-block">AD</span>
</a>
);
}Ad Types
Banner Ads
Wide horizontal ads, typically in sidebars:
const { ad } = useDisplayAd({
adType: 'banner'
});Thinking Ads
Ads shown during AI loading states. These are contextual ads that update based on the conversation topic.
const { ad } = useDisplayAd({
adType: 'thinking',
autoFetch: true,
thinkingAdTimeout: 4 // Wait up to 4 seconds for contextual ads
});Important: The thinkingAdTimeout parameter (in seconds) tells the backend how long to wait for the MCP server to populate contextual ads. Recommended value is 4 seconds to account for:
- Time for your AI to call the MCP server (typically 0.5-2 seconds)
- MCP processing time
- Network latency
If the timeout is reached, a fallback ad is shown instead. See Phase 2: Thinking Ads for detailed implementation guide.
Hyperlink Ads
Text-based sponsored links (typically handled via MCP):
const { ad } = useDisplayAd({
adType: 'hyperlink'
});Inline Banner Ad Placement
To show banner ads inline with chat messages (after assistant responses), use the assistantMessageCount pattern:
// In your messages component
{messages.map((message, index) => {
const isLastAssistantMessage =
index === messages.length - 1 &&
message.role === 'assistant';
return (
<div key={message.id}>
<MessageComponent message={message} />
{/* Show banner ad after last assistant message */}
{isLastAssistantMessage &&
status !== "submitted" &&
status !== "streaming" && (
<DisplayAdComponent
assistantMessageCount={
messages.filter(m => m.role === 'assistant').length
}
/>
)}
</div>
);
})}
// In DisplayAdComponent - refetch when message count increases
function DisplayAdComponent({ assistantMessageCount }: { assistantMessageCount: number }) {
const { ad, isLoading, refetch } = useDisplayAd({
adType: 'banner',
autoFetch: false // Manual control
});
const prevMessageCountRef = useRef(assistantMessageCount);
// Initial fetch on mount
useEffect(() => {
refetch();
}, [refetch]);
// Refetch when assistant message count increases
useEffect(() => {
if (assistantMessageCount > prevMessageCountRef.current) {
prevMessageCountRef.current = assistantMessageCount;
refetch();
}
}, [assistantMessageCount, refetch]);
// ... render ad
}For a complete production-ready implementation with error handling and styling, see the Advanced Display Ad Implementation guide.
Auto-Refresh Banner Ads
Banner ads can automatically refresh to show different ads from the queue:
function AutoRefreshBannerAd() {
const { ad, isLoading, refetchWithRefresh } = useDisplayAd({
adType: 'banner',
autoFetch: false
});
const [refreshCount, setRefreshCount] = useState(0);
const MAX_REFRESHES = 3; // Show 4 total ads: initial + 3 refreshes
// Auto-refresh every 5 seconds
useEffect(() => {
if (!ad || isLoading || refreshCount >= MAX_REFRESHES) return;
const timer = setTimeout(() => {
refetchWithRefresh();
setRefreshCount(prev => prev + 1);
}, 5000); // 5 seconds
return () => clearTimeout(timer);
}, [ad?.id, isLoading, refreshCount, refetchWithRefresh]);
// Reset count when manually fetching new ad
const handleManualRefresh = () => {
refetch();
setRefreshCount(0);
};
if (isLoading || !ad) return null;
return (
<a
href={ad.url}
target="_blank"
rel="noopener noreferrer"
className="block p-4 border rounded-lg hover:shadow-lg transition"
>
{ad.imageUrl && (
<img
src={ad.imageUrl}
alt={ad.title}
className="w-full h-auto rounded mb-2"
/>
)}
<h3 className="font-bold text-lg">{ad.title}</h3>
{ad.description && (
<p className="text-sm text-gray-600 mt-1">{ad.description}</p>
)}
<span className="text-xs text-blue-600 mt-2 inline-block">AD</span>
</a>
);
}Best Practices:
- Limit refreshes to avoid overwhelming users (recommended: 3-5 refreshes)
- Use
refetchWithRefresh()instead ofrefetch()to avoid showing the same ad twice - Reset refresh count when manually fetching new ads
- Timer automatically resets when ad changes (new ad loaded)
Fade Animations
Display ads can use fade in/out animations for smooth transitions during refreshes:
CSS Animations
Add to your global CSS:
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn 0.3s ease-in-out;
}Component Implementation
function AnimatedBannerAd() {
const { ad, isLoading, refetchWithRefresh } = useDisplayAd({
adType: 'banner',
autoFetch: false
});
const [isVisible, setIsVisible] = useState(false);
const [displayAd, setDisplayAd] = useState(ad);
// Handle ad changes with fade animation
useEffect(() => {
if (!ad) {
setIsVisible(false);
return;
}
if (displayAd?.id !== ad.id && displayAd) {
// Fade out current ad
setIsVisible(false);
// After fade out, update and fade in
setTimeout(() => {
setDisplayAd(ad);
setIsVisible(true);
}, 300);
} else if (!displayAd || displayAd.id === ad.id) {
setDisplayAd(ad);
setTimeout(() => setIsVisible(true), 10);
}
}, [ad, displayAd?.id]);
if (!displayAd || isLoading) return null;
return (
<div
className={`transition-opacity duration-300 ${
isVisible ? 'opacity-100' : 'opacity-0'
}`}
>
<a
href={displayAd.url}
target="_blank"
rel="noopener noreferrer"
className="block p-4 border rounded-lg hover:shadow-lg transition"
>
{displayAd.imageUrl && (
<img
src={displayAd.imageUrl}
alt={displayAd.title}
className="w-full h-auto rounded mb-2"
/>
)}
<h3 className="font-bold text-lg">{displayAd.title}</h3>
{displayAd.description && (
<p className="text-sm text-gray-600 mt-1">{displayAd.description}</p>
)}
</a>
</div>
);
}Key Points:
- Use
displayAdstate to hold currently displayed ad during transitions - Fade out (300ms) → Update ad → Fade in (300ms)
- Smooth user experience during ad refreshes
- Works great with auto-refresh functionality
Key Points
Automatic Tracking
ad.url includes backend redirect for automatic click tracking.
Contextual Updates
Use refetch() or refetchWithRefresh() after each AI response for contextually relevant ads. Use refetchWithRefresh() to avoid showing the same ad twice.
Customizable Styling Style ads however you want - the component is fully customizable.
Example: Sidebar Ad
return (
<div className="flex gap-4">
<div className="flex-1">
{/* Your chat messages */}
</div>
<aside className="w-80">
<h2 className="text-xl font-bold mb-2">Sponsored</h2>
{conversationId && <DisplayAdComponent shouldRefetch={shouldRefetchAd} />}
</aside>
</div>
);DisplayAd Object
interface DisplayAd {
id: string;
impressionId: string;
title: string;
description?: string;
url: string; // Use in <a href={ad.url}>
imageUrl?: string;
adType: 'hyperlink' | 'thinking' | 'banner';
source: 'queue' | 'fallback';
}Next Steps
- useDisplayAd Hook
- Phase 2 Guide
- Advanced Display Ad Implementation - Production-ready patterns
- Thinking Ads