Skip to main content

Overview

Data providers allow you to integrate your offer redirect URLs and cashback offers directly into AI chat conversations. When the AI recommends retailers, cashback information is automatically displayed to users. When users click on merchant or product links, your offer redirect URLs are pulled, creating seamless monetization opportunities.
Data providers are recommended for full functionality of the chat interface, especially when displaying merchant recommendations.

Provider types

The SDK supports two types of data providers:
  1. Offer Redirect URL Provider: Returns offer redirect URLs for individual merchant URLs
  2. Merchant Cashback Provider: Returns cashback information for multiple merchants in batch

Offer redirect URL provider

Type definition

type OfferRedirectUrlProvider = (
  url: string
) => Promise<string | null | undefined> | string | null | undefined;

Purpose

Converts regular merchant URLs into your offer redirect URLs. Called when the user clicks a merchant link or product link that the AI mentions or recommends for a specific retailer or product.

Implementation

import type { OfferRedirectUrlProvider } from '@sleek/ai-chat-sdk';

const offerRedirectUrlProvider: OfferRedirectUrlProvider = async (url) => {
  try {
    // Call your offer redirect API
    const response = await fetch(
      `https://api.yourapp.com/offer-redirect?url=${encodeURIComponent(url)}`,
      {
        headers: {
          'Authorization': `Bearer ${YOUR_API_TOKEN}`,
        },
      }
    );

    if (!response.ok) {
      console.error('Offer redirect API error:', response.status);
      return null;
    }

    const data = await response.json();

    // Return the offer redirect URL or null if unavailable
    return data.offerRedirectUrl || null;
  } catch (error) {
    console.error('Error fetching offer redirect URL:', error);
    return null;  // Always return null on error
  }
};

Input

  • url: The merchant URL to convert (e.g., "https://www.amazon.com")

Output

  • string: The offer redirect URL
  • null/undefined: No offer redirect URL available for this merchant

Best practices

Always return null on errors. Never throw exceptions or let errors propagate:
const offerRedirectUrlProvider: OfferRedirectUrlProvider = async (url) => {
  try {
    const response = await fetch(/* ... */);
    return response.offerRedirectUrl || null;
  } catch (error) {
    console.error('Error:', error);
    return null;  // Critical: return null, don't throw
  }
};
Cache offer redirect URLs to improve performance and reduce API calls:
const offerRedirectUrlCache = new Map<string, string>();

const offerRedirectUrlProvider: OfferRedirectUrlProvider = async (url) => {
  // Check cache first
  if (offerRedirectUrlCache.has(url)) {
    return offerRedirectUrlCache.get(url)!;
  }

  try {
    const response = await fetch(/* ... */);
    const offerRedirectUrl = response.offerRedirectUrl;

    if (offerRedirectUrl) {
      // Cache the result
      offerRedirectUrlCache.set(url, offerRedirectUrl);
    }

    return offerRedirectUrl || null;
  } catch (error) {
    return null;
  }
};
Handle different merchant domains and URL formats:
const offerRedirectUrlProvider: OfferRedirectUrlProvider = async (url) => {
  try {
    const parsedUrl = new URL(url);
    const domain = parsedUrl.hostname.replace('www.', '');

    // Handle redirect URLs for different affiliate networks
    if (domain === 'amazon.com') {
      return buildAmazonOfferRedirect(url);
    } else if (domain === 'walmart.com') {
      return buildWalmartOfferRedirect(url);
    } else {
      // Generic offer redirect URL API
      return fetchGenericOfferRedirect(url);
    }
  } catch (error) {
    return null;
  }
};
Aim for <500ms response times:
const offerRedirectUrlProvider: OfferRedirectUrlProvider = async (url) => {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 500);

  try {
    const response = await fetch(apiUrl, {
      signal: controller.signal,
    });

    clearTimeout(timeoutId);
    return response.offerRedirectUrl || null;
  } catch (error) {
    clearTimeout(timeoutId);
    if (error.name === 'AbortError') {
      console.warn('Offer redirect URL fetch timeout');
    }
    return null;
  }
};

Merchant cashback provider

Type definition

type MerchantCashbackProvider = (
  merchantUrls: string[]
) => Promise<Record<string, MerchantCashback>> | Record<string, MerchantCashback>;

interface MerchantCashback {
  text: string;  // e.g., "5% cash back", "Up to $10 back", "$25 bonus"
}

Purpose

Provides cashback information for multiple merchants in a single batch request. Called when the AI displays a list of retailers or product recommendations.

Implementation

import type {
  MerchantCashbackProvider,
  MerchantCashback,
} from '@sleek/ai-chat-sdk';

