Published at

Scraping API-sports for automated betting notifications

Scraping API-sports for automated betting notifications

Here is my approach to automatic a betting strategy that made me DOZENS of Danish Kroner

Authors
  • Name
    Joachim Bülow
    Twitter
  • Cofounder and CTO at Doubble
Sharing is caring!
Table of Contents

Introduction

Recently, I realized that whenever football matches reach 3 goals in the first half, odds for goals in the second half are inflated due to the momentum of the game.

Bookmakers forget that people in the halftime become complacent when ahead, and defensive when behind.

So I needed a way to get notified when these key events occur.

I devised a plan to run a lambda function periodically and email notify me if any matches lived up to my criteria.

Services

For a sports API, I chose API-sports because they have a free tier of up to 100 API calls per day, And a very easy to use and flexible API. For emails, I used MailerSend, as they have 12000 emails per month for free. For hosting I chose AWS Lambda. I schedules this with using AWS EventBridge Schedule.

The architecture of the automated betting notifier

Now that is what i would call

Free money

Code snippets

API-sports client

package apisports

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"

	"github.com/joachimbulow/autobetter/src"
)

// Get a response from the API
func getResponse(url string) (*[]byte, error) {
	client := &http.Client{}

	req, err := http.NewRequest("GET", url, nil)

	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	req.Header.Add("x-apisports-key", "MY_API_KEY")

	res, err := client.Do(req)

	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	defer res.Body.Close()
	body, err := io.ReadAll(res.Body) // Read the stream

	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	return &body, nil
}

func GetFixtures() (*src.ApiResponse, error) {
	var url = "https://v3.football.api-sports.io/fixtures?live=all"
	var body, err = getResponse(url)

	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	// Parse the response
	var parsedResponse src.ApiResponse
	err = json.Unmarshal(*body, &parsedResponse)

	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	return &parsedResponse, nil
}

const overUnderLineBetId = 36

func GetLiveOdds(fixtureId string) (*src.BetResponse, error) {
	// Endpoint for fetching all events from live fixtures
	var url = fmt.Sprintf("https://v3.football.api-sports.io/odds/live?fixture=%s&bet=%d", fixtureId, overUnderLineBetId)
	var body, err = getResponse(url)

	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	// Parse the response
	var parsedResponse src.BetResponse
	err = json.Unmarshal(*body, &parsedResponse)

	if err != nil {
		fmt.Println(err)
		return nil, err
	}

	return &parsedResponse, nil
}

Lambda function

package main

import (
	"bytes"
	"fmt"
	"slices"
	"time"

	// Local modules

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/joachimbulow/autobetter/src"
	"github.com/joachimbulow/autobetter/src/apisports"
	"github.com/joachimbulow/autobetter/src/emails"
)

var accepted_league_ids = []int{
	135, // Series A
	39,  // Premier League
	78,  // Bundesliga
	140, // La Liga
	88,  // Eredivisie
	61,  // Ligue 1
}

const halftimeLeewayMinutes = 30
const halfTimeMinutes = 45
const minGoals = 3

func main() {
	lambda.Start(handler)
	// handler()
}

func handler() {
	apiResponse, err := apisports.GetFixtures()

	if err != nil {
		fmt.Println(err)
		return
	}

	// Filter out the events from the leagues we care about
	var relevantFixtures []src.FixtureResponse
	for _, fixture := range apiResponse.Response {
		if slices.Contains(accepted_league_ids, fixture.League.ID) {
			relevantFixtures = append(relevantFixtures, fixture)
		}
	}

	var fixturesWithinRequirements []src.FixtureResponse
	for _, fixture := range relevantFixtures {
		timeStartedUtc, err := time.Parse(time.RFC3339, fixture.Fixture.Date)

		if err != nil {
			fmt.Println(err)
			return
		}

		now := time.Now()
		beforeHalfTime := timeStartedUtc.Add(time.Minute * (halfTimeMinutes + halftimeLeewayMinutes)).After(now)
		anyTeamThreePlusGoals := *fixture.Goals.Away >= minGoals || *fixture.Goals.Home >= minGoals

		if beforeHalfTime && anyTeamThreePlusGoals {
			fixturesWithinRequirements = append(fixturesWithinRequirements, fixture)
		}
	}

	if len(fixturesWithinRequirements) < 1 {
		fmt.Println("No fixtures within requirements")
		return
	}

	var buffer bytes.Buffer

	buffer.WriteString(`
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>New Cool Bets Available</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f9f9f9;
            color: #333;
            margin: 0;
            padding: 20px;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
            background: #fff;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        .header {
            text-align: center;
            font-size: 24px;
            margin-bottom: 20px;
        }
        .fixture {
            margin-bottom: 15px;
            padding-bottom: 10px;
            border-bottom: 1px solid #eee;
        }
        .fixture:last-child {
            border-bottom: none;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">New Cool Bets Available</div>
`)

	for _, fixture := range fixturesWithinRequirements {
		// Test the odds thingy
		odds, _ := apisports.GetLiveOdds(fmt.Sprint(fixture.Fixture.ID))
		goalsScored := *fixture.Goals.Away + *fixture.Goals.Home

		value := "Under"
		handicap := fmt.Sprintf("%g", (float64(goalsScored+1) + 0.5))

		var oddsValue string

		for _, betValue := range odds.Response[0].Odds[0].Values {
			if betValue.Value == value && betValue.Handicap != nil && *betValue.Handicap == handicap {
				oddsValue = betValue.Odd
			}
		}

		buffer.WriteString(`
        <div class="fixture">
            <h2>Match: ` + fixture.Teams.Home.Name + ` vs ` + fixture.Teams.Away.Name + `</h2>
			<strong>` + fmt.Sprint(*fixture.Goals.Home) + `  -   ` + fmt.Sprint(*fixture.Goals.Away) + `</strong>
			<p>Under ` + handicap + ` goals gives odds: ` + oddsValue + `</p>
        </div>
    `)
	}

	buffer.WriteString(`
    	</div>
	</body>
</html>
`)

	emails.SendEmail("New cool bets available", buffer.String())
}

Email client

package emails

import (
	"context"
	"fmt"
	"time"

	"github.com/mailersend/mailersend-go"
)

type EmailRequest struct {
	Recipients []string `json:"recipients"`
	Subject    string   `json:"subject"`
	Content    string   `json:"content"`
}

func SendEmail(subject string, content string) {
	ms := mailersend.NewMailersend("MY_API_KEY")

	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
	defer cancel()

	text := "There are news from the auto better..."

	from := mailersend.From{
		Name:  "Byllefar Productions",
		Email: "automation@joachimbulow.net",
	}

	recipients := []mailersend.Recipient{
		{
			Name:  "Joachim Bülow",
			Email: "joachimbulow@hotmail.dk",
		},
		{
			Name:  "Rasmus Topp",
			Email: "rasmus_topp@hotmail.com",
		},
		{
			Name:  "Dennis Rosenlund",
			Email: "Dennis.rosenlund@hotmail.com",
		},
	}

	tags := []string{"foo", "bar"}

	message := ms.Email.NewMessage()

	message.SetFrom(from)
	message.SetSubject(subject)
	message.SetHTML(content)
	message.SetText(text)
	message.SetTags(tags)

	for _, recipient := range recipients {
		message.SetRecipients([]mailersend.Recipient{recipient})
		_, err := ms.Email.Send(ctx, message)
		if err != nil {
			fmt.Println(err)
			return
		}

	}

	fmt.Println("Emails were sent successfully")
}
Sharing is caring!