npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

owls-insight-ts

v0.66.0

Published

Official TypeScript SDK for the Owls Insight real-time sports betting odds API

Readme

Owls Insight SDK

Official TypeScript/JavaScript SDK for the Owls Insight real-time sports betting odds API.

Features

  • Full TypeScript types for all endpoints and events
  • REST client for odds, scores, props, history, and stats
  • WebSocket client for real-time streaming updates
  • Works in Node.js, Bun, Deno, and modern browsers
  • ESM and CommonJS support

Installation

npm install owls-insight-ts

Quick Start

import { OwlsInsight } from "owls-insight-ts";

const client = new OwlsInsight({ apiKey: process.env.OWLS_INSIGHT_API_KEY! });

// Get NBA odds from all books
const odds = await client.rest.getOdds("nba");

// odds.data is keyed by book: { pinnacle: [...], fanduel: [...], ... }
for (const [book, events] of Object.entries(odds.data)) {
  console.log(`${book}: ${events.length} events`);
}

// Get odds from specific books
const filtered = await client.rest.getOdds("nba", {
  books: ["pinnacle", "fanduel"],
});

REST API

Odds

// All markets (moneyline + spreads + totals)
const odds = await client.rest.getOdds("nba");

// Single market
const ml = await client.rest.getMoneyline("nba");
const spreads = await client.rest.getSpreads("nfl");
const totals = await client.rest.getTotals("nhl");

// ...or narrow the full board to any one market, including the ones without
// their own route (first_half_totals, corners, btts, 1st_set_winner, ...)
const firstHalf = await client.rest.getOdds("nfl", { market: "first_half_totals" });

// With filters
const filtered = await client.rest.getOdds("soccer", {
  books: ["pinnacle", "bet365"],
  league: "England - Premier League",
  alternates: true,
});

// A parameter name the endpoint does not read, or a market it does not
// recognise, is reported rather than rejected: you get the board it could
// serve plus the names it ignored. Check this before concluding a board was
// filtered. (A `books` value with no valid book key still answers 400.)
if (filtered.meta.ignored_params) {
  console.log(filtered.meta.ignored_params, filtered.meta.ignored_reasons);
}

// Teams carry a stable id, the same across every book and spelling, so you can
// join on it rather than on the name. Absent for names not on a roster we carry.
for (const ev of odds.data.pinnacle ?? []) {
  console.log(ev.home_team_id, "vs", ev.away_team_id);
}

Esports (CS2, Valorant, LoL)

const cs2 = await client.rest.getOdds("cs2");
const valorant = await client.rest.getOdds("valorant");
const lol = await client.rest.getOdds("lol");

CS2 odds from 1xBet. Valorant and LoL from both 1xBet and Pinnacle. Esports use a separate WebSocket event (esports-update).

Live Scores

// All sports
const allScores = await client.rest.getScores();

// Specific sport
const nbaScores = await client.rest.getScores("nba");

Player Props

// All books
const props = await client.rest.getProps("nba");

// Filtered
const filtered = await client.rest.getProps("nba", {
  player: "LeBron",
  category: "points",
  books: ["pinnacle", "fanduel"],
});

// Single book (typed per-book response)
const fdProps = await client.rest.getBookProps("nba", "fanduel");
const mgmProps = await client.rest.getBookProps("nba", "betmgm"); // BetMGMPropsResponse
// ⚠️ Deprecated (2026-05-31) — the v1 Bet365 props endpoint was retired and now
// returns HTTP 410. Bet365 raw GAME LINES (no player props) are available via
// getBet365V2(sport) for nba / mlb / nhl / soccer.
const b365Props = await client.rest.getBookProps("nba", "bet365"); // Bet365PropsResponse

// Props line movement history
const propsHistory = await client.rest.getPropsHistory("nba", {
  game_id: "game_123",
  player: "LeBron James",
  category: "points",
  book: "pinnacle",
  hours: 24,
});

// Props cache statistics
const propsStats = await client.rest.getPropsStats();
// ⚠️ Deprecated (2026-05-31) — the v1 Bet365 props stats endpoint was retired (HTTP 410).
const bet365Stats = await client.rest.getBookPropsStats("bet365");

Box Scores & Stats

// NBA box scores for today
const stats = await client.rest.getStats("nba");

