Bet Insights provides betting trends, popular bets, and analytics to help users make informed betting decisions. The module displays real-time insights and suggestions powered by Sportradar data.
This module relies on Sportradar Unified Odds Feed (UOF) market data. If you're not using Sportradar UOF markets, you will need to map your own market and outcome identifiers to the corresponding Sportradar format.
Bet Insights shows users betting trends, popular bets, and analytics based on aggregated betting data and Sportradar's intelligent suggestions.

For custom adapter implementations, register your adapter using the SIR('registerAdapter', ...) method with a function that handles bet insights requests.
requestName string required
The request type identifier. For Bet Insights, this will be 'outcomes'.
args OutcomesRequest required
Request parameters containing outcome insights data. See OutcomesRequest object for details.
callback function optional
Callback function to return outcome data in the format (error, response) => void.
Callback Arguments:
error Error | null - Error object if request failed, otherwise nullresponse OutcomesResponse | undefined - Response object containing outcomes array. See OutcomesResponse object for details.Self-Service Adapter - Bet Insights Handler:
const onRequest = function onRequest(requestName, args, callback) {
if (requestName === 'outcomes') {
// Store callback for future updates
lastOutcomesCallbackFn = callback;
const response = {
outcomes: args.outcomes.map(insight => {
// Map Sportradar insight to your system
return {
event: {
id: insight.eventId,
date: '14/2/2025',
teams: [
{
id: 'home_team_id',
name: 'Home Team'
},
{
id: 'away_team_id',
name: 'Away Team'
}
],
liveCurrentTime: 'First half',
isLive: true,
sport: {
id: 'sport_1',
name: 'Soccer'
},
category: {
id: 'category_u18',
name: 'U18'
},
tournament: {
id: 'sr_tournament_id',
externalId: 'your_tournament_id',
name: 'U18 World Cup'
},
result1: {
result: [1, 3]
}
},
market: {
id: insight.marketId,
specifier: {
value: insight.specifier?.value || 'total=1.5',
displayValue: 'Total Goals Over 1.5'
},
externalId: 'your_market_id',
name: 'Over/Under',
status: {
status: 'active'
}
},
outcome: {
id: insight.outcomeId,
externalId: 'your_outcome_id',
name: 'Over 1.5',
oddsDecimal: 2.05,
odds: '1/2',
status: {
isActive: true
},
isSelected: false
}
};
})
};
callback(null, response);
return function unsubscribe() {
// Cleanup logic
};
}
};
SIR('registerAdapter', onRequest);The OutcomesRequest object contains Sportradar's insight suggestions that need to be mapped to your betting system.
outcomes OutcomeInsight[] required
Array of outcome insight objects that Sportradar has generated suggestions for. Each insight contains identifiers to match against your betting data.
OutcomesRequest Example:
{
outcomes: [
{
eventId: '51099405',
marketId: 19,
outcomeId: 13,
specifier: {
value: 'total=0.5'
}
},
{
eventId: '51099405',
marketId: 1,
outcomeId: 3
}
]
}The OutcomeInsight object represents a single betting insight suggestion from Sportradar.
eventId string required
Sportradar event ID for the match.
marketId number required
Sportradar market ID.
outcomeId number required
Sportradar outcome ID.
specifier object optional
Market specifier details with a value property (e.g., "total=0.5").
OutcomeInsight Example:
{
eventId: '51099405',
marketId: 19,
outcomeId: 13,
specifier: {
value: 'total=0.5'
}
}The response object returned to the callback containing mapped outcome data from your betting system.
outcomes OutcomeData[] required
Array of outcome data objects containing event, market, and outcome information.
OutcomesResponse Example:
{
outcomes: [
{
event: {
id: '51099405',
date: '14/2/2025',
teams: [
{ id: 'home_id', name: 'Home Team' },
{ id: 'away_id', name: 'Away Team' }
],
isLive: true,
sport: { id: '1', name: 'Soccer' }
},
market: {
id: 19,
name: 'Over/Under',
status: { isActive: true }
},
outcome: {
id: 13,
name: 'Over 0.5',
oddsDecimal: 1.85,
status: { isActive: true }
}
}
]
}The OutcomeData object contains complete betting information for a single outcome insight.
event Event required
Event information including teams, score, and tournament details.
market Market required
Market information including name, specifiers, and status.
outcome Outcome required
Outcome information including name, odds, and selection status.
OutcomeData Example:
{
event: {
id: '51099405',
date: '14/2/2025',
teams: [
{ id: 'home_id', name: 'Home Team' },
{ id: 'away_id', name: 'Away Team' }
],
liveCurrentTime: 'First half',
isLive: true,
sport: { id: '1', name: 'Soccer' },
category: { id: 'u18', name: 'U18' },
tournament: {
id: 'sr_tournament_id',
name: 'U18 World Cup'
},
result1: { result: [1, 3] }
},
market: {
id: 19,
specifier: {
value: 'total=1.5',
displayValue: 'Total Goals Over 1.5'
},
externalId: 'your_market_id',
name: 'Over/Under',
status: {
status: 'active'
}
},
outcome: {
id: 13,
externalId: 'your_outcome_id',
name: 'Over 1.5',
oddsDecimal: 2.05,
odds: '1/2',
status: { isActive: true },
isSelected: false
}
}Store the callback reference to update selections when the user's bet slip changes. This allows you to reflect real-time updates in the Bet Insights widget.
When a user adds or removes a selection from their bet slip, call the stored callback with updated outcome data where isSelected reflects the current state.
Callback Storage Example:
let lastOutcomesCallbackFn;
const onRequest = function onRequest(requestName, args, callback) {
if (requestName === 'outcomes') {
// Store callback for future updates
lastOutcomesCallbackFn = callback;
const response = {
outcomes: args.outcomes.map(insight => ({
// ... build response
}))
};
callback(null, response);
return function unsubscribe() {
lastOutcomesCallbackFn = null;
};
}
};
// Later, when bet slip changes:
if (lastOutcomesCallbackFn) {
const updatedResponse = {
outcomes: [
// Updated outcomes with isSelected status
]
};
lastOutcomesCallbackFn(null, updatedResponse);
}If you're not using Sportradar UOF markets, you need to create a mapping between your market/outcome IDs and Sportradar's standard IDs.
The adapter's onRequest function will receive:
requestName with value 'outcomes'args object containing an outcomes arrayEach object in the outcomes array is of type OutcomeRequestEntry and contains Sportradar's market identifiers that you need to map to your system.
Properties explained:
eventId: Sportradar event IDmarketId: Sportradar market type ID (e.g., 20 = Total goals)outcomeId: Sportradar outcome ID (e.g., 12 = Over)specifier: Optional market parameters (e.g., total=1.5 for Over/Under 1.5 goals)OutcomeRequestEntry Example:
{
"eventId": "50850527",
"marketId": 20,
"outcomeId": 12,
"specifier": {
"value": "total=1.5"
}
}Map the Sportradar eventId to your corresponding event/match in your system.
Event Mapping Example:
const onRequest = function(requestName, args, callback) {
if (requestName === 'outcomes') {
const response = {
outcomes: args.outcomes.map(insight => {
// Step 2: Map Sportradar eventId to your match
const sportradarEventId = insight.eventId; // "50850527"
// Look up your match using Sportradar event ID
const yourMatch = findMatchBySportradarEventId(sportradarEventId);
// Example: { id: 'match_123', name: 'Team A vs Team B', ... }
if (!yourMatch) {
console.warn(`No match found for eventId: ${sportradarEventId}`);
return null;
}
// Continue with market mapping in next steps...
return {
event: {
id: sportradarEventId, // Keep Sportradar ID
externalId: yourMatch.id, // Your internal ID
// ... more event data
},
// ... market and outcome data
};
}).filter(Boolean) // Remove null entries
};
callback(null, response);
return () => {};
}
};Look up the Sportradar marketId to understand the market type and its parameters.
In this example, "marketId": 20 can be found in the All Soccer Markets documentation.
Understanding Market Templates:
Market names may contain placeholders that need to be replaced with actual team names:
{$competitor1} = HOME team name{$competitor2} = AWAY team nameOnce you've identified the match from Step 2, you know the home and away team names. Use these to replace the placeholders in the market template.
For example, if the market template is "{$competitor2} total" and the away team is "Team B", the final market name becomes "Team B total".
Market Lookup Example:
// Market ID 20 definition from documentation
const marketDefinitions = {
20: {
name: '{$competitor2} total', // Template with placeholder
description: 'Total goals scored by away team',
// ... other market properties
},
// ... other markets
};
// Step 3: Find and process market definition
const marketDef = marketDefinitions[insight.marketId]; // marketId: 20
if (!marketDef) {
console.warn(`Unknown marketId: ${insight.marketId}`);
return null;
}
// Replace placeholders with actual team names from your match
const marketName = marketDef.name
.replace('{$competitor1}', yourMatch.homeTeam.name) // "Team A"
.replace('{$competitor2}', yourMatch.awayTeam.name); // "Team B"
// Example result: "Team B total"
// Now find this market in your system
const yourMarket = findMarketByNameAndSpecifier(
yourMatch.id,
marketName,
insight.specifier?.value // "total=1.5"
);In some cases, the market name requires the outcome information to be complete. Look up the outcomeId in the same market documentation.
In this example, "outcomeId": 12 has the outcome name "over {total}".
Combining the market name from Step 3 with the outcome name gives us: "Team B total – over {total}"
This market name still contains a placeholder {total} that will be replaced in the next step using the specifier value.
Outcome Lookup Example:
// Market and outcome definitions
const marketDefinitions = {
20: {
name: '{$competitor2} total',
outcomes: {
12: { name: 'over {total}' },
13: { name: 'under {total}' },
// ... other outcomes
}
}
};
// Step 4: Get outcome name and combine with market name
const marketDef = marketDefinitions[insight.marketId]; // 20
const outcomeDef = marketDef.outcomes[insight.outcomeId]; // 12
if (!outcomeDef) {
console.warn(`Unknown outcomeId: ${insight.outcomeId}`);
return null;
}
// Build complete market name with outcome
let marketName = marketDef.name
.replace('{$competitor1}', yourMatch.homeTeam.name)
.replace('{$competitor2}', yourMatch.awayTeam.name); // "Team B total"
const fullMarketName = `${marketName} - ${outcomeDef.name}`;
// Result: "Team B total - over {total}"
// Still has {total} placeholder to be replaced with specifierThe {total} placeholder matches the specifier name, which means it should be replaced with the value from the specifier.
Extract the value from the specifier object and replace the placeholder in the outcome name:
"specifier": {
"value": "total=1.5"
}Parse the specifier value string to extract "1.5" and replace {total} with it.
This makes the final outcome name 'over 1.5'.
In this example, outcome ID 12 will always represent the 'Over' market. The specifier value determines the specific threshold (e.g., 1.5, 2.5, etc.).
Specifier Replacement Example:
// Parse specifier value (format: "key=value")
const specifierValue = insight.specifier?.value; // "total=1.5"
const specifierPairs = {};
if (specifierValue) {
// Split by '|' for multiple specifiers, then by '=' for key-value pairs
specifierValue.split('|').forEach(pair => {
const [key, value] = pair.split('=');
specifierPairs[key] = value;
});
}
// Result: { total: "1.5" }
// Replace placeholders in outcome name
let finalOutcomeName = outcomeDef.name; // "over {total}"
// Replace each specifier placeholder
Object.keys(specifierPairs).forEach(key => {
const placeholder = `{${key}}`;
finalOutcomeName = finalOutcomeName.replace(placeholder, specifierPairs[key]);
});
// Result: "over 1.5"
// Now you can search for this exact market and outcome in your system
const yourOutcome = findOutcomeByName(
yourMatch.id,
marketName,
finalOutcomeName
);
console.log(finalOutcomeName); // "over 1.5"Now that you have your market, it's time to build the OutcomeResponseEntry for this suggestion.
Important: Properties highlighted in the response example must match with the object that was given in the request for the suggestion to show:
{
"eventId": "50850527",
"marketId": 20,
"outcomeId": 12,
"specifier": {
"value": "total=1.5"
}
}The response event.id, market.id, outcome.id, and market.specifier.value must match the request values exactly.
If you do not want to show a suggestion, simply do not return a response entry for it (return null or filter it out).
OutcomeResponseEntry Example:
{
"event": {
"id": "50850527", // Must match request eventId
"date": "2023-07-18",
"teams": [
{
"name": "Team A"
},
{
"name": "Team B"
}
],
"liveCurrentTime": "12:30",
"isLive": true,
"sport": {
"id": "1",
"name": "Soccer"
},
"category": {
"id": "sr:category:31",
"name": "Location"
},
"tournament": {
"id": "sr:tournament:1",
"name": "Mock League"
},
"result1": {
"result": [1, 0]
}
},
"market": {
"id": 20, // Must match request marketId
"name": "Market 19 (marketId: 20, specifier: total=1.5)",
"status": {
"status": "active"
},
"specifier": {
"value": "total=1.5" // Must match request specifier.value
}
},
"outcome": {
"id": 12, // Must match request outcomeId
"name": "Outcome 19 (outcomeId: 12)",
"status": {
"isActive": true
},
"competitor": "Competitor 19 (eventId: 50850527)",
"oddsDecimal": 1.37,
"odds": "1.37"
}
}When building your response, the teams array is used to replace placeholders in market names.

