overtime-api 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- overtime_api-0.1.0/.github/workflows/tests.yml +27 -0
- overtime_api-0.1.0/.gitignore +9 -0
- overtime_api-0.1.0/LICENSE +21 -0
- overtime_api-0.1.0/PKG-INFO +277 -0
- overtime_api-0.1.0/README.md +250 -0
- overtime_api-0.1.0/examples/live_odds.py +45 -0
- overtime_api-0.1.0/examples/quickstart.py +35 -0
- overtime_api-0.1.0/examples/stream_markets.py +38 -0
- overtime_api-0.1.0/overtime/__init__.py +86 -0
- overtime_api-0.1.0/overtime/auth.py +56 -0
- overtime_api-0.1.0/overtime/client.py +356 -0
- overtime_api-0.1.0/overtime/endpoints/__init__.py +0 -0
- overtime_api-0.1.0/overtime/endpoints/games.py +90 -0
- overtime_api-0.1.0/overtime/endpoints/markets.py +167 -0
- overtime_api-0.1.0/overtime/endpoints/players.py +45 -0
- overtime_api-0.1.0/overtime/endpoints/quote.py +61 -0
- overtime_api-0.1.0/overtime/endpoints/reference.py +86 -0
- overtime_api-0.1.0/overtime/endpoints/users.py +46 -0
- overtime_api-0.1.0/overtime/enums.py +36 -0
- overtime_api-0.1.0/overtime/exceptions.py +34 -0
- overtime_api-0.1.0/overtime/helpers.py +61 -0
- overtime_api-0.1.0/overtime/models.py +199 -0
- overtime_api-0.1.0/overtime/session.py +153 -0
- overtime_api-0.1.0/pyproject.toml +43 -0
- overtime_api-0.1.0/tests/__init__.py +0 -0
- overtime_api-0.1.0/tests/test_auth.py +58 -0
- overtime_api-0.1.0/tests/test_client.py +148 -0
- overtime_api-0.1.0/tests/test_enums.py +17 -0
- overtime_api-0.1.0/tests/test_markets_parsing.py +126 -0
- overtime_api-0.1.0/tests/test_models.py +116 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: pip install -e ".[dev]"
|
|
25
|
+
|
|
26
|
+
- name: Run tests
|
|
27
|
+
run: pytest tests/ -v
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 overtime-api contributors
|
|
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.
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: overtime-api
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial Python client for the Overtime Markets API. Access sports betting odds, live scores, and market data without an API key.
|
|
5
|
+
Project-URL: Homepage, https://github.com/glowww/overtime-api
|
|
6
|
+
Project-URL: Documentation, https://github.com/glowww/overtime-api#readme
|
|
7
|
+
Project-URL: Issues, https://github.com/glowww/overtime-api/issues
|
|
8
|
+
Author: overtime-api contributors
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,crypto,odds,overtime,sports-betting,web3
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# overtime-api
|
|
29
|
+
|
|
30
|
+
Unofficial Python client for the [Overtime Markets](https://www.overtimemarkets.xyz/) API. Grab odds, live scores, and market data without an API key.
|
|
31
|
+
|
|
32
|
+
No API key required for most endpoints. For odds data, you need a [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WBGRP77j2drv) account to handle Cloudflare Turnstile captchas.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install overtime-api
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To access odds data (`get_markets`, `get_live_markets`), you need a [CapSolver API key](https://dashboard.capsolver.com/passport/register?inviteCode=WBGRP77j2drv) for automatic Turnstile captcha solving.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import asyncio
|
|
46
|
+
from overtime import OvertimeClient
|
|
47
|
+
|
|
48
|
+
async def main():
|
|
49
|
+
async with OvertimeClient() as client:
|
|
50
|
+
# No API key needed
|
|
51
|
+
sports = await client.get_sports()
|
|
52
|
+
scores = await client.get_live_scores()
|
|
53
|
+
games = await client.get_games_info()
|
|
54
|
+
players = await client.get_players_info()
|
|
55
|
+
market_types = await client.get_market_types()
|
|
56
|
+
|
|
57
|
+
asyncio.run(main())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Get odds data
|
|
61
|
+
|
|
62
|
+
The `/markets` and `/live-markets` endpoints are protected by Cloudflare Turnstile. You can either solve it automatically with [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WBGRP77j2drv), or set a session ID manually.
|
|
63
|
+
|
|
64
|
+
### Option 1: Automatic (recommended)
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from overtime import OvertimeClient
|
|
68
|
+
|
|
69
|
+
async with OvertimeClient(capsolver_api_key="CAI-...") as client:
|
|
70
|
+
# Turnstile is solved automatically behind the scenes
|
|
71
|
+
markets = await client.get_markets()
|
|
72
|
+
|
|
73
|
+
for market in markets.markets:
|
|
74
|
+
home = market.home_team
|
|
75
|
+
away = market.away_team
|
|
76
|
+
odds = market.result_odds.odds # implied probabilities
|
|
77
|
+
print(f"{home} vs {away}: {odds}")
|
|
78
|
+
|
|
79
|
+
# Live odds with scores
|
|
80
|
+
live = await client.get_live_markets()
|
|
81
|
+
for m in live:
|
|
82
|
+
print(f"{m.home_team} {m.home_score}-{m.away_score} {m.away_team}")
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Option 2: Manual session
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from overtime import OvertimeClient
|
|
89
|
+
|
|
90
|
+
async with OvertimeClient() as client:
|
|
91
|
+
client.set_session("your-session-id-here")
|
|
92
|
+
markets = await client.get_markets()
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Delta updates
|
|
96
|
+
|
|
97
|
+
Avoid re-fetching all markets every time. Pass the `response_hash` from a previous response to only get markets that changed:
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
result = await client.get_markets()
|
|
101
|
+
print(f"Full: {len(result.markets)} markets")
|
|
102
|
+
|
|
103
|
+
# Later, only get changes:
|
|
104
|
+
result2 = await client.get_markets(response_hash=result.response_hash)
|
|
105
|
+
print(f"Changed: {len(result2.markets)} markets")
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Proxy support
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# Simple proxy URL
|
|
112
|
+
async with OvertimeClient(proxy="http://user:pass@host:port") as client:
|
|
113
|
+
sports = await client.get_sports()
|
|
114
|
+
|
|
115
|
+
# SOCKS5
|
|
116
|
+
async with OvertimeClient(proxy="socks5://host:port") as client:
|
|
117
|
+
markets = await client.get_markets()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Bring your own HTTP client
|
|
121
|
+
|
|
122
|
+
If you need to configure TLS, connection pooling, or retries yourself, pass your own `httpx.AsyncClient`:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
import httpx
|
|
126
|
+
from overtime import OvertimeClient
|
|
127
|
+
|
|
128
|
+
http = httpx.AsyncClient(
|
|
129
|
+
proxy="socks5://host:port",
|
|
130
|
+
verify=False,
|
|
131
|
+
timeout=60,
|
|
132
|
+
limits=httpx.Limits(max_connections=10),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async with OvertimeClient(http_client=http) as client:
|
|
136
|
+
markets = await client.get_markets()
|
|
137
|
+
|
|
138
|
+
# You manage the lifecycle of your own client
|
|
139
|
+
await http.aclose()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Raw JSON responses
|
|
143
|
+
|
|
144
|
+
Every convenience method returns parsed dataclasses. If you want the raw JSON dict instead, use `client.request()`:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
async with OvertimeClient() as client:
|
|
148
|
+
# Raw dict, no parsing
|
|
149
|
+
raw = await client.request("GET", "/overtime-v2/sports")
|
|
150
|
+
|
|
151
|
+
# Also works for any undocumented endpoint
|
|
152
|
+
raw = await client.request("GET", "/some/other/endpoint?foo=bar")
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Use from any language (standalone headers)
|
|
156
|
+
|
|
157
|
+
You can also skip the client entirely and just generate the auth headers for use with curl, Node.js, Go, or whatever you prefer.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from overtime import make_auth_headers, build_request
|
|
161
|
+
|
|
162
|
+
# Option A: just the TOTP auth headers
|
|
163
|
+
headers = make_auth_headers("GET", "/overtime-v2/sports")
|
|
164
|
+
# Returns: {"x-client-totp": "...", "x-client-timestamp": "...", ...}
|
|
165
|
+
|
|
166
|
+
# Option B: full URL + all headers, ready to copy-paste
|
|
167
|
+
url, headers = build_request("GET", "/overtime-v2/sports")
|
|
168
|
+
# url = "https://api.overtime.io/overtime-v2/sports"
|
|
169
|
+
# headers = {user-agent, accept, origin, referer, totp, timestamp, ...}
|
|
170
|
+
|
|
171
|
+
# Use with curl:
|
|
172
|
+
import subprocess
|
|
173
|
+
header_args = []
|
|
174
|
+
for k, v in headers.items():
|
|
175
|
+
header_args.extend(["-H", f"{k}: {v}"])
|
|
176
|
+
subprocess.run(["curl", *header_args, url])
|
|
177
|
+
|
|
178
|
+
# Use with requests (sync):
|
|
179
|
+
import requests
|
|
180
|
+
response = requests.get(url, headers=headers)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Implementing TOTP in other languages
|
|
184
|
+
|
|
185
|
+
The auth algorithm is simple HMAC-SHA256. Here's the logic to port to any language:
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
TOTP_KEY = "BEST_DECENTRALIZED_ONCHAIN_SPORTSBOOK"
|
|
189
|
+
TOTPM_KEY = "A_RANDOM_SELECTION_OF_WORDS"
|
|
190
|
+
|
|
191
|
+
timestamp_seconds = floor(now())
|
|
192
|
+
timestamp_millis = floor(now() * 1000)
|
|
193
|
+
window = timestamp_seconds / 5 (integer division)
|
|
194
|
+
|
|
195
|
+
x-client-totp = HMAC-SHA256(TOTP_KEY, "C5S|{window}|{METHOD}|{path+query}")
|
|
196
|
+
x-client-timestamp = str(timestamp_seconds)
|
|
197
|
+
x-client-totpm = HMAC-SHA256(TOTPM_KEY, "C1MS|{timestamp_millis}|{METHOD}|{path+query}")
|
|
198
|
+
x-client-timestampm = str(timestamp_millis)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
The `{path+query}` is the full path including query string, e.g. `/overtime-v2/networks/42161/markets?status=open`.
|
|
202
|
+
|
|
203
|
+
## All endpoints
|
|
204
|
+
|
|
205
|
+
| Method | Session? | Description |
|
|
206
|
+
|--------|:--------:|-------------|
|
|
207
|
+
| `get_sports()` | | All sports and leagues |
|
|
208
|
+
| `get_market_types()` | | Market type definitions (spread, total, etc.) |
|
|
209
|
+
| `get_collaterals(network)` | | Supported tokens per chain |
|
|
210
|
+
| `get_games_info()` | | All games with status, teams, tournament |
|
|
211
|
+
| `get_game_info(game_id)` | | Single game info |
|
|
212
|
+
| `get_live_scores()` | | Live scores for all in-progress games |
|
|
213
|
+
| `get_live_score(game_id)` | | Live score for single game |
|
|
214
|
+
| `get_players_info()` | | All player name mappings |
|
|
215
|
+
| `get_player_info(player_id)` | | Single player info |
|
|
216
|
+
| `get_user_history(address)` | | Betting history for a wallet |
|
|
217
|
+
| `get_markets(network, response_hash)` | Yes | All prematch odds |
|
|
218
|
+
| `get_live_markets(network)` | Yes | All live/in-play odds |
|
|
219
|
+
| `get_quote(trade_data, buy_in, collateral)` | | Price quote for a bet |
|
|
220
|
+
| `request(method, endpoint)` | | Raw JSON for any endpoint |
|
|
221
|
+
|
|
222
|
+
## Networks
|
|
223
|
+
|
|
224
|
+
Overtime is deployed on multiple chains. Default is Arbitrum.
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
from overtime import OvertimeClient, Network
|
|
228
|
+
|
|
229
|
+
async with OvertimeClient(network=Network.OPTIMISM) as client:
|
|
230
|
+
markets = await client.get_markets()
|
|
231
|
+
|
|
232
|
+
# Or per-request:
|
|
233
|
+
markets = await client.get_markets(network=Network.BASE)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Available networks: `Network.ARBITRUM` (42161), `Network.OPTIMISM` (10), `Network.BASE` (8453).
|
|
237
|
+
|
|
238
|
+
## Understanding odds
|
|
239
|
+
|
|
240
|
+
Overtime returns odds as implied probabilities (values between 0 and 1). To convert:
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
implied_prob = 0.45 # from the API
|
|
244
|
+
|
|
245
|
+
# To decimal odds:
|
|
246
|
+
decimal_odds = 1 / implied_prob # 2.22
|
|
247
|
+
|
|
248
|
+
# To American odds:
|
|
249
|
+
if implied_prob >= 0.5:
|
|
250
|
+
american = -100 * implied_prob / (1 - implied_prob) # negative
|
|
251
|
+
else:
|
|
252
|
+
american = 100 * (1 - implied_prob) / implied_prob # positive
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Error handling
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
from overtime import OvertimeClient, AuthenticationError, SessionError, RateLimitError
|
|
259
|
+
|
|
260
|
+
async with OvertimeClient(capsolver_api_key="...") as client:
|
|
261
|
+
try:
|
|
262
|
+
markets = await client.get_markets()
|
|
263
|
+
except AuthenticationError:
|
|
264
|
+
print("TOTP keys may have changed. Update the package.")
|
|
265
|
+
except SessionError as e:
|
|
266
|
+
print(f"Captcha solving failed: {e}")
|
|
267
|
+
except RateLimitError:
|
|
268
|
+
print("Too many requests. Slow down.")
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Who built this
|
|
272
|
+
|
|
273
|
+
I'm the founder of [Bet Hero](https://www.betherosports.com), a sports betting analytics platform. We use Overtime's odds data in production alongside 400+ other sportsbooks. When they cut off API access, I open-sourced how it works.
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# overtime-api
|
|
2
|
+
|
|
3
|
+
Unofficial Python client for the [Overtime Markets](https://www.overtimemarkets.xyz/) API. Grab odds, live scores, and market data without an API key.
|
|
4
|
+
|
|
5
|
+
No API key required for most endpoints. For odds data, you need a [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WBGRP77j2drv) account to handle Cloudflare Turnstile captchas.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install overtime-api
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
To access odds data (`get_markets`, `get_live_markets`), you need a [CapSolver API key](https://dashboard.capsolver.com/passport/register?inviteCode=WBGRP77j2drv) for automatic Turnstile captcha solving.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import asyncio
|
|
19
|
+
from overtime import OvertimeClient
|
|
20
|
+
|
|
21
|
+
async def main():
|
|
22
|
+
async with OvertimeClient() as client:
|
|
23
|
+
# No API key needed
|
|
24
|
+
sports = await client.get_sports()
|
|
25
|
+
scores = await client.get_live_scores()
|
|
26
|
+
games = await client.get_games_info()
|
|
27
|
+
players = await client.get_players_info()
|
|
28
|
+
market_types = await client.get_market_types()
|
|
29
|
+
|
|
30
|
+
asyncio.run(main())
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Get odds data
|
|
34
|
+
|
|
35
|
+
The `/markets` and `/live-markets` endpoints are protected by Cloudflare Turnstile. You can either solve it automatically with [CapSolver](https://dashboard.capsolver.com/passport/register?inviteCode=WBGRP77j2drv), or set a session ID manually.
|
|
36
|
+
|
|
37
|
+
### Option 1: Automatic (recommended)
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from overtime import OvertimeClient
|
|
41
|
+
|
|
42
|
+
async with OvertimeClient(capsolver_api_key="CAI-...") as client:
|
|
43
|
+
# Turnstile is solved automatically behind the scenes
|
|
44
|
+
markets = await client.get_markets()
|
|
45
|
+
|
|
46
|
+
for market in markets.markets:
|
|
47
|
+
home = market.home_team
|
|
48
|
+
away = market.away_team
|
|
49
|
+
odds = market.result_odds.odds # implied probabilities
|
|
50
|
+
print(f"{home} vs {away}: {odds}")
|
|
51
|
+
|
|
52
|
+
# Live odds with scores
|
|
53
|
+
live = await client.get_live_markets()
|
|
54
|
+
for m in live:
|
|
55
|
+
print(f"{m.home_team} {m.home_score}-{m.away_score} {m.away_team}")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Option 2: Manual session
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from overtime import OvertimeClient
|
|
62
|
+
|
|
63
|
+
async with OvertimeClient() as client:
|
|
64
|
+
client.set_session("your-session-id-here")
|
|
65
|
+
markets = await client.get_markets()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Delta updates
|
|
69
|
+
|
|
70
|
+
Avoid re-fetching all markets every time. Pass the `response_hash` from a previous response to only get markets that changed:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
result = await client.get_markets()
|
|
74
|
+
print(f"Full: {len(result.markets)} markets")
|
|
75
|
+
|
|
76
|
+
# Later, only get changes:
|
|
77
|
+
result2 = await client.get_markets(response_hash=result.response_hash)
|
|
78
|
+
print(f"Changed: {len(result2.markets)} markets")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Proxy support
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# Simple proxy URL
|
|
85
|
+
async with OvertimeClient(proxy="http://user:pass@host:port") as client:
|
|
86
|
+
sports = await client.get_sports()
|
|
87
|
+
|
|
88
|
+
# SOCKS5
|
|
89
|
+
async with OvertimeClient(proxy="socks5://host:port") as client:
|
|
90
|
+
markets = await client.get_markets()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Bring your own HTTP client
|
|
94
|
+
|
|
95
|
+
If you need to configure TLS, connection pooling, or retries yourself, pass your own `httpx.AsyncClient`:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import httpx
|
|
99
|
+
from overtime import OvertimeClient
|
|
100
|
+
|
|
101
|
+
http = httpx.AsyncClient(
|
|
102
|
+
proxy="socks5://host:port",
|
|
103
|
+
verify=False,
|
|
104
|
+
timeout=60,
|
|
105
|
+
limits=httpx.Limits(max_connections=10),
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
async with OvertimeClient(http_client=http) as client:
|
|
109
|
+
markets = await client.get_markets()
|
|
110
|
+
|
|
111
|
+
# You manage the lifecycle of your own client
|
|
112
|
+
await http.aclose()
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Raw JSON responses
|
|
116
|
+
|
|
117
|
+
Every convenience method returns parsed dataclasses. If you want the raw JSON dict instead, use `client.request()`:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
async with OvertimeClient() as client:
|
|
121
|
+
# Raw dict, no parsing
|
|
122
|
+
raw = await client.request("GET", "/overtime-v2/sports")
|
|
123
|
+
|
|
124
|
+
# Also works for any undocumented endpoint
|
|
125
|
+
raw = await client.request("GET", "/some/other/endpoint?foo=bar")
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Use from any language (standalone headers)
|
|
129
|
+
|
|
130
|
+
You can also skip the client entirely and just generate the auth headers for use with curl, Node.js, Go, or whatever you prefer.
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from overtime import make_auth_headers, build_request
|
|
134
|
+
|
|
135
|
+
# Option A: just the TOTP auth headers
|
|
136
|
+
headers = make_auth_headers("GET", "/overtime-v2/sports")
|
|
137
|
+
# Returns: {"x-client-totp": "...", "x-client-timestamp": "...", ...}
|
|
138
|
+
|
|
139
|
+
# Option B: full URL + all headers, ready to copy-paste
|
|
140
|
+
url, headers = build_request("GET", "/overtime-v2/sports")
|
|
141
|
+
# url = "https://api.overtime.io/overtime-v2/sports"
|
|
142
|
+
# headers = {user-agent, accept, origin, referer, totp, timestamp, ...}
|
|
143
|
+
|
|
144
|
+
# Use with curl:
|
|
145
|
+
import subprocess
|
|
146
|
+
header_args = []
|
|
147
|
+
for k, v in headers.items():
|
|
148
|
+
header_args.extend(["-H", f"{k}: {v}"])
|
|
149
|
+
subprocess.run(["curl", *header_args, url])
|
|
150
|
+
|
|
151
|
+
# Use with requests (sync):
|
|
152
|
+
import requests
|
|
153
|
+
response = requests.get(url, headers=headers)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Implementing TOTP in other languages
|
|
157
|
+
|
|
158
|
+
The auth algorithm is simple HMAC-SHA256. Here's the logic to port to any language:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
TOTP_KEY = "BEST_DECENTRALIZED_ONCHAIN_SPORTSBOOK"
|
|
162
|
+
TOTPM_KEY = "A_RANDOM_SELECTION_OF_WORDS"
|
|
163
|
+
|
|
164
|
+
timestamp_seconds = floor(now())
|
|
165
|
+
timestamp_millis = floor(now() * 1000)
|
|
166
|
+
window = timestamp_seconds / 5 (integer division)
|
|
167
|
+
|
|
168
|
+
x-client-totp = HMAC-SHA256(TOTP_KEY, "C5S|{window}|{METHOD}|{path+query}")
|
|
169
|
+
x-client-timestamp = str(timestamp_seconds)
|
|
170
|
+
x-client-totpm = HMAC-SHA256(TOTPM_KEY, "C1MS|{timestamp_millis}|{METHOD}|{path+query}")
|
|
171
|
+
x-client-timestampm = str(timestamp_millis)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
The `{path+query}` is the full path including query string, e.g. `/overtime-v2/networks/42161/markets?status=open`.
|
|
175
|
+
|
|
176
|
+
## All endpoints
|
|
177
|
+
|
|
178
|
+
| Method | Session? | Description |
|
|
179
|
+
|--------|:--------:|-------------|
|
|
180
|
+
| `get_sports()` | | All sports and leagues |
|
|
181
|
+
| `get_market_types()` | | Market type definitions (spread, total, etc.) |
|
|
182
|
+
| `get_collaterals(network)` | | Supported tokens per chain |
|
|
183
|
+
| `get_games_info()` | | All games with status, teams, tournament |
|
|
184
|
+
| `get_game_info(game_id)` | | Single game info |
|
|
185
|
+
| `get_live_scores()` | | Live scores for all in-progress games |
|
|
186
|
+
| `get_live_score(game_id)` | | Live score for single game |
|
|
187
|
+
| `get_players_info()` | | All player name mappings |
|
|
188
|
+
| `get_player_info(player_id)` | | Single player info |
|
|
189
|
+
| `get_user_history(address)` | | Betting history for a wallet |
|
|
190
|
+
| `get_markets(network, response_hash)` | Yes | All prematch odds |
|
|
191
|
+
| `get_live_markets(network)` | Yes | All live/in-play odds |
|
|
192
|
+
| `get_quote(trade_data, buy_in, collateral)` | | Price quote for a bet |
|
|
193
|
+
| `request(method, endpoint)` | | Raw JSON for any endpoint |
|
|
194
|
+
|
|
195
|
+
## Networks
|
|
196
|
+
|
|
197
|
+
Overtime is deployed on multiple chains. Default is Arbitrum.
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from overtime import OvertimeClient, Network
|
|
201
|
+
|
|
202
|
+
async with OvertimeClient(network=Network.OPTIMISM) as client:
|
|
203
|
+
markets = await client.get_markets()
|
|
204
|
+
|
|
205
|
+
# Or per-request:
|
|
206
|
+
markets = await client.get_markets(network=Network.BASE)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Available networks: `Network.ARBITRUM` (42161), `Network.OPTIMISM` (10), `Network.BASE` (8453).
|
|
210
|
+
|
|
211
|
+
## Understanding odds
|
|
212
|
+
|
|
213
|
+
Overtime returns odds as implied probabilities (values between 0 and 1). To convert:
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
implied_prob = 0.45 # from the API
|
|
217
|
+
|
|
218
|
+
# To decimal odds:
|
|
219
|
+
decimal_odds = 1 / implied_prob # 2.22
|
|
220
|
+
|
|
221
|
+
# To American odds:
|
|
222
|
+
if implied_prob >= 0.5:
|
|
223
|
+
american = -100 * implied_prob / (1 - implied_prob) # negative
|
|
224
|
+
else:
|
|
225
|
+
american = 100 * (1 - implied_prob) / implied_prob # positive
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Error handling
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from overtime import OvertimeClient, AuthenticationError, SessionError, RateLimitError
|
|
232
|
+
|
|
233
|
+
async with OvertimeClient(capsolver_api_key="...") as client:
|
|
234
|
+
try:
|
|
235
|
+
markets = await client.get_markets()
|
|
236
|
+
except AuthenticationError:
|
|
237
|
+
print("TOTP keys may have changed. Update the package.")
|
|
238
|
+
except SessionError as e:
|
|
239
|
+
print(f"Captcha solving failed: {e}")
|
|
240
|
+
except RateLimitError:
|
|
241
|
+
print("Too many requests. Slow down.")
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Who built this
|
|
245
|
+
|
|
246
|
+
I'm the founder of [Bet Hero](https://www.betherosports.com), a sports betting analytics platform. We use Overtime's odds data in production alongside 400+ other sportsbooks. When they cut off API access, I open-sourced how it works.
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Fetch live and prematch odds (requires capsolver API key)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
from overtime import OvertimeClient
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def main():
|
|
9
|
+
api_key = os.environ.get("CAPSOLVER_API_KEY")
|
|
10
|
+
if not api_key:
|
|
11
|
+
print("Set CAPSOLVER_API_KEY environment variable to use this example.")
|
|
12
|
+
print(" export CAPSOLVER_API_KEY=CAI-...")
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
async with OvertimeClient(capsolver_api_key=api_key) as client:
|
|
16
|
+
|
|
17
|
+
# Fetch all open prematch markets
|
|
18
|
+
result = await client.get_markets()
|
|
19
|
+
print(f"Prematch markets: {len(result.markets)}")
|
|
20
|
+
print(f"Response hash (for delta updates): {result.response_hash[:20]}...")
|
|
21
|
+
|
|
22
|
+
for market in result.markets[:10]:
|
|
23
|
+
odds_str = " / ".join(f"{o:.4f}" for o in market.result_odds.odds)
|
|
24
|
+
print(f" {market.home_team} vs {market.away_team}: {odds_str}")
|
|
25
|
+
print(f" Lines available: {len(market.child_markets)}")
|
|
26
|
+
|
|
27
|
+
# Fetch live markets
|
|
28
|
+
live = await client.get_live_markets()
|
|
29
|
+
print(f"\nLive markets: {len(live)}")
|
|
30
|
+
|
|
31
|
+
for market in live[:5]:
|
|
32
|
+
odds_str = " / ".join(f"{o:.4f}" for o in market.odds)
|
|
33
|
+
print(f" {market.home_team} vs {market.away_team}: {odds_str}")
|
|
34
|
+
print(f" Score: {market.home_score}-{market.away_score}")
|
|
35
|
+
print(f" Lines available: {len(market.child_markets)}")
|
|
36
|
+
|
|
37
|
+
# Delta update: pass previous hash to only get changed markets
|
|
38
|
+
result2 = await client.get_markets(response_hash=result.response_hash)
|
|
39
|
+
if result2.markets:
|
|
40
|
+
print(f"\nDelta update: {len(result2.markets)} markets changed")
|
|
41
|
+
else:
|
|
42
|
+
print("\nDelta update: no changes since last fetch")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Quick start: fetch sports, scores, and game info (no API key needed)."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from overtime import OvertimeClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def main():
|
|
8
|
+
async with OvertimeClient() as client:
|
|
9
|
+
|
|
10
|
+
# Get all sports and leagues
|
|
11
|
+
sports = await client.get_sports()
|
|
12
|
+
print(f"Available sports: {len(sports)}")
|
|
13
|
+
for sport in sports[:5]:
|
|
14
|
+
print(f" {sport.name}: {len(sport.leagues)} leagues")
|
|
15
|
+
|
|
16
|
+
# Get market type definitions
|
|
17
|
+
market_types = await client.get_market_types()
|
|
18
|
+
print(f"\nMarket types: {len(market_types)}")
|
|
19
|
+
for mt in market_types[:5]:
|
|
20
|
+
print(f" [{mt.id}] {mt.name}: {mt.description}")
|
|
21
|
+
|
|
22
|
+
# Get live scores
|
|
23
|
+
scores = await client.get_live_scores()
|
|
24
|
+
print(f"\nLive games: {len(scores)}")
|
|
25
|
+
for score in scores[:5]:
|
|
26
|
+
print(f" {score.game_id[:20]}... {score.home_score}-{score.away_score} ({score.status})")
|
|
27
|
+
|
|
28
|
+
# Get all game info
|
|
29
|
+
games = await client.get_games_info()
|
|
30
|
+
print(f"\nAll games: {len(games)}")
|
|
31
|
+
for game in games[:5]:
|
|
32
|
+
print(f" {game.home_team} vs {game.away_team} ({game.sport})")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
asyncio.run(main())
|