// Filter by date and player
const filtered = await client.rest.getStats("nba", {
  date: "2026-03-05",
  player: "LeBron",
});

Real-Time Odds

Sub-second sharp Pinnacle odds via a dedicated realtime stream. Available for all sports.

const realtime = await client.rest.getRealtime("nba");

console.log(realtime.meta.source);    // "pinnacle"
console.log(realtime.meta.events);    // number of events
console.log(realtime.meta.available); // true if data exists
console.log(realtime.meta.freshness); // { ageSeconds, stale }

The freshness object indicates data age. Data is considered stale after 30 seconds.

PS3838 Real-Time

Independent real-time stream from PS3838 (Pinnacle whitelabel). Same shape as /realtime, but sourced from a separate WebSocket pipeline so customers can compare or fall back.

const ps3838 = await client.rest.getPS3838Realtime("nba");
console.log(ps3838.meta.source);  // "ps3838"
console.log(ps3838.data.length);  // events with PS3838 markets

Esports Real-Time

Per-game realtime from Pinnacle's esports feed. Available for CS2, LoL, Valorant, Dota2. A league filter is required — pick the tournament you want (e.g. "BLAST", "LEC", "VCT").

const cs2 = await client.rest.getEsportsRealtime("cs2", "BLAST");
const lec = await client.rest.getEsportsRealtime("lol", "LEC");

ProphetX (Exchange)

ProphetX is a betting exchange with order-book liquidity per outcome. The endpoint returns the raw ProphetX JSON structure — no canonical normalization.

const px = await client.rest.getProphetxOdds({
  sport: "basketball",          // raw ProphetX sport slug (or array)
  kind: "game",                 // optional: "game" | "prop"
});

console.log(px.data.totalEvents); // raw ProphetX event count
for (const [sport, events] of Object.entries(px.data.sports)) {
  console.log(`${sport}: ${events.length} events`);
}

Each markets entry (keyed "{market_id}:{line}") is typed as ProphetXMarket. The order book is an array of sides in outcomes order, each side its price levels best-first or null when nothing is resting. On spread and total deltas the book sits under marketLines[0] rather than the top level, so use the helpers:

import { prophetxBook, prophetxOutcomes, prophetxBest } from "owls-insight-ts";

for (const [key, market] of Object.entries(events[0].markets)) {
  const outcomes = prophetxOutcomes(market);          // labels, one per side
  prophetxBook(market).forEach((side, i) => {
    const best = prophetxBest(market, i);
    if (!best) return;                                  // null side = nothing resting
    // best.value = amount you can bet now; best.odds = American; payout from those two, not from stake
    console.log(key, outcomes[i]?.name, best.odds, best.value);
  });
}

Odds History

// Line movement history
const history = await client.rest.getOddsHistory({
  eventId: "event_123",
  book: "pinnacle",
  market: "spreads",
  side: "home",
  hours: 24,
});

Historical Archive

Retries are opt-in for direct calls: new OwlsInsight({ apiKey, retry: { maxRetries: 3 } }) retries 429 and 503 with Retry-After or exponential backoff. The paging helpers below retry by default.

// List completed games
const games = await client.rest.getHistoryGames({
  sport: "nba",
  startDate: "2026-03-01",
  endDate: "2026-03-06",
});

// Get archived odds snapshots (one page; limit up to 5000, offset to page)
const snapshots = await client.rest.getHistoryOdds({
  eventId: "event_123",
  book: "pinnacle",
  market: "spreads",
});

// Export a whole game's history: pages sequentially, stops on a short page,
// retries 429/503 per page honouring Retry-After. Do not fan out one request
// per book or market in parallel: historical endpoints share a per-key
// in-flight allowance (3 on MVP, 4 on Hall of Fame) and the client queues
// history requests to stay under it (`historyConcurrency` option).
for await (const s of client.rest.iterHistoryOdds({ eventId: "event_123", book: "pinnacle" })) {
  rows.push(s);
}

// Get archived props snapshots
const propsSnapshots = await client.rest.getHistoryProps({
  eventId: "event_123",
  playerName: "LeBron James",
  propType: "points",
});

// Get archived player stats
const playerStats = await client.rest.getHistoryStats({
  sport: "nba",
  playerName: "LeBron James",
  startDate: "2026-01-01",
  endDate: "2026-03-01",
});

// Get archived tennis match stats
const tennisStats = await client.rest.getHistoryTennisStats("event_123");

