Sports betting website crawler that looks for sure bets profit opportunities and sends them to a Telegram channel.
- TypeScript
- Node.js
- Puppeteer
- Docker
- Kubernetes
This was built for educational purposes only. Use it with caution.
Profit opportunities from sports betting.
Opportunities like:
do not make sense since both will lose if the result is 1.
To fix this, when any value is an integer, we need to make sure that the under value is larger than the over value and vice versa.
Also, we want to make sure that the difference between the values is equal 0.5 to avoid too many improbable combinations.
0.25 differences are not acceptable because part of the invested amount can be returned to the user.
Examples:
They're in UTC. People need in BRT
(node:35) UnhandledPromiseRejectionWarning: Error: Page crashed!
at Page._onTargetCrashed (/app/.yarn/unplugged/puppeteer-npm-2.1.1-01d5fbb78f/node_modules/puppeteer/lib/Page.js:213:24)
at CDPSession.<anonymous> (/app/.yarn/unplugged/puppeteer-npm-2.1.1-01d5fbb78f/node_modules/puppeteer/lib/Page.js:122:56)
at CDPSession.emit (events.js:315:20)
at CDPSession.EventEmitter.emit (domain.js:485:12)
at CDPSession._onMessage (/app/.yarn/unplugged/puppeteer-npm-2.1.1-01d5fbb78f/node_modules/puppeteer/lib/Connection.js:200:12)
at Connection._onMessage (/app/.yarn/unplugged/puppeteer-npm-2.1.1-01d5fbb78f/node_modules/puppeteer/lib/Connection.js:112:17)
at WebSocket.<anonymous> (/app/.yarn/unplugged/puppeteer-npm-2.1.1-01d5fbb78f/node_modules/puppeteer/lib/WebSocketTransport.js:44:24)
at WebSocket.onMessage (/app/.yarn/cache/ws-npm-6.2.1-bbe0ef9859-2.zip/node_modules/ws/lib/event-target.js:120:16)
at WebSocket.emit (events.js:315:20)
at WebSocket.EventEmitter.emit (domain.js:485:12)
(node:35) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:35) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
{
ok: false,
error_code: 400,
description: 'Bad Request: group chat was upgraded to a supergroup chat',
parameters: { migrate_to_chat_id: -1001355004972 }
}
Ideally we would have a more strict type for params, you can just get it from the params type of the telegram api. See this comment for context: #29 (comment)
Originally posted by @gcangussu in #29
In order to get the most accurate odds from Pinnacle one must be signed in.
To do that, all that's required is that a session token is sent with the API requests to the proper host.
The host for signed in requests is
api.arcadia.pinnacle.com
instead of
guest.api.arcadia.pinnacle.com
The session token needs to be sent as a header like this:
X-Session: SESSION_TOKEN_HERE
One needs to go over the sign in process in order to get the session token. The token is available in the browser's local storage once logged in.
This is incorrect:
🍀 1.87%
🏦 Pinnacle + Marathon
🛒 Handicap de gols no jogo
________________________
🏦 Pinnacle
🛒 game score_handicap: away -1
⚖️ Odd: 2.33
💰Stake: 56.3%
🔗 https://www.pinnacle.com/pt/soccer/a/b/1116752525/
🏦 Marathon
🛒 game score_handicap: home 1
⚖️ Odd: 1.81
💰Stake: 43.7%
🔗 https://www.marathonbet.com/pt/betting/Football/Friendlies/Clubs/Vaster+vs+Virgo+1909+-+9264976
________________________
🎭 IF Vaster × IK Virgo
🗓 26/Mar 07:00
The correct stake is the inverse:
🏦 Pinnacle
⚖️ Odd: 2.33
💰Stake: 43.7%
🏦 Marathon
⚖️ Odd: 1.81
💰Stake: 56.3%
{
"_id" : "5e7a25d913e05cba2c3c9576/5e7b2ffda814e993d9b9b7ca",
"stakeables" : [
{
"stake" : 0.4640371229698375,
"_id" : ObjectId("5e7b2ffda814e993d9b9b7ca"),
"odd" : 2,
"market" : {
"key" : "game_score_handicap",
"type" : "spread",
"operation" : {
"operator" : "home",
"value" : -1
}
},
"house" : "pinnacle",
"sport" : "soccer",
"event" : {
"league" : "Club Friendlies",
"starts_at" : ISODate("2020-03-25T18:00:00Z"),
"participants" : {
"home" : "Oskarshamns Aik",
"away" : "Almeboda/Linneryd"
}
},
"extracted_at" : ISODate("2020-03-25T17:24:44.712Z"),
"url" : "https://www.pinnacle.com/pt/soccer/a/b/1116546389/"
},
{
"stake" : 0.5359628770301623,
"_id" : ObjectId("5e7a25d913e05cba2c3c9576"),
"odd" : 2.31,
"market" : {
"key" : "game_score_handicap",
"type" : "spread",
"operation" : {
"operator" : "away",
"value" : 1
}
},
"house" : "marathon",
"sport" : "soccer",
"event" : {
"league" : null,
"starts_at" : ISODate("2020-03-25T18:00:00Z"),
"participants" : {
"home" : "Oskarshamns AIK",
"away" : "Almeboda / Linneryd"
}
},
"extracted_at" : ISODate("2020-03-25T17:25:15.123Z"),
"url" : "https://www.marathonbet.com/pt/betting/Football/Friendlies/Clubs/Oskarshamns+AIK+vs+Almeboda%252FLinneryd+-+9259882"
}
],
"profit" : 0.07192575406032464,
"createdAt" : ISODate("2020-03-25T17:12:17.500Z"),
"updatedAt" : ISODate("2020-03-25T17:25:15.388Z")
}
Take some time to improve the formatting.
parse_mode
param as `Markdown' to support markdowndisable_web_page_preview
as true
1.5
, to display oddsReferences:
marathon_1 | 💾 Marathon soccer game_score_handicap (home -3.5 ⇢ 6.95) 25/03 06:00
marathon_1 | (node:17) UnhandledPromiseRejectionWarning: Error: Member 'Almeboda/Linneryd' in price.sn 'Almeboda/Linneryd (+3.5)' doesn't match any of the extracted members in {"home":"Oskarshamns AIK","away":"Almeboda / Linneryd"}
marathon_1 | at normalizeMarket (/usr/app/src/houses/marathon/index.ts:130:15)
marathon_1 | at normalizeBet (/usr/app/src/houses/marathon/index.ts:150:13)
marathon_1 | at retriveBetsAndUpdateDb (/usr/app/src/houses/marathon/index.ts:171:22)
marathon_1 | at runMicrotasks (<anonymous>)
marathon_1 | at processTicksAndRejections (internal/process/task_queues.js:97:5)
marathon_1 | at Manager.run (/usr/app/src/index.ts:23:19)
marathon_1 | at Manager.start (/usr/app/src/index.ts:15:5)
marathon_1 | (node:17) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
marathon_1 | (node:17) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
We do a check of the team name in order to map it to home or away. Sometimes the team names are different in the market title. This case it's lacking some spaces in the original
Some events get a very granular start time.
One example from Pinnacle:
💾 Pinnacle soccer game_score_handicap (home -0.5 ⇢ 1.86) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (away 0.5 ⇢ 1.97) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (home 0.5 ⇢ 1.2) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (away -0.5 ⇢ 4.36) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (home 0 ⇢ 1.34) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (away 0 ⇢ 3.22) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (home -0.25 ⇢ 1.6) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (away 0.25 ⇢ 2.35) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (home -0.75 ⇢ 2.16) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (away 0.75 ⇢ 1.7) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (home -1 ⇢ 2.78) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (away 1 ⇢ 1.44) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (home -1.5 ⇢ 3.73) 29/03 01:01
💾 Pinnacle soccer game_score_handicap (away 1.5 ⇢ 1.26) 29/03 01:01
It could be a good idea to round the times to the nearest quarter before comparing
The straight
endpoint response has the status
property. Sometimes it's "closed"
. We should be careful to only read things which status
is "open"
.
game_score_total
to something like Total de gols
game_score_handicap
to Handicap de gols
There's a bug in node 13.10 that causes this
nodejs/node#32110
Results from Marathon are more often than not paginated. This is the case for the events list and for the bets list.
Currently, we only get the first page for those requests. We need to get all the pages available.
Currently, the timezone for scraping is one that could be affected by Daylight Savings Time. So at least twice a year the dates scraped could have 1h difference and would hinder the bettables uncomparable.
We need to find a way to always get UTC datetimes.
According to the logs, it seems that the Marathon scrapper is 1h ahead of the Pinnacle scrapper. We can get hundreds of events from both of them but none matches.
This could be related to #34
poluga.com.br -> https://www.notion.so/poluga/Poluga-beta-065c628dd02e4e0080d699910eb21a5a
poluga.com.br/beta/convite -> https://www.notion.so/poluga/Poluga-beta-Canal-de-Oportunidades-e288a8aac30e4cc99d0a600696f128db
poluga.com.br/beta/manual -> https://www.notion.so/poluga/Poluga-beta-Manual-e-Ferramentas-074dc67ba0f94c55959ad018ba04b3c6
🍀 1.12%
🏦 Pinnacle + Marathon
🛒 Handicap de gols no jogo
________________________
🏦 Pinnacle
🛒 game score_handicap: away 0
⚖️ Odd: 3.24
💰Stake: 31.2%
🔗 https://www.pinnacle.com/pt/hockey/a/b/1116680455/
🏦 Marathon
🛒 game score_handicap: home 0
⚖️ Odd: 1.47
💰Stake: 68.8%
🔗 https://www.marathonbet.com/pt/betting/Ice+Hockey/Belarus/Extraleague/Play-Offs/Final/Yunost-Minsk+vs+Shakhtyor+Soligorsk+-+9263620
________________________
🎭 Yunost Minsk × Shakhtar Soligorsk
🗓 27/Mar 05:00
These:
🛒 game score_handicap: away 0
🛒 game score_handicap: home 0
didn't get the conversion.
Make sure to convert home/away to the team name
The health check should be done in a way to be easy to use in kubernetes health checks (livenessProbe).
Resources:
It's easier to understand what we're dealing with when we know which sport it is.
Error getting data Protocol error (Runtime.callFunctionOn): Target closed. Error: Protocol error (Runtime.callFunctionOn): Target closed.
at /usr/app/node_modules/puppeteer/lib/Connection.js:183:56
at new Promise (<anonymous>)
at CDPSession.send (/usr/app/node_modules/puppeteer/lib/Connection.js:182:12)
at ExecutionContext._evaluateInternal (/usr/app/node_modules/puppeteer/lib/ExecutionContext.js:107:44)
at ExecutionContext.evaluate (/usr/app/node_modules/puppeteer/lib/ExecutionContext.js:48:23)
at ExecutionContext.<anonymous> (/usr/app/node_modules/puppeteer/lib/helper.js:112:23)
at ElementHandle.evaluate (/usr/app/node_modules/puppeteer/lib/JSHandle.js:54:42)
at ElementHandle.<anonymous> (/usr/app/node_modules/puppeteer/lib/helper.js:112:23)
at FootballMatchPage.parseMembers (/usr/app/src/houses/marathon/pages/footballMatchPage.ts:71:32)
at runMicrotasks (<anonymous>)
-- ASYNC --
at ExecutionContext.<anonymous> (/usr/app/node_modules/puppeteer/lib/helper.js:111:15)
at ElementHandle.evaluate (/usr/app/node_modules/puppeteer/lib/JSHandle.js:54:42)
at ElementHandle.<anonymous> (/usr/app/node_modules/puppeteer/lib/helper.js:112:23)
at FootballMatchPage.parseMembers (/usr/app/src/houses/marathon/pages/footballMatchPage.ts:71:32)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at FootballMatchPage.parseContent (/usr/app/src/houses/marathon/pages/footballMatchPage.ts:103:21)
at FootballMatchPage.getData (/usr/app/src/houses/marathon/pages/basePage.ts:49:14)
at MarathonWebsite.retrieveBets (/usr/app/src/houses/marathon/index.ts:55:24)
at retriveBetsAndUpdateDb (/usr/app/src/houses/marathon/index.ts:171:20)
-- ASYNC --
at ElementHandle.<anonymous> (/usr/app/node_modules/puppeteer/lib/helper.js:111:15)
at FootballMatchPage.parseMembers (/usr/app/src/houses/marathon/pages/footballMatchPage.ts:71:32)
at runMicrotasks (<anonymous>)
at processTicksAndRejections (internal/process/task_queues.js:97:5)
at FootballMatchPage.parseContent (/usr/app/src/houses/marathon/pages/footballMatchPage.ts:103:21)
at FootballMatchPage.getData (/usr/app/src/houses/marathon/pages/basePage.ts:49:14)
at MarathonWebsite.retrieveBets (/usr/app/src/houses/marathon/index.ts:55:24)
at retriveBetsAndUpdateDb (/usr/app/src/houses/marathon/index.ts:171:20)
at Manager.run (/usr/app/src/index.ts:23:19)
Work on markets abstraction: https://docs.google.com/spreadsheets/d/1BmVTK7UUkYmI1x33Db8Fqhc37gnsPnDVnrY1HBUW8z0/edit#gid=709225428
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.