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

> Build an automatic bet settlement system with Node.js. Cron job checks finished matches, resolves bets, credits payouts, and sends notifications. Full tutorial.

# 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

```mermaid theme={null}
graph TD
    A["Every 60 seconds"] --> B["Query DB for pending bets"]
    B --> C["Group by event_id"]
    C --> D["Call FieldFunded settlement endpoint"]
    D --> E{"Result?"}
    E -->|won| F["Credit user balance"]
    E -->|refund| F
    E -->|lost| G["Mark bet as lost"]
    E -->|pending| H["Skip — retry next cycle"]
    F --> I["Send notification"]
    G --> I
    I --> J["Log for auditing"]
```

## Why Automatic Settlement Matters

| Manual settlement                | Automatic settlement                        |
| -------------------------------- | ------------------------------------------- |
| Check each match result by hand  | API returns won/lost/refund per market      |
| Look up specific market outcomes | Player props, corners, cards — all resolved |
| Calculate payouts yourself       | Payout included in response                 |
| Delay: hours to days             | Delay: seconds after match ends             |
| Does not scale past 50 bets/day  | Handles thousands per cycle                 |

The FieldFunded API is one of the few [odds APIs with built-in settlement](/compare/settlement-api) — 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](/quickstart))

```bash theme={null}
npm install pg node-cron
```

## Step 1: Database Schema

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

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

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

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

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

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

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

```javascript theme={null}
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](/guides/build-parlay-calculator).

## Rate Limit Math

| Scenario                    | Bets/cycle | Cycles/day | API calls/day | Monthly | Plan           |
| --------------------------- | ---------- | ---------- | ------------- | ------- | -------------- |
| Small app (20 pending bets) | 20         | 1,440      | 1,440         | 43,200  | Starter (\$29) |
| Medium app (100 pending)    | 100        | 1,440      | 5,000         | 150,000 | Starter (\$29) |
| Large app (500 pending)     | 500        | 1,440      | 12,000        | 360,000 | Pro (\$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](/compare/pricing).

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

***

## Related Guides

* [Build a Settlement Engine (API Deep Dive)](/guides/build-settlement-engine) — advanced settlement patterns and edge cases
* [Build a Sports Betting Website](/blog/build-sports-betting-website) — full-stack tutorial including this settlement system
* [Build a Discord Bot](/guides/build-discord-bot) — send settlement alerts as Discord embeds
* [Settlement API Overview](/compare/settlement-api) — why FieldFunded is the only affordable API with built-in settlement
* [Build a Parlay Calculator](/guides/build-parlay-calculator) — parlay resolution logic

<CardGroup cols={2}>
  <Card title="Get Your Free API Key" icon="key" href="/quickstart">
    Start settling bets in minutes — 10,000 free requests/month
  </Card>

  <Card title="See Pricing" icon="dollar-sign" href="/compare/pricing">
    All plans compared — settlement included at every tier
  </Card>
</CardGroup>