// CS2 match history
const cs2Matches = await client.rest.getCS2Matches({
  team: "Natus Vincere",
  stars: 4,
});

// Full match detail with map scores and player stats
const matchDetail = await client.rest.getCS2Match(2374324);

// Player stats across matches
const cs2Players = await client.rest.getCS2Players({
  playerName: "s1mple",
  minRating: 1.3,
});

// Closing odds per sportsbook (9 books, 2016-present)
const closing = await client.rest.getClosingOdds({
  sport: "nba",
  startDate: "2026-03-01",
  endDate: "2026-03-10",
});

// Historical player props (4 books, 2022-present)
const histProps = await client.rest.getHistoricalPlayerProps({
  sport: "nba",
  player: "LeBron James",
  propType: "points",
});

// Public betting percentages (2019-present)
const publicBetting = await client.rest.getPublicBetting({
  sport: "nba",
  limit: 50,
});

Kalshi Prediction Markets

⚠️ Deprecated (2026-05-31) — The v1 Kalshi market endpoints below were retired and now return HTTP 410 Gone (these SDK methods throw at runtime). Use the v2 raw pass-through endpoints instead: getKalshiV2(sport, { league }) and getKalshiV2Leagues(sport). See Kalshi v2 below.

Kalshi player props are unaffected — they remain available through the aggregated getProps(sport) endpoint.

// ⚠️ Deprecated — all three now throw HTTP 410. Use getKalshiV2 instead.
const markets = await client.rest.getKalshiMarkets("nba", {
  status: "open",
});

// Browse series (e.g., KXNBAGAME, KXNFLSBMVP)
const series = await client.rest.getKalshiSeries();

// Markets within a series
const nbaGame = await client.rest.getKalshiSeriesMarkets("KXNBAGAME");
// ✅ v2 replacement
const leagues = await client.rest.getKalshiV2Leagues("nba"); // discover series tickers
const k = await client.rest.getKalshiV2("nba", { league: "kxnbagame" });

Polymarket

⚠️ Deprecated (2026-05-31)getPolymarketMarkets() was retired and now returns HTTP 410 Gone (the SDK method throws at runtime). Use the v2 raw pass-through endpoints instead: getPolymarketV2(sport, { league }) and getPolymarketV2Leagues(sport). See Polymarket v2 below.

Kalshi and Polymarket are no longer included as bookmakers in the standard /odds response — fetch their game-line data through the v2 endpoints.

// ⚠️ Deprecated — now throws HTTP 410. Use getPolymarketV2 instead.
const markets = await client.rest.getPolymarketMarkets("nba");
// ✅ v2 replacement
const leagues = await client.rest.getPolymarketV2Leagues("soccer"); // discover tag slugs
const pm = await client.rest.getPolymarketV2("soccer", { league: "epl" });

Betting Splits

const splits = await client.rest.getSplits("nba");

v2 Raw Pass-Through

The v2 endpoints expose each book's raw market shape without canonical mapping. Use these when you need the source book's full market depth (alt lines, every prop type, micro markets) instead of the normalized 3-market view in /odds.

Each v2 source has:

  • A REST endpoint for the current snapshot
  • A WebSocket event ({book}-v2-update) for sparse deltas — opt in via client.ws.subscribe({ v2: { [book]: { [sport]: '*' } } })
  • An optional /leagues endpoint to list available league slugs

For books that require a league parameter (Bet365 soccer, DraftKings, FanDuel, Polymarket, Kalshi, theScore soccer and tennis), call get{Book}V2Leagues(sport) first — slugs change with the calendar and hard-coding can leave you with empty 200 responses.

Bet365 v2

const nba = await client.rest.getBet365V2("nba");

// Soccer leagues are geographic buckets (uk, spain, italy, germany,
// france, uefa, americas, europe, australia, top-leagues, rest-of-world).
const ukSoccer = await client.rest.getBet365V2("soccer", { league: "uk" });
const leagues = await client.rest.getBet365V2Leagues("soccer");

Sports: nba, mlb, nhl, soccer

DraftKings v2

const dk = await client.rest.getDraftKingsV2("soccer", { league: "epl" });
const leagues = await client.rest.getDraftKingsV2Leagues("soccer");

Sports: soccer

FanDuel v2

