Pull Pinnacle odds into Google Sheets
This guide writes Pinnacle soccer moneylines into a Google Sheet: one row per match, with decimal prices, no-vig fair prices and the market margin. A time trigger refreshes the tab every 15 minutes. The script uses the PinnFeed REST API, so it runs on the Free plan (€0, 30-second delay, 1,000 requests/day).
Step 1 — Get a Free key#
- Create an account at pinnfeed.io and open the dashboard.
- Create a key and copy it. Keys look like
pf_live_…. - On Free, pick up to two sports in the dashboard. This script defaults to soccer; change
SPORTif you chose something else.
You do not need Builder for Sheets. Builder (€99/month, the WebSocket plan) is for real-time push clients such as the Python and Node guide.
Step 2 — Create the spreadsheet#
- Create a new Google Sheet.
- Rename the first tab to Odds (exact name; the script looks it up).
- Open Extensions → Apps Script. Delete the stub
myFunctionand leave the project empty for the next step.
Step 3 — Paste the script and set properties#
Paste this into Code.gs:
// Google Apps Script: Extensions → Apps Script, paste this file.
// Script properties (Project Settings → Script properties):
// PINNFEED_BASE = https://pinnfeed.io
// PINNFEED_KEY = pf_live_...
// Add a sheet tab named "Odds", run refreshOdds once, then add a 15-minute trigger.
const PROPS = PropertiesService.getScriptProperties();
const BASE = PROPS.getProperty('PINNFEED_BASE') || 'https://pinnfeed.io';
const KEY = PROPS.getProperty('PINNFEED_KEY');
const SPORT = 'soccer'; // change to basketball, tennis, … (must be in your plan)
function refreshOdds() {
if (!KEY) {
console.error('Set script property PINNFEED_KEY first');
return;
}
const url = BASE + '/v1/odds'
+ '?sport=' + encodeURIComponent(SPORT)
+ '&market=moneyline&main=true&live=false&limit=500';
const res = UrlFetchApp.fetch(url, {
headers: { 'x-api-key': KEY },
muteHttpExceptions: true,
});
const code = res.getResponseCode();
if (code !== 200) {
console.warn('PinnFeed HTTP ' + code + ': ' + res.getContentText().slice(0, 300));
return; // leave the tab alone on a bad minute
}
const data = JSON.parse(res.getContentText());
const rows = [];
for (const ev of data.events || []) {
const ml = (ev.markets || []).find((m) => m.type === 'moneyline' && m.main);
if (!ml) continue;
const byName = {};
for (const o of ml.outcomes || []) byName[o.name] = o;
const home = byName.home || {};
const draw = byName.draw || {};
const away = byName.away || {};
rows.push([
ev.startTime ? new Date(ev.startTime) : '',
(ev.league && ev.league.name) || '',
ev.home || '',
ev.away || '',
home.decimal || '',
draw.decimal || '',
away.decimal || '',
home.fair || '',
draw.fair || '',
away.fair || '',
ml.margin != null ? ml.margin : '',
ev.id,
]);
}
const sheet = SpreadsheetApp.getActive().getSheetByName('Odds');
if (!sheet) {
console.error('Create a tab named Odds first');
return;
}
sheet.clearContents();
sheet.appendRow([
'Starts', 'League', 'Home', 'Away',
'1', 'X', '2',
'Fair 1', 'Fair X', 'Fair 2',
'Margin', 'Event id',
]);
if (rows.length) {
sheet.getRange(2, 1, rows.length, 12).setValues(rows);
}
console.log('Wrote ' + rows.length + ' moneyline rows for ' + SPORT);
}
Then open the project gear (Project Settings) and under Script properties add:
| Property | Value |
|---|---|
PINNFEED_KEY | your key (pf_live_…) |
PINNFEED_BASE | https://pinnfeed.io (optional; that is the default) |
Never paste the key into the spreadsheet cells or share the Apps Script project publicly.
Step 4 — Run it once and authorize#
- Select
refreshOddsin the function dropdown and click Run. - Google asks for permission to connect to an external service and edit the sheet. Approve it for your account.
- Reload the spreadsheet. The Odds tab should show columns
Starts,League,Home,Away,1,X,2, the three fair prices,MarginandEvent id.
A failed call (bad key, rate limit, sport not in plan) logs a warning and leaves the previous rows alone, so a bad minute never blanks the board.
Step 5 — Add a 15-minute trigger#
- In Apps Script, open Triggers (clock icon) → Add Trigger.
- Function:
refreshOdds. Event source: Time-driven. Type: Minutes timer. Interval: Every 15 minutes. - Save.
That is 96 calls a day for one sport, well inside the Free plan's 1,000 requests/day and 1 req/s. Two sports every 30 minutes is also 96. See plans and rate limits.
| Trigger | Calls / day | Fits Free? |
|---|---|---|
| Every 15 min, 1 sport | 96 | yes |
| Every 30 min, 2 sports | 96 | yes |
| Every 15 min, 2 sports | 192 | yes |
| Every 1 min, 1 sport | 1,440 | no — needs Hobby or above (no daily quota) |
Apps Script has its own quotas on top; the shortest time trigger is one minute.
Step 6 — Read fair prices and margins#
Each moneyline row carries PinnFeed's no-vig fields from the schema:
Fair 1/Fair X/Fair 2— thefairdecimal price on each outcome (margin removed, multiplicative method).Margin— the market'smargin(overround = sum of implied probabilities − 1).
Use those columns when you compare a soft-book price in another tab: expected value is soft × (1 / fair) − 1, the same math as the value betting guide. For live steam moves, Sheets is the wrong tool; use the WebSocket on Builder and the steam detection guide.
Questions people ask#
Which PinnFeed plan do I need?#
Free is enough for a delayed board refreshed every few minutes. Hobby (€29/month) removes the daily quota and the 30-second delay but is still REST-only. Real-time push needs Builder (€99/month).
Why does the script use x-api-key instead of a query string?#
That is the preferred auth header in the docs. Apps Script's UrlFetchApp can set headers, so there is no reason to put the key in the URL (where it can leak into logs).
Can I chart price history?#
Yes. Keep the Odds tab as a live board (cleared and rewritten each run). Add a second tab that only appends rows after each refresh, then insert a line chart. Do not clear the history tab.
Is this educational content for adults?#
Yes. Odds data and spreadsheet tooling are educational and for adults (18+). Nothing here is a tip, a sure bet or a promise of profit.
Related#
- REST API reference —
/v1/oddsfilters and pagination - Value betting with no-vig fair odds
- Telegram picks bot — if you outgrow a sheet
- Pricing