npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@earnlayer/sdk

v1.4.1

Published

Monetize your AI chat applications with contextual ads

Readme

EarnLayer SDK

Monetize your AI application with context-aware ads.

Works with ANY LLM - OpenAI, Google Gemini, Claude, or custom models

Quick Start

Already have a working chat app? Perfect! Add EarnLayer in ~20 minutes with our phased approach.

Phase 1A (5 min): See hyperlink ads immediately - quick win!
Phase 1B (10 min): Complete setup - production ready!
Phase 2 (5-10 min): Add display ads - maximize revenue!

Prerequisites:

  • ✅ Node.js 16+
  • ✅ Next.js 13+ with App Router
  • ✅ Working chat app with API route
  • ✅ EarnLayer API key - Get one here

Phase 1A: Quick Start - See Hyperlink Ads

Get sponsored hyperlinks showing in your chat responses immediately.

Time: 5 minutes

💡 This gets you started fast! You'll see sponsored links immediately. Complete Phase 1B to ensure all ad types bill correctly.

1. Install Dependencies

OpenAI:

npm install openai

Gemini:

npm install @google/genai @modelcontextprotocol/sdk

2. Add API Keys

ADD to your .env.local:

# ADD: Required for SDK (Phase 1B and Phase 2)
EARNLAYER_API_KEY=el_your_api_key_here

# ADD: Required ONLY for MCP integration in chat route (Phase 1A)
EARNLAYER_MCP_URL=https://mcp.earnlayerai.com/mcp

# Your existing LLM provider key (keep as-is)
# OpenAI
OPENAI_API_KEY=sk_your_openai_key_here

# OR Gemini
GOOGLE_API_KEY=your_google_api_key_here

Note: EARNLAYER_MCP_URL is only needed in your chat API route for hyperlink ads. The SDK (used in Phase 1B and Phase 2) only needs EARNLAYER_API_KEY.

3. Add MCP to Your Chat Route

OpenAI (Responses API):

In your existing app/api/chat/route.ts:

// Your existing imports (keep as-is)
import OpenAI from 'openai';

// Your existing client setup (keep as-is)
const client = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// ADD: New environment variables
const EARNLAYER_MCP_URL = process.env.EARNLAYER_MCP_URL!;
const EARNLAYER_API_KEY = process.env.EARNLAYER_API_KEY!;

// ADD: System instructions for ad integration
const SYSTEM_INSTRUCTIONS = 
  'You are a helpful assistant. IMPORTANT: Whenever a user asks about products, services, tools, software, or anything that could be purchased or subscribed to, ' +
  'you MUST FIRST call the earnlayer_content_ads_search tool to find sponsored recommendations. ' +
  'After getting results, review the ads and ONLY include the ones that are RELEVANT to the user\'s question. ' +
  'Ignore any ads that are not related to what the user is asking about. ' +
  'For relevant ads, include them in your response with their clickable links in markdown format [Product Name](url). ' +
  'ALWAYS include the URLs as clickable links.';

// MODIFY: Update your existing API call to use Responses API with MCP tools
const resp = await client.responses.create({
  model: 'gpt-4o',
  // ADD: Tools array for MCP integration
  tools: [
    {
      type: 'mcp',
      server_label: 'earnlayer',
      server_url: EARNLAYER_MCP_URL,
      require_approval: 'never',
      headers: {
        'x-api-key': EARNLAYER_API_KEY
      }
    }
  ],
  // ADD: Input format for Responses API
  input: [
    {
      role: 'developer',
      content: [{ type: 'input_text', text: SYSTEM_INSTRUCTIONS }]
    },
    {
      role: 'user',
      content: [{ type: 'input_text', text: message }]
    }
  ]
});

// Your existing response handling (keep as-is)
return NextResponse.json({ response: resp.output_text });

Gemini:

// ADD: New imports for MCP integration
import { GoogleGenAI, mcpToTool } from '@google/genai';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

// ADD: New environment variables
const EARNLAYER_MCP_URL = process.env.EARNLAYER_MCP_URL!;
const EARNLAYER_API_KEY = process.env.EARNLAYER_API_KEY!;

