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

oddsrelay

v0.1.1

Published

TypeScript client for OddsRelay: matched odds boards, raw bookmaker and exchange odds, events, with token-aware calls.

Readme

oddsrelay

The OddsRelay API client for JavaScript and TypeScript: matched odds boards, raw bookmaker and exchange odds, sports, events and coverage, with the token meter on every reply.

  • No runtime dependencies. It uses the fetch built into Node 18 and later.
  • ESM with TypeScript types. Return types follow your parameters (format, quote).
  • Handles the API's rules for you: Bearer key, gzip, ETag polling, quotes, Retry-After and typed errors.

API docs: https://oddsrelay.io/docs

Install

npm install oddsrelay

TypeScript projects also need @types/node (or the DOM lib) for the Response and Headers types.

Get a key

Sign up at app.oddsrelay.io/signup and copy your key from the dashboard. Free gives you 50,000 tokens for 14 days from sign-up, with no card, then no tokens until you choose a plan, on the same key. Plans and token prices. Paid plans are for your own use. A product that serves other people runs on Enterprise.

First call

The status route needs no key, so this works straight after install:

import { OddsRelay } from "oddsrelay";

const client = new OddsRelay();
const status = await client.status();
console.log(status.data);

Save it as first.mjs and run node first.mjs. (The examples use top-level await, so they need an ES module: a .mjs file, or "type": "module" in your package.json.)

Data routes need a key (Get a key, above). Put it in the environment:

export ODDSRELAY_KEY=your_key_here
import { OddsRelay } from "oddsrelay";

const client = new OddsRelay(); // reads ODDSRELAY_KEY

const res = await client.odds("standard", {
  region: "uk",
  sports: "soccer",
  markets: "h2h",
  bookmakers: "ladbrokes",
});

if (!res.notModified) {
  for (const event of res.data.data) {
    console.log(event.sport_title, event.home_team, "v", event.away_team, event.commence_time);
  }
}
console.log("tokens spent:", res.tokens.cost, "left:", res.tokens.remaining);

You can also pass the key yourself: new OddsRelay({ apiKey }).

Methods

One method per GET route. Filters go in a params object and are sent as query parameters.

| Method | Route | Key | |---|---|---| | odds(type, params?, options?) | /v2/odds/{type}: a matched board (standard, 2up, dutching, each-way, extra-place, bog) | yes | | raw(params?, options?) | /v2/odds/raw: all bookmaker and exchange prices by event, paged with cursor | yes | | event(id, params?, options?) | /v2/odds/event/{id}: every price for one event | yes | | sports(params?) | /v2/sports: the sport keys you can filter on, paged | yes | | events({ sport, ...params }) | /v2/events: events for one sport, paged | yes | | bookmakers(params?) | /v2/bookmakers: the bookmaker and exchange catalogue | yes | | regions() | /v2/regions: which regions your key reaches | yes | | coverage(params?) | /v2/coverage: venues and sports per product; a key adds detail | no | | status(params?) | /v2/status: each product's state | no | | health() | /v2/health: board-by-board health | yes | | usage() | /v2/usage: your plan, token balance and key scope | yes | | pricing() | /v2/pricing: the token rate card | yes | | openapi() | /v2/openapi.json: the API's OpenAPI document | no | | healthz() | /healthz: plain-text liveness check | no |

Parameters:

  • Lists can be arrays or comma strings: sports: ["soccer", "tennis"] and sports: "soccer,tennis" send the same thing.
  • true and false are sent as strings. undefined and null are left out.
  • Anything else you pass is sent as given, and the API answers 400 bad_request for an unknown parameter.
  • format: "bare" returns just the rows array, and the return type follows.

Responses are the API's JSON, untouched. Fields added to the API after this version still arrive.

Every reply

Each method resolves to a result with the same fields:

| Field | What it holds | |---|---| | data | The parsed body. undefined on a 304. | | notModified | true when the API answered 304 (see below). | | status | The HTTP status. | | tokens | X-Tokens-*: cost, used, remaining, limit (a number or "unlimited"), reset (ISO 8601). | | rateLimit | X-RateLimit-*: limit, remaining, reset (seconds until the window ends). | | requestId | X-OddsRelay-Request-Id, worth quoting when you ask for help. | | etag | The reply's ETag. | | headers | Every response header, as a Headers object. |

Quotes

Pass quote: true to price a request without running it. You get { cost, remaining, rate_card } back instead of data, and the quote itself is not charged.

For calls that could be large, pass maxTokens as the last argument. The client quotes first and makes the call only if the quote fits; otherwise it throws OverBudgetError and nothing is spent.

import { OverBudgetError } from "oddsrelay";

try {
  // budget: the most tokens you are willing to spend on this one call
  const res = await client.raw({ region: "uk", sports: "tennis" }, { maxTokens: budget });
} catch (err) {
  if (err instanceof OverBudgetError) console.log(`quote was ${err.quote.cost}: narrow the filters`);
  else throw err;
}

Conditional polling

The client keeps the ETag of each URL it has read and sends it back as If-None-Match. When nothing has changed, the API answers 304 with no body, which is not charged, and the result has notModified: true. Keep the last full reply and swap it only when new data arrives:

let board;

async function refresh() {
  const res = await client.odds("standard", { region: "uk", sports: "soccer" });
  if (!res.notModified) board = res.data;
  return board;
}

ETags are kept in the client instance, so a new OddsRelay() starts with none.

Errors

Every reply other than 2xx and 304 throws an OddsRelayError. Branch on err.code, the API's own error code (invalid_api_key, unknown_sport, not_in_key_scope and so on). Codes added after this version arrive as strings like any other. A reply with no JSON body gets the code http_<status>.

import { InsufficientTokensError, OddsRelayError } from "oddsrelay";

try {
  await client.odds("standard", { region: "uk" });
} catch (err) {
  if (err instanceof InsufficientTokensError) {
    console.log(`needs ${err.cost}, ${err.remaining} left, resets ${err.resetsAt}`);
  } else if (err instanceof OddsRelayError) {
    console.log(err.status, err.code, err.message, err.requestId);
  } else {
    throw err; // network failure
  }
}

Each error also carries tokens, rateLimit, headers and body (the API's error object, unknown fields included). A 402 is an InsufficientTokensError with cost, remaining and resetsAt.

Rate limits

On a 429 the client waits for Retry-After and tries again, up to three tries in all. If the last try is still 429, it throws an OddsRelayError with status 429. A Retry-After longer than a minute is not waited out: the client throws at once and leaves the decision to you. res.rateLimit shows where you stand in the current window.

Your key

The key is sent only in the Authorization: Bearer header. It never goes in a URL, the client logs nothing, and no error message or inspected client shows it. A key with a line break or another character a header cannot carry is refused when the client is created, without echoing it. Keep it in the environment rather than in code, and leave it out of anything you commit.

API version

CONTRACT_VERSION is the info.version of the OpenAPI document this client was written against. To compare it with the API you are calling:

import { CONTRACT_VERSION } from "oddsrelay";

const spec = await client.openapi();
if (!spec.notModified && spec.data.info.version !== CONTRACT_VERSION) {
  console.log(`the API is at ${spec.data.info.version}, this client follows ${CONTRACT_VERSION}`);
}

More from OddsRelay

License

MIT