Skip to content

Commit c637218

Browse files
Mlaz-codeclaude
andcommitted
feat: auto-generate sportsbooks docs from API at build time
Add scripts/generate-sportsbooks.mjs that fetches /api/v1/sportsbooks during Vercel builds and regenerates the category tables and tier summary in sportsbooks.mdx. Fails gracefully if the API is down. Removes stale "Coming Soon" section since the API is now the source of truth. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 30e26ce commit c637218

3 files changed

Lines changed: 197 additions & 21 deletions

File tree

content/en/api-reference/sportsbooks.mdx

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -392,13 +392,15 @@ The `event_count` may be 0 and `last_update` may be `null` if a sportsbook is te
392392

393393
The `requires_tier` field indicates the minimum subscription tier needed to access a sportsbook's odds data through the API.
394394

395+
{/* AUTO:START:tier-summary */}
395396
| Tier | Books Available | Included Sportsbooks |
396397
|------|-----------------|----------------------|
397-
| **Free** | 2 | DraftKings, FanDuel |
398-
| **Hobby** | 5 | + BetMGM, Caesars, theScore Bet |
399-
| **Pro** | 15 | + Bet365, BetRivers, and more |
400-
| **Sharp** | All | All available sportsbooks |
401-
| **Enterprise** | All | All available sportsbooks |
398+
| **Free** | 23 | Kalshi, BetMGM, DraftKings, Bet365, and 19 more |
399+
| **Hobby** | 23 | Same as Free |
400+
| **Pro** | 23 | Same as Hobby |
401+
| **Sharp** | 25 | All available sportsbooks |
402+
| **Enterprise** | 25 | All available sportsbooks |
403+
{/* AUTO:END:tier-summary */}
402404

403405
<Callout type="warning">
404406
**Pinnacle**, **Bookmaker**, **ProphetX**, and **Betfair** require **Sharp tier or higher**. Pinnacle is a sharp book whose efficient lines are used as the reference for +EV calculations. Requesting sharp book odds on Free, Hobby, or Pro tier will return a `403 tier_restricted` error.
@@ -417,55 +419,68 @@ The `requires_tier` field indicates the minimum subscription tier needed to acce
417419

418420
### Major US Books
419421

422+
{/* AUTO:START:us-books */}
420423
| ID | Name | Live | Props | Tier |
421424
|----|------|------|-------|------|
425+
| `ballybet` | Bally Bet | Yes | No | Free |
426+
| `betmgm` | BetMGM | Yes | Yes | Free |
427+
| `betonline` | BetOnline | Yes | Yes | Free |
428+
| `betrivers` | BetRivers | Yes | Yes | Free |
429+
| `bovada` | Bovada | Yes | Yes | Free |
430+
| `caesars` | Caesars | Yes | Yes | Free |
422431
| `draftkings` | DraftKings | Yes | Yes | Free |
432+
| `fanatics` | Fanatics | Yes | Yes | Free |
423433
| `fanduel` | FanDuel | Yes | Yes | Free |
424-
| `betmgm` | BetMGM | Yes | Yes | Hobby |
425-
| `caesars` | Caesars | Yes | Yes | Hobby |
426-
| `espnbet` | theScore Bet | Yes | Yes | Hobby |
427-
| `betrivers` | BetRivers | Yes | Yes | Pro |
434+
| `fliff` | Fliff | Yes | Yes | Free |
435+
| `novig` | Novig | No | No | Free |
428436
| `rebet` | Rebet | Yes | Yes | Free |
437+
| `thescorebet` | theScore Bet | Yes | Yes | Free |
438+
{/* AUTO:END:us-books */}
429439

430440
### Sharp Books
431441

442+
{/* AUTO:START:sharp-books */}
432443
| ID | Name | Live | Props | Tier |
433444
|----|------|------|-------|------|
434445
| `pinnacle` | Pinnacle | Yes | Yes | **Sharp** |
435-
| `bookmaker` | Bookmaker | Yes | No | **Sharp** |
446+
{/* AUTO:END:sharp-books */}
436447

437448
### International
438449

450+
{/* AUTO:START:intl-books */}
439451
| ID | Name | Live | Props | Tier |
440452
|----|------|------|-------|------|
441-
| `bet365` | Bet365 | Yes | Yes | Pro |
453+
| `bet105` | Bet105 | Yes | Yes | Free |
454+
| `bet365` | Bet365 | Yes | Yes | Free |
442455
| `betway` | Betway | Yes | Yes | Free |
456+
| `ladbrokes` | Ladbrokes | Yes | Yes | Free |
457+
| `saba` | SABA | Yes | No | Free |
443458
| `skybet` | Sky Bet | Yes | No | Free |
459+
| `stake` | Stake | Yes | Yes | Free |
460+
| `unibet` | Unibet | Yes | Yes | Free |
461+
{/* AUTO:END:intl-books */}
444462

445463
### Exchanges
446464