// ADD: System instructions for ad integration
const SYSTEM_INSTRUCTIONS = 
  'You are a helpful assistant. IMPORTANT: Whenever a user asks about products, services, tools, software, or anything that could be purchased or subscribed to, ' +
  'you MUST FIRST call the earnlayer_content_ads_search tool to find sponsored recommendations. ' +
  'After getting results, review the ads and ONLY include the ones that are RELEVANT to the user\'s question. ' +
  'Ignore any ads that are not related to what the user is asking about. ' +
  'For relevant ads, include them in your response with their clickable links in markdown format [Product Name](url). ' +
  'ALWAYS include the URLs as clickable links.';

// ADD: Set up MCP client
const transport = new StreamableHTTPClientTransport(
  new URL(EARNLAYER_MCP_URL),
  {
    requestInit: {
      headers: { 'x-api-key': EARNLAYER_API_KEY }
    }
  }
);

const client = new Client(
  { name: 'earnlayer-client', version: '1.0.0' },
  { capabilities: {} }
);

await client.connect(transport);

// MODIFY: Update your existing Gemini call to include MCP tools
const ai = new GoogleGenAI({
  apiKey: process.env.GOOGLE_API_KEY!
});

const response = await ai.models.generateContent({
  model: 'gemini-2.0-flash-exp',
  contents: SYSTEM_INSTRUCTIONS + '\n\nUser: ' + message,
  config: {
    // ADD: MCP tools integration
    tools: [mcpToTool(client)],  // Automatically calls MCP tools
  },
});

await client.close();

// Your existing response handling (keep as-is)
return NextResponse.json({ response: response.text });

4. Test It!

npm run dev

Ask: "What are the best VPNs?" or "What project management tools should I use?"

✅ You should see sponsored links in the AI response.

🎉 Phase 1A Complete! You're seeing hyperlink ads. Now complete Phase 1B for production.


Phase 1B: Complete Setup - Production Ready

Enable full impression tracking and billing for all ad types.

Time: 10 minutes

⚠️ Required for production. This ensures all ads bill correctly.

1. Install SDK

npm install @earnlayer/sdk

2. 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).

3. 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
}

4. Add Impression Confirmation

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:

  • 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

5. 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 to MCP headers
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
      }
    }
  ],
  // 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 to MCP headers
const transport = new StreamableHTTPClientTransport(
  new URL(EARNLAYER_MCP_URL),
  {
    requestInit: {
      headers: { 
        'x-api-key': EARNLAYER_API_KEY,
        'x-conversation-id': conversationId  // ADD: conversationId header
      }
    }
  }
);

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

🎉 Phase 1B Complete! You're production-ready with full billing.


Phase 2: Add Display Ads

Add visual banner/popup/video ads for maximum revenue.

Time: 5-10 minutes

1. 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
}

2. 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 } = useDisplayAd({
    adType: 'banner',
    placement: 'sidebar',
    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;
      refetch();
    } else if (!shouldRefetch) {
      prevShouldRefetch.current = false;
    }
  }, [shouldRefetch]);

  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>
  );
}

3. 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>
);

4. Test Phase 2

npm run dev

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

🎉 Phase 2 Complete! You now have hyperlinks + display ads.


Advanced: Thinking Ads (Optional)

Show ads during AI loading states:

// CREATE: New thinking ad component
import { useDisplayAd } from '@earnlayer/sdk/react';

function ThinkingAdComponent() {
  // ADD: EarnLayer thinking ad hook
  const { ad, isLoading } = useDisplayAd({ 
    adType: 'thinking',
    autoFetch: true  // Auto-fetch when component mounts
  });

  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>
)}

Note: Thinking ads use autoFetch: true since they fetch once when the loading state appears.


Conversation Management

Users control when to create new conversations:

// Your existing hooks (keep as-is)
const { conversationId, initializeConversation } = useEarnLayerClient();

// ADD: Create conversation on page load (recommended)
useEffect(() => {
  initializeConversation();
}, []);

// ADD: Create new conversation on "New Chat" button
const handleNewChat = async () => {
  const newConversationId = await initializeConversation();
  // This resets the ad queue for fresh contextual ads
};

Best practice: Call initializeConversation() on page load and when user clicks "New Chat".


Environment Variables Reference

