├── .idea ├── .gitignore ├── misc.xml ├── vcs.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── stake.com-Scraper-and-Autobetting-tool.iml └── modules.xml ├── requirements.txt ├── LICENSE ├── main.py ├── stake_postgresql.py ├── README.md ├── stake_sport_id_updater.py ├── stake_scraper_markets.py ├── stake_anti_captcha.py ├── stake_autobet.py ├── stake_auth.py └── stake_scraper.py /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pandas==1.3.4 2 | requests==2.25.1 3 | SQLAlchemy==1.4.31 4 | SQLAlchemy_Utils==0.38.2 5 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/stake.com-Scraper-and-Autobetting-tool.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 matthews-g 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from stake_auth import AuthClass 2 | from stake_scraper import EventScraper 3 | from stake_scraper_markets import MoneyLine 4 | from stake_autobet import MoneyLineBet 5 | 6 | if __name__ == "__main__": 7 | # SAMPLE USAGE.... 8 | 9 | # Declaring variables, and creating objects # 10 | 11 | username = "STAKE_USERNAME" 12 | password = "STAKE_PASSWORD" 13 | anti_cpt_api_key = "ANTI_CAPTCHA_API_KEY" 14 | 15 | bet_currency = "ltc" 16 | bet_stake = 0.1 17 | 18 | main_user = AuthClass(username, password, anti_cpt_api_key) 19 | events_table_tennis_pre = EventScraper("table_tennis", False) 20 | market_table_tennis_moneyline_pre = MoneyLine("table_tennis", False) 21 | autobet_tabletennis_moneyline = MoneyLineBet(market_table_tennis_moneyline_pre.table_name, 0.1, "ltc", main_user) 22 | 23 | # Do the authentication, ETL processes... 24 | #main_user.cycle() 25 | events_table_tennis_pre.cycle() 26 | market_table_tennis_moneyline_pre.cycle() 27 | 28 | df = market_table_tennis_moneyline_pre.dataframe 29 | print(df) 30 | 31 | """start_time = df["TIME"][0] 32 | home_player = df["HOME"][0] 33 | away_player = df["AWAY"][0] 34 | autobet_tabletennis_moneyline.home(start_time, home_player, away_player)""" 35 | 36 | input("PRESS ENTER TO EXIT") -------------------------------------------------------------------------------- /stake_postgresql.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from sqlalchemy import create_engine 3 | from sqlalchemy_utils import database_exists, create_database 4 | 5 | POSTGRES_USERNAME = 'postgres' 6 | POSTGRES_PASSWORD = 'asd' 7 | POSTGRES_IP = 'localhost' 8 | POSTGRES_PORT = '5432' 9 | POSTGRES_DB_NAME = 'stake_data' 10 | POSTGRES_SCHEMA_NAME = 'public' 11 | 12 | DATABASE_URL = f'postgresql://{POSTGRES_USERNAME}:{POSTGRES_PASSWORD}@{POSTGRES_IP}:{POSTGRES_PORT}/{POSTGRES_DB_NAME}' 13 | 14 | ENGINE = create_engine(DATABASE_URL) 15 | 16 | 17 | def set_table(dataframe: pd.DataFrame, table): 18 | """ 19 | Function to upload dataframe to database as a given table 20 | Drop the table first if it exists, then create a new one and load the data! 21 | """ 22 | global ENGINE 23 | global POSTGRES_SCHEMA_NAME 24 | 25 | # First, clear the given table 26 | try: 27 | with ENGINE.connect() as con: 28 | # statement = f"""DELETE FROM public.{table}""" || Use this if you want to delete the existing table. 29 | statement = f"""DROP TABLE IF EXISTS public.{table}""" 30 | con.execute(statement) 31 | except Exception as e: 32 | print("Exception happened when deleting from the table") 33 | print(e) 34 | 35 | dataframe.to_sql(table, ENGINE, schema=POSTGRES_SCHEMA_NAME, if_exists='append', 36 | index=False) 37 | 38 | 39 | def get_table(table): 40 | global ENGINE 41 | 42 | dataframe: pd.DataFrame = pd.read_sql(f"SELECT * FROM {POSTGRES_SCHEMA_NAME}.{table}", ENGINE) 43 | 44 | return dataframe 45 | 46 | 47 | if __name__ == "__main__": 48 | 49 | """ If you run the script individually, it will create the database! """ 50 | 51 | if not database_exists(DATABASE_URL): 52 | create_database(DATABASE_URL) 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stake.com-Scraper-and-Autobetting-tool 2 | 3 | ## Scrape "Stake.com" odds easily, place bets, even bypassing their CAPTCHA protection with "anti-captcha"! 4 | 5 | *Personal note: This is was one of my actual Stake.com scrapers before my account was limited to pennies and two factor auth has gotten forcefully enabled, so I decided to abandon this project. The same thing is likely to happen to you, although two factor authentication can be bypassed with additional work. **I am not responsible for any account closures or lost funds with the use of this tool**!* 6 | 7 | ## REQUIREMENTS 8 | 9 | - Libraries: `pip install -r requirements.txt` 10 | - AntiCaptcha: [You need to have a funded account with API KEY](https://anti-captcha.com/) 11 | - [PostgreSQL](https://www.postgresql.org/download/) 12 | 13 | ## USAGE 14 | ### You need to create a database. 15 | - This can be done by simply executing *stake_postgresql.py* OR creating it manually via pgAdmin 4. 16 | - Make sure that the connection data is correct in *stake_postgresql.py* 17 | ``` 18 | POSTGRES_USERNAME = 'postgres' 19 | POSTGRES_PASSWORD = 'asd' 20 | POSTGRES_IP = 'localhost' 21 | POSTGRES_PORT = '5432' 22 | POSTGRES_DB_NAME = 'stake_data' 23 | POSTGRES_SCHEMA_NAME = 'public' 24 | ``` 25 | - The tables will be stored in the *public* schema, so no need to create an additional one. 26 | 27 | ### General usage, logic of the program 28 | - Just simply run [main.py](https://github.com/matthews-g/stake.com-Scraper-and-Autobetting-tool/blob/main/main.py) and check code to easily understand how it works, and how it should be extended. 29 | - The tables in the database are handled automatically using prewritten functions. No need to worry about creating them before. 30 | - For automatic sport id updates, add `import stake_sport_id_updater.py` to the main code, the update will get executed on every program launch. 31 | 32 | ### You should know 33 | - The authentication is only working for accounts **_without_** two factor authentication (2FA) enabled. 34 | 35 | - I recommend extracting out bits from this code and building your own tool, this one serving as a helper for CAPTCHA bypassing, scraping, data storing and automatic betting. It is very similiar for all bookmakers. 36 | 37 | ## TO DO (recommended) 38 | - 2FA bypass 39 | - OneXTwo, OverUnder, Handicap scraping & betting modules 40 | - Currency converter 41 | 42 | ## LEARN MORE ABOUT ODDS SCRAPING & PROGRAMMATIC BETTING! JOIN THE COMMUNITY! 43 | - [PRIMARY DISCORD (General)](https://discord.gg/NsSRzJk) 44 | - [SECONDARY DISCORD (Bet365)](https://discord.gg/MjFr2HvUtK) 45 | 46 | 47 | -------------------------------------------------------------------------------- /stake_sport_id_updater.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import pandas as pd 4 | 5 | from stake_auth import AuthClass 6 | from stake_postgresql import set_table, get_table 7 | 8 | 9 | class SportIdUpdater: 10 | """ 11 | Bookmakers can create dynamically changing sport IDs in order to make scraping harder 12 | This script has been created to make sure the sport IDs are always up to date! 13 | """ 14 | 15 | SPORT_DICT = { 16 | 'table_tennis': 'table-tennis', # Sport names to be used in API | Created for myself because of pref. 17 | 'football': 'soccer', 18 | 'tennis': 'tennis', 19 | 'basketball': 'basketball', 20 | } 21 | 22 | SPORT_ID_TABLE = "sport_ids" 23 | 24 | 25 | 26 | def __init__(self): 27 | self.__sport_id_dict = { 28 | "soccer": None, 29 | "table-tennis": None, 30 | "tennis": None, 31 | "basketball": None} 32 | 33 | self.sport_id_dataframe = pd.DataFrame() 34 | 35 | @staticmethod 36 | def get_sport_id(sport): 37 | """ Return the ID of the given sport """ 38 | 39 | payload = json.dumps({"operationName": "AllSportGroups", "variables": {"sport": sport}, 40 | "query": "query AllSportGroups($sport: String!) {\n slugSport(sport: $sport) " 41 | "{\n id\n allGroups {\n " 42 | " name\n translation\n rank\n id\n __typename\n }" 43 | "\n __typename\n }\n}\n"}) 44 | 45 | resp = json.loads( 46 | requests.post(AuthClass.API_URL, headers=AuthClass.get_headers(), data=payload).text) 47 | 48 | return resp['data']['slugSport']['id'] 49 | 50 | @staticmethod 51 | def get_sport_id_table(): 52 | """ Return the sport IDs dataframe from the database """ 53 | return get_table(f"public.{SportIdUpdater.SPORT_ID_TABLE}") 54 | 55 | 56 | def get_all_sport_ids(self): 57 | """ Iterate through sports and save their IDs """ 58 | 59 | for a in self.__sport_id_dict: 60 | self.__sport_id_dict[a] = SportIdUpdater.get_sport_id(a) 61 | 62 | def build_dataframe(self): 63 | """ Build the sport ID dataframe, it will be uploaded to PostgreSQL! """ 64 | data = { 65 | "SPORT": [s for s in SportIdUpdater.SPORT_DICT], 66 | "SPORT_API": [SportIdUpdater.SPORT_DICT[s] for s in SportIdUpdater.SPORT_DICT], 67 | "SPORT_ID": [self.__sport_id_dict[SportIdUpdater.SPORT_DICT[s]] for s in SportIdUpdater.SPORT_DICT] 68 | } 69 | 70 | return pd.DataFrame(data=data) 71 | 72 | def cycle(self): 73 | """ Helper function to do the full ETL process! """ 74 | self.get_all_sport_ids() 75 | sport_id_df = self.build_dataframe() 76 | set_table(sport_id_df, SportIdUpdater.SPORT_ID_TABLE) 77 | 78 | 79 | sport_ids = SportIdUpdater() 80 | sport_ids.cycle() -------------------------------------------------------------------------------- /stake_scraper_markets.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | from stake_scraper import DataParser 3 | 4 | 5 | class MoneyLine(DataParser): 6 | def __init__(self, sport, live): 7 | super().__init__(sport, live) 8 | self.__home_odds = [] 9 | self.__away_odds = [] 10 | self.__home_odds_ID = [] 11 | self.__away_odds_ID = [] 12 | if live: 13 | self.table_name = f"{self.sport}_live_moneyline" 14 | else: 15 | self.table_name = f"{self.sport}_prematch_moneyline" 16 | 17 | def cleaner_individual(self): 18 | self.__home_odds.clear() 19 | self.__away_odds.clear() 20 | self.__home_odds_ID.clear() 21 | self.__away_odds_ID.clear() 22 | 23 | def parse_data(self): 24 | 25 | fixture_list = self.get_fixture_list() 26 | 27 | for match in fixture_list: 28 | start_time = match['data']['startTime'][5:-4] # Sat, 21 Aug 2021 15:00:00 GMT 29 | for market_groups in match['groups']: 30 | if market_groups['name'] == 'winner': # The market of ML! 31 | for templates in market_groups['templates']: 32 | for markets in templates['markets']: 33 | if markets['name'] == 'Winner': 34 | if markets['status'] == 'active': 35 | 36 | try: 37 | home_player = markets['outcomes'][0]['name'] 38 | home_odds = float(markets['outcomes'][0]['odds']) 39 | away_player = markets['outcomes'][1]['name'] 40 | away_odds = float(markets['outcomes'][1]['odds']) 41 | home_odds_id = markets['outcomes'][0]['id'] 42 | away_odds_id = markets['outcomes'][1]['id'] 43 | 44 | self.start_time.append(start_time) 45 | self.home_team.append(home_player) 46 | self.away_team.append(away_player) 47 | self.__home_odds.append(home_odds) 48 | self.__away_odds.append(away_odds) 49 | self.__home_odds_ID.append(home_odds_id) 50 | self.__away_odds_ID.append(away_odds_id) 51 | except Exception as e: 52 | # Errors could happen due to incorrect data regarding the match 53 | print("Error while parsing! Skipping...") 54 | print(e) 55 | pass 56 | 57 | def build_dataframe(self): 58 | data = { 59 | "TIME": self.start_time, 60 | "HOME": self.home_team, 61 | "AWAY": self.away_team, 62 | "HOME_ODDS": self.__home_odds, 63 | "AWAY_ODDS": self.__away_odds, 64 | "HOME_ODDS_ID": self.__home_odds_ID, 65 | "AWAY_ODDS_ID": self.__away_odds_ID, 66 | } 67 | 68 | self.dataframe = pd.DataFrame(data=data) 69 | 70 | self.dataframe = self.dataframe[ 71 | (self.dataframe["HOME_ODDS"] > 1.00) & (self.dataframe["AWAY_ODDS"] > 1.00)] 72 | 73 | self.dataframe = self.dataframe.sort_values(by=["TIME"], ignore_index=True) 74 | -------------------------------------------------------------------------------- /stake_anti_captcha.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import requests 4 | 5 | 6 | class AntiCaptcha: 7 | HEADERS = { 8 | "Accept": "application/json", 9 | "Content-Type": "application/json" 10 | } 11 | 12 | CREATE_TASK_URL = "https://api.anti-captcha.com/createTask" 13 | GET_TASK_RESULT_URL = "https://api.anti-captcha.com/getTaskResult" 14 | 15 | STAKE_LOGIN_URL = "https://stake.com/?action=login&modal=auth" 16 | STAKE_SITE_KEY = "7830874c-13ad-4cfe-98d7-e8b019dc1742" 17 | 18 | def __init__(self, api_key): 19 | self.__api_key = api_key 20 | 21 | @property 22 | def api_key(self): 23 | return self.__api_key 24 | 25 | @api_key.setter 26 | def api_key(self, api_key): 27 | self.__api_key = api_key 28 | 29 | @staticmethod 30 | def post_request(json_payload): 31 | """Custom post request method for AntiCaptcha""" 32 | try: 33 | return json.loads( 34 | requests.post(AntiCaptcha.CREATE_TASK_URL, headers=AntiCaptcha.HEADERS, data=json_payload).text) 35 | except Exception as e: 36 | print("Error while sending request to the AntiCaptcha API!") 37 | print(e) 38 | 39 | def create_task(self): 40 | """" Creates the task returns the task id for checking...""" 41 | payload = json.dumps({ 42 | "clientKey": self.__api_key, 43 | "task": 44 | { 45 | "type": "HCaptchaTaskProxyless", 46 | "websiteURL": AntiCaptcha.STAKE_LOGIN_URL, 47 | "websiteKey": AntiCaptcha.STAKE_SITE_KEY 48 | } 49 | }) 50 | 51 | resp = requests.post(AntiCaptcha.CREATE_TASK_URL, headers=AntiCaptcha.HEADERS, data=payload) 52 | return json.loads(resp.text)['taskId'] 53 | 54 | def get_task_result(self, task_id): 55 | """ Get the task result response """ 56 | payload = json.dumps({"clientKey": self.__api_key, 57 | "taskId": str(task_id)}) 58 | 59 | resp = requests.post(AntiCaptcha.GET_TASK_RESULT_URL, headers=AntiCaptcha.HEADERS, data=payload) 60 | return json.loads(resp.text) 61 | 62 | def wait_for_captcha_response(self, task_id, wait_seconds=240): 63 | """ Wait until the task is finished and return the result""" 64 | now = time.time() 65 | while time.time() - now <= wait_seconds: # wait_seconds mean try until the seconds... 66 | time.sleep(0.1) 67 | try: 68 | result = self.get_task_result(task_id) 69 | if result['status'] == "processing": 70 | print("The captcha is still being processed...") 71 | print("Waiting for 3 seconds before checking again...") 72 | time.sleep(3) 73 | elif result['status'] == "ready": 74 | if result['errorId'] == 0: 75 | print("Captcha has been processed, returning the response!") 76 | return result['solution']['gRecaptchaResponse'] 77 | else: 78 | return False 79 | except Exception as e: 80 | print("Exception while waiting for captcha response!") 81 | print(e) 82 | print("Trying again to get task result in 3 seconds...") 83 | time.sleep(3) 84 | 85 | return False 86 | 87 | def cycle(self): 88 | # Aggregator function to get the solved captcha code required for login... 89 | task_id = self.create_task() 90 | # print(task_id) 91 | return self.wait_for_captcha_response(task_id) 92 | -------------------------------------------------------------------------------- /stake_autobet.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from stake_auth import AuthClass 4 | from stake_postgresql import get_table 5 | from abc import ABC, abstractmethod 6 | import pandas as pd 7 | 8 | 9 | class AutoBet(ABC): 10 | 11 | def __init__(self, table_name, stake, currency, auth_object): 12 | self.table_name = table_name 13 | self.stake = stake 14 | self.currency = currency 15 | self.auth_object = auth_object 16 | 17 | @abstractmethod 18 | def get_match(self): 19 | """ Function to get the given match from the database """ 20 | pass 21 | 22 | def send_bet_request(self, odds, odds_id): 23 | # DIRTY CODE AHEAD, COPIED AND PASTED API DATA AS IS... 24 | payload = json.dumps({"operationName": "SportBet", 25 | "variables": {"multiplier": odds, "amount": self.stake, "currency": self.currency, 26 | "outcomeIds": [odds_id], "oddsChange": "none"}, 27 | "query": "mutation SportBet($amount: Float!, $currency: CurrencyEnum!, $outcomeIds: " 28 | "[String!]!, $oddsChange: SportOddsChangeEnum!, $identifier: String, $multiplier:" 29 | " Float = 1) {\n sportBet(amount: $amount, currency: $currency, outcomeIds: " 30 | "$outcomeIds, oddsChange: $oddsChange, identifier: $identifier, multiplier: " 31 | "$multiplier) {\n id\n amount\n currency\n payoutMultiplier\n " 32 | "potentialMultiplier\n cashoutMultiplier\n outcomes {\n odds\n " 33 | "outcome {\n ...Outcome\n market {\n ...BetSlip\n " 34 | " __typename\n }\n __typename\n }\n __typename\n }\n " 35 | " __typename\n }\n}\n\nfragment Outcome on SportMarketOutcome {\n active\n " 36 | "id\n odds\n name\n __typename\n}\n\nfragment BetSlip on SportMarket {\n " 37 | "...MarketFragment\n fixture {\n id\n status\n slug\n tournament " 38 | "{\n ...TournamentTree\n __typename\n }\n data {\n " 39 | "...FixtureDataMatch\n ...FixtureDataOutright\n __typename\n }\n " 40 | "__typename\n }\n __typename\n}\n\nfragment MarketFragment on SportMarket {\n " 41 | "id\n name\n status\n extId\n specifiers\n outcomes {\n id\n active\n " 42 | " name\n odds\n __typename\n }\n __typename\n}\n\nfragment " 43 | "TournamentTree on SportTournament {\n id\n name\n slug\n category " 44 | "{\n id\n name\n slug\n contentNotes {\n id\n createdAt\n " 45 | " publishAt\n expireAt\n linkText\n linkUrl\n message\n " 46 | " publishAt\n __typename\n }\n sport {\n id\n name\n " 47 | "slug\n contentNotes {\n id\n createdAt\n publishAt\n " 48 | " expireAt\n linkText\n linkUrl\n message\n " 49 | "publishAt\n __typename\n }\n __typename\n }\n " 50 | "__typename\n }\n contentNotes {\n id\n createdAt\n publishAt\n " 51 | "expireAt\n linkText\n linkUrl\n message\n publishAt\n " 52 | "__typename\n }\n __typename\n}\n\nfragment FixtureDataMatch on " 53 | "SportFixtureDataMatch {\n startTime\n competitors {\n " 54 | "...Competitor\n __typename\n }\n __typename\n}\n\nfragment " 55 | "Competitor on SportFixtureCompetitor {\n name\n extId\n " 56 | "countryCode\n abbreviation\n __typename\n}\n\nfragment FixtureDataOutright on " 57 | "SportFixtureDataOutright {\n name\n startTime\n endTime\n __typename\n}\n"}) 58 | 59 | response = requests.post(AuthClass.API_URL, headers=self.auth_object.get_headers(self.auth_object.read_token()), 60 | data=payload) 61 | 62 | json_resp = json.loads(response.text) 63 | 64 | try: 65 | print(json_resp) 66 | if json_resp['data']['sportBet']['id'] != "": 67 | print("Successful bet on STAKE.COM!") 68 | return True 69 | else: 70 | print("Bet unsuccessful on STAKE.COM!") 71 | return False 72 | except Exception as e: 73 | print("Bet unsuccessful on STAKE.COM!") 74 | print(e) 75 | return False 76 | 77 | 78 | class MoneyLineBet(AutoBet): 79 | 80 | def __init__(self, table_name, stake, currency, auth_object): 81 | super().__init__(table_name, stake, currency, auth_object) 82 | 83 | def get_match(self, start_time, home_player, away_player): 84 | match_df: pd.DataFrame = get_table(self.table_name) 85 | 86 | match = match_df[(match_df["HOME"] == home_player) 87 | & 88 | (match_df["AWAY"] == away_player) 89 | & 90 | (match_df["TIME"] == start_time)] 91 | return match.reset_index().iloc[0] 92 | 93 | def home(self, start_time, home_player, away_player): 94 | data = self.get_match(start_time, home_player, away_player) 95 | return self.send_bet_request(data.HOME_ODDS, data.HOME_ODDS_ID) 96 | 97 | def away(self, start_time, home_player, away_player): 98 | data = self.get_match(start_time, home_player, away_player) 99 | return self.send_bet_request(data.AWAY_ODDS, data.AWAY_ODDS_ID) 100 | -------------------------------------------------------------------------------- /stake_auth.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import pickle 4 | 5 | import stake_anti_captcha 6 | 7 | 8 | class AuthClass: 9 | API_URL = "https://api.stake.com/graphql" 10 | 11 | def __init__(self, username, password, anti_cpt_api_key): 12 | self.__username = username 13 | self.__password = password 14 | self.__recaptcha_code = "" 15 | self.__login_token = "" 16 | self.__final_token = "" 17 | self.__driver = None 18 | self.__captcha_object = stake_anti_captcha.AntiCaptcha(anti_cpt_api_key) 19 | self.__captcha_code = "" 20 | 21 | @staticmethod 22 | def get_headers(auth_token: str = "") -> dict: 23 | headers = { 24 | 'sec-ch-ua': '"Chromium";v="92", " Not A;Brand";v="99", "Google Chrome";v="92"', 25 | 'accept': '*/*', 26 | 'x-language': 'en', 27 | 'x-lockdown-token': '', 28 | 'sec-ch-ua-mobile': '?0', 29 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' 30 | '/92.0.4515.159 Safari/537.36', 31 | 'content-type': 'application/json', 32 | 'Origin': 'https://stake.com', 33 | 'Referer': 'https://stake.com/'} 34 | 35 | if auth_token: 36 | headers['x-access-token'] = auth_token 37 | 38 | return headers 39 | 40 | def solve_captcha(self): 41 | self.__captcha_code = self.__captcha_object.cycle() 42 | 43 | def read_token(self) -> str: 44 | # FROM EXTERNAL FILE! 45 | try: 46 | with open(f"{self.__username}_token.pkl", "rb") as f: 47 | return pickle.load(f) 48 | except Exception as e: 49 | print("Error while trying to load 'token.pkl'") 50 | print(e) 51 | return "" 52 | 53 | def save_token(self): 54 | # FROM EXTERNAL FILE! 55 | try: 56 | with open(f"{self.__username}_token.pkl", "wb") as f: 57 | pickle.dump(self.__final_token, f) 58 | except Exception as e: 59 | print("Error while saving 'token.pkl'") 60 | print(e) 61 | 62 | def request_login_user(self): # login phase #1 63 | payload = json.dumps({ 64 | "operationName": "RequestLoginUser", 65 | "variables": { 66 | "name": self.__username, 67 | "password": self.__password, 68 | "captcha": self.__captcha_code, 69 | }, 70 | "query": "mutation RequestLoginUser($name: String, $email: String, $password: String!, " 71 | "$captcha: String) {\n requestLoginUser(name: $name, email: $email, password: " 72 | "$password, captcha: $captcha) {\n loginToken\n hasTfaEnabled\n " 73 | "requiresLoginCode\n user {\n id\n name\n __typename\n }\n" 74 | " __typename\n }\n}\n" 75 | }) 76 | 77 | resp = json.loads( 78 | requests.post(AuthClass.API_URL, headers=AuthClass.get_headers(), data=payload).text) 79 | self.__login_token = resp['data']['requestLoginUser']['loginToken'] 80 | 81 | def complete_login_user(self): # login phase 2 82 | # This is only optimized for single logins, not for 2FA or email code verifications... 83 | # If your account has 2FA it will throw an error 84 | # You need to add other functions for getting external data from emails/2FA... 85 | # You also need to modify the payload 86 | # Email logincode payload: 87 | # variables-> loginCode: "logincode_received_in_email" 88 | 89 | payload = json.dumps({ 90 | "operationName": "CompleteLoginUser", 91 | "variables": { 92 | "loginToken": self.__login_token, 93 | "sessionName": "Chrome (Unknown)", 94 | "blackbox": "blackbox" 95 | }, 96 | "query": "mutation CompleteLoginUser($loginToken: String, $tfaToken: String, $sessionName: String!, " 97 | "$blackbox: String, $loginCode: String) {\n completeLoginUser(loginToken: $loginToken, " 98 | "tfaToken: $tfaToken, sessionName: $sessionName, blackbox: $blackbox, loginCode: $loginCode) " 99 | "{\n ...UserAuthenticatedSession\n __typename\n }\n}\n\nfragment UserAuthenticatedSession " 100 | "on UserAuthenticatedSession {\n token\n session {\n ...UserSession\n " 101 | "user {\n ...UserAuth\n __typename\n }\n __typename\n }\n __typename\n}" 102 | "\n\nfragment UserSession on UserSession {\n id\n sessionName\n ip\n " 103 | "country\n city\n active\n updatedAt\n __typename\n}\n\nfragment UserAuth on " 104 | "User {\n id\n name\n email\n hasPhoneNumberVerified\n hasEmailVerified\n " 105 | "hasPassword\n intercomHash\n createdAt\n hasTfaEnabled\n mixpanelId\n " 106 | "hasOauth\n isKycBasicRequired\n isKycExtendedRequired\n isKycFullRequired\n " 107 | "kycBasic {\n id\n status\n __typename\n }\n kycExtended {\n id\n " 108 | "status\n __typename\n }\n kycFull {\n id\n status\n __typename\n }\n " 109 | "flags {\n flag\n __typename\n }\n roles {\n name\n __typename\n }\n " 110 | "balances {\n ...UserBalanceFragment\n __typename\n }\n activeClientSeed {\n id\n " 111 | "seed\n __typename\n }\n previousServerSeed {\n id\n seed\n __typename\n }\n " 112 | "activeServerSeed {\n id\n seedHash\n nextSeedHash\n nonce\n blocked\n " 113 | "__typename\n }\n __typename\n}\n\nfragment UserBalanceFragment on UserBalance {\n " 114 | " available {\n amount\n currency\n __typename\n }\n vault {\n amount\n " 115 | "currency\n __typename\n }\n __typename\n}\n" 116 | }) 117 | 118 | resp = json.loads( 119 | requests.post(AuthClass.API_URL, headers=AuthClass.get_headers(), data=payload).text) 120 | self.__final_token = resp['data']['completeLoginUser']['token'] 121 | 122 | def login_check(self) -> bool: 123 | if not self.read_token(): 124 | print("Not logged in to Stake.com!") 125 | return False 126 | 127 | payload = json.dumps({"operationName": "UserVaultBalances", "variables": {}, 128 | "query": "query UserVaultBalances {\n user {\n id\n " 129 | "balances {\n available {\n amount\n " 130 | " currency\n __typename\n }\n " 131 | "vault {\n amount\n currency\n __" 132 | "typename\n }\n __typename\n }\n __typename\n }\n}\n"}) 133 | 134 | try: 135 | response = json.loads( 136 | requests.post(AuthClass.API_URL, headers=AuthClass.get_headers(self.read_token()), data=payload).text) 137 | if response['data']['user']['id']: 138 | print(f"Logged in as {self.__username} to stake.com!") 139 | return True 140 | else: 141 | print("Not logged in!") 142 | return False 143 | except Exception as e: 144 | # If the request fails, we just simply return False 145 | print("Not logged in!") 146 | print(e) 147 | return False 148 | 149 | def cycle(self): 150 | # Intended to use as a constant loop 151 | if self.login_check(): 152 | return True 153 | else: 154 | self.solve_captcha() 155 | self.request_login_user() 156 | self.complete_login_user() 157 | self.save_token() 158 | -------------------------------------------------------------------------------- /stake_scraper.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import pickle 4 | import pandas as pd 5 | from abc import ABC, abstractmethod 6 | 7 | from stake_auth import AuthClass 8 | from stake_postgresql import get_table, set_table 9 | 10 | 11 | class EventFiles(ABC): 12 | 13 | # Class for grouping event file related functions and variables 14 | 15 | def __init__(self, sport, live): 16 | self.sport = sport 17 | self.live = live 18 | 19 | if self.live: 20 | self.__event_file_name = sport + "_event_live.pkl" 21 | else: 22 | self.__event_file_name = sport + "_event.pkl" 23 | 24 | self.dataframe = pd.DataFrame() 25 | self.match_events = {} # Must be used by EventScraper for storing event data 26 | 27 | def save_event_file(self): 28 | with open(self.__event_file_name, "wb") as f: 29 | pickle.dump(self.match_events, f) 30 | 31 | def get_event_file(self): 32 | with open(self.__event_file_name, "rb") as f: 33 | return pickle.load(f) 34 | 35 | 36 | class EventScraper(EventFiles): 37 | 38 | def __init__(self, sport, live): 39 | super().__init__(sport, live) 40 | self.__actual_sport_id = self.__get_sport_id() 41 | 42 | def __get_sport_id(self): 43 | """ Reads the sport_id from the database and returns it!""" 44 | 45 | sport_id_dataframe = get_table("sport_ids") 46 | 47 | sport_id = sport_id_dataframe[sport_id_dataframe["SPORT"] == self.sport].iloc[0].SPORT_ID 48 | 49 | return sport_id 50 | 51 | def get_event_payload(self): 52 | 53 | # API QUERY REPLICATED AS IS, DIRTY CODE AHEAD.... 54 | 55 | if self.live: 56 | payload = json.dumps({"operationName": "liveSportFixtureList", "variables": 57 | {"tournamentLimit": 50, "sportId": self.__actual_sport_id, "groups": "winner"}, 58 | "query": 59 | "query liveSportFixtureList" 60 | "($sportId: String!, $groups: String!, $tournamentLimit: Int = 25) " 61 | "{\n sport(sportId: $sportId) {\n id\n tournamentList(type: live, " 62 | "limit: $tournamentLimit) {\n ...TournamentTreeFragment\n " 63 | "fixtureList(type: live) {\n ...FixturePreviewFragment\n " 64 | "groups(groups: [$groups], status: [active, suspended, deactivated]) " 65 | "{\n " 66 | "...GroupFixtureFragment\n __typename\n }\n " 67 | "__typename\n " 68 | " }\n __typename\n }\n __typename\n }\n}\n\nfragment " 69 | "GroupFixtureFragment on SportGroup {\n ...Group\n templates {\n " 70 | "...TemplateFragment\n markets {\n ...MarketFragment\n " 71 | "__typename\n }\n __typename\n }\n " 72 | "__typename\n}\n\nfragment MarketFragment on " 73 | "SportMarket {\n id\n name\n status\n " 74 | "extId\n specifiers\n outcomes {\n " 75 | "id\n active\n name\n odds\n " 76 | "__typename\n }\n __typename\n}\n\nfragment " 77 | "TemplateFragment on SportGroupTemplate {\n " 78 | "extId\n rank\n __typename\n}\n\nfragment " 79 | "Group on SportGroup {\n name\n " 80 | "translation\n rank\n " 81 | "__typename\n}\n\nfragment " 82 | "FixturePreviewFragment " 83 | "on SportFixture {\n id\n extId\n status\n slug\n " 84 | "marketCount(status: [active, suspended])\n data " 85 | "{\n ...FixtureDataMatchFragment\n " 86 | "...FixtureDataOutrightFragment\n __typename\n }" 87 | "\n eventStatus {\n ...FixtureEventStatus\n " 88 | "__typename\n }\n tournament {\n " 89 | "...TournamentTreeFragment\n " 90 | "__typename\n }\n ...LiveStreamExistsFragment\n __typename\n}\n\nfragment " 91 | "FixtureDataMatchFragment on SportFixtureDataMatch {\n startTime\n competitors " 92 | "{\n ...CompetitorFragment\n __typename\n }\n __typename\n}\n\nfragment " 93 | "CompetitorFragment on SportFixtureCompetitor {\n name\n extId\n " 94 | "countryCode\n abbreviation\n __typename\n}\n\nfragment " 95 | "FixtureDataOutrightFragment on SportFixtureDataOutright " 96 | "{\n name\n startTime\n endTime\n " 97 | "__typename\n}\n\nfragment FixtureEventStatus on SportFixtureEventStatus " 98 | "{\n homeScore\n awayScore\n matchStatus\n clock {\n " 99 | "matchTime\n remainingTime\n __typename\n }\n periodScores " 100 | "{\n homeScore\n awayScore\n matchStatus\n __typename\n }" 101 | "\n currentServer {\n extId\n __typename\n }\n " 102 | "homeGameScore\n awayGameScore\n statistic {\n " 103 | "yellowCards {\n away\n home\n " 104 | "__typename\n }\n redCards {\n away\n " 105 | "home\n __typename\n }\n corners {\n " 106 | "home\n away\n __typename\n }\n " 107 | "__typename\n }\n __typename\n}\n\nfragment " 108 | "TournamentTreeFragment on SportTournament " 109 | "{\n id\n name\n slug\n category " 110 | "{\n id\n name\n " 111 | "slug\n sport {\n id\n name\n slug\n __typename\n }" 112 | "\n __typename\n }\n __typename\n}\n\nfragment LiveStreamExistsFragment on " 113 | "SportFixture {\n abiosStream {\n exists\n __typename\n }\n " 114 | "betradarStream {\n exists\n __typename\n }\n diceStream {\n " 115 | "exists\n __typename\n }\n __typename\n}\n"}) 116 | else: 117 | payload = json.dumps({"operationName": "SportFixtureList", 118 | "variables": {"type": "upcoming", "sportId": self.__actual_sport_id, 119 | "groups": "winner", "limit": 50, "offset": 0}, 120 | "query": "query SportFixtureList($type: SportSearchEnum!, $sportId: String!, " 121 | "$groups: String!, $limit: Int!, $offset: Int!) {\n sport(sportId: " 122 | "$sportId) {\n id\n name\n fixtureCount(type: $type)\n " 123 | "fixtureList(type: $type, limit: $limit, offset: $offset) {\n " 124 | "...FixturePreview\n groups(groups: [$groups], status: [active, " 125 | "suspended, deactivated]) {\n ...GroupFixture\n " 126 | "__typename\n }\n __typename\n }\n __typename\n }\n}\n\n" 127 | "fragment FixturePreview on SportFixture {\n id\n status\n slug\n " 128 | "betradarStream {\n exists\n __typename\n }\n marketCount(status: " 129 | "[active, suspended])\n data {\n ...FixtureDataMatch\n " 130 | "...FixtureDataOutright\n __typename\n }\n tournament {\n " 131 | "...TournamentTree\n __typename\n }\n eventStatus {\n " 132 | "...FixtureEventStatus\n __typename\n }\n __typename\n}\n\nfragment " 133 | "FixtureDataMatch on SportFixtureDataMatch {\n startTime\n " 134 | "competitors {\n ...Competitor\n __typename\n }\n " 135 | "__typename\n}\n\nfragment Competitor on " 136 | "SportFixtureCompetitor {\n name\n extId\n countryCode\n " 137 | "abbreviation\n __typename\n}\n\nfragment FixtureDataOutright " 138 | "on SportFixtureDataOutright {\n name\n startTime\n endTime\n " 139 | "__typename\n}\n\nfragment TournamentTree on SportTournament " 140 | "{\n id\n name\n slug\n category {\n id\n name\n " 141 | "slug\n contentNotes {\n id\n createdAt\n " 142 | "publishAt\n expireAt\n linkText\n linkUrl\n " 143 | "message\n publishAt\n __typename\n }\n " 144 | "sport {\n id\n name\n slug\n " 145 | "contentNotes {\n id\n createdAt\n " 146 | "publishAt\n expireAt\n linkText\n " 147 | "linkUrl\n message\n publishAt" 148 | "\n __typename\n }\n " 149 | " __typename\n }\n __typename\n }\n contentNotes {\n id\n " 150 | "createdAt\n publishAt\n expireAt\n linkText\n linkUrl\n " 151 | "message\n publishAt\n __typename\n }\n __typename\n}\n\nfragment " 152 | "FixtureEventStatus on SportFixtureEventStatus {\n homeScore\n " 153 | "awayScore\n matchStatus\n clock {\n matchTime\n " 154 | "remainingTime\n __typename\n }\n periodScores {\n " 155 | "homeScore\n awayScore\n matchStatus\n __typename\n }\n " 156 | "currentServer {\n extId\n __typename\n }\n homeGameScore\n " 157 | "awayGameScore\n statistic {\n yellowCards {\n away\n " 158 | "home\n __typename\n }\n redCards {\n away\n " 159 | "home\n __typename\n }\n corners {\n home\n " 160 | "away\n __typename\n }\n __typename\n }\n " 161 | "__typename\n}\n\nfragment GroupFixture on SportGroup {\n ..." 162 | "Group\n templates {\n ...Template\n markets {\n " 163 | "...MarketFragment\n __typename\n }\n __typename\n }\n " 164 | "__typename\n}\n\nfragment Group on SportGroup {\n name\n " 165 | "translation\n rank\n __typename\n}\n\nfragment Template on " 166 | "SportGroupTemplate {\n extId\n rank\n __typename\n}\n\nfragment " 167 | "MarketFragment on SportMarket {\n id\n name\n status\n extId\n " 168 | "specifiers\n outcomes {\n id\n active\n name\n odds\n " 169 | " __typename\n }\n __typename\n}\n"}) 170 | return payload 171 | 172 | def scrape_events(self): 173 | resp = requests.post(AuthClass.API_URL, headers=AuthClass.get_headers(), data=self.get_event_payload(), 174 | timeout=5) 175 | 176 | self.match_events = json.loads(resp.text) 177 | 178 | def cycle(self): 179 | """ Helper function for the extract process! """ 180 | self.scrape_events() 181 | self.save_event_file() 182 | 183 | 184 | class DataParser(EventFiles): 185 | 186 | def __init__(self, sport, live): 187 | super().__init__(sport, live) 188 | 189 | self.start_time = [] # List of scraped start times... 190 | self.home_team = [] # List of home players... 191 | self.away_team = [] # List of away players... 192 | 193 | self.table_name = "" # Table name for the database. Must be filled in every subclass! 194 | 195 | def get_fixture_list(self): 196 | 197 | # Both live and pre parsing are based on fixture lists.. 198 | # So it could be wise to extract the relevant fixture list and return it to avoid code duplication 199 | # The get_event_file returns the data from the external file! 200 | 201 | fixture_list = [] 202 | match_data = self.get_event_file() 203 | if self.live: 204 | for tournament in match_data['data']['sport']['tournamentList']: 205 | for match in tournament['fixtureList']: 206 | fixture_list.append(match) 207 | else: 208 | for match in match_data['data']['sport']['fixtureList']: 209 | fixture_list.append(match) 210 | 211 | return fixture_list 212 | 213 | def cleaner_base(self): 214 | # This function is to clean all common data lists 215 | self.start_time.clear() 216 | self.home_team.clear() 217 | self.away_team.clear() 218 | self.dataframe = pd.DataFrame() 219 | 220 | @abstractmethod 221 | def cleaner_individual(self): 222 | # This function is to scrape individual markets! 223 | # like self.market or etc... 224 | pass 225 | 226 | def cleaner(self): 227 | self.cleaner_base() 228 | self.cleaner_individual() 229 | 230 | @abstractmethod 231 | def parse_data(self): 232 | """ This function is to parse raw data from the fixture list! """ 233 | pass 234 | 235 | @abstractmethod 236 | def build_dataframe(self): 237 | """ This function is to build the DataFrame which has the event and odds data """ 238 | pass 239 | 240 | def load_dataframe(self): 241 | """ Upload the dataframe to the database! """ 242 | 243 | set_table(self.dataframe, self.table_name) 244 | 245 | def cycle(self): 246 | """ Helper function to do the full ETL process """ 247 | 248 | self.cleaner() 249 | self.parse_data() 250 | self.build_dataframe() 251 | self.load_dataframe() 252 | --------------------------------------------------------------------------------