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 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 — 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 )
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\n Result: ${ result } \n Payout: $ ${ 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
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 .
Production Hardening
Before running this in production:
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