465+
{/* AUTO:START:exchange-books */}
447466
| ID | Name | Live | Props | Tier |
448467
|----|------|------|-------|------|
449468
| `prophetx` | ProphetX | Yes | No | **Sharp** |
450-
| `betfair` | Betfair | Yes | No | **Sharp** |
469+
{/* AUTO:END:exchange-books */}
451470

452471
### Prediction Markets
453472

473+
{/* AUTO:START:prediction-books */}
454474
| ID | Name | Live | Props | Tier |
455475
|----|------|------|-------|------|
456-
| `polymarket` | Polymarket | No | No | Free |
457476
| `kalshi` | Kalshi | Yes | No | Free |
477+
| `polymarket` | Polymarket | No | No | Free |
478+
{/* AUTO:END:prediction-books */}
458479

459480
<Callout type="info">
460481
**Polymarket** and **Kalshi** are prediction market platforms. Unlike traditional sportsbooks, they use binary outcome contracts priced between $0 and $1. SharpAPI normalizes contract prices into standard odds formats (American, decimal, implied probability) so you can compare them directly with sportsbook odds. Kalshi is CFTC-regulated.
461482
</Callout>
462483

463-
### Coming Soon
464-
465-
| ID | Name | Category |
466-
|----|------|----------|
467-
| `fanatics` | Fanatics | Major US |
468-
469484
## Sharp vs Soft Books
470485

471486
### Sharp Books