const merchantCashbackProvider: MerchantCashbackProvider = async (urls) => {
  try {
    // Batch API call for multiple merchants
    const response = await fetch('https://api.yourapp.com/cashback/batch', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${YOUR_API_TOKEN}`,
      },
      body: JSON.stringify({ merchantUrls: urls }),
    });

    if (!response.ok) {
      console.error('Cashback API error:', response.status);
      return {};
    }

    const data = await response.json();

    // Transform API response to required format
    const cashbackData: Record<string, MerchantCashback> = {};
    for (const [url, info] of Object.entries(data.cashback || {})) {
      cashbackData[url] = {
        text: info.cashbackText || info.text,
      };
    }

    return cashbackData;
  } catch (error) {
    console.error('Error fetching cashback:', error);
    return {};  // Always return {} on error
  }
};

Input

  • merchantUrls: Array of merchant URLs (e.g., ["https://amazon.com", "https://walmart.com"])

Output

A record mapping merchant URLs to cashback information:
{
  "https://amazon.com": { text: "5% cash back" },
  "https://walmart.com": { text: "Up to $10 back" },
  "https://target.com": { text: "$25 welcome bonus" }
}
If a merchant doesn’t have cashback, simply omit it from the result.

Best practices

Always return {} on errors:
const merchantCashbackProvider: MerchantCashbackProvider = async (urls) => {
  try {
    const response = await fetch(/* ... */);
    return response.cashback || {};
  } catch (error) {
    console.error('Error:', error);
    return {};  // Critical: return {}, don't throw
  }
};
Cache cashback data with TTL (time-to-live):
interface CacheEntry {
  data: MerchantCashback;
  expiresAt: number;
}

const cashbackCache = new Map<string, CacheEntry>();
const CACHE_TTL = 60 * 60 * 1000; // 1 hour

const merchantCashbackProvider: MerchantCashbackProvider = async (urls) => {
  const now = Date.now();
  const result: Record<string, MerchantCashback> = {};
  const urlsToFetch: string[] = [];

  // Check cache for each URL
  for (const url of urls) {
    const cached = cashbackCache.get(url);
    if (cached && cached.expiresAt > now) {
      result[url] = cached.data;
    } else {
      urlsToFetch.push(url);
    }
  }

  // Fetch uncached URLs
  if (urlsToFetch.length > 0) {
    try {
      const response = await fetch(/* ... */, {
        body: JSON.stringify({ merchantUrls: urlsToFetch }),
      });

      for (const [url, cashback] of Object.entries(response.cashback)) {
        result[url] = cashback;
        cashbackCache.set(url, {
          data: cashback,
          expiresAt: now + CACHE_TTL,
        });
      }
    } catch (error) {
      console.error('Error:', error);
    }
  }

  return result;
};
Provide consistent, user-friendly text:
function formatCashbackText(cashback: any): string {
  if (typeof cashback === 'string') {
    return cashback;
  }

  if (cashback.percentage) {
    return `${cashback.percentage}% cash back`;
  }

  if (cashback.amount) {
    return `$${cashback.amount} cash back`;
  }

  if (cashback.upTo) {
    return `Up to $${cashback.upTo} back`;
  }

  return 'Cash back available';
}

const merchantCashbackProvider: MerchantCashbackProvider = async (urls) => {
  const response = await fetch(/* ... */);
  const result: Record<string, MerchantCashback> = {};

  for (const [url, cashback] of Object.entries(response.data)) {
    result[url] = {
      text: formatCashbackText(cashback),
    };
  }

  return result;
};
Some APIs have batch size limits. Split large requests:
const MAX_BATCH_SIZE = 50;

const merchantCashbackProvider: MerchantCashbackProvider = async (urls) => {
  if (urls.length === 0) return {};

  const result: Record<string, MerchantCashback> = {};

  // Split into batches
  for (let i = 0; i < urls.length; i += MAX_BATCH_SIZE) {
    const batch = urls.slice(i, i + MAX_BATCH_SIZE);

    try {
      const response = await fetch(/* ... */, {
        body: JSON.stringify({ merchantUrls: batch }),
      });

      Object.assign(result, response.cashback || {});
    } catch (error) {
      console.error(`Error fetching batch ${i / MAX_BATCH_SIZE + 1}:`, error);
    }
  }

  return result;
};

Complete example

Here’s a production-ready implementation with caching, error handling, and performance optimization:
import type {
  OfferRedirectUrlProvider,
  MerchantCashbackProvider,
  MerchantCashback,
} from '@sleek/ai-chat-sdk';

// ----- Offer Redirect URL Provider -----

const offerRedirectUrlCache = new Map<string, string>();

const offerRedirectUrlProvider: OfferRedirectUrlProvider = async (url) => {
  // Check cache
  if (offerRedirectUrlCache.has(url)) {
    return offerRedirectUrlCache.get(url)!;
  }

  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 500);

    const response = await fetch(
      `https://api.yourapp.com/offer-redirect?url=${encodeURIComponent(url)}`,
      {
        headers: {
          'Authorization': `Bearer ${process.env.API_TOKEN}`,
        },
        signal: controller.signal,
      }
    );

    clearTimeout(timeoutId);

    if (!response.ok) {
      return null;
    }

    const data = await response.json();
    const offerRedirectUrl = data.offerRedirectUrl;

    if (offerRedirectUrl) {
      offerRedirectUrlCache.set(url, offerRedirectUrl);
    }

    return offerRedirectUrl || null;
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('Offer redirect URL error:', error);
    }
    return null;
  }
};