| Variable | Required For | Description | |----------|-------------|-------------| | EARNLAYER_API_KEY | SDK (Phase 1B, Phase 2) | Your EarnLayer API key. Used server-side only in proxy endpoints. | | EARNLAYER_MCP_URL | MCP (Phase 1A) | EarnLayer MCP server URL. Only needed in your chat API route for hyperlink ads. | | OPENAI_API_KEY | OpenAI users | Your OpenAI API key for chat completions. | | GOOGLE_API_KEY | Gemini users | Your Google API key for Gemini chat. |

Where to use each:

  • EARNLAYER_API_KEY: In your proxy endpoint (app/api/earnlayer/[...slug]/route.ts)
  • EARNLAYER_MCP_URL: In your chat route (app/api/chat/route.ts)
  • LLM keys: In your chat route (app/api/chat/route.ts)

API Reference

useEarnLayerClient()

// ADD: EarnLayer client hook
const { 
  client,                    // EarnLayerClient instance
  conversationId,            // Current conversation ID (string | null)
  isReady,                   // Provider initialized (boolean)
  initializeConversation     // Create new conversation (() => Promise<string>)
} = useEarnLayerClient();

useDisplayAd(options)

// ADD: EarnLayer display ad hook
const { 
  ad,          // DisplayAd object | null
  isLoading,   // boolean
  error,       // Error | null
  refetch      // () => Promise<void>
} = useDisplayAd({
  adType: 'banner' | 'popup' | 'video' | 'thinking',  // optional
  placement: 'sidebar' | 'inline',                      // optional
  autoFetch: true,                                      // optional, default: true
  debug: false,                                         // optional, default: false - enables console error logs
  onAdFetched: (ad) => {},                             // optional callback
  onError: (error) => {}                               // optional callback
});

DisplayAd Object

// REFERENCE: DisplayAd interface (for understanding)
interface DisplayAd {
  id: string;
  impressionId: string;
  title: string;
  description?: string;
  url: string;              // Use this in <a href={ad.url}>
  imageUrl?: string;
  adType: 'banner' | 'popup' | 'video' | 'thinking';
  source: 'queue' | 'fallback';
}

Important: Always use ad.url directly - it includes backend redirect for automatic tracking.


EarnLayerClient Methods

initializeConversation(options?): Promise<Conversation>

Initializes a new conversation and returns conversation metadata.

Parameters:

// REFERENCE: Options interface
options?: {
  visitorId?: string;                      // Optional custom visitor ID
  adTypes?: ('hyperlink' | 'display')[];   // Default: both
  frequency?: 'low' | 'normal' | 'high';   // Default: 'normal'
}

Returns:

// REFERENCE: Return type
Promise<{
  conversation_id: string;
  creator_id: string;
  ad_settings: object;
  status: string;
  created_at: string;
}>

Error Handling:

// EXAMPLE: Error handling pattern
try {
  const conversation = await client.initializeConversation();
  console.log('Conversation ID:', conversation.conversation_id);
} catch (error) {
  // Error is automatically logged to console before being thrown
  // Check console for: [EarnLayer SDK] Failed to initialize conversation:
  if (error.message.includes('401')) {
    console.error('Invalid API key');
  } else if (error.message.includes('timeout')) {
    console.error('Request timed out after 10 seconds');
  } else {
    console.error('Failed to initialize:', error);
  }
}

getDisplayAd(conversationId, adType?): Promise<DisplayAd | null>

Fetches a display ad for the conversation.

Parameters:

  • conversationId: string (required) - Must be non-empty
  • adType?: string (optional) - Filter by ad type

Returns:

  • Promise<DisplayAd | null> - Returns null if no ads available (404)

Error Handling:

// EXAMPLE: Error handling pattern
try {
  const ad = await client.getDisplayAd(conversationId);
  if (ad === null) {
    console.log('No ads available for this conversation');
  } else {
    console.log('Got ad:', ad.title);
  }
} catch (error) {
  // Error is automatically logged to console before being thrown
  // Check console for: [EarnLayer SDK] Failed to get display ad:
  if (error.message.includes('Invalid conversationId')) {
    console.error('conversationId cannot be empty');
  } else if (error.message.includes('timeout')) {
    console.error('Request timed out');
  } else {
    console.error('Error fetching ad:', error);
  }
}

