Next.js
Full-stack Next.js with route handlers for token minting and browser-side subscriptions.
Next.js is the recommended path: server components for one-shot reads, client components for live subscriptions, and a route handler that mints JWTs so your API key never leaves the server.
Install
npm i @mrdoge/node @mrdoge/react@mrdoge/node for the server (API key auth). @mrdoge/react is the
recommended browser package: hooks wrapping @mrdoge/client, backed by
a shared cache so multiple components watching the same match or list
don't duplicate a subscription. Drop to @mrdoge/client directly for
manual control outside React's render cycle.
Server: mint tokens for the browser
import { NextResponse } from "next/server";
import { MrDoge } from "@mrdoge/node";
const mrdoge = new MrDoge({ apiKey: process.env.MRDOGE_API_KEY! });
export async function POST() {
// Optional: gate by your auth session
// const session = await auth();
// if (!session) return new Response("Unauthorized", { status: 401 });
const { token, expiresAt } = await mrdoge.tokens.create({ ttl: 600 });
return NextResponse.json({ token, expiresAt });
}Browser: subscribe to live matches
@mrdoge/react needs no Provider and no client instance of your own.
It defaults to POSTing to /api/mrdoge/token, matching the route handler
above out of the box:
"use client";
import { useLiveMatches } from "@mrdoge/react";
export default function LivePage() {
const matches = useLiveMatches({ sports: ["soccer"] });
if (!matches) return <p>Loading…</p>;
return (
<ul>
{matches.map((m) => (
<li key={m.id}>
{m.homeTeam.name} {m.stats?.homeScore}–{m.stats?.awayScore}{" "}
{m.awayTeam.name}
</li>
))}
</ul>
);
}Mount useLiveMatches({ sports: ["soccer"] }) in ten different
components and they share one matches.subscribeLive() call and one
copy of the data: the first mount starts it, the last unmount cancels
it. useLiveMatch(matchId)
is the single-match equivalent for a detail page. See the
full hooks list for the rest.
Manual control
Reach for @mrdoge/client directly when you need more than a hook gives
you: subscribing outside a component, or driving your own cache:
"use client";
import { useEffect, useState } from "react";
import { MrDoge, type Match } from "@mrdoge/client";
const mrdoge = new MrDoge({
authEndpoint: "https://your-api.com/mrdoge/token",
});
export default function LivePage() {
const [matches, setMatches] = useState<Match[]>([]);
useEffect(() => {
let cancelled = false;
let sub: Awaited<ReturnType<typeof mrdoge.matches.subscribeLive>> | null =
null;
(async () => {
sub = await mrdoge.matches.subscribeLive({ sports: ["soccer"] });
if (cancelled) {
sub.cancel();
return;
}
setMatches(sub.snapshot);
sub.on("match.upd", (match) => {
setMatches((prev) => {
const i = prev.findIndex((m) => m.id === match.id);
if (i >= 0) {
const next = [...prev];
next[i] = match;
return next;
}
return [...prev, match];
});
});
sub.on("match.del", ({ id }) => {
setMatches((prev) => prev.filter((m) => m.id !== id));
});
})();
return () => {
cancelled = true;
sub?.cancel();
};
}, []);
return (
<ul>
{matches.map((m) => (
<li key={m.id}>
{m.homeTeam.name} {m.stats?.homeScore}–{m.stats?.awayScore}{" "}
{m.awayTeam.name}
</li>
))}
</ul>
);
}Foreground wake-up (optional)
When the user tabs away and comes back, the WebSocket may be dead while the
SDK's reconnect loop is sleeping in backoff. Wire pingOrReconnect() to
visibilitychange so the first interaction after refocus is instant:
"use client";
import { useEffect } from "react";
import { getMrDogeClient } from "@mrdoge/react"; // or your own MrDoge instance
useEffect(() => {
const onVisible = () => {
if (document.visibilityState === "visible") getMrDogeClient().pingOrReconnect();
};
const onOnline = () => getMrDogeClient().pingOrReconnect();
document.addEventListener("visibilitychange", onVisible);
window.addEventListener("online", onOnline);
return () => {
document.removeEventListener("visibilitychange", onVisible);
window.removeEventListener("online", onOnline);
};
}, []);pingOrReconnect() is a no-op when the socket is healthy, so it's cheap
to call on every event. See the
Subscriptions reference →
for full mechanics.
Server components: one-shot reads
For SSR / RSC, use @mrdoge/node directly: no client component, no
hydration cost:
import { MrDoge } from "@mrdoge/node";
const mrdoge = new MrDoge({ apiKey: process.env.MRDOGE_API_KEY! });
export default async function RecommendationsPage() {
const { data: recs } = await mrdoge.ai.recommendations.list({
minEdge: 0.05,
confidence: "High",
limit: 20,
});
return (
<ul>
{recs.map((r) => (
<li key={r.id}>
<strong>{r.outcome}</strong> @ {r.odds.toFixed(2)} ({r.confidence})
<span> · edge {(r.edgePercentage * 100).toFixed(1)}%</span>
<ul>
{r.rationale.map((line, i) => (
<li key={i}>{line}</li>
))}
</ul>
</li>
))}
</ul>
);
}Pair with export const revalidate = 60 for ISR.
Environment variables
MRDOGE_API_KEY=sk_live_…Don't prefix with NEXT_PUBLIC_: that exposes the key to the bundle.
Edge runtime
The route handler can run on the edge by exporting runtime:
export const runtime = "edge";@mrdoge/node doesn't work on edge (it opens WebSockets). Swap to
@mrdoge/http:
import { createHttpClient } from "@mrdoge/http";
const mrdoge = createHttpClient({ apiKey: process.env.MRDOGE_API_KEY! });
export async function POST() {
const { token, expiresAt } = await mrdoge.call("tokens.create", {
ttl: 600,
});
return Response.json({ token, expiresAt });
}