This data is displayed in the Bet Insights widget as shown in the example, where Team A and Team B are clearly identified along with their statistics.
Teams Array Structure:
"teams": [
{
"name": "Team A" // Home team - first item
},
{
"name": "Team B" // Away team - second item
}
]The order matters - home team must be the first item and away team must be the second item in the teams array.
The following event properties are displayed at the tournament level in the Bet Insights widget:
result array containing [home score, away score]
These fields provide real-time context for the betting suggestion, helping users understand the current state of the match.
Match Status Properties:
"liveCurrentTime": "12:30",
"isLive": true,
"result1": {
"result": [1, 0] // [home score, away score]
}While sport and category are included in the event object structure, they are currently not displayed in the Bet Insights widget.
These fields should still be provided in the response for potential future use and to maintain a complete data structure.
Sport and Category Properties:
"sport": {
"id": "1",
"name": "Soccer"
},
"category": {
"id": "sr:category:31",
"name": "Location"
}The tournament object is displayed in the Bet Insights widget.

Tournament Properties:
"tournament": {
"id": "sr:tournament:1",
"name": "Mock League"
}The market and outcome data is displayed within each bet insight card:

You have flexibility in how to provide odds:
oddsDecimal and odds are provided, oddsDecimal will be used for the change indicator functionality, while odds determines the display format.Market and Outcome Display Example:
{
"market": {
"name": "Market 17 (marketId: 18, specifier: total=3.5)",
},
"outcome": {
"name": "Outcome 17 (outcomeId: 12)",
"oddsDecimal": 1.72, // Used for format conversion and change indicators
"odds": "1.72" // Used for display as-is
}
}Odds Handling Best Practice
oddsDecimal if you want automatic odds format conversionodds if you want to control the exact display formatThe status.status (for market) and status.isActive (for outcome) property controls the availability of markets and outcomes in the Bet Insights widget. The possible values for market status are: active, deactivated, suspended, settled and cancelled.