trackImpression(impressionId): Promise<boolean>

Tracks when an ad impression occurs. Automatically retries up to 3 times with exponential backoff.

Parameters:

  • impressionId: string (required)

Returns:

  • Promise<boolean> - true if tracked successfully, false if all retries failed

Error Handling:

// EXAMPLE: Error handling pattern
const success = await client.trackImpression(impressionId);
if (!success) {
  console.warn('Failed to track impression after 3 retries');
  // Non-critical - continue anyway
}

trackClick(impressionId): Promise<boolean>

Tracks when a user clicks an ad. Automatically retries up to 3 times with exponential backoff.

Parameters:

  • impressionId: string (required)

Returns:

  • Promise<boolean> - true if tracked successfully, false if all retries failed

Error Handling:

// EXAMPLE: Error handling pattern
const success = await client.trackClick(impressionId);
if (!success) {
  console.warn('Failed to track click after 3 retries');
  // Non-critical - user still navigates to ad URL
}

confirmHyperlinkImpressions(conversationId, messageText): Promise<{ confirmed_count: number; impression_ids: string[] }>

Confirms hyperlink ad impressions in an LLM response message.

Parameters:

  • conversationId: string (required) - Must be non-empty
  • messageText: string (required) - Must be non-empty

Returns:

// REFERENCE: Return type
Promise<{
  confirmed_count: number;    // Number of ads confirmed
  impression_ids: string[];   // Array of impression IDs
}>

Error Handling:

// EXAMPLE: Error handling pattern
try {
  const result = await client.confirmHyperlinkImpressions(
    conversationId,
    messageText
  );
  console.log(`Confirmed ${result.confirmed_count} ad impressions`);
} catch (error) {
  // Error is automatically logged to console before being thrown
  // Check console for: [EarnLayer SDK] Failed to confirm hyperlink impressions:
  if (error.message.includes('Invalid conversationId')) {
    console.error('conversationId is required and must not be empty');
  } else if (error.message.includes('Invalid messageText')) {
    console.error('messageText is required and must not be empty');
  } else if (error.message.includes('timeout')) {
    console.error('Request timed out after 10 seconds');
  } else {
    console.error('Error confirming impressions:', error);
  }
}

All Methods Include:

  • ✅ 10-second timeout with AbortController
  • ✅ Input validation (where applicable)
  • ✅ Automatic retry with exponential backoff (trackImpression, trackClick)
  • ✅ Detailed error messages

Debugging

Enable Debug Logging

The SDK provides optional debug logging for React components to help troubleshoot integration issues:

// Enable debug logs for the entire provider
<EarnLayerProvider proxyBaseUrl="/api/earnlayer" debug={true}>
  <YourApp />
</EarnLayerProvider>

// Or enable debug logs per-hook
const { ad, error } = useDisplayAd({ 
  conversationId, 
  debug: true  // Logs errors to console
});

const { ads } = useDisplayAds({ 
  conversationId, 
  debug: true  // Logs errors to console
});

Debug logs include:

  • [EarnLayer SDK - useDisplayAd] - Display ad fetch errors
  • [EarnLayer SDK - useDisplayAds] - Multiple ads fetch errors
  • [EarnLayer SDK - Provider] - Initialization errors

Note: Debug logging is opt-in and disabled by default. Core SDK methods (like initializeConversation, confirmHyperlinkImpressions) always log errors automatically.

Error Logs from Core SDK

The following errors are always logged automatically (no debug flag needed):

Core Client Methods:

  • [EarnLayer SDK] Failed to initialize conversation: - Issues with conversation initialization
  • [EarnLayer SDK] Failed to confirm hyperlink impressions: - Issues confirming impressions
  • [EarnLayer SDK] Failed to get display ad: - Issues fetching display ads

Next.js Proxy (server-side):

  • [EarnLayer SDK] Failed to fetch JWT token: - Authentication issues with API key

MCP Configuration:

  • [EarnLayer SDK] Invalid MCP configuration: - Missing required MCP options

All error logs include structured data with timestamps for debugging.


Troubleshooting

Common Setup Mistakes

❌ Using EARNLAYER_API_KEY in browser code

