@earnlayer/sdk
v1.4.1
Published
Monetize your AI chat applications with contextual ads
Maintainers
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 openaiGemini:
npm install @google/genai @modelcontextprotocol/sdk2. 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_hereNote:
EARNLAYER_MCP_URLis only needed in your chat API route for hyperlink ads. The SDK (used in Phase 1B and Phase 2) only needsEARNLAYER_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 devAsk: "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/sdk2. 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 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
🎉 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 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
🎉 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-emptyadType?: string(optional) - Filter by ad type
Returns:
Promise<DisplayAd | null>- Returnsnullif 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>-trueif tracked successfully,falseif 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>-trueif tracked successfully,falseif 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-emptymessageText: 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
- Check
EARNLAYER_API_KEYis set in.env.local - Restart dev server after adding env vars
- Check browser console for errors
- 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_INSTRUCTIONSis in agent instructions
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
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_KEYin.env.localstarts withel_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_KEYandEARNLAYER_AUTH_URLenvironment 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.