const fd = await client.rest.getFanDuelV2("nba");
const leagues = await client.rest.getFanDuelV2Leagues("soccer");

Sports: soccer, mlb, nba, nhl

MyBookie v2

const mb = await client.rest.getMyBookieV2("nba");

Sports: mlb, nba

Thunderpick v2

const tp = await client.rest.getThunderpickV2("cs2");

Sports: baseball, basketball, tennis, cs2, dota2, lol, valorant

Underdog v2

Raw Underdog Fantasy pick'em lines (higher/lower player props). REST-only.

const ud = await client.rest.getUnderdogV2("nba");
console.log(ud.count, "lines");
for (const line of ud.data?.over_under_lines ?? []) {
  const higher = line.options.find((o) => o.choice === "higher");
  console.log(line.over_under.title, line.stat_value, higher?.american_price);
}

Sports: nba, wnba, mlb, nhl, nfl, soccer, tennis, cs2, lol, valorant, pga, mma, kbo, npb

theScore Bet v2

Raw theScore Bet boards. theScore is Penn/Score Media's Ontario, Canada book, so its prices are an Ontario market view rather than a US one. REST-only, no WebSocket event. The feed cycles about every 20 seconds and meta.status flips to stale past a 60 second freshness bar — gate on 60, not on 20.

data is keyed by competition, not by event, and each value is theScore's raw GraphQL competitionSection — a page layout tree. The games sit under the MarketplaceShelf section child, one GridMarketCard each, so filter by __typename rather than by array position.

const mlb = await client.rest.getTheScoreV2("mlb");
for (const section of Object.values(mlb.data)) {
  for (const child of section.sectionChildren ?? []) {
    if (child.__typename !== "MarketplaceShelf") continue;
    for (const card of child.marketplaceShelfChildren ?? []) {
      for (const market of card.pagedMarkets ?? []) {
        for (const sel of market.selections ?? []) {
          // name is an OBJECT — `sel.name` alone prints "[object Object]"
          console.log(market.name, sel.name?.defaultName, sel.odds?.formattedOdds, sel.points?.formattedPoints);
        }
      }
    }
  }
}

Three things to know.

Odds are decimal, expressed as an exact rational. numeratorLong / denominatorLong is the decimal price — { numeratorLong: "43", denominatorLong: "20" } is 2.15, which is +115. These are not UK fractional odds, so do not add 1; that single mistake corrupts every price (measured on 650 live NFL selections, the decimal reading matched formattedOdds 650/650 and the fractional reading 0/650). Because the rational is exact, it is the field to use when precision matters.

formattedOdds is not always numeric. It is "Even" for a 2/1 rational (decimal 2.0, American +100) — 67 of 1,596 selections across five sports. parseInt gives NaN there.

A selection's name is an object, not a string: read name.defaultName.

mlb, nfl, ncaaf, nba, nhl are single-league, so omit league. soccer and tennis require it — omitting it is a 400, not an empty 200:

const leagues = await client.rest.getTheScoreV2Leagues("soccer"); // 100+ competitions
const epl = await client.rest.getTheScoreV2("soccer", { league: "premier-league" });

Sports: mlb, nfl, ncaaf, nba, nhl (single-league), soccer, tennis (require league)

Bookmaker.eu v2

Raw Bookmaker.eu lines boards: game lines (priced moneylines; spread/total lines carry no vig on the source), priced futures/awards boards, and priced series/proposition markets. REST-only. Not a live source: upstream pages regenerate every 25-60 minutes; each market carries its pageLastUpdate stamp.

const { leagues } = await client.rest.getBookmakerV2Leagues("basketball");
const nba = await client.rest.getBookmakerV2("basketball", { league: "nba" });
const futures = await client.rest.getBookmakerV2("basketball", { league: "nba-futures" });

