Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.byul.ai/llms.txt

Use this file to discover all available pages before exploring further.

Important Security Note

Never use API keys directly in React. API keys will be exposed to all website visitors in the browser. Always use a backend proxy.

Insecure Pattern (Don’t do this)

// This exposes your API key to everyone
const apiKey = 'byul_api_key';
fetch('https://api.byul.ai/api/v2/news', {
  headers: { 'X-API-Key': apiKey }  // Not recommended: Exposed in browser
});
// React calls your backend, backend calls Byul API
fetch('/api/news')  // Recommended: Your backend handles API key

Backend Proxy Setup

First, create a backend API that proxies requests to Byul:

Node.js/Express Backend

// server/server.js
const express = require('express');
const fetch = require('node-fetch');
const cors = require('cors');

const app = express();

app.use(cors());
app.use(express.json());

// Proxy endpoint
app.get('/api/news', async (req, res) => {
  try {
    const { limit, minImportance, symbol, q, startDate, endDate } = req.query;
    
    const params = new URLSearchParams({
      limit: limit || '20',
      minImportance: minImportance || '6'
    });
    
    if (symbol) params.append('symbol', symbol);
    if (q) params.append('q', q);
    if (startDate) params.append('startDate', startDate);
    if (endDate) params.append('endDate', endDate);
    
    const response = await fetch(`https://api.byul.ai/api/v2/news?${params}`, {
      headers: { 'X-API-Key': process.env.BYUL_API_KEY }
    });
    
    if (!response.ok) {
      return res.status(response.status).json({ error: 'API request failed' });
    }
    
    const data = await response.json();
    res.json(data);
    
  } catch (error) {
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.listen(3001, () => {
  console.log('Proxy server running on port 3001');
});

React Examples

Basic News Component

import React, { useState, useEffect } from 'react';

function NewsComponent() {
  const [news, setNews] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    fetchNews();
  }, []);

  const fetchNews = async () => {
    try {
      setLoading(true);
      
      // Call your backend, not Byul API directly
      const response = await fetch('/api/news?limit=10&minImportance=7&startDate=2024-01-01T00:00:00.000Z&endDate=2024-01-31T23:59:59.999Z');
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const data = await response.json();
      setNews(data.items);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  if (loading) return <div>Loading news...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div className="news-container">
      <h2>Financial News</h2>
      {news.map(article => (
        <div key={article._id} className="news-item">
          <h3>
            <a href={article.url} target="_blank" rel="noopener noreferrer">
              {article.title}
            </a>
          </h3>
          <p>
            Importance: {article.importanceScore}/10 | 
            Category: {article.category} |
            {article.symbols && article.symbols.length > 0 && (
              <span> Symbols: {article.symbols.join(', ')}</span>
            )}
          </p>
          <small>{new Date(article.date).toLocaleString()}</small>
        </div>
      ))}
    </div>
  );
}

export default NewsComponent;

News Hook with Filtering

import { useState, useEffect } from 'react';

function useNews(filters = {}) {
  const [news, setNews] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const fetchNews = async (newFilters = {}) => {
    setLoading(true);
    setError(null);
    
    try {
      const params = new URLSearchParams({
        limit: '20',
        minImportance: '6',
        ...filters,
        ...newFilters
      });
      
      const response = await fetch(`/api/news?${params}`);
      
      if (!response.ok) {
        throw new Error(`Failed to fetch news: ${response.status}`);
      }
      
      const data = await response.json();
      setNews(data.items);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchNews();
  }, []);

  return { 
    news, 
    loading, 
    error, 
    refetch: fetchNews 
  };
}

// Usage
function NewsApp() {
  const { news, loading, error, refetch } = useNews();
  const [symbol, setSymbol] = useState('');

  const handleSymbolFilter = (e) => {
    e.preventDefault();
    refetch({ symbol });
  };

  return (
    <div>
      <form onSubmit={handleSymbolFilter}>
        <input
          type="text"
          placeholder="Filter by symbol (e.g., AAPL)"
          value={symbol}
          onChange={(e) => setSymbol(e.target.value)}
        />
        <button type="submit">Filter</button>
      </form>
      
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error}</p>}
      
      <div>
        {news.map(article => (
          <div key={article._id}>
            <h3>{article.title}</h3>
            <p>Importance: {article.importanceScore}/10</p>
          </div>
        ))}
      </div>
    </div>
  );
}

Real-time News Dashboard

import React, { useState, useEffect, useRef } from 'react';

