Tiers & limits
What each plan unlocks — methods, request volume, and concurrent subscriptions.
Mr. Doge plans gate two things: which methods you can call and how many requests per minute. Upgrading is one click in the dashboard.
At a glance
| Tier | Connections (per key) | Requests/min (per key) | Subs (per connection) | Methods |
|---|---|---|---|---|
| Free | 10 | 60 | 5 | Discovery + basic reads |
| Starter | 25 | 300 | 25 | + trending, live snapshots, live subscriptions |
| Growth | 75 | 1,000 | 100 | + per-match deep subscriptions |
| Business | 250 | 5,000 | 500 | + AI picks, recommendations |
Caps stack: each WebSocket connection independently holds up to its tier's subscription cap. A Free key with 10 connections can hold 10 × 5 = 50 total subscriptions across them. Most apps use one connection per browser tab / mobile app instance.
Method availability by tier
auth, tokens.create, and subscription.cancel are always allowed —
no method gating, no rate limiting.
Free
Everything you need to evaluate the SDK:
regions.listcompetitions.listteams.list,teams.get,teams.formmatches.list,matches.get,matches.search
Starter
Free, plus:
matches.trending— top matches by sportmatches.getLive— live snapshot (one-shot, no deltas)matches.subscribeLive— full live subscription with WebSocket deltas
Growth
Starter, plus:
matches.subscribe— single-match deep subscription (stats + odds + status pushes)
Business
Growth, plus:
ai.picks.list— AI-generated picks with reasoningai.recommendations.list— recommendations with edge and confidence
Rate limits
Limits are per-key, per-minute, shared across HTTP and WebSocket — you can't double your quota by mixing transports.
When you hit a limit, the server returns rate_limited with metadata:
try {
await doge.matches.list({ sports: ["soccer"] });
} catch (err) {
if (err instanceof RateLimitError) {
await new Promise((r) => setTimeout(r, err.retryAfterMs));
// retry
}
}err.retryAfterMs is the precise wait time — the SDK doesn't auto-retry,
but the metadata makes it trivial to implement.
Subscription limits
Each tier caps subscriptions per WebSocket connection — not per key.
The check is local to each connection's subscription map; close a
subscription (sub.cancel()) or close the connection to free a slot.
| Tier | Subs per connection |
|---|---|
| Free | 5 |
| Starter | 25 |
| Growth | 100 |
| Business | 500 |
Because a key can hold multiple concurrent connections (see connection
limits below), the effective max subscriptions
per key is subs/conn × max connections:
| Tier | Effective max (subs × conns) |
|---|---|
| Free | 50 |
| Starter | 625 |
| Growth | 7,500 |
| Business | 125,000 |
If a single connection exceeds its cap, matches.subscribeLive /
matches.subscribe throws SubscriptionLimitError. Cancel an old
subscription, open a new connection, or upgrade.
Connection limits
WebSocket connections are also capped per key. A single browser tab is one connection; a load-balanced backend with 50 instances is 50 connections.
If you need more, use the HTTP transport for bulk reads — it doesn't count against the connection limit.
Forbidden errors
Calling a method outside your tier throws ForbiddenError with context:
try {
await doge.ai.picks.list({ sports: ["soccer"] });
} catch (err) {
if (err instanceof ForbiddenError) {
console.log(err.data);
// { method: "ai.picks.list", tier: "free" }
}
}Use this to show an in-app upgrade prompt.
Upgrading
Upgrades are instant — no key rotation, no redeployment. The dashboard flips
your sdkTier in Stripe metadata, and the next request gets the new limits.
JWTs minted before an upgrade keep their old tier until they expire (max 24 hours). For most browser apps this is fine; if you need an immediate flip, invalidate the current token and remount the client.