Skip to main content

Build a Bet Settlement Engine with Automatic Market Resolution

FieldFunded’s market resolution API settles bets automatically — poll resolved markets, match by ID, pay out winners. No manual checking, no missed results. This guide builds a production-ready settlement engine in under 100 lines of TypeScript.

What You’ll Use

SDK MethodEndpointPurpose
getSettlements()GET /v1/settlementsPoll for resolved markets
getEventResult()GET /v1/events/{id}/resultGet final score
checkBet()POST /v1/bets/checkVerify individual bet outcome
checkParlay()POST /v1/bets/check-parlayVerify parlay/accumulator outcome

Architecture

Game Ends → Poll getSettlements() every 60s → Match bets by market_id + selection_id → Resolve (won/lost/refund) → Update balance

Step 1: Set Up the SDK

import { FieldFundedSDK } from '@fieldfunded/sdk';

const client = new FieldFundedSDK({
  apiKey: process.env.FIELDFUNDED_API_KEY!,
  baseUrl: 'https://api.fieldfunded.com/v1',
});

Step 2: Poll for Settlements

FieldFunded’s settlement is gradual. Simple markets (1x2, Winner) resolve within 2-5 minutes of game end. Complex markets (player props, correct score) take 10-30 minutes.
async function pollSettlements() {
  const data = await client.getSettlements({ limit: 20 });

  for (const settlement of data.settlements) {
    console.log(
      `Event ${settlement.event_id}: ${settlement.settlement_status}`,
      `— ${settlement.resolved_markets_count}/${settlement.total_markets_count} markets`,
      `(${settlement.pending_markets_count} pending)`
    );

    // Process only when we have resolved markets
    if (settlement.resolved_markets_count > 0) {
      await processSettlement(settlement);
    }
  }
}

Step 3: Match Bets Against Resolved Markets

The key is matching by market_id and selection_id — these are unique identifiers that guarantee you’re resolving the correct bet.
async function processSettlement(settlement: any) {
  // Get all unresolved bets for this event from your database
  const pendingBets = await db.query(
    'SELECT * FROM bets WHERE event_id = $1 AND status = $2',
    [settlement.event_id, 'pending']
  );

  for (const bet of pendingBets) {
    // Use checkBet for precise resolution
    const result = await client.checkBet({
      selections: [{
        event_id: bet.event_id,
        market: bet.market_name,
        outcome: bet.outcome,
        odds: bet.odds,
        stake: bet.stake,
        market_id: bet.market_id,       // Use IDs for 100% accuracy
        selection_id: bet.selection_id,
      }],
    });

    const betResult = result.results[0];

    if (betResult.result === 'pending') continue; // Not yet resolved

    await resolveBet(bet, betResult);
  }
}

Step 4: Resolve and Pay

async function resolveBet(bet: any, result: any) {
  switch (result.result) {
    case 'won':
      // Payout = stake × odds (returned by API)
      await db.query(
        'UPDATE bets SET status = $1, payout = $2 WHERE id = $3',
        ['won', result.payout, bet.id]
      );
      await db.query(
        'UPDATE users SET balance = balance + $1 WHERE id = $2',
        [result.payout, bet.user_id]
      );
      break;

    case 'lost':
      await db.query(
        'UPDATE bets SET status = $1, payout = 0 WHERE id = $2',
        ['lost', bet.id]
      );
      break;

    case 'refund':
      // Return original stake
      await db.query(
        'UPDATE bets SET status = $1, payout = $2 WHERE id = $3',
        ['refunded', bet.stake, bet.id]
      );
      await db.query(
        'UPDATE users SET balance = balance + $1 WHERE id = $2',
        [bet.stake, bet.user_id]
      );
      break;
  }
}

Step 5: Handle Parlays

For parlays, use checkParlay(). The API handles void legs automatically (void leg = odds 1.0, recalculates combined odds).
async function resolveParlay(parlay: any) {
  const result = await client.checkParlay({
    legs: parlay.legs.map((leg: any) => ({
      event_id: leg.event_id,
      market: leg.market_name,
      outcome: leg.outcome,
      odds: leg.odds,
      market_id: leg.market_id,
      selection_id: leg.selection_id,
    })),
    stake: parlay.stake,
  });

  // result.payout contains recalculated odds if any leg was voided
  console.log(`Parlay status: ${result.payout.status}`);
  console.log(`Combined odds: ${result.payout.combined_odds}`);
  console.log(`Payout: ${result.payout.payout}`);
}

Step 6: Edge Cases

Postponed Games

FieldFunded follows the 48-hour rule: if a game is postponed but resumes within 48 hours, bets remain active. After 48 hours, all markets are refunded.

Cancelled / Walkover

Immediate refund. The getEventResult() endpoint returns status: 'walkover' or status: 'cancelled'.
const result = await client.getEventResult(eventId);
if (['walkover', 'cancelled', 'abandoned'].includes(result.status)) {
  await refundAllBets(eventId);
}

Timeout Protection

If some markets haven’t resolved after 30 minutes, they auto-refund. Your settlement loop simply picks up the refund status on the next poll.

Step 7: Run the Loop

import cron from 'node-cron';

// Poll every 60 seconds
cron.schedule('* * * * *', async () => {
  try {
    await pollSettlements();
  } catch (err) {
    console.error('Settlement poll failed:', err);
  }
});

Production Checklist

  • Use market_id + selection_id for matching (not market names)
  • Make resolution idempotent (re-processing won’t double-pay)
  • Log every resolution for audit trail
  • Handle SDK errors (RateLimitError, ServiceUnavailableError)
  • Set up alerts if settlement polling stops

Settlement API Reference

See full endpoint documentation →

Get Your Free API Key

Start building in 5 minutes — 10,000 free requests/month

See Pricing

All plans compared side by side