# 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](https://app.earnlayerai.com/api-keys)

---

## 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:**
```bash
npm install openai
```

**Gemini:**
```bash
npm install @google/genai @modelcontextprotocol/sdk
```

### 2. Add API Keys

**ADD** to your `.env.local`:

```bash
# 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`:

```typescript
// 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:**

```typescript
// 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!

```bash
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

```bash
npm install @earnlayer/sdk
```

### 2. Create Proxy Endpoint

**CREATE** new file `app/api/earnlayer/[...slug]/route.ts`:

```typescript
// 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`):

```typescript
// 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:

```typescript
// 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:**
```typescript
// 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:**
```typescript
// 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!

```bash
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:

```typescript
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):

```typescript
// 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:

```typescript
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

```bash
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:

```typescript
// 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:

```typescript
// 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()

```typescript
// 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)

```typescript
// 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

```typescript
// 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:**
```typescript
// REFERENCE: Options interface
options?: {
  visitorId?: string;                      // Optional custom visitor ID
  adTypes?: ('hyperlink' | 'display')[];   // Default: both
  frequency?: 'low' | 'normal' | 'high';   // Default: 'normal'
}
```

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

**Error Handling:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
// 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:**
```typescript
// REFERENCE: Return type
Promise<{
  confirmed_count: number;    // Number of ads confirmed
  impression_ids: string[];   // Array of impression IDs
}>
```

**Error Handling:**
```typescript
// 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:

```typescript
// 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:
```typescript
// ❌ 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:
   ```bash
   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: support@earnlayerai.com
- 💬 Discord: discord.gg/earnlayer
- 🐛 Issues: github.com/earnlayer/sdk/issues

---

## License

Proprietary - All Rights Reserved

Copyright © 2025 EarnLayer, Inc.
