├── 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 | Start Apps Script 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 | Rename Project 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 | Finding Output Params 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 | Run Apps Script 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 | Data Output 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 | Add Button Trigger 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 | Assign Script 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 | Add Time Trigger 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 --------------------------------------------------------------------------------