> ## Documentation Index
> Fetch the complete documentation index at: https://docs.onsleek.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Data providers

> Integrate offer redirect URLs and cashback offers into AI chat conversations

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

<Note>
  Data providers are recommended for full functionality of the chat interface, especially when displaying merchant recommendations.
</Note>

## 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

```typescript theme={null}
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

```typescript theme={null}
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

<AccordionGroup>
  <Accordion title="1. Handle errors gracefully">
    Always return `null` on errors. Never throw exceptions or let errors propagate:

    ```typescript theme={null}
    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
      }
    };
    ```
  </Accordion>

  <Accordion title="2. Implement caching">
    Cache offer redirect URLs to improve performance and reduce API calls:

    ```typescript theme={null}
    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;
      }
    };
    ```
  </Accordion>

  <Accordion title="3. Support multiple domains">
    Handle different merchant domains and URL formats:

    ```typescript theme={null}
    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;
      }
    };
    ```
  </Accordion>

  <Accordion title="4. Keep response times fast">
    Aim for `<500ms` response times:

    ```typescript theme={null}
    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;
      }
    };
    ```
  </Accordion>
</AccordionGroup>

## Merchant cashback provider

### Type definition

```typescript theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
{
  "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

<AccordionGroup>
  <Accordion title="1. Handle errors gracefully">
    Always return `{}` on errors:

    ```typescript theme={null}
    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
      }
    };
    ```
  </Accordion>

  <Accordion title="2. Implement caching">
    Cache cashback data with TTL (time-to-live):

    ```typescript theme={null}
    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;
    };
    ```
  </Accordion>

  <Accordion title="3. Normalize cashback text">
    Provide consistent, user-friendly text:

    ```typescript theme={null}
    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;
    };
    ```
  </Accordion>

  <Accordion title="4. Handle batch size limits">
    Some APIs have batch size limits. Split large requests:

    ```typescript theme={null}
    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;
    };
    ```
  </Accordion>
</AccordionGroup>

## Complete example

Here's a production-ready implementation with caching, error handling, and performance optimization:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
// 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

<AccordionGroup>
  <Accordion title="Provider not being called">
    **Symptom**: Data providers never execute

    **Causes**:

    * 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
  </Accordion>

  <Accordion title="Slow performance">
    **Symptom**: Chat feels sluggish, UI delays

    **Causes**:

    * Provider taking >500ms to respond
    * No caching implemented
    * Large batch requests

    **Solution**: Implement caching, add timeouts, optimize API responses
  </Accordion>

  <Accordion title="Errors breaking SDK">
    **Symptom**: SDK stops working after provider error

    **Causes**:

    * Throwing exceptions instead of returning null/{}
    * Unhandled promise rejections

    **Solution**: Wrap all code in try-catch, always return null or {} on errors
  </Accordion>
</AccordionGroup>
