ImplementationPhase 1B: Production Setup

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-id header (Step 5) is required for display ads to work correctly!

Install SDK

npm install @earnlayer/sdk

Create 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.

Learn more about Demo Mode

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 dev

Ask: “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

  1. Check that conversationId exists before calling confirmHyperlinkImpressions
  2. Verify the proxy endpoint is set up correctly at app/api/earnlayer/[...slug]/route.ts
  3. Check browser console for detailed error messages