Mr. Doge
Reference

Matches

List, get, search, and subscribe to live matches across every major sport.

The matches resource is the workhorse of the SDK. Every other resource hangs off it.

matches.list

Paginated list of matches matching the filters you pass.

const { data, pagination } = await mrdoge.matches.list({
  sports: ["soccer"],
  status: ["live"],
  limit: 20,
});

Params:

FieldTypeNotes
sportsstring[]One or more of "soccer", "basketball", "american_football", "baseball", "ice_hockey", "volleyball", "handball", "tennis"
competitionIdsnumber[]Filter to one or more competitions
regionIdsnumber[]Filter to one or more regions
teamIdsnumber[]Filter to matches involving any of the listed teams
statusMatchStatus[]["upcoming"], ["live"], ["completed"], or combinations
datestringYYYY-MM-DD, matches on this day
startDate / endDatestringRange filter
cursorstringPagination cursor (opaque)
limitnumberMax 100
selectMatchSelectField selector, see selectors
localestring"en", "pt-BR", "es"
timezonestringIANA timezone (e.g. "America/Sao_Paulo")

Returns: { data: Match[]; pagination: { nextCursor: string | null; hasMore: boolean } }

See pagination for the cursor walk.

matches.listAll

Auto-paginated version of matches.list. Walks all pages and returns the combined array.

const all = await mrdoge.matches.listAll(
  { sports: ["soccer"], status: ["live"] },
  {
    onPage: (page, accumulated) => render(accumulated),
    signal: controller.signal,
  },
);

Params: Same as matches.list minus cursor.

Options:

FieldTypeNotes
onPage(page, accumulated) => voidCalled per page for progressive rendering
signalAbortSignalCancel the walk mid-page

Returns: Match[], every match across all pages.

matches.get

Single match by ID with full detail (MatchDetail).

const match = await mrdoge.matches.get({ id: "match_abc123" });

Params:

FieldTypeNotes
idstringMatch ID, strings, not numbers
selectMatchDetailSelectField selector
localestringLocalization

Returns: MatchDetail, includes stats, markets, and current clock.

matches.trending

Top matches by sport, server-prioritised.

const trending = await mrdoge.matches.trending({
  sports: ["soccer"],
  limit: 5,
});

Params:

FieldTypeNotes
sportsstring[]Optional, defaults to all sports
statusMatchStatus[]Optional
limitnumberMax 50, default 5
selectMatchSelectField selector

Returns: Match[]

matches.search

Free-text search across team names and competitions.

const results = await mrdoge.matches.search({
  query: "liverpool",
  limit: 10,
});

Params:

FieldTypeNotes
querystringMin 2 chars
sportsstring[]Optional, narrow by one or more sports
statusMatchStatus[]Optional
limitnumberMax 20
selectMatchSelectField selector

Returns: Match[]

matches.subscribeLive

Live-match subscription with WebSocket deltas. The killer feature.

const sub = await mrdoge.matches.subscribeLive({
  sports: ["soccer"],
});

// Initial state, already populated from the HTTP cold-start cache
console.log("Snapshot:", sub.snapshot.length, "matches");

// Stream deltas
sub.on("match.upd", (match) => render(match));
sub.on("match.del", ({ id }) => removeFromUI(id));

// On any subscription closure (rate limit, auth expiry, server bounce, etc.)
sub.on("closed", ({ reason, message }) => {
  console.warn("Subscription closed:", reason, message);
});

// Tear down
await sub.cancel();

Params:

FieldTypeNotes
sportsstring[]Optional
regionIdsnumber[]Optional
competitionIdsnumber[]Optional
selectMatchSelectField selector, applies to snapshot AND every delta

Returns: Subscription<"matches.subscribeLive">, see subscriptions.

Push events: match.upd (full match payload), match.del ({ id: string })

The SDK races HTTP cache against WebSocket connect for the initial snapshot. sub.snapshot is populated in ~100ms instead of ~1.5s. Read the cold-start details →

matches.subscribe

Deep subscription for a single match: every stat update and status transition. For live order-book deltas on the same match, pair this with odds.subscribe.

const sub = await mrdoge.matches.subscribe({
  matchId: "match_abc123",
});

console.log("Match:", sub.snapshot);

sub.on("stats.upd", (stats) => updateScoreboard(stats));
sub.on("status.upd", ({ status }) => onPhaseChange(status));

Params:

FieldTypeNotes
matchIdstringRequired
selectMatchDetailSelectField selector, applies to snapshot and stats.upd

Returns: Subscription<"matches.subscribe">

Push events: stats.upd (MatchStats), status.upd ({ status })

matches.getLive

One-shot snapshot of all live matches matching the filters. No subscription, no deltas. Useful for cron jobs or edge runtimes.

const snapshot = await mrdoge.matches.getLive({ sports: ["soccer"] });

Params:

FieldTypeNotes
sportsstring[]Optional
regionIdsnumber[]Optional
competitionIdsnumber[]Optional
selectMatchSelectField selector