Sport groups (Bookmaker's own slugs): aussie-rules, baseball, basketball, boxing, cricket, darts, e-gaming, entertainment, esports, football, futsal, golf, ice-hockey, mma, motorsport, politics, rugby, soccer, surfing, table-tennis, tennis

Kalshi v2

const k = await client.rest.getKalshiV2("nba", { league: "kxnbagame" });
const leagues = await client.rest.getKalshiV2Leagues("nba");

// Esports — discover the active series ticker first, then fetch it.
const cs2Leagues = await client.rest.getKalshiV2Leagues("cs2");
const cs2 = await client.rest.getKalshiV2("cs2", { league: "kxcs2game" });

Sports: nba, nhl, mlb, soccer, tennis, plus esports — cs2, dota2, lol, valorant, overwatch, rainbow6, rocketleague, pubg

Esports markets are tournament-driven: a game with no active event returns an empty leagues array (meta.status: "no-data"), not an error. Always call getKalshiV2Leagues(sport) first to get the live series tickers.

Polymarket v2

const pm = await client.rest.getPolymarketV2("soccer", { league: "epl" });
const leagues = await client.rest.getPolymarketV2Leagues("soccer");

// Esports — each game maps to one canonical Gamma tag slug.
const cs2 = await client.rest.getPolymarketV2("cs2", { league: "counter-strike-2" });
const lol = await client.rest.getPolymarketV2("lol", { league: "league-of-legends" });

Sports: nba, nhl, mlb, soccer, tennis, plus esports — cs2, dota2, lol, valorant, overwatch, rainbow6, rocketleague, pubg

Esports league slugs: cs2counter-strike-2, dota2dota-2, lolleague-of-legends, valorantvalorant. Dormant games (e.g. rainbow6/rocketleague/pubg between tournaments) return an empty leagues array. Always call getPolymarketV2Leagues(sport) to confirm.

Hard Rock v2

const leagues = await client.rest.getHardRockLeagues("az", "ICE_HOCKEY");
const nhl = await client.rest.getHardRockEvents("az", "ICE_HOCKEY", "nhl");

// Small sport — no league needed
const tennis = await client.rest.getHardRockEvents("fl", "TENNIS");

States: az, fl. Sports use Hard Rock's enum naming (BASKETBALL, BASEBALL, ICE_HOCKEY, SOCCER, TENNIS, etc.). Big sports require a league slug; list via getHardRockLeagues.

WebSocket Streaming

Real-time odds updates via WebSocket.

Connect & Subscribe

const client = new OwlsInsight({ apiKey: process.env.OWLS_INSIGHT_API_KEY! });

await client.ws.connect();

// Subscribe to sports and books
client.ws.subscribe({
  sports: ["nba", "nfl"],
  books: ["pinnacle", "fanduel", "draftkings"],
});

// Listen for odds updates
client.ws.on("odds-update", (data) => {
  console.log(`Update at ${data.timestamp}`);
  // data.last_odds_change — when prices actually changed (vs heartbeat)
  for (const [sport, events] of Object.entries(data.sports)) {
    console.log(`${sport}: ${events.length} events`);
    for (const event of events) {
      console.log(`  ${event.away_team} @ ${event.home_team}`);
    }
  }
});

Update Subscription

// Update an existing subscription
client.ws.updateSubscription({ sports: ["nba", "nhl", "soccer"] });

Props Streaming

// Subscribe to Pinnacle props for specific sports
client.ws.subscribeProps({ sports: ["nba", "nhl"] });

// Subscribe to a specific book's props
client.ws.subscribeProps({ sports: ["nba"] }, "fanduel");

// Filter by categories
client.ws.subscribeProps({
  sports: ["nba"],
  categories: ["points", "rebounds", "assists"],
});

// Subscribe to all books
client.ws.subscribeProps({ sports: ["nba"] });                    // Pinnacle
client.ws.subscribeProps({ sports: ["nba"] }, "fanduel");         // FanDuel
client.ws.subscribeProps({ sports: ["nba"] }, "draftkings");      // DraftKings
client.ws.subscribeProps({ sports: ["nba"] }, "bet365");          // Bet365
client.ws.subscribeProps({ sports: ["nba"] }, "betmgm");          // BetMGM
client.ws.subscribeProps({ sports: ["nba"] }, "caesars");          // Caesars

client.ws.on("player-props-update", (data) => {
  console.log("Pinnacle props:", data.sports);
});

client.ws.on("fanduel-props-update", (data) => {
  console.log("FanDuel props:", data.sports);
});

// Confirmation events
client.ws.on("props-subscribed", (sub) => {
  console.log("Pinnacle props active:", sub);
});

// Unsubscribe
client.ws.unsubscribeProps();           // Pinnacle
client.ws.unsubscribeProps("fanduel");  // FanDuel

Real-Time Pinnacle

Pushed automatically — no subscription needed.

client.ws.on("pinnacle-realtime", (data) => {
  // data is keyed by sport: { nba: [...], tennis: [...], timestamp: "..." }
  for (const [sport, events] of Object.entries(data)) {
    if (sport === "timestamp" || !Array.isArray(events)) continue;
    console.log(`${sport}: ${events.length} events`);
    for (const event of events) {
      console.log(`  ${event.away_team} @ ${event.home_team}`);
      const pinnacle = event.bookmakers.find((b: any) => b.key === "pinnacle");
      for (const market of pinnacle?.markets || []) {
        console.log(`    ${market.key}:`, market.outcomes);
      }
    }
  }
});

Real-Time PS3838

Independent realtime stream from PS3838 (Pinnacle whitelabel). Same payload shape as pinnacle-realtime but sourced from a separate WS pipeline — use as a comparison source or fallback.

client.ws.on("ps3838-realtime", (data) => {
  // Same shape as pinnacle-realtime
});

Per-Sport Esports Realtime

Per-game realtime events from Pinnacle's esports feed. Subscribe via esportsRealtime with a league filter per game (required — no wildcard).

client.ws.subscribe({
  esportsRealtime: {
    cs2:      { league: "BLAST" },
    lol:      { league: "LEC" },
    valorant: { league: "VCT" },
  },
});

client.ws.on("cs2-realtime", (data) => console.log("CS2:", data));
client.ws.on("lol-realtime", (data) => console.log("LoL:", data));
client.ws.on("valorant-realtime", (data) => console.log("Valorant:", data));
client.ws.on("dota2-realtime", (data) => console.log("Dota2:", data));

Batched Esports Updates (legacy)

client.ws.subscribe({
  sports: ["nba"],
  books: ["pinnacle"],
  esports: true,
});

client.ws.on("esports-update", (data) => {
  console.log(`CS2: ${data.sports.cs2?.length}`);
});

v2 Raw Pass-Through Streams

Sparse deltas from each v2 source. Opt in via subscribe({ v2: { ... } }), listen on {book}-v2-update. Only changed buckets publish — unchanged buckets do not fire.

client.ws.subscribe({
  v2: {
    bet365:      { soccer: "*", nba: "*" },              // '*' = all leagues
    draftkings:  { soccer: ["epl", "ucl"] },            // or array of league slugs
    fanduel:     { soccer: "*" },
    kalshi:      { nba: "*", tennis: "*", cs2: "*" },    // esports: cs2/dota2/lol/valorant/...
    polymarket:  { soccer: ["epl"], cs2: "*", lol: "*" },// esports stream the same way
    mybookie:    { nba: "*", mlb: "*" },
    thunderpick: { basketball: "*", cs2: "*", dota2: "*" }, // esports: cs2/dota2/lol/valorant
    pinnacle:    { wnba: "*" },                          // raw Pinnacle sparse deltas
  },
});

client.ws.on("bet365-v2-update",      (data) => { /* snapshot or outcomes delta */ });
client.ws.on("draftkings-v2-update",  (data) => { /* per-tab bucket delta */ });
client.ws.on("fanduel-v2-update",     (data) => { /* per-tab bucket delta */ });
client.ws.on("kalshi-v2-update",      (data) => { /* per-league series delta */ });
client.ws.on("polymarket-v2-update",  (data) => { /* per-league tag delta */ });
client.ws.on("mybookie-v2-update",    (data) => { /* market snapshot */ });
client.ws.on("thunderpick-v2-update", (data) => { /* market snapshot */ });
client.ws.on("pinnacle-v2-update",    (data) => { /* sparse Pinnacle delta */ });

ProphetX Stream

// All ProphetX sports + kinds
client.ws.subscribe({ prophetx: true });

// Or filter to specific sports / kinds
client.ws.subscribe({
  prophetx: { sports: ["basketball"], kinds: ["game"] },
});

client.ws.on("prophetx-update", (delta) => {
  // Raw ProphetX order-book delta — passes through unchanged from the exchange
});

WebSocket Events Reference

| Event | Trigger | Opt-in via | |---|---|---| | odds-update | Any change to normalized /odds data | subscribe({ sports, books }) | | scores-update | Live score change | subscribe({ sports }) | | player-props-update | Pinnacle player props change | subscribeProps({ sports }) | | bet365-props-update | Bet365 player props change | subscribeProps({ sports }, "bet365") | | fanduel-props-update | FanDuel player props change | subscribeProps({ sports }, "fanduel") | | draftkings-props-update | DraftKings player props change | subscribeProps({ sports }, "draftkings") | | betmgm-props-update | BetMGM player props change | subscribeProps({ sports }, "betmgm") | | caesars-props-update | Caesars player props change | subscribeProps({ sports }, "caesars") | | pinnacle-realtime | Sub-second Pinnacle realtime odds | MVP+ tier (or WS addon) — auto-delivered when entitled | | ps3838-realtime | Sub-second PS3838 odds | MVP+ tier (or WS addon) — auto-delivered when entitled | | cs2-realtime / lol-realtime / valorant-realtime / dota2-realtime | Per-game Pinnacle esports odds | subscribe({ esportsRealtime: { [game]: { league: "<tournament>" } } }) | | esports-update | Batched esports odds (legacy) | subscribe({ esports: true }) | | bet365-v2-update | Bet365 raw market snapshot/delta | subscribe({ v2: { bet365: {...} } }) | | draftkings-v2-update | DraftKings raw bucket delta | subscribe({ v2: { draftkings: {...} } }) | | fanduel-v2-update | FanDuel raw bucket delta | subscribe({ v2: { fanduel: {...} } }) | | mybookie-v2-update | MyBookie raw snapshot | subscribe({ v2: { mybookie: {...} } }) | | thunderpick-v2-update | Thunderpick raw snapshot | subscribe({ v2: { thunderpick: {...} } }) | | kalshi-v2-update | Kalshi per-league series delta | subscribe({ v2: { kalshi: {...} } }) | | polymarket-v2-update | Polymarket per-league tag delta | subscribe({ v2: { polymarket: {...} } }) | | pinnacle-v2-update | Pinnacle sparse market delta | subscribe({ v2: { pinnacle: {...} } }) | | prophetx-update | ProphetX exchange order-book delta | subscribe({ prophetx: {...} }) | | 1xbet-update | Isolated 1xBet soccer odds change (not in /odds; also rest.getOneXBetSoccer()) | subscribe({ oneXBetSoccer: true }) |

Promise-Based Waiting

// Wait for the next odds update
const update = await client.ws.waitFor("odds-update", 10_000);

// Wait for subscription confirmation
client.ws.subscribe({ sports: ["nba"], books: ["pinnacle"] });
const sub = await client.ws.waitFor("subscribed");
console.log("Subscribed to:", sub.sports);

Error Handling

client.ws.on("error", (err) => {
  console.error("WebSocket error:", err.message, err.code);
});

client.ws.on("disconnect", (reason) => {
  console.log("Disconnected:", reason);
});

Cleanup

// Disconnect WebSocket
client.ws.disconnect();

// Or destroy everything
client.destroy();

Error Handling

The SDK throws typed errors for different failure modes:

import {
  OwlsInsightError,
  AuthenticationError,
  ForbiddenError,
  RateLimitError,
  ServiceBusyError,
} from "owls-insight-ts";

try {
  const odds = await client.rest.getOdds("nba");
} catch (err) {
  if (err instanceof RateLimitError) {
    // err.code is "HISTORY_CONCURRENCY" when too many history requests were in flight
    console.log(`Rate limited (${err.code}). Retry in ${err.retryAfterMs}ms`);
  } else if (err instanceof ServiceBusyError) {
    // 503: too expensive to serve in one request, or a transient outage
    console.log(`Busy. Retry in ${err.retryAfterMs ?? 5000}ms, or narrow the request`);
  } else if (err instanceof AuthenticationError) {
    console.log("Invalid API key");
  } else if (err instanceof ForbiddenError) {
    console.log("Upgrade required for this endpoint");
  } else if (err instanceof OwlsInsightError) {
    console.log(`API error ${err.status}: ${err.message}`);
  }
}

The API returns rate limit headers on every response:

  • X-RateLimit-Remaining-Minute — requests left this minute
  • X-RateLimit-Remaining-Month — requests left this month
  • X-RateLimit-Reset-Minute — Unix timestamp (ms) when the minute limit resets
  • X-RateLimit-Reset-Month — ISO 8601 timestamp when the month limit resets

Configuration

const client = new OwlsInsight({
  apiKey: process.env.OWLS_INSIGHT_API_KEY!,  // Required
  baseUrl: "https://api.owlsinsight.com", // Optional (default)
  wsUrl: "https://api.owlsinsight.com",   // Optional (default, same as baseUrl)
  timeout: 30000,                  // Optional request timeout in ms (default: 30s)
});

Supported Sports

| Sport | Key | Odds | Props | Scores | |---|---|---|---|---| | NBA | nba | Yes | Yes | Yes | | NCAAB | ncaab | Yes | Yes | Yes | | NFL | nfl | Yes | Yes | Yes | | NHL | nhl | Yes | Yes | Yes | | NCAAF | ncaaf | Yes | Yes | Yes | | WNBA | wnba | Yes (Pinnacle realtime) | — | — | | MLB | mlb | Yes | Yes | Yes | | Soccer | soccer | Yes | — | Yes | | Tennis | tennis | Yes | — | Yes | | MMA | mma | Yes (BetOnline) | — | — | | CS2 | cs2 | Yes (1xBet, Pinnacle) | — | Yes | | Valorant | valorant | Yes (1xBet, Pinnacle) | — | Yes | | LoL | lol | Yes (1xBet, Pinnacle) | — | Yes | | Dota 2 | dota2 | Yes (Pinnacle realtime) | — | — | | Handball | handball | Yes (Pinnacle realtime) | — | — |

Sportsbooks & Coverage

Coverage reflects what the SDK exposes through getOdds and the props endpoints. v2 raw pass-through (per-book deep markets) is documented separately above.

| Book | Game Odds | Player Props | Notes | |---|---|---|---| | Pinnacle | NBA, NCAAB, NFL, NHL, NCAAF, MLB, WNBA, Soccer, Tennis, Esports | NBA, NCAAB, NFL, NHL, NCAAF | Sharp/reference line. Realtime via /realtime + pinnacle-realtime | | PS3838 | Same as Pinnacle (whitelabel) | — | Realtime only via /ps3838-realtime + ps3838-realtime event | | FanDuel | NBA, NCAAB, NFL, NHL, NCAAF, MLB, Soccer | NBA, NCAAB, NFL, NHL, NCAAF | v2 raw available: soccer / mlb / nba / nhl | | DraftKings | NBA, NCAAB, NFL, NHL, NCAAF, MLB, Soccer | NBA, NCAAB, NFL, NHL, NCAAF | v2 raw available: soccer | | BetMGM | NBA, NCAAB, NFL, NHL, NCAAF, MLB | NBA, NCAAB, NFL, NHL, NCAAF | | | Bet365 | NBA, NCAAB, NFL, NHL, NCAAF, Soccer | NBA, NCAAB, NFL, NHL, NCAAF | v2 raw available: nba / mlb / nhl / soccer | | Caesars | NBA, NCAAB, NFL, NHL, NCAAF, MLB | NBA, NCAAB, NFL, NHL, NCAAF | | | Hard Rock | NBA, MLB, NHL, Soccer (per AZ/FL state) | — | v2 raw only — call getHardRockEvents(state, sport) | | BetOnline | MMA | — | UFC/MMA only | | 1xBet | CS2, Valorant, LoL, Soccer, MLB | — | Esports + soccer + MLB | | ProphetX | Multiple sports (raw exchange) | — | Exchange order-book — call getProphetxOdds() | | Kalshi | v2 only (not in /odds since 2026-05-31) | — | Prediction market. Game lines via getKalshiV2: nba / nhl / mlb / soccer / tennis. Player props remain in getProps() | | Polymarket | v2 only (not in /odds since 2026-05-31) | — | Decentralized exchange. Game lines via getPolymarketV2: nba / nhl / mlb / soccer / tennis | | Novig | NBA, NCAAB, NFL, NHL, NCAAF, MLB, Soccer, Tennis | — | Peer-to-peer exchange. Full order-book depth on Outcome.liquidity (bids only — Novig ladders carry no asks) | | MyBookie | MLB, NBA | — | v2 raw only | | Thunderpick | Baseball, Basketball, Tennis, CS2, Dota 2, LoL, Valorant | — | v2 raw only | | theScore Bet | MLB, NFL, NCAAF, NBA, NHL, Soccer, Tennis | — | v2 raw only. Ontario (Canada) market. Decimal odds as an exact rational, plus a preformatted American string |

License

MIT