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.

Automate Bet Settlement with a Node.js Cron Job

Most betting APIs give you odds. Very few settle your bets for you. This tutorial builds a complete automatic settlement system: a Node.js cron job that monitors finished matches, resolves pending bets (won/lost/refund), credits user balances, and sends notifications — all without manual intervention.

What This System Does

Why Automatic Settlement Matters

Manual settlementAutomatic settlement
Check each match result by handAPI returns won/lost/refund per market
Look up specific market outcomesPlayer props, corners, cards — all resolved
Calculate payouts yourselfPayout included in response
Delay: hours to daysDelay: seconds after match ends
Does not scale past 50 bets/dayHandles thousands per cycle
The FieldFunded API is one of the few odds APIs with built-in settlement — you send the bet details, it returns the result.

Prerequisites

  • Node.js 18+
  • PostgreSQL database with a bets table (see schema below)
  • FieldFunded API key (get one free)
npm install pg node-cron

Step 1: Database Schema

CREATE TABLE bets (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL,
  event_id VARCHAR(100) NOT NULL,
  market_id VARCHAR(100) NOT NULL,
  market_name VARCHAR(100) NOT NULL,
  selection_id VARCHAR(100) NOT NULL,
  selection_name VARCHAR(100) NOT NULL,
  odds DECIMAL(8,2) NOT NULL,
  stake DECIMAL(12,2) NOT NULL,
  potential_payout DECIMAL(12,2) NOT NULL,
  status VARCHAR(20) DEFAULT 'pending',
  payout DECIMAL(12,2) DEFAULT 0,
  settled_at TIMESTAMPTZ,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Index for the settlement worker
CREATE INDEX idx_bets_pending ON bets(status) WHERE status = 'pending';
CREATE INDEX idx_bets_event ON bets(event_id);
The partial index on status = 'pending' means the settlement query stays fast even with millions of historical bets.

Step 2: Settlement Worker

// workers/settlement.js
const { Pool } = require("pg");
const cron = require("node-cron");

const db = new Pool({ connectionString: process.env.DATABASE_URL });
const API_KEY = process.env.FIELDFUNDED_API_KEY;
const BASE = "https://api.fieldfunded.com/v1";

async function checkSettlement(bet) {
  const res = await fetch(`${BASE}/bets/check`, {
    method: "POST",
    headers: {
      "X-API-Key": API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      event_id: bet.event_id,
      market: bet.market_name,
      selection: bet.selection_name,
      stake: parseFloat(bet.stake),
      odds: parseFloat(bet.odds),
      market_id: bet.market_id,
      selection_id: bet.selection_id,
    }),
  });

  if (res.status === 429) {
    const retry = parseInt(res.headers.get("Retry-After") || "2", 10);
    console.log(`[Rate limit] Waiting ${retry}s...`);
    await new Promise((r) => setTimeout(r, retry * 1000));
    return checkSettlement(bet); // Retry
  }

  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

async function settleBatch() {
  // 1. Get all pending bets, grouped by event
  const pending = await db.query(
    `SELECT * FROM bets WHERE status = 'pending'
     ORDER BY event_id, created_at`
  );

  if (pending.rows.length === 0) return;

  const byEvent = {};
  for (const bet of pending.rows) {
    if (!byEvent[bet.event_id]) byEvent[bet.event_id] = [];
    byEvent[bet.event_id].push(bet);
  }

  const eventCount = Object.keys(byEvent).length;
  console.log(
    `[Settlement] ${pending.rows.length} pending bets ` +
    `across ${eventCount} events`
  );

  let settled = 0;
  let skipped = 0;

  // 2. Process each event's bets
  for (const [eventId, bets] of Object.entries(byEvent)) {
    for (const bet of bets) {
      try {
        const result = await checkSettlement(bet);

        // Event not finished yet
        if (result.status === "pending") {
          skipped++;
          continue;
        }

        // 3. Settle in a transaction
        const client = await db.connect();
        try {
          await client.query("BEGIN");

          let payout = 0;
          if (result.status === "won") {
            payout = parseFloat(bet.potential_payout);
          } else if (result.status === "refund") {
            payout = parseFloat(bet.stake);
          }

          // Update bet
          await client.query(
            `UPDATE bets
             SET status = $1, payout = $2, settled_at = NOW()
             WHERE id = $3`,
            [result.status, payout, bet.id]
          );

          // Credit user balance
          if (payout > 0) {
            await client.query(
              `UPDATE users SET balance = balance + $1
               WHERE id = $2`,
              [payout, bet.user_id]
            );
          }

          await client.query("COMMIT");
          settled++;

          console.log(
            `  ${result.status.toUpperCase()} — ` +
            `${bet.selection_name} @ ${bet.odds} — ` +
            `$${payout > 0 ? payout.toFixed(2) : "0.00"}`
          );
        } catch (err) {
          await client.query("ROLLBACK");
          console.error(`  DB error for bet ${bet.id}:`, err.message);
        } finally {
          client.release();
        }

        // Rate limit protection: 200ms between calls
        await new Promise((r) => setTimeout(r, 200));
      } catch (err) {
        console.error(
          `  API error for bet ${bet.id}:`, err.message
        );
      }
    }
  }

  console.log(
    `[Settlement] Done: ${settled} settled, ${skipped} still pending`
  );
}

Step 3: Notification System

Send alerts when bets are settled. Here are three options:

Option A: Discord Webhook

async function notifyDiscord(bet, result, payout) {
  const WEBHOOK_URL = process.env.DISCORD_WEBHOOK;
  if (!WEBHOOK_URL) return;

  const color = result === "won" ? 0x53fc18 :
                result === "refund" ? 0xf59e0b : 0xef4444;
  const emoji = result === "won" ? "🏆" :
                result === "refund" ? "↩️" : "❌";

  await fetch(WEBHOOK_URL, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      embeds: [{
        title: `${emoji} Bet ${result.toUpperCase()}`,
        color,
        fields: [
          { name: "Selection", value: bet.selection_name, inline: true },
          { name: "Odds", value: `${bet.odds}`, inline: true },
          { name: "Stake", value: `$${bet.stake}`, inline: true },
          { name: "Payout", value: `$${payout.toFixed(2)}`, inline: true },
        ],
        timestamp: new Date().toISOString(),
      }],
    }),
  });
}