Returns: Match[]

The Match shape

type Match = {
  id: string;                       // string, not number
  startTime: string;                // ISO 8601
  status: "upcoming" | "live" | "completed";
  homeTeam: { id: number; name: string };
  awayTeam: { id: number; name: string };
  sport: { id: number; name: SportName } | null;
  competition: { id: number; name: string };
  region: { id: number; name: string };
  stats?: MatchStats | null;        // sport-discriminated, see below
  timeline?: TimelineEvent[];       // sport-tagged event log
};

type SportName =
  | "soccer" | "basketball" | "american_football" | "baseball"
  | "ice_hockey" | "volleyball" | "handball" | "tennis";

Where are the markets?

Markets live on the dedicated odds.* resource. See odds.list / odds.subscribe. The split keeps the Match payload lean: customers who render scores and stats only don't pay bandwidth for an order book they never use. Pull markets when you need them via mrdoge.odds.list({ matchId }).

MatchStats: sport-discriminated

MatchStats is a discriminated union on stats.sport. Common fields (clock, periods, homeScore, awayScore) are typed without narrowing. Sport-specific fields require a narrow:

sub.on("match.upd", (match) => {
  // Common, always typed
  console.log(match.stats?.homeScore, match.stats?.clock?.display);

  // Sport-specific, TS narrows on `stats.sport`
  if (match.stats?.sport === "tennis") {
    console.log(
      `${match.stats.homeCurrentGamePoints} - ${match.stats.awayCurrentGamePoints}`,
      match.stats.homeServes ? "serving" : "",
    );
  } else if (match.stats?.sport === "soccer") {
    console.log(`Yellow: ${match.stats.homeYellowCards}`);
  }
});

Primary score unit per sport

homeScore and awayScore are always present, but the unit depends on the sport:

SportUnit
Soccer / Ice Hockey / HandballGoals
Basketball / American FootballTotal points
BaseballTotal runs
Volleyball / TennisSets won (per-set point totals live in periods)

Per-sport fields

type SoccerStats = {
  sport: "soccer";
  clock: Clock | null;
  periods?: Period[];
  homeScore: number; awayScore: number;   // goals
  // Cards
  homeYellowCards?: number; awayYellowCards?: number;
  homeRedCards?: number; awayRedCards?: number;
  // Fouls
  homeFouls?: number; awayFouls?: number;
  // Set pieces
  homeCorners?: number; awayCorners?: number;
  homeThrowIns?: number; awayThrowIns?: number;
  homeGoalKicks?: number; awayGoalKicks?: number;
  homePenaltyKicks?: number; awayPenaltyKicks?: number;
  // Play
  homeTackles?: number; awayTackles?: number;
  homeOffsides?: number; awayOffsides?: number;
  homeShots?: number; awayShots?: number;
  homeShotsOnTarget?: number; awayShotsOnTarget?: number;
  homeWoodworkHits?: number; awayWoodworkHits?: number;
  homePossession?: number; awayPossession?: number;  // 0–1 fraction
  homeExpectedGoals?: number; awayExpectedGoals?: number;
  injuryMinutes?: number;
  // Player-level (StatPlayer[] = { name, value }[])
  homePlayersGoals?: StatPlayer[]; awayPlayersGoals?: StatPlayer[];
  homePlayersAssists?: StatPlayer[]; awayPlayersAssists?: StatPlayer[];
  homePlayersFouls?: StatPlayer[]; awayPlayersFouls?: StatPlayer[];
  homePlayersShots?: StatPlayer[]; awayPlayersShots?: StatPlayer[];
  homePlayersShotsOnTarget?: StatPlayer[]; awayPlayersShotsOnTarget?: StatPlayer[];
  homePlayersTackles?: StatPlayer[]; awayPlayersTackles?: StatPlayer[];
  homePlayersOffsides?: StatPlayer[]; awayPlayersOffsides?: StatPlayer[];
  homePlayersWoodworkHits?: StatPlayer[]; awayPlayersWoodworkHits?: StatPlayer[];
};

Timeline

match.timeline is a sport-tagged event log: every notable in-match event in chronological order. Sibling to stats, not nested inside it:

type TimelineEvent = {
  /** Event kind: sport-specific. See per-sport unions below. */
  type: string;
  /** Normalized side: "home" / "away" / "match" (game-level events). */
  side: "home" | "away" | "match";
  /** Phase code: "1H" / "Q1" / "S1" / "P1" / "TB" / "OT" / … */
  phase: string;
  /** Sport- and type-specific display strings. Positional, see below. */
  captions: string[];
  /**
   * Time offset from match start, in seconds. Populated for sports where
   * the data feed tracks per-event time (soccer, ice hockey, handball).
   * `0` for sports without per-event timing (tennis: game-by-game order
   * is preserved by array index; basketball / football boundary events).
   * Always read events in array order; the field is for display only.
   */
  timeOffsetSeconds: number;
};

Per-sport type is open string at runtime (forward-compatible with new event types from the data feed). TypeScript unions are exported for narrowing:

import type { SoccerTimelineEventType } from "@mrdoge/node";

if (match.sport?.name === "soccer") {
  for (const event of match.timeline ?? []) {
    const type = event.type as SoccerTimelineEventType;
    if (type === "GoalWithScorer") {
      const [minute, team, player] = event.captions;
      console.log(`Goal at ${minute}': ${player} (${team})`);
    }
  }
}

Known event types per sport

Sporttype values
SoccerStartOfMatch, EndOfFirstHalf, StartOfSecondHalf, EndOfNormalTime, GoalWithScorer, OwnGoal, ShotWithPlayer, ShotOnTargetWithPlayer, FoulWithPlayer, TackleWithPlayer, ThrowIn, GoalKick, Corner, PenaltyKick, YellowCardWithPlayer, RedCardWithPlayer, Substitution
TennisGameWithPoints, Game, Set, Tiebreak
BasketballStartOfGame, EndOf<N>Quarter, StartOf<N>Quarter, EndOfHalfTime, StartOf/EndOfOvertime
American FootballSame shape as Basketball
Ice HockeyStartOfGame, EndOf<N>Period, StartOf<N>Period, StartOf/EndOfOvertime, GoalWithoutScorer
VolleyballSetWithPoints, Set
HandballStartOfGame, EndOfFirstHalf, StartOfSecondHalf, EndOfNormalTime, GoalWithoutScorer
Baseball(feed currently emits no baseball events; timeline is [])

captions shape per event (selected)

  • Soccer GoalWithScorer: [matchMinute, teamName, playerName]
  • Soccer EndOfFirstHalf / EndOfNormalTime: [homeScore, awayScore]
  • Tennis GameWithPoints: [setNumber, gameInSet, playerName, currentPoints]
  • Tennis Set: [setNumber, winnerName, p1Games, p2Games]
  • Volleyball SetWithPoints: [setNumber, teamName, homePoints, awayPoints]
  • Hockey / Handball GoalWithoutScorer: [matchMinute, teamName]
  • Basketball / Football EndOf<N>Quarter: [homeScore, awayScore]

Other event types follow the same convention: the first element is typically a time marker; remaining elements are team / player / score data.

Clock semantics

The stats.clock object handles count-up sports (soccer, ice hockey, handball), count-down sports (basketball, American football), and sports without a traditional clock (baseball, volleyball, tennis) under one schema. Each numeric field's semantics depend on the sport:

FieldSoccer / Hockey / HandballBasketball / FootballBaseball / Volleyball / Tennis
elapsedSecondsTotal match-elapsed (0–5400+)Elapsed within current periodnull
remainingSecondsnull (count-up)Seconds left on the period clocknull
periodDurationSecondsnull (no fixed half length)720s NBA quarter, 1200s NHL period, etc.null
minuteRunning match minute (soccer only)nullnull
stoppageAdded/injury time minutes (soccer only), null outside stoppagenullnull
display"44'", "45+3'", "HT", "FT""Q3 7:42", "OT 0:34", "FT""Set 4", "9º Inning"

The display / displayLong strings are pre-formatted and localized by the server. Render them directly for the lazy path. For custom clocks, read the numeric fields and switch on clock.state.

Default limits

Each method has a server-enforced default and maximum on limit:

MethodDefaultMax
matches.list20100
matches.trending550
matches.search1020
matches.getLiveunbounded snapshotN/A

Selectors

GraphQL-style projection: request only the fields you need. Reduces payload size, deserialization time, and re-render cost.

const { data } = await mrdoge.matches.list({
  sports: ["soccer"],
  select: {
    id: true,
    homeTeam: { name: true },
    awayTeam: { name: true },
    stats: { homeScore: true, awayScore: true, clock: { display: true } },
    // omit sport, competition, region, etc.
  },
});

Selectors apply to every match-bearing method:

  • matches.list, matches.get, matches.trending, matches.search, matches.getLive: applies to the response
  • matches.subscribeLive: applies to the initial snapshot AND every match.upd push for the lifetime of the subscription
  • matches.subscribe: same for stats.upd and odds.upd deltas

The select type (MatchSelect or MatchDetailSelect) walks the response shape recursively, so autocomplete drives every key. Selector rules:

  • field: true → include the full subtree
  • field: { …nested } → include only the listed nested fields
  • field omitted → not returned

Performance: projection is post-cache

The server caches the full response shape once, then projects per request. Two calls with different select shapes share the same upstream fetch and cache entry: you don't pay a cache miss for picking different fields.

This means feature-flag-driven selectors (e.g. one component renders homeScore, another renders homeScore + clock.display) are free, no extra round-trips.

Subscriptions: bandwidth savings compound

For long-lived subscribeLive subscriptions, the selector applies to every push. A subscription rendering 50 live matches with a 10-field selector instead of the full ~60-field default cuts your bandwidth and JSON-parse cost ~6×. The server stores the selector per-subscription and projects each delta before sending.

Next

On this page