Monetize your AI chat applications with contextual ads
npm install @earnlayer/sdkMonetize your AI application with context-aware ads.
✨ Works with ANY LLM - OpenAI, Google Gemini, Claude, or custom models
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
---
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.
OpenAI:
``bash`
npm install openai
Gemini:
`bash`
npm install @google/genai @modelcontextprotocol/sdk
ADD to your .env.local:
`bashADD: Required for SDK (Phase 1B and Phase 2)
EARNLAYER_API_KEY=el_your_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
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. ' +
'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. ' +
'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 });
`$3
`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.
$3
`bash
npm install @earnlayer/sdk
`$3
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
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 (
);
}
// 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
}
`$3
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
$3
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
}
}
}
);
`$3
`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
$3
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
}
`$3
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
Loading ad...;
if (!ad) return null; return (
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 && (
src={ad.imageUrl}
alt={ad.title}
className="w-full h-auto rounded mb-2"
/>
)}
{ad.title}
{ad.description && (
{ad.description}
)}
AD
);
}
`$3
MODIFY your chat component, add a sidebar for the display ad:
`typescript
return (
{/ Your existing chat UI (keep as-is) /}
{/ Your messages, input, etc. /}
{/ ADD: New sidebar for display ad /}
);
`$3
`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 (
href={ad.url}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 text-sm text-blue-600"
>
●
{ad.title}
);
}
// ADD: Use in your chat (only shown during AI thinking):
{isThinking && (
Thinking...
)}
`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
$3
`typescript
// ADD: EarnLayer client hook
const {
client, // EarnLayerClient instance
conversationId, // Current conversation ID (string | null)
isReady, // Provider initialized (boolean)
initializeConversation // Create new conversation (() => Promise)
} = useEarnLayerClient();
`$3
`typescript
// ADD: EarnLayer display ad hook
const {
ad, // DisplayAd object | null
isLoading, // boolean
error, // Error | null
refetch // () => Promise
} = 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
});
`$3
`typescript
// REFERENCE: DisplayAd interface (for understanding)
interface DisplayAd {
id: string;
impressionId: string;
title: string;
description?: string;
url: string; // Use this in
imageUrl?: string;
adType: 'banner' | 'popup' | 'video' | 'thinking';
source: 'queue' | 'fallback';
}
`Important: Always use
ad.url directly - it includes backend redirect for automatic tracking.---
$3
####
initializeConversation(options?): PromiseInitializes 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?): PromiseFetches a display ad for the conversation.
Parameters:
-
conversationId: string (required) - Must be non-empty
- adType?: string (optional) - Filter by ad typeReturns:
-
Promise - 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): PromiseTracks when an ad impression occurs. Automatically retries up to 3 times with exponential backoff.
Parameters:
-
impressionId: string (required)Returns:
-
Promise - true if tracked successfully, false if all retries failedError 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): PromiseTracks when a user clicks an ad. Automatically retries up to 3 times with exponential backoff.
Parameters:
-
impressionId: string (required)Returns:
-
Promise - true if tracked successfully, false if all retries failedError 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-emptyReturns:
`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
$3
The SDK provides optional debug logging for React components to help troubleshoot integration issues:
`typescript
// Enable debug logs for the entire provider
// 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 errorsNote: Debug logging is opt-in and disabled by default. Core SDK methods (like
initializeConversation, confirmHyperlinkImpressions) always log errors automatically.$3
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 adsNext.js Proxy (server-side):
-
[EarnLayer SDK] Failed to fetch JWT token: - Authentication issues with API keyMCP Configuration:
-
[EarnLayer SDK] Invalid MCP configuration: - Missing required MCP optionsAll error logs include structured data with timestamps for debugging.
---
Troubleshooting
$3
#### ❌ 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$3
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 '{}'
`$3
- Try specific questions: "What database should I use?" ✅
- Avoid simple questions: "Hello" ❌
- Check DEFAULT_MCP_INSTRUCTIONS is in agent instructions$3
- Make sure conversationId exists before rendering display ads
- Check Network tab - is /api/earnlayer/displayad/... returning 200?
- Try manually calling refetch() from useDisplayAd$3
#### "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:
$3
- 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$3
- 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)$3
`
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)
``✅ 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
---
- 📧 Email: support@earnlayerai.com
- 💬 Discord: discord.gg/earnlayer
- 🐛 Issues: github.com/earnlayer/sdk/issues
---
Proprietary - All Rights Reserved
Copyright © 2025 EarnLayer, Inc.