When status.isActive is set to false, the outcome becomes temporarily unavailable and unselectable by the user. This is useful for:
Both market-level and outcome-level status can be controlled independently.
Status Control Example:
{
"market": {
"status": {
"status": 'suspended' // Market is suspended
}
},
"outcome": {
"status": {
"isActive": false // Outcome is unavailable
}
}
}When isActive is false, the outcome will be visually indicated as unavailable and users will not be able to select it for betting.
Here's the complete implementation combining all steps to map Sportradar insights to your system and build the response.
Complete Data Example:
{
event: {
id: "50850527",
date: "2023-07-18",
teams: [ {
name: "Team A"
}, {
name: "Team B"
}
],
tournament: {
id: "sr:tournament:1",
name: "The League"
},
result1: {
result: [1, 0]
}
},
market: {
id: 20,
name: "Total",
status: {
status: 'active' | 'deactivated' | 'suspended' | 'settled' | 'cancelled'
},
specifier: {
value: "total=1.5"
}
},
outcome: {
id: 12,
name: "Over 3.5",
status: {
isActive: true
},
oddsDecimal: 1.37,
odds: "1.36"
}
}Complete Mapping Example:
const onRequest = function(requestName, args, callback) {
if (requestName === 'outcomes') {
const response = {
outcomes: args.outcomes.map(insight => {
// Step 2: Get match
const match = findMatchBySportradarEventId(insight.eventId);
if (!match) {
return null; // Don't show this suggestion
}
// Step 3: Get market definition and replace team placeholders
const marketDef = marketDefinitions[insight.marketId];
let marketName = marketDef.name
.replace('{$competitor1}', match.homeTeam.name)
.replace('{$competitor2}', match.awayTeam.name);
// Step 4: Get outcome name
const outcomeDef = marketDef.outcomes[insight.outcomeId];
// Step 5: Parse and replace specifier placeholders
const specifierPairs = {};
if (insight.specifier?.value) {
insight.specifier.value.split('|').forEach(pair => {
const [key, value] = pair.split('=');
specifierPairs[key] = value;
});
}
let outcomeName = outcomeDef.name;
Object.keys(specifierPairs).forEach(key => {
outcomeName = outcomeName.replace(`{${key}}`, specifierPairs[key]);
});
// Find this in your system
const yourOutcome = findYourOutcome(
match.id,
marketName,
outcomeName
);
if (!yourOutcome) {
return null; // Don't show if not found
}
// Step 6: Build OutcomeResponseEntry
return {
event: {
id: insight.eventId, // MUST match request
date: match.date,
teams: match.teams,
liveCurrentTime: match.liveTime,
isLive: match.isLive,
sport: match.sport,
category: match.category,
tournament: match.tournament,
result1: { result: match.score }
},
market: {
id: insight.marketId, // MUST match request
name: yourOutcome.marketName,
status: { isActive: yourOutcome.isActive },
specifier: {
value: insight.specifier?.value // MUST match request
}
},
outcome: {
id: insight.outcomeId, // MUST match request
name: yourOutcome.name,
status: { isActive: yourOutcome.isActive },
oddsDecimal: yourOutcome.odds,
odds: yourOutcome.odds.toString()
}
};
}).filter(Boolean) // Remove null entries
};
callback(null, response);
return () => {};
}
};