Phase 2: Add Display Ads
Add visual banner/thinking ads for maximum revenue.
Time: 5-10 minutes
IMPORTANT: This phase is ONLY needed if you want display ads (banners, thinking ads, popups, videos). If you only want hyperlink ads, you can skip this phase entirely.
Prerequisites: You MUST complete Phase 1A and Phase 1B before starting this phase. Display ads require the conversation tracking setup from Phase 1B.
Note: If you’re testing, make sure demo mode is enabled for display ads:
// In your conversation initialization (from Phase 1B)
initializeConversation({
demoMode: true // Shows demo display ads for testing
});Set Up Display Ad Trigger
MODIFY your chat component, add state to trigger ad refreshes:
function YourChatComponent() {
// Your existing hooks (keep as-is)
const { conversationId } = useEarnLayerClient(); // Already added in Phase 1B
const [shouldRefetchAd, setShouldRefetchAd] = useState(false);
const handleSendMessage = async (message: string) => {
// Your existing send message code (keep as-is)
// ... your existing send message code ...
const data = await response.json();
setMessages(prev => [...prev, { role: 'assistant', content: data.response }]);
// ADD: Trigger ad refetch AFTER AI response
setShouldRefetchAd(true);
setTimeout(() => setShouldRefetchAd(false), 100);
};
// Your existing component code (keep as-is)
// ... rest of component
}Add Display Ad Component
CREATE a display ad component (or add to existing file):
// CREATE: New display ad component
import { useDisplayAd } from '@earnlayer/sdk/react';
import { useEffect, useRef } from 'react';
function DisplayAdComponent({ shouldRefetch }: { shouldRefetch: boolean }) {
// ADD: EarnLayer display ad hook
const { ad, isLoading, refetch, refetchWithRefresh } = useDisplayAd({
adType: 'banner',
autoFetch: false // We'll control fetching manually
});
const prevShouldRefetch = useRef(false);
// ADD: Fetch when shouldRefetch toggles from false to true
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} // Includes automatic click tracking
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>
);
}Add Sidebar to Your Layout
MODIFY your chat component, add a sidebar for the display ad:
return (
<div className="flex gap-4">
{/* Your existing chat UI (keep as-is) */}
<div className="flex-1">
{/* Your messages, input, etc. */}
</div>
{/* ADD: New sidebar for display ad */}
<aside className="w-80">
<h2 className="text-xl font-bold mb-2">Sponsored</h2>
{conversationId && <DisplayAdComponent shouldRefetch={shouldRefetchAd} />}
</aside>
</div>
);Test Phase 2
npm run devAsk: “What are the best database tools?”
Validate:
- AI response includes sponsored hyperlinks
- Display ad visible in sidebar
- Display ad updates after each AI response
- Check EarnLayer dashboard for analytics
What You Accomplished
Phase 2 Complete! You now have hyperlinks + display ads.
- Hyperlink ads in responses
- Display ads in sidebar
- Contextual ad updates
- Full monetization experience
Advanced: Thinking Ads (Optional)
Show ads during AI loading states for additional revenue opportunities.
How Thinking Ads Work
Thinking ads are contextual ads shown while your AI is processing a request. Here’s the flow:
- User sends a message → Your thinking ad component mounts
- Component requests a thinking ad with a timeout (e.g., 4 seconds)
- Your AI calls the MCP server with conversation context (typically 0.5-2 seconds)
- MCP server finds relevant most ads and queues them
- Backend receives notification and returns the ad immediately
- Ad displays while AI continues processing
The thinkingAdTimeout parameter tells the backend how long to wait for the MCP server to queue contextual ads before falling back to a default ad.
Implementation
// CREATE: New thinking ad component
import { useDisplayAd } from '@earnlayer/sdk/react';
function ThinkingAdComponent() {
// ADD: EarnLayer thinking ad hook with timeout
const { ad, isLoading, refetchWithRefresh } = useDisplayAd({
adType: 'thinking',
autoFetch: true, // Auto-fetch when component mounts
thinkingAdTimeout: 4 // Wait up to 4 seconds for contextual ads
});
if (isLoading || !ad) return null;
return (
<a
href={ad.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm text-blue-600"
>
<span className="animate-pulse">●</span>
{ad.title}
</a>
);
}
// ADD: Use in your chat (only shown during AI thinking):
{isThinking && (
<div className="flex items-center gap-2">
<span>Thinking...</span>
<ThinkingAdComponent />
</div>
)}Understanding thinkingAdTimeout
What it does:
- Tells the backend to wait for the MCP server to populate contextual ads
- Typical time before MCP is called by your LLM: 0.5-2 seconds
- Recommended value: 4 seconds (gives ample time for network latency)
Why 4 seconds?
- MCP call happens after AI starts processing (not instant)
- MCP needs time to generate embeddings and find relevant ads
- Network latency between your server, AI provider, and EarnLayer
- 4 seconds ensures contextual ads appear consistently
What happens if timeout is reached?
- Backend returns a fallback default ad (e.g., EarnLayer)
- No error or blank state - users always see an ad
Can I use a shorter timeout?
- Yes, but you may see more fallback ads
- If your internal testing shows <2s consistently, you can try 2-3 seconds
Testing Thinking Ads
Ask: “What are the best cloud storage solutions?”
Expected behavior:
- Thinking ad appears immediately (within 100ms)
- Shows contextual ad related to cloud/storage (if MCP responds within 4s)
- Ad remains visible until AI response completes
- After AI responds, ad disappears and conversation continues
Troubleshooting:
- If you always see fallback ads, check that Phase 1B is complete
- Verify
x-conversation-idheader is being passed in MCP calls
Advanced: Auto-Refresh Display Ads
Display ads can automatically refresh to show different ads from the queue.
Banner Ad Auto-Refresh
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]);
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>
)}
</a>
);
}Thinking Ad Auto-Refresh
function AutoRefreshThinkingAd() {
const { ad, isLoading, refetchWithRefresh } = useDisplayAd({
adType: 'thinking',
autoFetch: true,
thinkingAdTimeout: 4
});
// Auto-refresh every 5 seconds while ad is visible
useEffect(() => {
if (!ad || isLoading) return;
const timer = setTimeout(() => {
refetchWithRefresh();
}, 5000); // 5 seconds
return () => clearTimeout(timer);
}, [ad?.id, isLoading, refetchWithRefresh]);
if (isLoading || !ad) return null;
return (
<a
href={ad.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm text-blue-600"
>
<span className="animate-pulse">●</span>
{ad.title}
</a>
);
}Best Practices:
- Use
refetchWithRefresh()to avoid showing the same ad twice - Limit refreshes to prevent overwhelming users (recommended: 3-5 refreshes)
- Consider user experience - don’t refresh too frequently
- Timer automatically resets when ad changes (new ad loaded)
Next Steps
Advanced Implementation
For production-ready patterns including:
- Thinking ads with minimum display duration
- Inline banner ad placement
- Race condition prevention
- Configuration options
→ Advanced Display Ad Implementation
Monitor Performance
Check your earnings and analytics:
Join the Community
Get help and share feedback:
Troubleshooting
Display ads not showing
- Make sure
conversationIdexists before rendering display ads - Check Network tab - is
/api/earnlayer/displayad/...returning 200? - Try manually calling
refetch()fromuseDisplayAd
Ads not updating after messages
Verify the shouldRefetch state is toggling correctly. Check with:
useEffect(() => {
console.log('shouldRefetch:', shouldRefetch);
}, [shouldRefetch]);Wrong ad type showing
Check the adType parameter in useDisplayAd. Valid types:
hyperlinkthinkingbanner
No ads available / Empty ad queue
If you’re seeing “No ads available”:
- Check demo mode: Enable demo mode for testing:
initializeConversation({ demoMode: true }) - Check partnerships: In production, ensure you have partnerships configured