Option B: Email (Nodemailer)

const nodemailer = require("nodemailer");

const mailer = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: 587,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS,
  },
});

async function notifyEmail(userEmail, bet, result, payout) {
  const subject = result === "won"
    ? `You won $${payout.toFixed(2)}!`
    : result === "refund"
    ? `Bet refunded — $${parseFloat(bet.stake).toFixed(2)} returned`
    : `Bet settled — ${bet.selection_name} lost`;

  await mailer.sendMail({
    from: "bets@yoursite.com",
    to: userEmail,
    subject,
    text: `Your bet on ${bet.selection_name} @ ${bet.odds} has been settled.\n\nResult: ${result}\nPayout: $${payout.toFixed(2)}`,
  });
}

Option C: Generic Webhook

async function notifyWebhook(webhookUrl, bet, result, payout) {
  await fetch(webhookUrl, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      event: "bet.settled",
      data: {
        betId: bet.id,
        userId: bet.user_id,
        selection: bet.selection_name,
        odds: parseFloat(bet.odds),
        stake: parseFloat(bet.stake),
        result,
        payout,
        settledAt: new Date().toISOString(),
      },
    }),
  });
}

Step 4: Schedule and Run

// workers/settlement.js (add to bottom)

// Run every 60 seconds
cron.schedule("* * * * *", async () => {
  try {
    await settleBatch();
  } catch (err) {
    console.error("[Settlement] Fatal error:", err);
  }
});

console.log("[Settlement Worker] Started — runs every 60 seconds");
console.log("[Settlement Worker] Running initial check...");
settleBatch();
Run it:
export FIELDFUNDED_API_KEY="your_key"
export DATABASE_URL="postgresql://user:pass@localhost/mydb"
export DISCORD_WEBHOOK="https://discord.com/api/webhooks/..."
node workers/settlement.js
Output:
[Settlement Worker] Started — runs every 60 seconds
[Settlement Worker] Running initial check...
[Settlement] 12 pending bets across 4 events
  WON — Arsenal @ 1.90 — $95.00
  LOST — Over 2.5 @ 2.10 — $0.00
  REFUND — Chelsea @ 3.20 — $25.00 (match postponed)
  WON — Lakers @ 1.45 — $72.50
[Settlement] Done: 4 settled, 8 still pending

Step 5: Parlay Settlement

For parlays, use the parlay endpoint instead:
async function settleParlay(parlay) {
  const res = await fetch(`${BASE}/bets/check-parlay`, {
    method: "POST",
    headers: {
      "X-API-Key": API_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      stake: parlay.stake,
      legs: parlay.legs.map((leg) => ({
        event_id: leg.event_id,
        market: leg.market_name,
        selection: leg.selection_name,
        odds: leg.odds,
        market_id: leg.market_id,
        selection_id: leg.selection_id,
      })),
    }),
  });

  return res.json();
  // { status: "won", payout: 348.25 }
  // Refund legs are treated as odds 1.00 automatically
}
For a deeper dive on parlay math and edge cases, see the Parlay Calculator guide.

Rate Limit Math

ScenarioBets/cycleCycles/dayAPI calls/dayMonthlyPlan
Small app (20 pending bets)201,4401,44043,200Starter ($29)
Medium app (100 pending)1001,4405,000150,000Starter ($29)
Large app (500 pending)5001,44012,000360,000Pro ($59)
The settlement endpoint is lightweight — one call per bet. With event-level grouping and smart caching, most apps stay well within the Starter tier.

Production Hardening

Before running this in production:
  • Add a settlement_attempts counter to prevent infinite retries on broken bets
  • Set a max age (e.g., 7 days) — bets older than that get flagged for manual review
  • Add health check endpoint for your monitoring system
  • Log all settlements to a separate audit table
  • Add Prometheus/Grafana metrics (settled/min, errors/min)
  • Run the worker as a separate process (PM2, systemd, or Docker)

Get Your Free API Key

Start settling bets in minutes — 10,000 free requests/month

See Pricing

All plans compared — settlement included at every tier