// ----- Merchant Cashback Provider -----

interface CashbackCacheEntry {
  data: MerchantCashback;
  expiresAt: number;
}

const cashbackCache = new Map<string, CashbackCacheEntry>();
const CASHBACK_CACHE_TTL = 60 * 60 * 1000; // 1 hour

const merchantCashbackProvider: MerchantCashbackProvider = async (urls) => {
  if (urls.length === 0) return {};

  const now = Date.now();
  const result: Record<string, MerchantCashback> = {};
  const urlsToFetch: string[] = [];

  // Check cache
  for (const url of urls) {
    const cached = cashbackCache.get(url);
    if (cached && cached.expiresAt > now) {
      result[url] = cached.data;
    } else {
      urlsToFetch.push(url);
      cashbackCache.delete(url); // Clean up expired entry
    }
  }

  // Fetch uncached URLs
  if (urlsToFetch.length > 0) {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 1000);

      const response = await fetch('https://api.yourapp.com/cashback', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${process.env.API_TOKEN}`,
        },
        body: JSON.stringify({ merchantUrls: urlsToFetch }),
        signal: controller.signal,
      });

      clearTimeout(timeoutId);

      if (response.ok) {
        const data = await response.json();

        for (const [url, cashback] of Object.entries<any>(data.cashback || {})) {
          const cashbackData: MerchantCashback = {
            text: cashback.text || cashback.cashbackText || 'Cash back available',
          };

          result[url] = cashbackData;
          cashbackCache.set(url, {
            data: cashbackData,
            expiresAt: now + CASHBACK_CACHE_TTL,
          });
        }
      }
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error('Cashback fetch error:', error);
      }
    }
  }

  return result;
};

// ----- SDK Initialization -----

import { initializeAiChatSdk } from '@sleek/ai-chat-sdk';

initializeAiChatSdk('your-zero-click-api-key', {
  // ... other config
  dataProviders: {
    offerRedirectUrlProvider,
    merchantCashbackProvider,
  },
});

Testing data providers

Unit testing

Test your providers with mock data:
import { describe, it, expect, vi } from 'vitest';

describe('offerRedirectUrlProvider', () => {
  it('returns offer redirect URL for supported merchant', async () => {
    const url = 'https://amazon.com';
    const result = await offerRedirectUrlProvider(url);

    expect(result).toMatch(/amazon\.com/);
    expect(result).toContain('tag=');
  });

  it('returns null for unsupported merchant', async () => {
    const url = 'https://unknown-merchant.com';
    const result = await offerRedirectUrlProvider(url);

    expect(result).toBeNull();
  });

  it('handles API errors gracefully', async () => {
    // Mock API failure
    global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));

    const result = await offerRedirectUrlProvider('https://amazon.com');

    expect(result).toBeNull();
  });
});

describe('merchantCashbackProvider', () => {
  it('returns cashback for multiple merchants', async () => {
    const urls = ['https://amazon.com', 'https://walmart.com'];
    const result = await merchantCashbackProvider(urls);

    expect(result).toHaveProperty('https://amazon.com');
    expect(result['https://amazon.com'].text).toBeTruthy();
  });

  it('handles empty array', async () => {
    const result = await merchantCashbackProvider([]);
    expect(result).toEqual({});
  });

  it('handles API errors gracefully', async () => {
    global.fetch = vi.fn().mockRejectedValue(new Error('Network error'));

    const result = await merchantCashbackProvider(['https://amazon.com']);

    expect(result).toEqual({});
  });
});

Manual testing

Test in the browser console:
// Test offer redirect URL provider
const offerRedirectUrl = await offerRedirectUrlProvider('https://amazon.com');
console.log('Offer redirect URL:', offerRedirectUrl);

// Test cashback provider
const cashback = await merchantCashbackProvider([
  'https://amazon.com',
  'https://walmart.com',
  'https://target.com',
]);
console.log('Cashback data:', cashback);

Common issues

Symptom: Data providers never executeCauses:
  • Chat panel not opened
  • AI not recommending merchants
  • Provider not configured properly
Solution: Open the chat and ask about products or merchants to trigger provider calls
Symptom: Chat feels sluggish, UI delaysCauses:
  • Provider taking >500ms to respond
  • No caching implemented
  • Large batch requests
Solution: Implement caching, add timeouts, optimize API responses
Symptom: SDK stops working after provider errorCauses:
  • Throwing exceptions instead of returning null/
  • Unhandled promise rejections
Solution: Wrap all code in try-catch, always return null or on errors

Next steps