Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.fieldfunded.com/llms.txt

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

Build a Mobile Sports Odds App with React Native

Ship a native iOS and Android odds app from a single codebase. This guide connects React Native to the FieldFunded API for live odds, real-time scores, and automatic bet settlement — all optimized for mobile data usage and battery life.

Why Mobile Needs a Different Approach

Mobile apps have constraints that web dashboards do not:
ConstraintImpactSolution
Battery drainFrequent polling kills batteryAdaptive polling intervals
Cellular dataUsers on metered connectionsRequest compression + caching
Screen sizeLess space for market tablesCollapsed market groups
Background limitsiOS/Android kill background tasksPush notifications instead of polling

What You’ll Use

SDK MethodEndpointPurpose
getEvents()GET /v1/eventsList upcoming matches
getEventOdds()GET /v1/events/{id}/oddsGet odds for a match
getLive()GET /v1/liveCurrently in-play events
getScores()GET /v1/scoresLive scores
checkBet()POST /v1/bets/checkSettle bets

Step 1: Set Up the API Client

Create a lightweight client optimized for mobile:
// api/fieldfunded.ts
const BASE_URL = 'https://api.fieldfunded.com/v1';
const API_KEY = process.env.FIELDFUNDED_API_KEY!;

const headers = {
  'X-API-Key': API_KEY,
  'Accept-Encoding': 'gzip',  // Reduce data usage
};

export async function apiGet<T>(path: string, params?: Record<string, string>): Promise<T> {
  const url = new URL(`${BASE_URL}${path}`);
  if (params) {
    Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
  }

  const res = await fetch(url.toString(), { headers });
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json();
}

Step 2: Build the Events List Screen

// screens/EventsScreen.tsx
import React, { useEffect, useState } from 'react';
import { FlatList, Text, View, TouchableOpacity, StyleSheet } from 'react-native';
import { apiGet } from '../api/fieldfunded';

interface Event {
  id: string;
  home_team: string;
  away_team: string;
  commence_time: string;
  sport_key: string;
}

export function EventsScreen({ navigation }: any) {
  const [events, setEvents] = useState<Event[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    apiGet<{ events: Event[] }>('/events', { sport: 'soccer' })
      .then(data => setEvents(data.events || []))
      .finally(() => setLoading(false));
  }, []);

  return (
    <FlatList
      data={events}
      keyExtractor={item => item.id}
      renderItem={({ item }) => (
        <TouchableOpacity
          style={styles.card}
          onPress={() => navigation.navigate('Odds', { eventId: item.id })}
        >
          <Text style={styles.teams}>
            {item.home_team} vs {item.away_team}
          </Text>
          <Text style={styles.time}>
            {new Date(item.commence_time).toLocaleString()}
          </Text>
        </TouchableOpacity>
      )}
    />
  );
}

const styles = StyleSheet.create({
  card: { padding: 16, borderBottomWidth: 1, borderColor: '#eee' },
  teams: { fontSize: 16, fontWeight: '600' },
  time: { fontSize: 12, color: '#888', marginTop: 4 },
});

Step 3: Odds Detail Screen

// screens/OddsScreen.tsx
import React, { useEffect, useState } from 'react';
import { ScrollView, Text, View, StyleSheet } from 'react-native';
import { apiGet } from '../api/fieldfunded';

interface Market {
  id: string;
  name: string;
  selections: { id: string; name: string; odds: number }[];
}

export function OddsScreen({ route }: any) {
  const { eventId } = route.params;
  const [markets, setMarkets] = useState<Market[]>([]);

  useEffect(() => {
    apiGet<{ markets: Market[] }>(`/events/${eventId}/odds`)
      .then(data => setMarkets(data.markets || []));
  }, [eventId]);

  return (
    <ScrollView style={styles.container}>
      {markets.map(market => (
        <View key={market.id} style={styles.marketCard}>
          <Text style={styles.marketName}>{market.name}</Text>
          {market.selections.map(sel => (
            <View key={sel.id} style={styles.selectionRow}>
              <Text style={styles.selName}>{sel.name}</Text>
              <Text style={styles.odds}>{sel.odds.toFixed(2)}</Text>
            </View>
          ))}
        </View>
      ))}
    </ScrollView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  marketCard: { marginBottom: 16, padding: 12, backgroundColor: '#f9f9f9', borderRadius: 8 },
  marketName: { fontSize: 14, fontWeight: '700', marginBottom: 8 },
  selectionRow: { flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 6 },
  selName: { fontSize: 14 },
  odds: { fontSize: 14, fontWeight: '600', color: '#2563eb' },
});

Step 4: Adaptive Polling for Live Scores

On mobile, polling frequency should adapt to save battery and data:
// hooks/useLiveScores.ts
import { useEffect, useRef, useState } from 'react';
import { AppState } from 'react-native';
import { apiGet } from '../api/fieldfunded';

export function useLiveScores(sport: string) {
  const [scores, setScores] = useState<any[]>([]);
  const intervalRef = useRef<NodeJS.Timer | null>(null);

  useEffect(() => {
    const poll = async () => {
      const data = await apiGet<{ scores: any[] }>('/scores', { sport });
      setScores(data.scores || []);
    };

    // Start polling
    poll();
    intervalRef.current = setInterval(poll, 30_000); // 30s when app is active

    // Reduce polling when app is backgrounded
    const sub = AppState.addEventListener('change', (state) => {
      if (intervalRef.current) clearInterval(intervalRef.current);
      if (state === 'active') {
        poll(); // Immediate refresh on foreground
        intervalRef.current = setInterval(poll, 30_000);
      }
      // Stop polling entirely when backgrounded
    });

    return () => {
      if (intervalRef.current) clearInterval(intervalRef.current);
      sub.remove();
    };
  }, [sport]);

  return scores;
}

Step 5: Local Caching

Cache API responses to reduce data usage and enable offline browsing:
// utils/cache.ts
import AsyncStorage from '@react-native-async-storage/async-storage';

const CACHE_TTL = 60_000; // 1 minute

interface CacheEntry<T> {
  data: T;
  timestamp: number;
}

export async function cachedFetch<T>(
  key: string,
  fetcher: () => Promise<T>
): Promise<T> {
  const cached = await AsyncStorage.getItem(key);

  if (cached) {
    const entry: CacheEntry<T> = JSON.parse(cached);
    if (Date.now() - entry.timestamp < CACHE_TTL) {
      return entry.data;
    }
  }

  const data = await fetcher();
  await AsyncStorage.setItem(key, JSON.stringify({
    data,
    timestamp: Date.now(),
  }));
  return data;
}

Rate Limit Math for Mobile

Mobile apps typically make fewer requests than web dashboards because of adaptive polling:
ScenarioRequests/dayMonthlyPlan
Browse events + check odds (casual)20600Free
Track 3 live matches with 30s polling60018,000Starter ($29)
Production app with 100 daily users3,00090,000Starter ($29)
High-traffic app with 1,000+ users15,000450,000Pro ($79)
With caching, most user actions hit the local cache instead of the API. A well-optimized mobile app can serve 100 daily active users on the Starter plan.

Get Your Free API Key

Start building in 5 minutes — works with any mobile framework

See Pricing

All plans compared side by side