Phase 1B: Complete Setup - Production Ready
Enable full impression tracking and conversation ID tracking for all ad types.
Time: 10 minutes
Required for production AND required for display ads (thinking ads, banners, popups, videos). This adds conversation tracking which is critical for display ads to work.
What you need to implement:
- Hyperlink ads only: Complete ALL steps in this phase, then stop (skip Phase 2)
- Display ads only: Complete ALL steps in this phase EXCEPT Step 6 (Impression Confirmation), then continue to Phase 2
- Both ad types: Complete ALL steps in this phase, then continue to Phase 2
The
x-conversation-idheader (Step 5) is required for display ads to work correctly!
Install SDK
npm install @earnlayer/sdkCreate Proxy Endpoint
CREATE new file app/api/earnlayer/[...slug]/route.ts:
// CREATE: New file - EarnLayer proxy endpoint
import { createEarnLayerProxy } from '@earnlayer/sdk/nextjs';
const handler = createEarnLayerProxy({
apiKey: process.env.EARNLAYER_API_KEY!
});
export { handler as GET, handler as POST };This handles all EarnLayer API calls securely (no API key exposed to browser).
Wrap with Provider
MODIFY your chat page component (e.g., app/page.tsx):
// ADD: New imports for EarnLayer
import { EarnLayerProvider, useEarnLayerClient } from '@earnlayer/sdk/react';
// MODIFY: Wrap your existing chat component with the provider
export default function ChatPage() {
return (
<EarnLayerProvider proxyBaseUrl="/api/earnlayer">
<YourChatComponent />
</EarnLayerProvider>
);
}
// ADD: Inside your existing chat component
function YourChatComponent() {
// ADD: EarnLayer hooks
const { conversationId, initializeConversation } = useEarnLayerClient();
const hasInitialized = useRef(false);
// ADD: Initialize conversation once on page load
useEffect(() => {
if (!hasInitialized.current) {
hasInitialized.current = true;
initializeConversation();
}
}, []);
// Your existing component code (keep as-is)
// ... rest of your component
}Configure Demo Mode
In Phase 1A, you enabled demo mode to see test ads immediately. Now configure it properly for production.
For Production (Real Ads & Revenue):
Remove 'x-demo-mode': 'true' from your chat route MCP headers, OR pass the conversationId header instead:
// OPTION 1: Remove demo mode header entirely
headers: {
'x-api-key': EARNLAYER_API_KEY,
'x-conversation-id': conversationId // Use real conversation ID
}
// OPTION 2: Set to false
headers: {
'x-api-key': EARNLAYER_API_KEY,
'x-demo-mode': 'false',
'x-conversation-id': conversationId
}For Continued Testing:
Keep 'x-demo-mode': 'true' in development, but use environment variables:
headers: {
'x-api-key': EARNLAYER_API_KEY,
'x-demo-mode': process.env.NODE_ENV === 'development' ? 'true' : 'false',
'x-conversation-id': conversationId
}Important: Demo ads don’t generate revenue. Always disable demo mode in production.
Note: This controls demo mode for hyperlink ads (MCP). For display ads in Phase 2, you’ll also configure demoMode in the SDK’s initializeConversation() method.
Update Chat Route with ConversationId
MODIFY your app/api/chat/route.ts to receive and pass conversationId:
OpenAI:
// MODIFY: Receive conversationId from client
const { message, conversationId } = await req.json();
// MODIFY: Add conversationId and remove/configure demo mode
const resp = await client.responses.create({
model: 'gpt-4o',
tools: [
{
type: 'mcp',
server_label: 'earnlayer',
server_url: EARNLAYER_MCP_URL,
require_approval: 'never',
headers: {
'x-api-key': EARNLAYER_API_KEY,
'x-conversation-id': conversationId, // ADD: conversationId header
// REMOVE OR UPDATE: 'x-demo-mode': 'true' from Phase 1A
// For production, either remove this line or set to 'false'
'x-demo-mode': process.env.NODE_ENV === 'development' ? 'true' : 'false'
}
}
],
// Your existing code (keep as-is)
// ... rest of your existing code
});Gemini:
// MODIFY: Receive conversationId from client
const { message, conversationId } = await req.json();
// MODIFY: Add conversationId and remove/configure demo mode
const transport = new StreamableHTTPClientTransport(
new URL(EARNLAYER_MCP_URL),
{
requestInit: {
headers: {
'x-api-key': EARNLAYER_API_KEY,
'x-conversation-id': conversationId, // ADD: conversationId header
// REMOVE OR UPDATE: 'x-demo-mode': 'true' from Phase 1A
// For production, either remove this line or set to 'false'
'x-demo-mode': process.env.NODE_ENV === 'development' ? 'true' : 'false'
}
}
}
);Add Impression Confirmation
Note: This step is ONLY required for hyperlink ads. If you’re only implementing display ads, you can skip this step.
MODIFY your chat component, after receiving and displaying the AI response:
// Your existing imports (keep as-is)
import { useEarnLayerClient } from '@earnlayer/sdk/react';
function YourChatComponent() {
// Your existing state and hooks (keep as-is)
const { client, conversationId, initializeConversation } = useEarnLayerClient();
const [messages, setMessages] = useState([]);
const handleSendMessage = async (message: string) => {
// Your existing message sending code (keep as-is)
const response = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, conversationId }),
});
const data = await response.json();
const aiResponseText = data.response;
// Your existing message display code (keep as-is)
setMessages(prev => [...prev, { role: 'assistant', content: aiResponseText }]);
// ADD: Confirm impressions (secure - goes through proxy)
if (conversationId && aiResponseText) {
client.confirmHyperlinkImpressions(conversationId, aiResponseText)
.then(result => {
console.log(`Confirmed ${result.confirmed_count} impressions`);
})
.catch(error => {
console.error('Failed to confirm impressions:', error);
});
}
};
// Your existing component code (keep as-is)
// ... rest of component
}Why this is required for hyperlink ads:
- MCP creates impressions when returning ads to your LLM
- Not all ads returned are included in the final response
- This confirms which ads were actually shown to users
- Only confirmed impressions are billed and tracked
Test Again!
npm run devAsk: “What are the best database tools?”
Validate:
- AI response includes sponsored links
- Browser console shows:
Confirmed X impressions - Check Network tab for POST to
/api/earnlayer/impressions/confirm
What You Accomplished
Phase 1B Complete! You’re production-ready with full billing.
- Hyperlink ads showing
- Impression tracking working
- Billing correctly configured
- Next: Add display ads for more revenue
Next Steps
→ Phase 2: Display Ads - Add visual ads for maximum revenue
Troubleshooting
”Cannot fetch display ad: conversationId not available”
Make sure initializeConversation() is called on mount and completes successfully.
Network tab shows 401 errors
Verify your EARNLAYER_API_KEY in .env.local is correct and starts with el_.
Impressions not confirming
- Check that
conversationIdexists before callingconfirmHyperlinkImpressions - Verify the proxy endpoint is set up correctly at
app/api/earnlayer/[...slug]/route.ts - Check browser console for detailed error messages