Problem: API key exposed to browser, security risk
Solution: Only use EARNLAYER_API_KEY in server-side proxy endpoint

❌ Forgot to restart dev server after adding environment variables

Problem: Environment variables not loaded
Solution: Stop server (Ctrl+C) and run npm run dev again

❌ Using apiKey in EarnLayerClient constructor

Problem: Type error - apiKey doesn't exist on EarnLayerConfig
Solution: Use proxyBaseUrl instead:

// ❌ REPLACE: Wrong approach
const client = new EarnLayerClient({ apiKey: 'el_...' });

// ✅ REPLACE: Correct approach
const client = new EarnLayerClient({ proxyBaseUrl: '/api/earnlayer' });

❌ Calling SDK methods without initializing conversation

Problem: conversationId is null
Solution: Call initializeConversation() before using SDK methods

No ads showing

  1. Check EARNLAYER_API_KEY is set in .env.local
  2. Restart dev server after adding env vars
  3. Check browser console for errors
  4. Verify proxy endpoint:
    curl -X POST http://api.earnlayerai.com/api/earnlayer/initialize \
      -H "Content-Type: application/json" \
      -d '{}'

AI isn't calling MCP tool

  • Try specific questions: "What database should I use?" ✅
  • Avoid simple questions: "Hello" ❌
  • Check DEFAULT_MCP_INSTRUCTIONS is in agent instructions

Display ads not showing

  • Make sure conversationId exists before rendering display ads
  • Check Network tab - is /api/earnlayer/displayad/... returning 200?
  • Try manually calling refetch() from useDisplayAd

Common Error Messages

"API rate limit exceeded. Please wait a minute for it to reset."

  • Cause: Too many requests to EarnLayer API
  • Solution: Wait 1 minute before retrying, or contact support to increase limits

"Invalid EarnLayer API key. Please check if your EarnLayer API key is valid."

  • Cause: Invalid or expired API key
  • Solution: Verify EARNLAYER_API_KEY in .env.local starts with el_ and is valid
  • Console log: Check for [EarnLayer SDK] Failed to fetch JWT token: with status 401

"Failed to initialize conversation"

  • Cause: Network issue, backend unavailable, or authentication problem
  • Solution: Check network connection, verify API key, and check browser console for detailed error logs
  • Console log: Check for [EarnLayer SDK] Failed to initialize conversation: with error details

"Authentication failed"

  • Cause: API key authentication issue
  • Solution: Verify your EARNLAYER_API_KEY and EARNLAYER_AUTH_URL environment variables

"Cannot fetch display ad: conversationId not available"

  • Cause: Trying to fetch ads before calling initializeConversation()
  • Solution: Ensure initializeConversation() is called on mount and completes successfully

Understanding EarnLayer Architecture

EarnLayer has two components that work together:

1. MCP Server (Phase 1A)

  • Purpose: Provides hyperlink ads to your LLM via function calling
  • Location: Hosted by EarnLayer at https://mcp.earnlayerai.com/mcp
  • Used in: Your chat API route only
  • Environment variable: EARNLAYER_MCP_URL

2. EarnLayer SDK (Phase 1B & Phase 2)

  • Purpose: Client-side SDK for tracking impressions and fetching display ads
  • Location: Installed in your project via npm install @earnlayer/sdk
  • Used in: React components and proxy endpoints
  • Environment variable: EARNLAYER_API_KEY (server-side only)

How They Work Together

User Question
    ↓
Your Chat Route → MCP Server (gets hyperlink ads)
    ↓                           ↓
LLM Response ← ← ← ← ← ← ← ← ← Ads
    ↓
Browser (displays response with hyperlink ads)
    ↓
EarnLayer SDK → Your Proxy → EarnLayer API (tracks clicks, gets display ads)

Security

Secure by default:

  • API keys never exposed to browser
  • All requests go through your Next.js proxy
  • Server-to-server JWT authentication
  • Click tracking via backend redirects

⚠️ Never put your API key in client-side code


Support

  • 📧 Email: [email protected]
  • 💬 Discord: discord.gg/earnlayer
  • 🐛 Issues: github.com/earnlayer/sdk/issues

License

Proprietary - All Rights Reserved

Copyright © 2025 EarnLayer, Inc.