scripts/generate-sportsbooks.mjs

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Fetches sportsbook data from the SharpAPI and regenerates the
5+
* dynamic tables in content/en/api-reference/sportsbooks.mdx.
6+
*
7+
* Sections between {/* AUTO:START:<name> *\/} and {/* AUTO:END:<name> *\/}
8+
* markers are replaced. Everything else is left untouched.
9+
*
10+
* Usage: node scripts/generate-sportsbooks.mjs
11+
*/
12+
13+
import { readFileSync, writeFileSync } from 'node:fs';
14+
import { resolve, dirname } from 'node:path';
15+
import { fileURLToPath } from 'node:url';
16+
17+
const __dirname = dirname(fileURLToPath(import.meta.url));
18+
const MDX_PATH = resolve(__dirname, '../content/en/api-reference/sportsbooks.mdx');
19+
const API_URL = 'https://api.sharpapi.io/api/v1/sportsbooks';
20+
21+
// -- Classification ----------------------------------------------------------
22+
23+
const EXCHANGES = new Set(['prophetx', 'betfair']);
24+
const PREDICTION_MARKETS = new Set(['polymarket', 'kalshi']);
25+
26+
function classify(book) {
27+
if (PREDICTION_MARKETS.has(book.id)) return 'prediction';
28+
if (EXCHANGES.has(book.id)) return 'exchange';
29+
if (book.is_sharp) return 'sharp';
30+
const regions = book.regions.map(r => r.toUpperCase());
31+
if (!regions.includes('US')) return 'international';
32+
return 'us';
33+
}
34+
35+
// -- Table Helpers -----------------------------------------------------------
36+
37+
function yn(val) { return val ? 'Yes' : 'No'; }
38+
39+
function tierLabel(tier) {
40+
if (!tier || tier === 'free') return 'Free';
41+
return tier.charAt(0).toUpperCase() + tier.slice(1);
42+
}
43+
44+
function tierDisplay(tier) {
45+
const label = tierLabel(tier);
46+
return label === 'Sharp' ? '**Sharp**' : label;
47+
}
48+
49+
function bookTable(books) {
50+
const rows = books
51+
.sort((a, b) => {
52+
// Sort by tier weight then alphabetically
53+
const tw = { free: 0, hobby: 1, pro: 2, sharp: 3, enterprise: 4 };
54+
const ta = tw[a.requires_tier] ?? 0;
55+
const tb = tw[b.requires_tier] ?? 0;
56+
if (ta !== tb) return ta - tb;
57+
return a.display_name.localeCompare(b.display_name);
58+
})
59+
.map(b =>
60+
`| \`${b.id}\` | ${b.display_name} | ${yn(b.has_live_odds)} | ${yn(b.has_player_props)} | ${tierDisplay(b.requires_tier)} |`
61+
);
62+
63+
return [
64+
'| ID | Name | Live | Props | Tier |',
65+
'|----|------|------|-------|------|',
66+
...rows,
67+
].join('\n');
68+
}
69+
70+
// -- Tier Summary ------------------------------------------------------------
71+
72+
function tierSummary(books) {
73+
const free = books.filter(b => !b.requires_tier || b.requires_tier === 'free');
74+
const hobby = books.filter(b => b.requires_tier === 'hobby');
75+
const pro = books.filter(b => b.requires_tier === 'pro');
76+
const sharp = books.filter(b => b.requires_tier === 'sharp');
77+
78+
const freeCount = free.length;
79+
const hobbyCount = freeCount + hobby.length;
80+
const proCount = hobbyCount + pro.length;
81+
const totalCount = books.length;
82+
83+
const freeNames = free.slice(0, 4).map(b => b.display_name).join(', ');
84+
const freeSuffix = free.length > 4 ? `, and ${free.length - 4} more` : '';
85+
const hobbyNames = hobby.length ? hobby.map(b => b.display_name).join(', ') : '';
86+
const proNames = pro.length ? pro.map(b => b.display_name).join(', ') : '';
87+
88+
return [
89+
'| Tier | Books Available | Included Sportsbooks |',
90+
'|------|-----------------|----------------------|',
91+
`| **Free** | ${freeCount} | ${freeNames}${freeSuffix} |`,
92+
`| **Hobby** | ${hobbyCount} | ${hobbyNames ? `+ ${hobbyNames}` : 'Same as Free'} |`,
93+
`| **Pro** | ${proCount} | ${proNames ? `+ ${proNames}` : 'Same as Hobby'} |`,
94+
`| **Sharp** | ${totalCount} | All available sportsbooks |`,
95+
`| **Enterprise** | ${totalCount} | All available sportsbooks |`,
96+
].join('\n');
97+
}
98+
99+
// -- MDX Replacement ---------------------------------------------------------
100+
101+
function replaceSection(content, name, replacement) {
102+
// Matches {/* AUTO:START:<name> */} ... {/* AUTO:END:<name> */}
103+
const pattern = new RegExp(
104+
`(\\{/\\* AUTO:START:${name} \\*/\\})\n[\\s\\S]*?\n(\\{/\\* AUTO:END:${name} \\*/\\})`,
105+
'm'
106+
);
107+
if (!pattern.test(content)) {
108+
console.error(`Warning: marker AUTO:START:${name} / AUTO:END:${name} not found in MDX`);
109+
return content;
110+
}
111+
return content.replace(pattern, `$1\n${replacement}\n$2`);
112+
}
113+
114+
// -- Main --------------------------------------------------------------------
115+
116+
async function main() {
117+
console.log(`Fetching sportsbooks from ${API_URL}...`);
118+
119+
const res = await fetch(API_URL);
120+
if (!res.ok) {
121+
// Non-fatal: keep existing MDX so builds don't break if API is down
122+
console.error(`API returned ${res.status} — skipping sportsbook generation, keeping existing content.`);
123+
process.exit(0);
124+
}
125+
126+
const { data: books } = await res.json();
127+
console.log(`Received ${books.length} sportsbooks from API.`);
128+
129+
// Filter out unlisted books
130+
const active = books.filter(b => !b.unlisted);
131+
132+
// Classify
133+
const grouped = { us: [], sharp: [], international: [], exchange: [], prediction: [] };
134+
for (const book of active) {
135+
grouped[classify(book)].push(book);
136+
}
137+
138+
// Read existing MDX
139+
let mdx = readFileSync(MDX_PATH, 'utf-8');
140+
141+
// Replace tier summary
142+
mdx = replaceSection(mdx, 'tier-summary', tierSummary(active));
143+
144+
// Replace each category table
145+
if (grouped.us.length) mdx = replaceSection(mdx, 'us-books', bookTable(grouped.us));
146+
if (grouped.sharp.length) mdx = replaceSection(mdx, 'sharp-books', bookTable(grouped.sharp));
147+
if (grouped.international.length) mdx = replaceSection(mdx, 'intl-books', bookTable(grouped.international));
148+
if (grouped.exchange.length) mdx = replaceSection(mdx, 'exchange-books', bookTable(grouped.exchange));
149+
if (grouped.prediction.length) mdx = replaceSection(mdx, 'prediction-books', bookTable(grouped.prediction));
150+
151+
writeFileSync(MDX_PATH, mdx);
152+
console.log(`Updated ${MDX_PATH}`);
153+
154+
// Summary
155+
console.log(` US: ${grouped.us.length}, Sharp: ${grouped.sharp.length}, International: ${grouped.international.length}, Exchanges: ${grouped.exchange.length}, Prediction: ${grouped.prediction.length}`);
156+
}
157+
158+
main().catch(err => {
159+
console.error('Sportsbook generation failed (non-fatal):', err.message);
160+
process.exit(0); // Don't break the build
161+
});

vercel.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
2-
"buildCommand": "pnpm install && pnpm build",
2+
"buildCommand": "pnpm install && node scripts/generate-sportsbooks.mjs && pnpm build",
33
"outputDirectory": "out",
44
"framework": null,
55
"cleanUrls": true,
6-
"ignoreCommand": "git diff --quiet HEAD^ HEAD -- app/ components/ content/ pages/ public/ styles/ theme.config.tsx next.config.mjs proxy.ts package.json vercel.json",
6+
"ignoreCommand": "git diff --quiet HEAD^ HEAD -- app/ components/ content/ pages/ public/ scripts/ styles/ theme.config.tsx next.config.mjs proxy.ts package.json vercel.json",
77
"redirects": [
88
{ "source": "/", "destination": "/en", "permanent": false },
99
{ "source": "/streaming", "destination": "/en/streaming/overview", "permanent": true },

0 commit comments

Comments
 (0)