function NewsDashboard() {
  const [articles, setArticles] = useState([]);
  const [connected, setConnected] = useState(false);
  const [error, setError] = useState(null);
  const intervalRef = useRef(null);

  useEffect(() => {
    startPolling();
    return () => stopPolling();
  }, []);

  const startPolling = () => {
    setConnected(true);
    
    // Fetch immediately
    fetchLatestNews();
    
    // Then poll every 30 seconds
    intervalRef.current = setInterval(fetchLatestNews, 30000);
  };

  const stopPolling = () => {
    setConnected(false);
    if (intervalRef.current) {
      clearInterval(intervalRef.current);
    }
  };

  const fetchLatestNews = async () => {
    try {
      const response = await fetch('/api/news?limit=10&minImportance=7');
      
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`);
      }
      
      const data = await response.json();
      
      // Update articles, avoiding duplicates
      setArticles(prevArticles => {
        const existingIds = new Set(prevArticles.map(a => a._id));
        const newArticles = data.items.filter(a => !existingIds.has(a._id));
        
        return [...newArticles, ...prevArticles].slice(0, 50); // Keep latest 50
      });
      
      setError(null);
    } catch (err) {
      setError(err.message);
      setConnected(false);
    }
  };

  return (
    <div className="dashboard">
      <header className="dashboard-header">
        <h1>Byul Financial News Dashboard</h1>
        <div className={`status ${connected ? 'connected' : 'disconnected'}`}>
          {connected ? 'Connected' : 'Disconnected'}
        </div>
      </header>

      {error && (
        <div className="error-banner">
          Error: {error}
          <button onClick={startPolling}>Retry</button>
        </div>
      )}

      <div className="news-grid">
        {articles.map(article => (
          <div 
            key={article._id} 
            className={`news-card importance-${article.importanceScore}`}
          >
            <div className="news-header">
              <span className="importance-badge">
                {article.importanceScore}/10
              </span>
              <span className="category">{article.category}</span>
            </div>
            
            <h3>
              <a href={article.url} target="_blank" rel="noopener noreferrer">
                {article.title}
              </a>
            </h3>
            
            {article.symbols && article.symbols.length > 0 && (
              <div className="symbols">
                {article.symbols.map(symbol => (
                  <span key={symbol} className="symbol-tag">{symbol}</span>
                ))}
              </div>
            )}
            
            <div className="news-meta">
              <small>{new Date(article.date).toLocaleString()}</small>
              {article.sentiment && (
                <span className={`sentiment ${article.sentiment}`}>
                  {article.sentiment}
                </span>
              )}
            </div>
          </div>
        ))}
      </div>

      {articles.length === 0 && !error && (
        <div className="no-news">
          {connected ? 'Waiting for news updates...' : 'Not connected'}
        </div>
      )}
    </div>
  );
}

export default NewsDashboard;

Portfolio News Tracker

import React, { useState, useEffect } from 'react';

function PortfolioTracker() {
  const [portfolio, setPortfolio] = useState(['AAPL', 'GOOGL', 'MSFT']);
  const [news, setNews] = useState([]);
  const [newSymbol, setNewSymbol] = useState('');
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (portfolio.length > 0) {
      fetchPortfolioNews();
    }
  }, [portfolio]);

  const fetchPortfolioNews = async () => {
    setLoading(true);
    try {
      // Fetch news for each symbol in parallel
      const promises = portfolio.map(symbol =>
        fetch(`/api/news?symbol=${symbol}&minImportance=6&limit=20`)
          .then(res => res.json())
      );
      
      const results = await Promise.all(promises);
      
      // Combine and deduplicate
      const allNews = [];
      const seenIds = new Set();
      
      results.forEach(data => {
        data.items?.forEach(article => {
          if (!seenIds.has(article._id)) {
            allNews.push(article);
            seenIds.add(article._id);
          }
        });
      });
      
      // Sort by importance and date
      allNews.sort((a, b) => {
        if (b.importanceScore !== a.importanceScore) {
          return b.importanceScore - a.importanceScore;
        }
        return new Date(b.date) - new Date(a.date);
      });
      
      setNews(allNews);
    } catch (error) {
      console.error('Failed to fetch portfolio news:', error);
    } finally {
      setLoading(false);
    }
  };

  const addSymbol = (e) => {
    e.preventDefault();
    const symbol = newSymbol.trim().toUpperCase();
    
    if (symbol && !portfolio.includes(symbol)) {
      setPortfolio(prev => [...prev, symbol]);
      setNewSymbol('');
    }
  };

  const removeSymbol = (symbolToRemove) => {
    setPortfolio(prev => prev.filter(s => s !== symbolToRemove));
  };

  return (
    <div className="portfolio-tracker">
      <h2>Portfolio News Tracker</h2>
      
      <div className="portfolio-management">
        <div className="portfolio-symbols">
          <h3>Portfolio ({portfolio.length} symbols)</h3>
          <div className="symbols-list">
            {portfolio.map(symbol => (
              <div key={symbol} className="symbol-item">
                <span>{symbol}</span>
                <button 
                  onClick={() => removeSymbol(symbol)}
                  className="remove-btn"
                >
                  ×
                </button>
              </div>
            ))}
          </div>
        </div>
        
        <form onSubmit={addSymbol} className="add-symbol-form">
          <input
            type="text"
            placeholder="Add symbol (e.g., TSLA)"
            value={newSymbol}
            onChange={(e) => setNewSymbol(e.target.value)}
            pattern="[A-Z]{1,5}"
            title="Enter 1-5 uppercase letters"
          />
          <button type="submit">Add Symbol</button>
        </form>
      </div>

      <div className="news-section">
        <div className="news-header">
          <h3>Portfolio News ({news.length} articles)</h3>
          <button onClick={fetchPortfolioNews} disabled={loading}>
            {loading ? 'Loading...' : 'Refresh'}
          </button>
        </div>
        
        {loading && <div className="loading">Loading portfolio news...</div>}
        
        <div className="news-list">
          {news.map(article => (
            <div key={article._id} className="portfolio-news-item">
              <div className="news-symbols">
                {article.symbols?.join(', ') || 'General'}
              </div>
              
              <h4>
                <a href={article.url} target="_blank" rel="noopener noreferrer">
                  {article.title}
                </a>
              </h4>
              
              <div className="news-details">
                <span className="importance">
                  Importance: {article.importanceScore}/10
                </span>
                <span className="category">{article.category}</span>
                <span className="date">
                  {new Date(article.date).toLocaleDateString()}
                </span>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

export default PortfolioTracker;

Next.js Integration

API Route (Next.js 13+ App Router)

// app/api/news/route.js
import { NextResponse } from 'next/server';

export async function GET(request) {
  const { searchParams } = new URL(request.url);
  
  const limit = searchParams.get('limit') || '20';
  const minImportance = searchParams.get('minImportance') || '6';
  const symbol = searchParams.get('symbol');
  const q = searchParams.get('q');

  try {
    const params = new URLSearchParams({ limit, minImportance });
    if (symbol) params.append('symbol', symbol);
    if (q) params.append('q', q);

    const response = await fetch(`https://api.byul.ai/api/v2/news?${params}`, {
      headers: { 
        'X-API-Key': process.env.BYUL_API_KEY 
      },
      // Next.js caching
      next: { revalidate: 300 } // 5 minutes
    });

    if (!response.ok) {
      return NextResponse.json(
        { error: 'Failed to fetch news' },
        { status: response.status }
      );
    }

    const data = await response.json();
    return NextResponse.json(data);

  } catch (error) {
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

React Server Component (Next.js 13+)

// app/news/page.js
async function getNews() {
  const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/news`, {
    next: { revalidate: 300 } // 5 minute cache
  });
  
  if (!response.ok) {
    throw new Error('Failed to fetch news');
  }
  
  return response.json();
}

export default async function NewsPage() {
  const newsData = await getNews();

  return (
    <div>
      <h1>Latest Financial News</h1>
      {newsData.items.map(article => (
        <div key={article._id} className="news-item">
          <h2>{article.title}</h2>
          <p>Importance: {article.importanceScore}/10</p>
          <a href={article.url} target="_blank" rel="noopener noreferrer">
            Read more
          </a>
        </div>
      ))}
    </div>
  );
}

Environment Setup

Create React App

# Install dependencies
npm install

# Create .env.local file
echo "REACT_APP_API_URL=http://localhost:3001" > .env.local

# Start development server
npm start

Next.js

# Create .env.local file  
echo "BYUL_API_KEY=byul_api_key" > .env.local
echo "NEXT_PUBLIC_API_URL=http://localhost:3000" >> .env.local

# Start development server
npm run dev

Testing

# Test backend proxy
curl http://localhost:3001/api/news

# Test React app
npm test

# Check for security issues
npm audit

Production Deployment

Environment Variables:
# Backend server
BYUL_API_KEY=byul_api_key

# Frontend (if needed for API URL)
REACT_APP_API_URL=https://your-backend-api.com
Security Checklist:
  • API key only in backend environment
  • CORS properly configured
  • Rate limiting on proxy endpoints
  • Input validation on all parameters
  • Error messages don’t expose internal details

CSS Styling Example

.news-container {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.news-item {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 16px;
  background: white;
}

.news-item h3 {
  margin: 0 0 8px 0;
}

.news-item h3 a {
  color: #2563eb;
  text-decoration: none;
}

.news-item h3 a:hover {
  text-decoration: underline;
}

.importance-badge {
  background: #f3f4f6;
  padding: 4px 8px;
  border-radius: 4px;
  font-size: 0.875rem;
  font-weight: 500;
}

.symbol-tag {
  background: #3b82f6;
  color: white;
  padding: 2px 6px;
  border-radius: 4px;
  font-size: 0.75rem;
  margin-right: 4px;
}

.status.connected {
  color: #059669;
}

.status.disconnected {
  color: #dc2626;
}

.sentiment.positive {
  color: #059669;
}

.sentiment.negative {
  color: #dc2626;
}

.sentiment.neutral {
  color: #6b7280;
}