├── ClosingLinesAnyMarket.gs
├── ClosingLinesFeaturedMarkets.gs
├── HistoricalEventOdds.gs
├── HistoricalOdds.gs
├── Odds.gs
├── OddsLoop.gs
├── OddsMultipleSports.gs
├── PlayerProps.gs
├── README.md
├── Scores.gs
├── ScoresLoop.gs
└── screenshots
├── add_button_trigger.jpg
├── add_time_trigger.jpg
├── assign_script.jpg
├── data_output.jpg
├── output_params.jpg
├── rename_project.jpg
├── run_apps_script.jpg
└── start_apps_script.jpg
/ClosingLinesAnyMarket.gs:
--------------------------------------------------------------------------------
1 | function getClosingLinesEvent() {
2 | /**
3 | * Query historical closing lines from the The Odds API and output the response to Google Sheets.
4 | *
5 | * This script will work with any market. If you only need featured markets (h2h, spreads, totals), it is more cost-effective use ClosingLinesFeaturedMarkets.gs
6 | *
7 | * This script works by first finding the commence times of each game within the specified time range, accounting for possible delays. This makes use of the historical events endpoint.
8 | *
9 | * Once a list of each event id and the final commence time is found, odds are queried for each event, using the event's commence time as the timestamp.
10 | *
11 | * Historical data is only available for paid subscriptions.
12 | *
13 | * The usage quota cost of each historical timestamp query is calculated as: 10 x [number of markets] x [number of regions]
14 | * More info: https://the-odds-api.com/liveapi/guides/v4/
15 | *
16 | * Depending on the specified FROM_DATE, TO_DATE and INTERVAL_MINS, the volume of data can be large. Google Sheets has a limit of 10 million cells.
17 | *
18 | * This script will currently run for a maximum of 6 minutes at a time [see Apps Script service quotas](https://developers.google.com/apps-script/guides/services/quotas#current_limitations)
19 | * If the timeout is reached, you may need to trigger this script multiple times for smaller time ranges.
20 | *
21 | * If the spreadsheet has existing data, newly queried data will be appended to the next available row.
22 | *
23 | */
24 |
25 | const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/123' // Get this from your browser
26 | const SHEET_NAME = 'Sheet1' // The name of the spreadsheet tab. Note all data in this sheet will be cleared with each request
27 | const API_KEY = 'YOUR_API_KEY' // Get an API key from https://the-odds-api.com/#get-access
28 |
29 | const SPORT_KEY = 'baseball_mlb' // For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
30 | const MARKETS = 'batter_home_runs' // Comma separated list of betting markets. Valid values are h2h, spreads & totals
31 | const REGIONS = 'us' // Comma separated list of bookmaker regions. Valid values are us, us2, uk, eu and au
32 | const BOOKMAKERS = '' // Optional - if specified, it overrides REGIONS. A list of comma separated bookmakers from any region. For example: draftkings,pinnacle See all bookmakers at https://the-odds-api.com/sports-odds-data/bookmaker-apis.html
33 | const ODDS_FORMAT = 'american' // Valid values are american and decimal.
34 |
35 | const FROM_DATE = '2024-04-03T00:00:00Z'
36 | const TO_DATE = '2024-04-04T00:00:00Z'
37 | const INTERVAL_MINS = 60*24 // The interval between historical snapshots (this number should be 5 or more)
38 |
39 | const headers = [
40 | 'timestamp',
41 | 'id',
42 | 'commence_time',
43 | 'bookmaker',
44 | 'last_update',
45 | 'home_team',
46 | 'away_team',
47 | 'market',
48 | 'name',
49 | 'description',
50 | 'price',
51 | 'point',
52 | ]
53 |
54 | const ws = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
55 |
56 | let eventCommenceTimes = {}
57 | let eventsResponse, formattedTimestamp
58 | let currentUnixTimestamp = new Date(FROM_DATE).getTime()
59 | while (currentUnixTimestamp <= (new Date(TO_DATE)).getTime()) {
60 | formattedTimestamp = Utilities.formatDate(new Date(currentUnixTimestamp), 'UTC', "yyyy-MM-dd'T'HH:mm:ss'Z'")
61 | Logger.log(`Gathering games ${formattedTimestamp}`)
62 | eventsResponse = fetchEvents(API_KEY, SPORT_KEY, formattedTimestamp)
63 | eventCommenceTimes = {...eventCommenceTimes,...extractCommenceTimes(eventsResponse.responseContent.data, FROM_DATE, TO_DATE)}
64 | currentUnixTimestamp = Math.max(currentUnixTimestamp + (INTERVAL_MINS * 60 * 1000), (new Date(eventsResponse.responseContent.next_timestamp)).getTime())
65 | }
66 |
67 | // Refine commence times, in case of event delays
68 | let currentCommenceTime = Object.values(eventCommenceTimes)[0]
69 | while (currentCommenceTime !== null) {
70 | Logger.log(`Refining commence times ${currentCommenceTime}`)
71 | eventsResponse = fetchEvents(API_KEY, SPORT_KEY, currentCommenceTime)
72 | eventCommenceTimes = {...eventCommenceTimes,...extractCommenceTimes(eventsResponse.responseContent.data, FROM_DATE, TO_DATE)}
73 | currentCommenceTime = getNextCommenceTime(Object.values(eventCommenceTimes), currentCommenceTime)
74 | }
75 |
76 | // Group eventIds by commence time
77 | const groupedEvents = {}
78 | for (const commenceTime of new Set(Object.values(eventCommenceTimes))) {
79 | groupedEvents[commenceTime] = getKeyByValue(eventCommenceTimes, commenceTime)
80 | }
81 |
82 | let output_row = ws.getLastRow() + 1
83 | if (output_row === 1) {
84 | // Output headers
85 | ws.getRange(3, 1, 1, headers.length).setValues([headers])
86 |
87 | // 1st 2 rows are for meta data, headers are on the 3rd row
88 | output_row = 4
89 | }
90 |
91 | // iterate on commence time keys, query on t, filter events, output all
92 | let oddsResponse
93 | let formattedResponse
94 | for (const commenceTime in groupedEvents) {
95 | Logger.log(`Querying closing lines ${commenceTime}, ${groupedEvents[commenceTime]}`)
96 | for (const eventId of groupedEvents[commenceTime]) {
97 | oddsResponse = fetchEventOdds(API_KEY, SPORT_KEY, REGIONS, BOOKMAKERS, MARKETS, ODDS_FORMAT, commenceTime, eventId)
98 | if (oddsResponse.responseCode === 404) {
99 | Logger.log(`event id ${eventId} at ${commenceTime} was not found`)
100 | continue
101 | }
102 | formattedResponse = formatEventOutput(oddsResponse.responseContent)
103 | if (formattedResponse.length > 0) {
104 | ws.getRange(output_row, 1, formattedResponse.length, formattedResponse[0].length).setValues(formattedResponse)
105 | SpreadsheetApp.flush()
106 | }
107 | output_row = output_row + formattedResponse.length
108 | }
109 | }
110 | }
111 |
112 | function fetchEvents(apiKey, sportKey, timestamp) {
113 | const url = `https://api.the-odds-api.com/v4/historical/sports/${sportKey}/events?apiKey=${apiKey}&date=${timestamp}`
114 |
115 | const response = UrlFetchApp.fetch(url, {
116 | headers: {
117 | 'content-type': 'application/json'
118 | },
119 | })
120 |
121 | return {
122 | metaData: formatResponseMetaData(response.getHeaders()),
123 | responseContent: JSON.parse(response.getContentText()),
124 | }
125 | }
126 |
127 | function fetchEventOdds(apiKey, sportKey, regions, bookmakers, markets, oddsFormat, timestamp, eventId) {
128 | const bookmakersParam = bookmakers ? `bookmakers=${bookmakers}` : `regions=${regions}`
129 |
130 | const url = `https://api.the-odds-api.com/v4/historical/sports/${sportKey}/events/${eventId}/odds?apiKey=${apiKey}&${bookmakersParam}&markets=${markets}&oddsFormat=${oddsFormat}&date=${timestamp}`
131 |
132 | const response = UrlFetchApp.fetch(url, {
133 | headers: {
134 | 'content-type': 'application/json'
135 | },
136 | muteHttpExceptions: true,
137 | })
138 |
139 | return {
140 | responseCode: response.getResponseCode(),
141 | metaData: formatResponseMetaData(response.getHeaders()),
142 | responseContent: JSON.parse(response.getContentText()),
143 | }
144 | }
145 |
146 | function formatEventOutput(response) {
147 | /**
148 | * Restructure the JSON response into a 2D array, suitable for outputting to a spreadsheet
149 | */
150 | const rows = []
151 | const event = response.data
152 | for (const bookmaker of event.bookmakers) {
153 | for (const market of bookmaker.markets) {
154 | for (const outcome of market.outcomes) {
155 | rows.push([
156 | response.timestamp,
157 | event.id,
158 | event.commence_time,
159 | bookmaker.key,
160 | bookmaker.last_update,
161 | event.home_team,
162 | event.away_team,
163 | market.key,
164 | outcome.name,
165 | outcome?.description,
166 | outcome.price,
167 | outcome?.point,
168 | ])
169 | }
170 |
171 | }
172 | }
173 |
174 |
175 | return rows
176 | }
177 |
178 | function formatResponseMetaData(headers) {
179 | return [
180 | ['Requests Used', headers['x-requests-used']],
181 | ['Requests Remaining', headers['x-requests-remaining']],
182 | ]
183 | }
184 |
185 | function extractCommenceTimes(events, fromDate, toDate) {
186 | const eventCommenceTimes = {}
187 | events.forEach(event => {
188 | if (event.commence_time < fromDate || event.commence_time > toDate) {
189 | // Event start time falls outside the requested range
190 | return true
191 | }
192 | eventCommenceTimes[event.id] = event.commence_time
193 | })
194 | return eventCommenceTimes
195 | }
196 |
197 | function getNextCommenceTime(commenceTimes, currentCommenceTime) {
198 | // assumes commenceTimes ordered asc
199 | for (const commenceTime of commenceTimes) {
200 | if (commenceTime <= currentCommenceTime) {
201 | continue
202 | }
203 | return commenceTime
204 | }
205 | return null
206 | }
207 |
208 | function getKeyByValue(d, value) {
209 | return Object.keys(d).filter((key) => d[key] === value)
210 | }
--------------------------------------------------------------------------------
/ClosingLinesFeaturedMarkets.gs:
--------------------------------------------------------------------------------
1 | function getClosingLines() {
2 | /**
3 | * Query historical closing lines from the The Odds API and output the data to Google Sheets.
4 | *
5 | * This script will only work with featured markets (h2h, spreads, totals). To query any market, see ClosingLinesAnyMarket.gs
6 | *
7 | * Historical data is only available for paid subscriptions.
8 | *
9 | * The usage quota cost of each historical timestamp query is calculated as: 10 x [number of markets] x [number of regions]
10 | * More info: https://the-odds-api.com/liveapi/guides/v4/
11 | *
12 | * Depending on the specified FROM_DATE, TO_DATE and INTERVAL_MINS, the volume of data can be large. Google Sheets has a limit of 10 million cells.
13 | *
14 | * This script will currently run for a maximum of 6 minutes at a time [see Apps Script service quotas](https://developers.google.com/apps-script/guides/services/quotas#current_limitations)
15 | * If the timeout is reached, you may need to trigger this script multiple times for smaller time ranges.
16 | *
17 | * If the spreadsheet has existing data, newly queried data will be appended to the next available row.
18 | *
19 | * Note this code does not handle futures (outrights) markets.
20 | */
21 |
22 | const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/abc123/edit#gid=0' // Get this from your browser
23 | const SHEET_NAME = 'Sheet1' // The name of the spreadsheet tab. Note all data in this sheet will be cleared with each request
24 | const API_KEY = 'YOUR_API_KEY' // Get an API key from https://the-odds-api.com/#get-access
25 |
26 | const SPORT_KEY = 'icehockey_nhl' // For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
27 | const MARKETS = 'h2h' // Comma separated list of betting markets. Valid values are h2h, spreads & totals
28 | const REGIONS = 'us' // Comma separated list of bookmaker regions. Valid values are us, uk, eu and au
29 | const BOOKMAKERS = 'betonlineag' // Optional - if specified, it overrides REGIONS. A list of comma separated bookmakers from any region. For example: draftkings,pinnacle See all bookmakers at https://the-odds-api.com/sports-odds-data/bookmaker-apis.html
30 | const ODDS_FORMAT = 'american' // Valid values are american and decimal.
31 |
32 | const FROM_DATE = '2023-12-31T00:00:00Z'
33 | const TO_DATE = '2024-01-01T00:00:00Z'
34 | const INTERVAL_MINS = 60 * 24 // The interval between historical snapshots (this number should be 5 or more)
35 |
36 | const headers = [
37 | 'timestamp',
38 | 'id',
39 | 'commence_time',
40 | 'bookmaker',
41 | 'last_update',
42 | 'home_team',
43 | 'away_team',
44 | 'market',
45 | 'label_1',
46 | 'odd_1',
47 | 'point_1',
48 | 'label_2',
49 | 'odd_2',
50 | 'point_2',
51 | 'odd_draw',
52 | ]
53 |
54 | const ws = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
55 |
56 | let eventCommenceTimes = {}
57 | let eventsResponse, formattedTimestamp
58 | let currentUnixTimestamp = new Date(FROM_DATE).getTime()
59 | while (currentUnixTimestamp <= (new Date(TO_DATE)).getTime()) {
60 | formattedTimestamp = Utilities.formatDate(new Date(currentUnixTimestamp), 'UTC', "yyyy-MM-dd'T'HH:mm:ss'Z'")
61 | Logger.log(`Gathering games ${formattedTimestamp}`)
62 | eventsResponse = fetchEvents(API_KEY, SPORT_KEY, formattedTimestamp)
63 | eventCommenceTimes = {...eventCommenceTimes,...extractCommenceTimes(eventsResponse.responseContent.data, FROM_DATE, TO_DATE)}
64 | currentUnixTimestamp = Math.max(currentUnixTimestamp + (INTERVAL_MINS * 60 * 1000), (new Date(eventsResponse.responseContent.next_timestamp)).getTime())
65 | }
66 |
67 | // Refine commence times, in case of event delays
68 | let currentCommenceTime = Object.values(eventCommenceTimes)[0]
69 | while (currentCommenceTime !== null) {
70 | Logger.log(`Refining commence times ${currentCommenceTime}`)
71 | eventsResponse = fetchEvents(API_KEY, SPORT_KEY, currentCommenceTime)
72 | eventCommenceTimes = {...eventCommenceTimes,...extractCommenceTimes(eventsResponse.responseContent.data, FROM_DATE, TO_DATE)}
73 | currentCommenceTime = getNextCommenceTime(Object.values(eventCommenceTimes), currentCommenceTime)
74 | }
75 |
76 | // Group eventIds by commence time
77 | const groupedEvents = {}
78 | for (const commenceTime of new Set(Object.values(eventCommenceTimes))) {
79 | groupedEvents[commenceTime] = getKeyByValue(eventCommenceTimes, commenceTime)
80 | }
81 |
82 | let output_row = ws.getLastRow() + 1
83 | if (output_row === 1) {
84 | // Output headers
85 | ws.getRange(3, 1, 1, headers.length).setValues([headers])
86 |
87 | // 1st 2 rows are for meta data, headers are on the 3rd row
88 | output_row = 4
89 | }
90 |
91 | // iterate on commence time keys, query on t, filter events, output all
92 | let oddsResponse
93 | let formattedResponse
94 | for (const commenceTime in groupedEvents) {
95 | Logger.log(`Querying closing lines ${commenceTime}, ${groupedEvents[commenceTime]}`)
96 | oddsResponse = fetchOdds(API_KEY, SPORT_KEY, REGIONS, BOOKMAKERS, MARKETS, ODDS_FORMAT, commenceTime, groupedEvents[commenceTime])
97 | formattedResponse = formatEventOutput(oddsResponse.responseContent)
98 | if (formattedResponse.length > 0) {
99 | ws.getRange(output_row, 1, formattedResponse.length, formattedResponse[0].length).setValues(formattedResponse)
100 | SpreadsheetApp.flush()
101 | }
102 | output_row = output_row + formattedResponse.length
103 | }
104 | }
105 |
106 | function fetchEvents(apiKey, sportKey, timestamp) {
107 | const url = `https://api.the-odds-api.com/v4/historical/sports/${sportKey}/events?apiKey=${apiKey}&date=${timestamp}`
108 |
109 | const response = UrlFetchApp.fetch(url, {
110 | headers: {
111 | 'content-type': 'application/json'
112 | },
113 | })
114 |
115 | return {
116 | metaData: formatResponseMetaData(response.getHeaders()),
117 | responseContent: JSON.parse(response.getContentText()),
118 | }
119 | }
120 |
121 | function fetchOdds(apiKey, sportKey, regions, bookmakers, markets, oddsFormat, timestamp, eventIds) {
122 | const bookmakersParam = bookmakers ? `bookmakers=${bookmakers}` : `regions=${regions}`
123 | const eventIdsParam = eventIds ? `&eventIds=${eventIds.join(',')}` : ''
124 | const url = `https://api.the-odds-api.com/v4/historical/sports/${sportKey}/odds?apiKey=${apiKey}&${bookmakersParam}&markets=${markets}&oddsFormat=${oddsFormat}&date=${timestamp}${eventIdsParam}`
125 |
126 | const response = UrlFetchApp.fetch(url, {
127 | headers: {
128 | 'content-type': 'application/json'
129 | },
130 | })
131 |
132 | return {
133 | metaData: formatResponseMetaData(response.getHeaders()),
134 | responseContent: JSON.parse(response.getContentText()),
135 | }
136 | }
137 |
138 | function formatEventOutput(response) {
139 | /**
140 | * Restructure the JSON response into a 2D array, suitable for outputting to a spreadsheet
141 | */
142 | const rows = []
143 | let outcome_home
144 | let outcome_away
145 | let outcome_draw
146 | for (const event of response.data) {
147 | for (const bookmaker of event.bookmakers) {
148 | for (const market of bookmaker.markets) {
149 | if (market.key === 'totals') {
150 | outcome_home = market.outcomes.filter(outcome => outcome.name === 'Over')[0]
151 | outcome_away = market.outcomes.filter(outcome => outcome.name === 'Under')[0]
152 | outcome_draw = {}
153 | } else {
154 | outcome_home = market.outcomes.filter(outcome => outcome.name === event.home_team)[0]
155 | outcome_away = market.outcomes.filter(outcome => outcome.name === event.away_team)[0]
156 | outcome_draw = market.outcomes.filter(outcome => outcome.name === 'Draw')[0] ?? {}
157 | }
158 |
159 | rows.push([
160 | response.timestamp,
161 | event.id,
162 | event.commence_time,
163 | bookmaker.key,
164 | bookmaker.last_update,
165 | event.home_team,
166 | event.away_team,
167 | market.key,
168 | outcome_home.name,
169 | outcome_home.price,
170 | outcome_home?.point,
171 | outcome_away.name,
172 | outcome_away.price,
173 | outcome_away?.point,
174 | outcome_draw?.price,
175 | ])
176 | }
177 | }
178 | }
179 |
180 | return rows
181 | }
182 |
183 | function formatResponseMetaData(headers) {
184 | return [
185 | ['Requests Used', headers['x-requests-used']],
186 | ['Requests Remaining', headers['x-requests-remaining']],
187 | ]
188 | }
189 |
190 | function extractCommenceTimes(events, fromDate, toDate) {
191 | const eventCommenceTimes = {}
192 | events.forEach(event => {
193 | if (event.commence_time < fromDate || event.commence_time > toDate) {
194 | // Event start time falls outside the requested range
195 | return true
196 | }
197 | eventCommenceTimes[event.id] = event.commence_time
198 | })
199 | return eventCommenceTimes
200 | }
201 |
202 | function getNextCommenceTime(commenceTimes, currentCommenceTime) {
203 | // assumes commenceTimes ordered asc
204 | for (const commenceTime of commenceTimes) {
205 | if (commenceTime <= currentCommenceTime) {
206 | continue
207 | }
208 | return commenceTime
209 | }
210 | return null
211 | }
212 |
213 | function getKeyByValue(d, value) {
214 | return Object.keys(d).filter((key) => d[key] === value)
215 | }
--------------------------------------------------------------------------------
/HistoricalEventOdds.gs:
--------------------------------------------------------------------------------
1 | function getOdds() {
2 | /**
3 | * Query historical odds from the The Odds API and output the response to Google Sheets.
4 | *
5 | * This script will query historical data at timestamps between the date range given (see FROM_DATE, TO_DATE and INTERVAL_MINS).
6 | * Odds are queried one game at a time for a given sport.
7 | *
8 | * This script is compatible with both featured and non-featured markets (see https://the-odds-api.com/sports-odds-data/betting-markets.html)
9 | * Historical data for non-featured markets are available from 2023-05-03.
10 | *
11 | * The maximum usage cost of each API call at a given timestamp is 10 x [number of markets] x [number of bookmaker regions] x [number of games] + 1
12 | * It can be lower if some markets are not available at the specified timestamp.
13 | * The +1 component comes from an initial query to list games at the given timestamp.
14 | *
15 | * If the spreadsheet has existing data, newly queried data will be appended to the next available row.
16 | *
17 | * Historical data is only available for paid subscriptions.
18 | *
19 | * Depending on the specified FROM_DATE, TO_DATE and INTERVAL_MINS, the volume of data can be large. Google Sheets has a limit of 10 million cells.
20 | *
21 | * This script will currently run for a maximum of 6 minutes at a time [see Apps Script service quotas](https://developers.google.com/apps-script/guides/services/quotas#current_limitations)
22 | * If the timeout is reached, you may need to trigger this script multiple times for smaller time ranges.
23 | *
24 | */
25 |
26 | const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/abc123456789/edit#gid=0' // Get this from your browser
27 | const SHEET_NAME = 'Sheet1' // The name of the spreadsheet tab. Note all data in this sheet will be cleared with each request
28 | const API_KEY = 'YOUR_API_KEY' // Get an API key from https://the-odds-api.com/#get-access
29 |
30 | const SPORT_KEY = 'baseball_mlb' // For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
31 | const MARKETS = 'h2h,spreads,totals' // Comma separated list of betting markets. For market keys, see https://the-odds-api.com/sports-odds-data/betting-markets.html
32 | const REGIONS = 'us' // Comma separated list of bookmaker regions. Valid values are us, uk, eu and au
33 | const BOOKMAKERS = '' // Optional - if specified, it overrides REGIONS. A list of comma separated bookmakers from any region. For example: draftkings,pinnacle See all bookmakers at https://the-odds-api.com/sports-odds-data/bookmaker-apis.html
34 | const ODDS_FORMAT = 'american' // Valid values are american and decimal.
35 |
36 | const FROM_DATE = '2023-09-10T00:00:00Z'
37 | const TO_DATE = '2023-09-10T12:00:00Z'
38 | const INTERVAL_MINS = 60 // The interval between historical snapshots (this number should be 5 or more)
39 |
40 | const headers = [
41 | 'timestamp',
42 | 'id',
43 | 'commence_time',
44 | 'bookmaker',
45 | 'home_team',
46 | 'away_team',
47 | 'market',
48 | 'last_update',
49 | 'label',
50 | 'description',
51 | 'price',
52 | 'point',
53 | ]
54 |
55 | const ws = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
56 | let data
57 | let formattedDate
58 | let currentDate = new Date(TO_DATE).getTime()
59 | let outputRow = ws.getLastRow() + 1
60 | let formattedResponse
61 |
62 | if (outputRow === 1) {
63 | // Output headers
64 | ws.getRange(3, 1, 1, headers.length).setValues([headers])
65 |
66 | // 1st 2 rows are for meta data, headers are on the 3rd row
67 | outputRow = 4
68 | }
69 |
70 | while (currentDate > (new Date(FROM_DATE)).getTime()) {
71 | formattedDate = Utilities.formatDate(new Date(currentDate), 'UTC', "yyyy-MM-dd'T'HH:mm:ss'Z'")
72 |
73 | for (const event of fetchEvents(API_KEY, SPORT_KEY, formattedDate).responseContent.data) {
74 | data = fetchEventOdds(API_KEY, SPORT_KEY, event.id, REGIONS, BOOKMAKERS, MARKETS, ODDS_FORMAT, formattedDate)
75 |
76 | // Output meta data starting in row 1, column 1
77 | ws.getRange(1, 1, 2, data.metaData[0].length).setValues(data.metaData)
78 |
79 | // Output event data
80 | formattedResponse = formatEventOutput(data.responseContent)
81 | if (formattedResponse.length > 0) {
82 | ws.getRange(outputRow, 1, formattedResponse.length, formattedResponse[0].length).setValues(formattedResponse)
83 | SpreadsheetApp.flush()
84 | }
85 |
86 | outputRow = outputRow + formattedResponse.length
87 | }
88 |
89 | if (data.responseContent.previous_timestamp === null) {
90 | // Earlier historical data is not available
91 | return;
92 | }
93 |
94 | currentDate = Math.min(currentDate - (INTERVAL_MINS * 60 * 1000), (new Date(data.responseContent.previous_timestamp)).getTime())
95 | }
96 | }
97 |
98 | function fetchEvents(apiKey, sportKey, timestamp) {
99 | const url = `https://api.the-odds-api.com/v4/historical/sports/${sportKey}/events?apiKey=${apiKey}&date=${timestamp}`
100 |
101 | const response = UrlFetchApp.fetch(url, {
102 | headers: {
103 | 'content-type': 'application/json'
104 | },
105 | })
106 |
107 | return {
108 | metaData: formatResponseMetaData(response.getHeaders()),
109 | responseContent: JSON.parse(response.getContentText()),
110 | }
111 | }
112 |
113 | function fetchEventOdds(apiKey, sportKey, eventId, regions, bookmakers, markets, oddsFormat, timestamp) {
114 | const bookmakersParam = bookmakers ? `bookmakers=${bookmakers}` : `regions=${regions}`
115 |
116 | const url = `https://api.the-odds-api.com/v4/historical/sports/${sportKey}/events/${eventId}/odds?apiKey=${apiKey}&${bookmakersParam}&markets=${markets}&oddsFormat=${oddsFormat}&date=${timestamp}`
117 |
118 | const response = UrlFetchApp.fetch(url, {
119 | headers: {
120 | 'content-type': 'application/json'
121 | },
122 | })
123 |
124 | return {
125 | metaData: formatResponseMetaData(response.getHeaders()),
126 | responseContent: JSON.parse(response.getContentText()),
127 | }
128 | }
129 |
130 | function formatEventOutput(response) {
131 | /**
132 | * Restructure the JSON response into a 2D array, suitable for outputting to a spreadsheet
133 | */
134 | const rows = []
135 | const event = response.data
136 | for (const bookmaker of event.bookmakers) {
137 | for (const market of bookmaker.markets) {
138 | for (const outcome of market.outcomes) {
139 | rows.push([
140 | response.timestamp,
141 | event.id,
142 | event.commence_time,
143 | bookmaker.key,
144 | event.home_team,
145 | event.away_team,
146 | market.key,
147 | market.last_update,
148 | outcome.name,
149 | outcome?.description,
150 | outcome.price,
151 | outcome?.point,
152 | ])
153 | }
154 | }
155 | }
156 |
157 | return rows
158 | }
159 |
160 | function formatResponseMetaData(headers) {
161 | return [
162 | ['Requests Used', headers['x-requests-used']],
163 | ['Requests Remaining', headers['x-requests-remaining']],
164 | ]
165 | }
--------------------------------------------------------------------------------
/HistoricalOdds.gs:
--------------------------------------------------------------------------------
1 | function getOdds() {
2 | /**
3 | * Query historical odds from the The Odds API and output the response to Google Sheets.
4 | *
5 | * If the spreadsheet has existing data, newly queried data will be appended to the next available row.
6 | *
7 | * Historical data is only available for paid subscriptions.
8 | *
9 | * The usage quota cost of each historical timestamp query is calculated as: 10 x [number of markets] x [number of regions]
10 | * More info: https://the-odds-api.com/liveapi/guides/v4/#usage-quota-costs-3
11 | *
12 | * Depending on the specified FROM_DATE, TO_DATE and INTERVAL_MINS, the volume of data can be large. Google Sheets has a limit of 10 million cells.
13 | *
14 | * This script will currently run for a maximum of 6 minutes at a time [see Apps Script service quotas](https://developers.google.com/apps-script/guides/services/quotas#current_limitations)
15 | * If the timeout is reached, you may need to trigger this script multiple times for smaller time ranges.
16 | *
17 | * Note this code does not handle futures (outrights) markets at this time.
18 | */
19 |
20 | const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/abc123456789/edit#gid=0' // Get this from your browser
21 | const SHEET_NAME = 'Sheet1' // The name of the spreadsheet tab. Note all data in this sheet will be cleared with each request
22 | const API_KEY = 'YOUR_API_KEY' // Get an API key from https://the-odds-api.com/#get-access
23 |
24 | const SPORT_KEY = 'baseball_mlb' // For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
25 | const MARKETS = 'h2h,spreads,totals' // Comma separated list of betting markets. Valid values are h2h, spreads & totals
26 | const REGIONS = 'us' // Comma separated list of bookmaker regions. Valid values are us, uk, eu and au
27 | const BOOKMAKERS = '' // Optional - if specified, it overrides REGIONS. A list of comma separated bookmakers from any region. For example: draftkings,pinnacle See all bookmakers at https://the-odds-api.com/sports-odds-data/bookmaker-apis.html
28 | const ODDS_FORMAT = 'american' // Valid values are american and decimal.
29 |
30 | const FROM_DATE = '2023-09-10T00:00:00Z'
31 | const TO_DATE = '2023-09-10T12:00:00Z'
32 | const INTERVAL_MINS = 60 // The interval between historical snapshots (this number should be 5 or more)
33 |
34 | const headers = [
35 | 'timestamp',
36 | 'id',
37 | 'commence_time',
38 | 'bookmaker',
39 | 'last_update',
40 | 'home_team',
41 | 'away_team',
42 | 'market',
43 | 'label_1',
44 | 'odd_1',
45 | 'point_1',
46 | 'label_2',
47 | 'odd_2',
48 | 'point_2',
49 | 'odd_draw',
50 | ]
51 |
52 | const ws = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
53 | let data
54 | let current_date = new Date(TO_DATE).getTime()
55 | let output_row = ws.getLastRow() + 1
56 | let formattedResponse
57 |
58 | if (output_row === 1) {
59 | // Output headers
60 | ws.getRange(3, 1, 1, headers.length).setValues([headers])
61 |
62 | // 1st 2 rows are for meta data, headers are on the 3rd row
63 | output_row = 4
64 | }
65 |
66 | while (current_date > (new Date(FROM_DATE)).getTime()) {
67 |
68 | // Request the data from the API
69 | data = fetchOdds(API_KEY, SPORT_KEY, REGIONS, BOOKMAKERS, MARKETS, ODDS_FORMAT, Utilities.formatDate(new Date(current_date), 'UTC', "yyyy-MM-dd'T'HH:mm:ss'Z'"))
70 |
71 | // Output meta data starting in row 1, column 1
72 | ws.getRange(1, 1, 2, data.metaData[0].length).setValues(data.metaData)
73 |
74 | // Output event data
75 | formattedResponse = formatEventOutput(data.responseContent)
76 | if (formattedResponse.length > 0) {
77 | ws.getRange(output_row, 1, formattedResponse.length, formattedResponse[0].length).setValues(formattedResponse)
78 | SpreadsheetApp.flush()
79 | }
80 |
81 | if (data.responseContent.previous_timestamp === null) {
82 | // Earlier historical data is not available
83 | break;
84 | }
85 |
86 | current_date = Math.min(current_date - (INTERVAL_MINS * 60 * 1000), (new Date(data.responseContent.previous_timestamp)).getTime())
87 | output_row = output_row + formattedResponse.length
88 | }
89 | }
90 |
91 | function fetchOdds(apiKey, sportKey, regions, bookmakers, markets, oddsFormat, timestamp) {
92 | const bookmakersParam = bookmakers ? `bookmakers=${bookmakers}` : `regions=${regions}`
93 |
94 | const url = `https://api.the-odds-api.com/v4/historical/sports/${sportKey}/odds?apiKey=${apiKey}&${bookmakersParam}&markets=${markets}&oddsFormat=${oddsFormat}&date=${timestamp}`
95 |
96 | const response = UrlFetchApp.fetch(url, {
97 | headers: {
98 | 'content-type': 'application/json'
99 | },
100 | })
101 |
102 | return {
103 | metaData: formatResponseMetaData(response.getHeaders()),
104 | responseContent: JSON.parse(response.getContentText()),
105 | }
106 | }
107 |
108 | function formatEventOutput(response) {
109 | /**
110 | * Restructure the JSON response into a 2D array, suitable for outputting to a spreadsheet
111 | */
112 | const rows = []
113 | let outcome_home
114 | let outcome_away
115 | let outcome_draw
116 | for (const event of response.data) {
117 | for (const bookmaker of event.bookmakers) {
118 | for (const market of bookmaker.markets) {
119 | if (market.key === 'totals') {
120 | outcome_home = market.outcomes.filter(outcome => outcome.name === 'Over')[0]
121 | outcome_away = market.outcomes.filter(outcome => outcome.name === 'Under')[0]
122 | outcome_draw = {}
123 | } else {
124 | outcome_home = market.outcomes.filter(outcome => outcome.name === event.home_team)[0]
125 | outcome_away = market.outcomes.filter(outcome => outcome.name === event.away_team)[0]
126 | outcome_draw = market.outcomes.filter(outcome => outcome.name === 'Draw')[0] ?? {}
127 | }
128 |
129 | rows.push([
130 | response.timestamp,
131 | event.id,
132 | event.commence_time,
133 | bookmaker.key,
134 | bookmaker.last_update,
135 | event.home_team,
136 | event.away_team,
137 | market.key,
138 | outcome_home.name,
139 | outcome_home.price,
140 | outcome_home?.point,
141 | outcome_away.name,
142 | outcome_away.price,
143 | outcome_away?.point,
144 | outcome_draw?.price,
145 | ])
146 | }
147 | }
148 | }
149 |
150 | return rows
151 | }
152 |
153 | function formatResponseMetaData(headers) {
154 | return [
155 | ['Requests Used', headers['x-requests-used']],
156 | ['Requests Remaining', headers['x-requests-remaining']],
157 | ]
158 | }
--------------------------------------------------------------------------------
/Odds.gs:
--------------------------------------------------------------------------------
1 |
2 | function getOdds() {
3 | /**
4 | * Get odds from the The Odds API and output the response to a spreadsheet.
5 | *
6 | * Note this code does not handle futures (outrights) markets at this time.
7 | */
8 |
9 | const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/abc123/edit#gid=0' // Get this from your browser
10 | const SHEET_NAME = 'Sheet1' // The name of the spreadsheet tab. Note all data in this sheet will be cleared with each request
11 |
12 | const API_KEY = 'YOUR_API_KEY' // Get an API key from https://the-odds-api.com/#get-access
13 | const SPORT_KEY = 'americanfootball_nfl' // For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
14 | const MARKETS = 'h2h,spreads' // Comma separated list of betting markets. Valid values are h2h, spreads & totals
15 | const REGIONS = 'us' // Comma separated list of bookmaker regions. Valid values are us, uk, eu and au
16 | const BOOKMAKERS = '' // Optional - if specified, it overrides REGIONS. A list of comma separated bookmakers from any region. For example: draftkings,pinnacle See all bookmakers at https://the-odds-api.com/sports-odds-data/bookmaker-apis.html
17 | const ODDS_FORMAT = 'american' // Valid values are american and decimal.
18 | const DATE_FORMAT = 'iso' // Valid values are unix and iso.
19 |
20 | // Request the data from the API
21 | const data = fetchOdds(API_KEY, SPORT_KEY, MARKETS, REGIONS, BOOKMAKERS, ODDS_FORMAT, DATE_FORMAT)
22 |
23 | // Prepare the spreadsheet for the data output
24 | // Note this clears any existing data on the spreadsheet
25 | const ws = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
26 | ws.clearContents()
27 |
28 | // Output meta data starting in row 1, column 1
29 | ws.getRange(1, 1, data.metaData.length, data.metaData[0].length).setValues(data.metaData)
30 |
31 | // Output event data 2 rows below the meta data
32 | ws.getRange(data.metaData.length + 2, 1, data.eventData.length, data.eventData[0].length).setValues(data.eventData)
33 | }
34 |
35 |
36 | /**
37 | * Calls v4 of The Odds API and returns odds data in a tabular structure
38 | * For details, see https://the-odds-api.com/liveapi/guides/v4/#parameters-2
39 | *
40 | * @param {string} apiKey Get an API key from https://the-odds-api.com/#get-access
41 | * @param {string} sportKey For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
42 | * @param {string} markets Comma separated list of betting markets. Valid values are h2h, spreads & totals
43 | * @param {string} regions Comma separated list of bookmaker regions. Valid values are us, uk, eu and au
44 | * @param {string} oddsFormat Valid values are american and decimal.
45 | * @param {string} dateFormat Valid values are unix and iso.
46 | * @return {object} A dictionary containing keys for metaData and eventData, each with a value as a 2D array (tabular) for easy output to a spreadsheet. If the request fails, event_data will be null.
47 | */
48 | function fetchOdds(apiKey, sportKey, markets, regions, bookmakers, oddsFormat, dateFormat) {
49 | const bookmakersParam = bookmakers ? `bookmakers=${bookmakers}` : `regions=${regions}`
50 | const url = `https://api.the-odds-api.com/v4/sports/${sportKey}/odds?apiKey=${apiKey}&${bookmakersParam}&markets=${markets}&oddsFormat=${oddsFormat}&dateFormat=${dateFormat}`
51 |
52 | const response = UrlFetchApp.fetch(url, {
53 | headers: {
54 | 'content-type': 'application/json'
55 | },
56 | })
57 |
58 | return {
59 | metaData: formatResponseMetaData(response.getHeaders()),
60 | eventData: formatEvents(JSON.parse(response.getContentText())),
61 | }
62 |
63 | }
64 |
65 | function formatEvents(events) {
66 | /**
67 | * Restructure the JSON response into a 2D array, suitable for outputting to a spreadsheet
68 | */
69 | const rows = [
70 | [
71 | 'id',
72 | 'commence_time',
73 | 'bookmaker',
74 | 'last_update',
75 | 'home_team',
76 | 'away_team',
77 | 'market',
78 | 'label_1',
79 | 'odd_1',
80 | 'point_1',
81 | 'label_2',
82 | 'odd_2',
83 | 'point_2',
84 | 'odd_draw',
85 | ]
86 | ]
87 |
88 | let outcome_home
89 | let outcome_away
90 | let outcome_draw
91 |
92 | for (const event of events) {
93 | for (const bookmaker of event.bookmakers) {
94 | for (const market of bookmaker.markets) {
95 | if (market.key === 'totals') {
96 | outcome_home = market.outcomes.filter(outcome => outcome.name === 'Over')[0]
97 | outcome_away = market.outcomes.filter(outcome => outcome.name === 'Under')[0]
98 | outcome_draw = {}
99 | } else {
100 | outcome_home = market.outcomes.filter(outcome => outcome.name === event.home_team)[0]
101 | outcome_away = market.outcomes.filter(outcome => outcome.name === event.away_team)[0]
102 | outcome_draw = market.outcomes.filter(outcome => outcome.name === 'Draw')[0] ?? {}
103 | }
104 |
105 | rows.push([
106 | event.id,
107 | event.commence_time,
108 | bookmaker.key,
109 | bookmaker.last_update,
110 | event.home_team,
111 | event.away_team,
112 | market.key,
113 | outcome_home.name,
114 | outcome_home.price,
115 | outcome_home?.point,
116 | outcome_away.name,
117 | outcome_away.price,
118 | outcome_away?.point,
119 | outcome_draw?.price,
120 | ])
121 | }
122 | }
123 | }
124 |
125 | return rows
126 | }
127 |
128 | function formatResponseMetaData(headers) {
129 | return [
130 | ['Requests Used', headers['x-requests-used']],
131 | ['Requests Remaining', headers['x-requests-remaining']],
132 | ]
133 | }
134 |
--------------------------------------------------------------------------------
/OddsLoop.gs:
--------------------------------------------------------------------------------
1 |
2 | function getOdds() {
3 | /**
4 | * Get odds from the The Odds API and output the response to a spreadsheet.
5 | * Using Apps Script triggers, code can be invoked as frequently as every minute by default.
6 | * For more frequent updates, use this script along with the Apps Script minute trigger (see "Triggers" in the README).
7 | */
8 |
9 | const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/abc123/edit#gid=0' // Get this from your browser
10 | const SHEET_NAME = 'Sheet1' // The name of the spreadsheet tab. Note all data in this sheet will be cleared with each request
11 |
12 | const API_KEY = 'YOUR_API_KEY' // Get an API key from https://the-odds-api.com/#get-access
13 | const SPORT_KEY = 'americanfootball_nfl' // For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
14 | const MARKETS = 'h2h,spreads' // Comma separated list of betting markets. Valid values are h2h, spreads & totals
15 | const REGIONS = 'us' // Comma separated list of bookmaker regions. Valid values are us, uk, eu and au
16 | const ODDS_FORMAT = 'american' // Valid values are american and decimal.
17 | const DATE_FORMAT = 'iso' // Valid values are unix and iso.
18 |
19 | const UPDATES_PER_MINUTE = 12 // Update data this many times per minute. For example if this is 12, data will refresh approximately every 60 / 12 = 5 seconds
20 |
21 | let data
22 | const ws = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
23 |
24 | for (let x = 0; x < UPDATES_PER_MINUTE ; x++) {
25 |
26 | // Request the data from the API
27 | data = fetchOdds(API_KEY, SPORT_KEY, MARKETS, REGIONS, ODDS_FORMAT, DATE_FORMAT)
28 |
29 | // Prepare the spreadsheet for the data output
30 | // Note this clears any existing data on the spreadsheet
31 | ws.clearContents()
32 |
33 | // Output meta data starting in row 1, column 1
34 | ws.getRange(1, 1, data.metaData.length, data.metaData[0].length).setValues(data.metaData)
35 |
36 | // Output event data 2 rows below the meta data
37 | ws.getRange(data.metaData.length + 2, 1, data.eventData.length, data.eventData[0].length).setValues(data.eventData)
38 | SpreadsheetApp.flush()
39 |
40 | // Space out requests
41 | Utilities.sleep(60000 / UPDATES_PER_MINUTE)
42 | }
43 | }
44 |
45 |
46 | /**
47 | * Calls v4 of The Odds API and returns odds data in a tabular structure
48 | * For details, see https://the-odds-api.com/liveapi/guides/v4/#parameters-2
49 | *
50 | * @param {string} apiKey Get an API key from https://the-odds-api.com/#get-access
51 | * @param {string} sportKey For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
52 | * @param {string} markets Comma separated list of betting markets. Valid values are h2h, spreads & totals
53 | * @param {string} regions Comma separated list of bookmaker regions. Valid values are us, uk, eu and au
54 | * @param {string} oddsFormat Valid values are american and decimal.
55 | * @param {string} dateFormat Valid values are unix and iso.
56 | * @return {object} A dictionary containing keys for metaData and eventData, each with a value as a 2D array (tabular) for easy output to a spreadsheet. If the request fails, event_data will be null.
57 | */
58 | function fetchOdds(apiKey, sportKey, markets, regions, oddsFormat, dateFormat) {
59 | const url = `https://api.the-odds-api.com/v4/sports/${sportKey}/odds?apiKey=${apiKey}®ions=${regions}&markets=${markets}&oddsFormat=${oddsFormat}&dateFormat=${dateFormat}`
60 |
61 | const response = UrlFetchApp.fetch(url, {
62 | headers: {
63 | 'content-type': 'application/json'
64 | },
65 | })
66 |
67 | return {
68 | metaData: formatResponseMetaData(response.getHeaders()),
69 | eventData: formatEvents(JSON.parse(response.getContentText())),
70 | }
71 |
72 | }
73 |
74 | function formatEvents(events) {
75 | /**
76 | * Restructure the JSON response into a 2D array, suitable for outputting to a spreadsheet
77 | */
78 | const rows = [
79 | [
80 | 'id',
81 | 'commence_time',
82 | 'bookmaker',
83 | 'last_update',
84 | 'market',
85 | 'home_team',
86 | 'home_odd',
87 | 'home_point',
88 | 'away_team',
89 | 'away_odd',
90 | 'away_point',
91 | 'draw_odd',
92 | ]
93 | ]
94 |
95 | for (const event of events) {
96 | for (const bookmaker of event.bookmakers) {
97 | for (const market of bookmaker.markets) {
98 | let outcome_home = market.outcomes.filter(outcome => outcome.name === event.home_team)[0]
99 | let outcome_away = market.outcomes.filter(outcome => outcome.name === event.away_team)[0]
100 | let outcome_draw = market.outcomes.filter(outcome => outcome.name === 'Draw')[0] ?? {}
101 | rows.push([
102 | event.id,
103 | event.commence_time,
104 | bookmaker.key,
105 | bookmaker.last_update,
106 | market.key,
107 | outcome_home.name,
108 | outcome_home.price,
109 | outcome_home?.point,
110 | outcome_away.name,
111 | outcome_away.price,
112 | outcome_away?.point,
113 | outcome_draw?.price,
114 | ])
115 | }
116 | }
117 | }
118 |
119 | return rows
120 | }
121 |
122 | function formatResponseMetaData(headers) {
123 | return [
124 | ['Requests Used', headers['x-requests-used']],
125 | ['Requests Remaining', headers['x-requests-remaining']],
126 | ]
127 | }
128 |
--------------------------------------------------------------------------------
/OddsMultipleSports.gs:
--------------------------------------------------------------------------------
1 |
2 | function getOdds() {
3 | /**
4 | * Get odds from the The Odds API and output the response to a spreadsheet.
5 | * This script loops through a list of sport keys (see SPORT_KEYS)
6 | * and outputs their aggregated games to a single spreadsheet.
7 | */
8 |
9 | const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/abc123/edit#gid=0' // Get this from your browser
10 | const SHEET_NAME = 'Sheet1' // The name of the spreadsheet tab. Note all data in this sheet will be cleared with each request
11 |
12 | const API_KEY = 'YOUR_API_KEY' // Get an API key from https://the-odds-api.com/#get-access
13 | const SPORT_KEYS = ['americanfootball_nfl', 'soccer_epl'] // For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
14 | const MARKETS = 'h2h,spreads' // Comma separated list of betting markets. Valid values are h2h, spreads & totals
15 | const REGIONS = 'us' // Comma separated list of bookmaker regions. Valid values are us, uk, eu and au
16 | const ODDS_FORMAT = 'american' // Valid values are american and decimal.
17 | const DATE_FORMAT = 'iso' // Valid values are unix and iso.
18 |
19 | // Request the data from the API for each sport
20 | const responses = SPORT_KEYS.map(sportKey => {
21 | return fetchOdds(API_KEY, sportKey, MARKETS, REGIONS, ODDS_FORMAT, DATE_FORMAT);
22 | });
23 |
24 | const HEADERS = [
25 | 'sport_key',
26 | 'id',
27 | 'commence_time',
28 | 'bookmaker',
29 | 'last_update',
30 | 'market',
31 | 'home_team',
32 | 'home_odd',
33 | 'home_point',
34 | 'away_team',
35 | 'away_odd',
36 | 'away_point',
37 | 'draw_odd',
38 | ];
39 |
40 | const aggregatedEvents = [].concat.apply([HEADERS], responses.map(response => {
41 | return response.eventData;
42 | }));
43 |
44 | const metaData = responses[responses.length - 1].metaData;
45 |
46 | // Prepare the spreadsheet for the data output
47 | // Note this clears any existing data on the spreadsheet
48 | const ws = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
49 | ws.clearContents()
50 |
51 | // Output meta data starting in row 1, column 1
52 | ws.getRange(1, 1, metaData.length, metaData[0].length).setValues(metaData)
53 |
54 | // Output event data 2 rows below the meta data
55 | ws.getRange(metaData.length + 2, 1, aggregatedEvents.length, aggregatedEvents[0].length).setValues(aggregatedEvents)
56 | }
57 |
58 |
59 | /**
60 | * Calls v4 of The Odds API and returns odds data in a tabular structure
61 | * For details, see https://the-odds-api.com/liveapi/guides/v4/#parameters-2
62 | *
63 | * @param {string} apiKey Get an API key from https://the-odds-api.com/#get-access
64 | * @param {string} sportKey For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
65 | * @param {string} markets Comma separated list of betting markets. Valid values are h2h, spreads & totals
66 | * @param {string} regions Comma separated list of bookmaker regions. Valid values are us, uk, eu and au
67 | * @param {string} oddsFormat Valid values are american and decimal.
68 | * @param {string} dateFormat Valid values are unix and iso.
69 | * @return {object} A dictionary containing keys for metaData and eventData, each with a value as a 2D array (tabular) for easy output to a spreadsheet. If the request fails, event_data will be null.
70 | */
71 | function fetchOdds(apiKey, sportKey, markets, regions, oddsFormat, dateFormat) {
72 | const url = `https://api.the-odds-api.com/v4/sports/${sportKey}/odds?apiKey=${apiKey}®ions=${regions}&markets=${markets}&oddsFormat=${oddsFormat}&dateFormat=${dateFormat}`
73 |
74 | const response = UrlFetchApp.fetch(url, {
75 | headers: {
76 | 'content-type': 'application/json'
77 | },
78 | })
79 |
80 | return {
81 | metaData: formatResponseMetaData(response.getHeaders()),
82 | eventData: formatEvents(sportKey, JSON.parse(response.getContentText())),
83 | }
84 |
85 | }
86 |
87 | function formatEvents(sportKey, events) {
88 | /**
89 | * Restructure the JSON response into a 2D array, suitable for outputting to a spreadsheet
90 | */
91 | const rows = []
92 |
93 | for (const event of events) {
94 | for (const bookmaker of event.bookmakers) {
95 | for (const market of bookmaker.markets) {
96 | let outcome_home
97 | let outcome_away
98 | let outcome_draw
99 |
100 | if (market.key === 'totals') {
101 | outcome_home = market.outcomes.filter(outcome => outcome.name === 'Over')[0]
102 | outcome_away = market.outcomes.filter(outcome => outcome.name === 'Under')[0]
103 | outcome_draw = {}
104 | } else {
105 | outcome_home = market.outcomes.filter(outcome => outcome.name === event.home_team)[0]
106 | outcome_away = market.outcomes.filter(outcome => outcome.name === event.away_team)[0]
107 | outcome_draw = market.outcomes.filter(outcome => outcome.name === 'Draw')[0] ?? {}
108 | }
109 |
110 | rows.push([
111 | sportKey,
112 | event.id,
113 | event.commence_time,
114 | bookmaker.key,
115 | bookmaker.last_update,
116 | market.key,
117 | outcome_home.name,
118 | outcome_home.price,
119 | outcome_home?.point,
120 | outcome_away.name,
121 | outcome_away.price,
122 | outcome_away?.point,
123 | outcome_draw?.price,
124 | ])
125 | }
126 | }
127 | }
128 |
129 | return rows
130 | }
131 |
132 | function formatResponseMetaData(headers) {
133 | return [
134 | ['Requests Used', headers['x-requests-used']],
135 | ['Requests Remaining', headers['x-requests-remaining']],
136 | ]
137 | }
138 |
--------------------------------------------------------------------------------
/PlayerProps.gs:
--------------------------------------------------------------------------------
1 | function getOdds() {
2 | /**
3 | * Get player props from the The Odds API and output the response to a spreadsheet.
4 | * This script loops live and upcoming games for a given sport (see SPORT_KEY). For each game, it queries odds for specified betting markets (see MARKETS)
5 | * and outputs the aggregated result to a single spreadsheet.
6 | */
7 |
8 | const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/abc123/edit#gid=0' // Get this from your browser
9 | const SHEET_NAME = 'Sheet1' // The name of the spreadsheet tab. Note all data in this sheet will be cleared with each request
10 |
11 | const API_KEY = 'YOUR_API_KEY' // Get an API key from https://the-odds-api.com/#get-access
12 | const SPORT_KEY = 'americanfootball_nfl' // For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
13 | const MARKETS = 'player_pass_tds,player_pass_yds,player_pass_completions' // Comma separated list of betting markets. See all markets at https://the-odds-api.com/sports-odds-data/betting-markets.html
14 | const REGIONS = 'us' // Comma separated list of bookmaker regions. Valid values are us, uk, eu and au
15 | const BOOKMAKERS = '' // Optional - if specified, it overrides REGIONS. A list of comma separated bookmakers from any region. For example: draftkings,pinnacle See all bookmakers at https://the-odds-api.com/sports-odds-data/bookmaker-apis.html
16 | const ODDS_FORMAT = 'american' // Valid values are american and decimal.
17 | const DATE_FORMAT = 'iso' // Valid values are unix and iso.
18 |
19 | // Request main markets data from the API
20 | const events = fetchEvents(API_KEY, SPORT_KEY, 'h2h', REGIONS, BOOKMAKERS, ODDS_FORMAT, DATE_FORMAT)
21 |
22 | if (events.length === 0) {
23 | Logger.log('No events found')
24 | return
25 | }
26 |
27 | let output = []
28 | let marketResponse
29 | for (const event of events) {
30 | // Concatenate output for all games
31 | marketResponse = fetchEventMarkets(API_KEY, SPORT_KEY, MARKETS, REGIONS, BOOKMAKERS, ODDS_FORMAT, DATE_FORMAT, event.id)
32 | output = output.concat(marketResponse.eventData)
33 | }
34 |
35 | // Prepare the spreadsheet for the data output
36 | // Note this clears any existing data on the spreadsheet
37 | const ws = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
38 | ws.clearContents()
39 |
40 | // Add headers to the data
41 | output.unshift([
42 | 'id',
43 | 'commence_time',
44 | 'bookmaker',
45 | 'last_update',
46 | 'home_team',
47 | 'away_team',
48 | 'market',
49 | 'label',
50 | 'description',
51 | 'price',
52 | 'point',
53 | ])
54 |
55 | // Output meta data starting in row 1, column 1
56 | ws.getRange(1, 1, marketResponse.metaData.length, marketResponse.metaData[0].length).setValues(marketResponse.metaData)
57 |
58 | // Output event data 2 rows below the meta data
59 | ws.getRange(marketResponse.metaData.length + 2, 1, output.length, output[0].length).setValues(output)
60 | }
61 |
62 | function fetchEvents(apiKey, sportKey, markets, regions, bookmakers, oddsFormat, dateFormat) {
63 | const bookmakersParam = bookmakers ? `bookmakers=${bookmakers}` : `regions=${regions}`
64 | const url = `https://api.the-odds-api.com/v4/sports/${sportKey}/odds?apiKey=${apiKey}&${bookmakersParam}&markets=${markets}&oddsFormat=${oddsFormat}&dateFormat=${dateFormat}`
65 |
66 | const response = UrlFetchApp.fetch(url, {
67 | headers: {
68 | 'content-type': 'application/json'
69 | },
70 | })
71 |
72 | return JSON.parse(response.getContentText())
73 | }
74 |
75 | function fetchEventMarkets(apiKey, sportKey, markets, regions, bookmakers, oddsFormat, dateFormat, eventId) {
76 | const bookmakersParam = bookmakers ? `bookmakers=${bookmakers}` : `regions=${regions}`
77 | const url = `https://api.the-odds-api.com/v4/sports/${sportKey}/events/${eventId}/odds?apiKey=${apiKey}&${bookmakersParam}&markets=${markets}&oddsFormat=${oddsFormat}&dateFormat=${dateFormat}`
78 |
79 | const response = UrlFetchApp.fetch(url, {
80 | headers: {
81 | 'content-type': 'application/json'
82 | },
83 | })
84 |
85 | return {
86 | metaData: formatResponseMetaData(response.getHeaders()),
87 | eventData: formatEventOutput(JSON.parse(response.getContentText())),
88 | }
89 | }
90 |
91 | function formatEventOutput(event) {
92 | /**
93 | * Restructure the JSON response into a 2D array, suitable for outputting to a spreadsheet
94 | */
95 | const rows = []
96 | for (const bookmaker of event.bookmakers) {
97 | for (const market of bookmaker.markets) {
98 | for (const outcome of market.outcomes) {
99 | rows.push([
100 | event.id,
101 | event.commence_time,
102 | bookmaker.key,
103 | market.last_update,
104 | event.home_team,
105 | event.away_team,
106 | market.key,
107 | outcome.name,
108 | outcome.description,
109 | outcome.price,
110 | outcome.point,
111 | ])
112 | }
113 | }
114 | }
115 |
116 | return rows
117 | }
118 |
119 | function formatResponseMetaData(headers) {
120 | return [
121 | ['Requests Used', headers['x-requests-used']],
122 | ['Requests Remaining', headers['x-requests-remaining']],
123 | ]
124 | }
125 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # The Odds API with Apps Script & Google Sheets
2 |
3 | Customize the way you bring odds data into Google Sheets.
4 |
5 | The Odds API offers an [add-on for Google Sheets](https://workspace.google.com/marketplace/app/sports_odds/426905155013?hl=en&pann=sheets_addon_widget) to bring odds data into a spreadsheet at the click of a button. Whilst the add-on is designed to be easy to use, it is limited in what it can do. This guide is for users who want more control and flexibility in the way they bring odds data into their spreadsheets. It doesn't require coding skills, but it does require some tweaks to the code in order set up the requests.
6 |
7 | Use this guide to:
8 |
9 | - Customize the request for odds data. For example, fetch odds for multiple markets in a single request.
10 | - Customize triggers. Decide on how the data should be refreshed. For example, you can add a button anywhere in your spreadsheet to refresh odds manually, or you can set a time-driven trigger to have the data automatically refresh on a schedule. This can refresh odds even when the spreadsheet is closed.
11 | - Add your own code to do more complex things.
12 |
13 | ## First Time Setup
14 |
15 | - In a *new* spreadsheet, on the top tool bar, click Extensions -> Apps Script
16 |
17 | - Copy and paste the code from one of the above files into the Apps Script editor
18 | - For example, code to query current odds can be found in the [Odds.gs](./Odds.gs) file
19 |
20 | - Give your project a name
21 |
22 | - Set the values for `SPREADSHEET_URL` and `SHEET_NAME`, which tells the code where to output the odds data. These values can be found from the spreadsheet.
23 |
24 | - Set the parameters for the API request, including `API_KEY`, `SPORT_KEY` and others.
25 | - See here for a [list of sports keys](https://the-odds-api.com/sports-odds-data/sports-apis.html). Data will only be returned if the sport is in season and listed by bookmakers.
26 | - For more information on each parameter, see [the docs](https://the-odds-api.com/liveapi/guides/v4/#parameters-2)
27 | - See here for a [description of each market](https://the-odds-api.com/sports-odds-data/betting-markets.html)
28 | - Once your parameters are set, save the changes (ctrl + s or cmd + s), hit the "Run" button. Note this may clear any data in the `SHEET_NAME` sheet before outputting odds data. If you want to add formulas or notes, do so in a different sheet.
29 |
30 | - For a first time run, this will ask for authorization. This sample code requires 2 permissions
31 | - Permission to access an "external service", which is simply a call to The Odds API to fetch the latest odds data
32 | - Permission to edit the spreadsheet specified with `SPREADSHEET_URL` and `SHEET_NAME`. With each run of the code, it will populate this sheet with the latest odds data.
33 |
34 | Since this is a code sample for personal use and not published as a library, it cannot be verified by Google, and you may see a warning. To bypass this warning, click "Advanced -> Go to 'name of your project'".
35 |
36 | - After the code has run, click back to your spreadsheet and you should see odds data populated.
37 |
38 |
39 | ## Triggers
40 |
41 | Now that we can bring odds data into a spreadsheet, we can look at easier ways to trigger the code to run.
42 |
43 | ### Get odds by clicking a button
44 | - In the spreadsheet, click Insert -> Drawing
45 | - Draw & style a button, and click "Save and Close"
46 |
47 | - Right click the button and click the 3 vertical dots, and select "Assign script"
48 | - Type the name of the function that updates odds & save.
49 | - For scripts that query odds, the function name is "getOdds". For example, you will see `function getOdds()` in [Odds.gs](./Odds.gs)
50 | - For scripts that query scores, the function name is "getScores"
51 |
52 | - Click the button. Odds (or scores) data should populate in the spreadsheet as configured with `SPREADSHEET_URL` and `SHEET_NAME`
53 |
54 | ### Get odds automatically on a time-driven schedule
55 |
56 | - Go to the [Apps Script Console](https://script.google.com/home/my)
57 | - Find your new Apps Script project, and click the 3 vertical dots on the far right
58 | - Click Triggers
59 | - Click Add Trigger (bottom right)
60 | - Set "Select event source" to "Time-driven"
61 |
62 | - Select the frequency with which to update odds (minimum 1 minute) and save
63 |
--------------------------------------------------------------------------------
/Scores.gs:
--------------------------------------------------------------------------------
1 |
2 | function getScores() {
3 | /**
4 | * Get scores from the The Odds API and output the response to a spreadsheet.
5 | */
6 |
7 | const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/abc123/edit#gid=0' // Get this from your browser
8 | const SHEET_NAME = 'Scores' // The name of the spreadsheet tab. Note all data in this sheet will be cleared with each request
9 |
10 | const API_KEY = 'YOUR_API_KEY' // Get an API key from https://the-odds-api.com/#get-access
11 | const SPORT_KEY = 'americanfootball_nfl' // For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
12 | const DAYS_FROM = 1 // Return scores from games this many days in the past (includes completed games). Valid values are 0 - 3. If 0, only live games will be returned.
13 | const DATE_FORMAT = 'iso' // Valid values are unix and iso.
14 |
15 | // Request the data from the API
16 | const data = fetchScores(API_KEY, SPORT_KEY, DAYS_FROM, DATE_FORMAT)
17 |
18 | // Prepare the spreadsheet for the data output
19 | // Note this clears any existing data on the spreadsheet
20 | const ws = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
21 | ws.clearContents()
22 |
23 | // Output meta data starting in row 1, column 1
24 | ws.getRange(1, 1, data.metaData.length, data.metaData[0].length).setValues(data.metaData)
25 |
26 | // Output event data 2 rows below the meta data
27 | ws.getRange(data.metaData.length + 2, 1, data.eventData.length, data.eventData[0].length).setValues(data.eventData)
28 | }
29 |
30 |
31 | /**
32 | * Calls v4 of The Odds API and returns scores data in a tabular structure
33 | * For details, see https://the-odds-api.com/liveapi/guides/v4/#parameters-2
34 | *
35 | * @param {string} apiKey Get an API key from https://the-odds-api.com/#get-access
36 | * @param {string} sportKey For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
37 | * @param {int} daysFrom Return scores from games this many days in the past (includes completed games). Valid values are 0 - 3. If 0, only live games will be returned.
38 | * @param {string} dateFormat Valid values are unix and iso.
39 | * @return {object} A dictionary containing keys for metaData and eventData, each with a value as a 2D array (tabular) for easy output to a spreadsheet. If the request fails, event_data will be null.
40 | */
41 | function fetchScores(apiKey, sportKey, daysFrom, dateFormat) {
42 |
43 | let url = `https://api.the-odds-api.com/v4/sports/${sportKey}/scores?apiKey=${apiKey}&dateFormat=${dateFormat}`
44 | if (daysFrom !== 0) {
45 | url += `&daysFrom=${daysFrom}`
46 | }
47 |
48 | const response = UrlFetchApp.fetch(url, {
49 | headers: {
50 | 'content-type': 'application/json'
51 | },
52 | })
53 |
54 | return {
55 | metaData: formatResponseMetaDataScores(response.getHeaders()),
56 | eventData: formatEventsScores(JSON.parse(response.getContentText())),
57 | }
58 |
59 | }
60 |
61 | function formatEventsScores(events) {
62 | /**
63 | * Restructure the JSON response into a 2D array, suitable for outputting to a spreadsheet
64 | */
65 | const rows = [
66 | [
67 | 'id',
68 | 'commence_time',
69 | 'completed',
70 | 'last_update',
71 | 'home_team',
72 | 'home_score',
73 | 'away_team',
74 | 'away_score',
75 | ]
76 | ]
77 |
78 | for (const event of events) {
79 | let home_score = event.scores ? event.scores.filter(outcome => outcome.name === event.home_team)[0].score : null
80 | let away_score = event.scores ? event.scores.filter(outcome => outcome.name === event.away_team)[0].score : null
81 |
82 | rows.push([
83 | event.id,
84 | event.commence_time,
85 | event.completed,
86 | event.last_update,
87 | event.home_team,
88 | home_score,
89 | event.away_team,
90 | away_score,
91 | ])
92 | }
93 |
94 | return rows
95 | }
96 |
97 | function formatResponseMetaDataScores(headers) {
98 | return [
99 | ['Requests Used', headers['x-requests-used']],
100 | ['Requests Remaining', headers['x-requests-remaining']],
101 | ]
102 | }
103 |
--------------------------------------------------------------------------------
/ScoresLoop.gs:
--------------------------------------------------------------------------------
1 |
2 | function getScores() {
3 | /**
4 | * Get scores from the The Odds API and output the response to a spreadsheet.
5 | */
6 |
7 | const SPREADSHEET_URL = 'https://docs.google.com/spreadsheets/d/abc123/edit#gid=0' // Get this from your browser
8 | const SHEET_NAME = 'Scores' // The name of the spreadsheet tab. Note all data in this sheet will be cleared with each request
9 |
10 | const API_KEY = 'YOUR_API_KEY' // Get an API key from https://the-odds-api.com/#get-access
11 | const SPORT_KEY = 'americanfootball_nfl' // For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
12 | const DAYS_FROM = 1 // Return games from this many days in the past. Valid values are 0 - 3. If 0, only live games will be returned.
13 | const DATE_FORMAT = 'iso' // Valid values are unix and iso.
14 |
15 | const UPDATES_PER_MINUTE = 2 // Update data this many times per minute. For example if this is 2, data will refresh approximately every 60 / 2 = 30 seconds
16 |
17 | let data
18 | const ws = SpreadsheetApp.openByUrl(SPREADSHEET_URL).getSheetByName(SHEET_NAME)
19 |
20 | for (let x = 0; x < UPDATES_PER_MINUTE ; x++) {
21 |
22 | // Request the data from the API
23 | data = fetchScores(API_KEY, SPORT_KEY, DAYS_FROM, DATE_FORMAT)
24 |
25 | // Prepare the spreadsheet for the data output
26 | // Note this clears any existing data on the spreadsheet
27 | ws.clearContents()
28 |
29 | // Output meta data starting in row 1, column 1
30 | ws.getRange(1, 1, data.metaData.length, data.metaData[0].length).setValues(data.metaData)
31 |
32 | // Output event data 2 rows below the meta data
33 | ws.getRange(data.metaData.length + 2, 1, data.eventData.length, data.eventData[0].length).setValues(data.eventData)
34 | SpreadsheetApp.flush()
35 |
36 | // Space out requests
37 | Utilities.sleep(60000 / UPDATES_PER_MINUTE)
38 | }
39 | }
40 |
41 | /**
42 | * Calls v4 of The Odds API and returns scores data in a tabular structure
43 | * For details, see https://the-odds-api.com/liveapi/guides/v4/#parameters-2
44 | *
45 | * @param {string} apiKey Get an API key from https://the-odds-api.com/#get-access
46 | * @param {string} sportKey For a list of sport keys, see https://the-odds-api.com/sports-odds-data/sports-apis.html
47 | * @param {int} daysFrom Return games from this many days in the past (includes completed games). Valid values are 0 - 3. If 0, only live games will be returned.
48 | * @param {string} dateFormat Valid values are unix and iso.
49 | * @return {object} A dictionary containing keys for metaData and eventData, each with a value as a 2D array (tabular) for easy output to a spreadsheet. If the request fails, event_data will be null.
50 | */
51 | function fetchScores(apiKey, sportKey, daysFrom, dateFormat) {
52 |
53 | let url = `https://api.the-odds-api.com/v4/sports/${sportKey}/scores?apiKey=${apiKey}&dateFormat=${dateFormat}`
54 | if (daysFrom !== 0) {
55 | url += `&daysFrom=${daysFrom}`
56 | }
57 |
58 | const response = UrlFetchApp.fetch(url, {
59 | headers: {
60 | 'content-type': 'application/json'
61 | },
62 | })
63 |
64 | return {
65 | metaData: formatResponseMetaDataScores(response.getHeaders()),
66 | eventData: formatEventsScores(JSON.parse(response.getContentText())),
67 | }
68 |
69 | }
70 |
71 | function formatEventsScores(events) {
72 | /**
73 | * Restructure the JSON response into a 2D array, suitable for outputting to a spreadsheet
74 | */
75 | const rows = [
76 | [
77 | 'id',
78 | 'commence_time',
79 | 'completed',
80 | 'last_update',
81 | 'home_team',
82 | 'home_score',
83 | 'away_team',
84 | 'away_score',
85 | ]
86 | ]
87 |
88 | for (const event of events) {
89 | let home_score = event.scores ? event.scores.filter(outcome => outcome.name === event.home_team)[0].score : null
90 | let away_score = event.scores ? event.scores.filter(outcome => outcome.name === event.away_team)[0].score : null
91 |
92 | rows.push([
93 | event.id,
94 | event.commence_time,
95 | event.completed,
96 | event.last_update,
97 | event.home_team,
98 | home_score,
99 | event.away_team,
100 | away_score,
101 | ])
102 | }
103 |
104 | return rows
105 | }
106 |
107 | function formatResponseMetaDataScores(headers) {
108 | return [
109 | ['Requests Used', headers['x-requests-used']],
110 | ['Requests Remaining', headers['x-requests-remaining']],
111 | ]
112 | }
113 |
--------------------------------------------------------------------------------
/screenshots/add_button_trigger.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-odds-api/apps-script/443bc3abad85ea90a5fe83c1af5a51e8fa7af94c/screenshots/add_button_trigger.jpg
--------------------------------------------------------------------------------
/screenshots/add_time_trigger.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-odds-api/apps-script/443bc3abad85ea90a5fe83c1af5a51e8fa7af94c/screenshots/add_time_trigger.jpg
--------------------------------------------------------------------------------
/screenshots/assign_script.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-odds-api/apps-script/443bc3abad85ea90a5fe83c1af5a51e8fa7af94c/screenshots/assign_script.jpg
--------------------------------------------------------------------------------
/screenshots/data_output.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-odds-api/apps-script/443bc3abad85ea90a5fe83c1af5a51e8fa7af94c/screenshots/data_output.jpg
--------------------------------------------------------------------------------
/screenshots/output_params.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-odds-api/apps-script/443bc3abad85ea90a5fe83c1af5a51e8fa7af94c/screenshots/output_params.jpg
--------------------------------------------------------------------------------
/screenshots/rename_project.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-odds-api/apps-script/443bc3abad85ea90a5fe83c1af5a51e8fa7af94c/screenshots/rename_project.jpg
--------------------------------------------------------------------------------
/screenshots/run_apps_script.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-odds-api/apps-script/443bc3abad85ea90a5fe83c1af5a51e8fa7af94c/screenshots/run_apps_script.jpg
--------------------------------------------------------------------------------
/screenshots/start_apps_script.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/the-odds-api/apps-script/443bc3abad85ea90a5fe83c1af5a51e8fa7af94c/screenshots/start_apps_script.jpg
--------------------------------------------------------------------------------