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

@predigy/edge-sdk

v8.3.0

Published

Official JavaScript/TypeScript SDK for the EDGE by Predigy prediction market API (retail, parlay, compliance/MICS, LP)

Readme

@predigy/edge-sdk

Official JavaScript/TypeScript SDK for the EDGE by Predigy prediction market API.

Installation

npm install @predigy/edge-sdk

Current version: 8.3.0. Requires Node.js >= 20.19.0 (or any modern browser runtime).

Quick Start

import { EdgeClient } from "@predigy/edge-sdk";

const client = new EdgeClient({
  baseUrl: "https://edge-production-7b77.up.railway.app",
  apiKey: "your-api-key",
});

// Quote, trade, sell and portfolio act as ONE player (see "Player identity" below)
const player = client.withUser("their-player-id");

// List markets
const { markets } = await client.listMarkets({ status: "OPEN" });

// Get a quote
const quote = await player.getQuote("mkt_3f9c1d2e4b5a", {
  side: "YES",
  amount: 100,
});

// Execute a trade
const trade = await player.executeTrade("mkt_3f9c1d2e4b5a", {
  side: "YES",
  amount: 100,
  max_avg_price: 0.65, // optional slippage protection
});

Idempotency

POST /markets/{id}/trade and POST /markets/{id}/sell accept an optional Idempotency-Key header — retrying with the same key and the same request replays the original response (idempotent_replay: true) instead of charging twice; the same key with a different request is refused with 409. On the SELL route a 409 always means the earlier request under that key was ACCEPTED and may already have executed; on the BUY route read the error's code first (err.code — from 8.1.0; 8.0.0 does not surface it; err.code is set only on 409s — the structured 400/403/422 refusals carry theirs in err.body.error), because the arbitrage guard also returns 409 and nothing is charged there: idempotency_key_reused is a real reuse, arbitrage_blocked / arbitrage_check_unavailable is the guard. Never parse detail. For a real reuse — never answer it by generating a new key; resend the original body for the same player, or reconcile that trade. The one safe reuse with a changed body is after a REFUSAL, which stores no key at all (docs/API.md, the Idempotency-Key note on the trade route). Pass it per call as idempotencyKey: the SDK forwards it exactly as given and never generates a key or retries on its own — you own the retry loop. Omit the option and no header is sent; the request is the one the SDK always made. The server bounds the key to 1–255 characters and refuses anything else with 422 (EdgeValidationError) — "" included; the SDK does not check it. A non-string key (null from a nullable column) is refused by the SDK with EdgeValidationError before any request is made — fetch would send it as "null", which the server accepts.

const key = crypto.randomUUID(); // generate ONCE per attempt and persist it with the order
const player = client.withUser("your-player-id");
const trade = await player.executeTrade(
  "mkt_3f9c1d2e4b5a",
  { side: "YES", amount: 100 },
  { idempotencyKey: key },
);
// A timeout? Retry with the SAME key — the replay carries idempotent_replay: true.
const sale = await player.sellPosition(
  "mkt_3f9c1d2e4b5a",
  { side: "YES", contracts: 50 },
  { idempotencyKey: crypto.randomUUID() },
);

Player identity (client.withUser)

Without this, every quote, trade, sell and portfolio read on your API key is refused with 400 (EdgeAPIError, Missing X-Edge-User-Id header…) — since 2026-09-06 the EDGE API requires the header on those seven routes for every real or sandbox operator. Only Predigy's own DEMO operator still accepts a header-less call — there it acts as ONE shared account (one informational balance, one set of cooldowns, one holding for the sell-quote check, no restricted-player match), which is exactly why everyone else must send it. The EDGE API tells players apart by the X-Edge-User-Id header; client.withUser(userId) returns a client that sends it on every request (flat methods and every sub-client alike), with your own reference for the player as the value. The client you built is unchanged and keeps sending no header. A scoped client is a handful of fields over the same transport — make one per request, and call withUser again to switch players (it replaces, never stacks).

const player = client.withUser("their-player-id");   // 1–64 chars of A-Z a-z 0-9 @ . _ : + -
const quote = await player.getQuote("mkt_3f9c1d2e4b5a", { side: "YES", amount: 100 });
const trade = await player.executeTrade("mkt_3f9c1d2e4b5a", { side: "YES", amount: 100 });
const portfolio = await player.getPortfolio();        // THIS player's positions

It is a client-level method rather than an argument on each call because who the caller is acting as is a property of a session, not of one request — and one seam covers every player-scoped route, today's seven and any added later. (idempotencyKey stays per call because idempotency genuinely is per call.) The server's rule for the value — 1–64 characters of A-Z a-z 0-9 @ . _ : + - — is enforced by the SDK before any request, with EdgeValidationError: the cheap failures are otherwise silent or surface only at the server (null would reach the wire as the accepted id "null"; "" is treated by the server as no header — a 400 for any operator that is not DEMO, the shared account again on DEMO). EDGE keys the player on its own user_external_id, derived from the value by a hash, and also stores the value itself: the trade, sell and portfolio replies and every player-scoped webhook carry it back unchanged as player_id, alongside user_external_id (in rebate.period_closed, on each entries item rather than on data), so you can credit the right wallet without keeping a mapping. (Quotes and getBalanceBonusHistory carry neither.) player_id is null for a player EDGE has not seen with the header: a header-less DEMO call, a retail ticket, or a player not seen with it since EDGE began storing it.

Data feeds (client.feeds)

EDGE exposes seven feed endpoints under /admin/feeds — status, sync, events, configure, manual-event, pending and import — and client.feeds wraps all seven (from 5.2.0; on 5.1.0 call them over raw HTTP):

// ADMIN key. `configure` is a partial update: send only what you are changing.
await client.feeds.configure({ feed_adapter: "sportsdataio", feed_sports: ["NBA"], sync_mode: "approval" });
const sync = await client.feeds.triggerSync();   // throws EdgeAPIError(409, code "feed_adapter_not_configured") if no adapter is configured
if (sync.stopped) console.warn("sync ended early:", sync.stopped);   // a 200 is not "complete"

// Approval mode: staged events wait in `listPending` until imported (or their event_time passes).
const pending = await client.feeds.listPending({ limit: 50 });
const refs = (pending.events as Array<{ event_ref: string }>).map((e) => e.event_ref);
if (refs.length) await client.feeds.importPending({ event_refs: refs });
// Also: getStatus() (READ_ONLY+), listEvents({ status, sport, limit, offset }) (any scope),
// createManualEvent({ sport, home_team, away_team, event_time, initial_probability_home }) — NOT idempotent.

getStatus, triggerSync and importPending return typed objects; the other four return the JSON object as Record<string, unknown>. Nothing is validated in the SDK — a bad value is the server's 422 for a value in the request, or a coded 409 when a STORED value you did not send is the problem. The feed reference is docs/API.md, section Data feeds — /admin/feeds/* — parameters, response shapes, scopes and error codes for all seven; docs/DATA_FEED_ADAPTER_GUIDE.md describes the adapter model behind them.

WebSocket (Real-Time Prices)

import { EdgeWebSocket } from "@predigy/edge-sdk";

const ws = new EdgeWebSocket({
  baseUrl: "https://edge-production-7b77.up.railway.app",
  apiKey: "your-api-key",
});

ws.onMessage((data) => {
  console.log("Price update:", data.prices);
}).onError((err) => {
  console.error("WS error:", err);
});

ws.connect("mkt_3f9c1d2e4b5a");

Webhook Verification

import { verifyWebhookSignature } from "@predigy/edge-sdk";

// In your webhook handler (Express, etc.)
const isValid = await verifyWebhookSignature(
  rawBody,
  req.headers["x-edge-signature"],
  process.env.WEBHOOK_SECRET,
);

Widget Embed

import { EdgeWidget } from "@predigy/edge-sdk";

const widget = new EdgeWidget({
  frontendUrl: "https://edge-by-predigy.netlify.app",
  container: "#trading-widget",
  theme: "draftkings", // "generic" | "draftkings" | "caesars" | "fanduel"
});

widget.mount();

mount() embeds the EDGE trading UI in an iframe. The theme and apiKey options are applied via the initial iframe URL and take effect today.

Reserved — not yet implemented by the hosted frontend. The widget's postMessage surface — navigateToMarket(), setTheme(), on(...) event listeners (e.g. on("trade", ...)), and the marketId deep-link option — is defined on the SDK side only. The current EDGE frontend does not listen for these messages or emit any events, so calling them silently does nothing. Do not build integration logic on them yet.

Risk controls & fees (Admin)

Your own risk dials and your own trading fees (SDK 2.2.0). GET needs READ_ONLY; PUT needs ADMIN.

// Read your fee terms. bounds + platform defaults are SERVED, so render your
// inputs from this payload rather than hardcoding limits.
const fees = await client.getFees();
// { base_fee_rate: 0.02, max_fee_rate: null, bounds: {...}, ... }

// Omitting a key leaves that fee alone; null CLEARS it.
await client.updateFees({ base_fee_rate: "0.02" });  // max_fee_rate untouched
await client.updateFees({ base_fee_rate: null });    // back to the 1.75% default

// Risk dials. A null value deletes a key and reverts to the engine default.
await client.updateRiskControlsConfig({ circuit_breaker: { caution: 0.3 } });

⚠️ A fee change takes effect immediately, including on markets already open. There is no per-market fee snapshot, so the next trade on every open market is priced at the new rate.

⚠️ Raising or clearing max_fee_rate CAN raise what your traders pay. It is a ceiling, not a second fee — but if the current ceiling is holding fees down, lifting it releases those charges.

Your fee is your revenue; there is no platform minimum, so 0 is legal on both fields. Non-finite numbers (NaN, Infinity) are rejected client-side rather than sent, because JSON.stringify turns them into null — which on these doors means clear this setting.

Balance Bonus (Admin / Compliance)

The counter-flow surge rebate ("Balance Bonus") read + config surface (SDK 2.1.0). Reads require a READ_ONLY key; config writes require an ADMIN key. The backend owns all validation, clamping, and audit — these methods are thin HTTP wrappers.

// Read the rebate ledger (locked credits the operator owes). Each row is untyped;
// pay the rows whose `status` is "earned" — not every row with `vested: true`, which
// can persist on a market voided after a regrade (its `status` reads "not_earned").
const ledger = await client.compliance.listRebateLedger({
  market_id: "mkt_3f9c1d2e4b5a",
  limit: 100,
});

// Per-market rebate period summary
const period = await client.compliance.getRebatePeriod("mkt_3f9c1d2e4b5a");

// Read the Balance Bonus config (tiers + resolved values + source + clamps)
const config = await client.getBalanceBonusConfig({ market_id: "mkt_3f9c1d2e4b5a" });

// Tune one market's config (null resets a knob to the inherited value)
await client.updateBalanceBonusConfig({
  scope: "market",
  market_id: "mkt_3f9c1d2e4b5a",
  config: { enabled: true, headline_cap: 0.12 },
});

Error Handling

import { EdgeClient, EdgeAuthError, EdgeRateLimitError } from "@predigy/edge-sdk";

const player = client.withUser("your-player-id");
try {
  await player.executeTrade("mkt_3f9c1d2e4b5a", { side: "YES", amount: 100 });
} catch (err) {
  if (err instanceof EdgeAuthError) {
    console.error("Invalid API key");
  } else if (err instanceof EdgeRateLimitError) {
    console.error(`Rate limited. Retry after ${err.retryAfter}s`);
  }
}

Busy (503) — from 8.1.0

When a trade or sell cannot get its market's lock or a place within the service's concurrent-trade capacity in time, EDGE answers 503 with a Retry-After header, and the SDK throws EdgeServiceBusyError (a subclass of EdgeAPIError) carrying the delay in retryAfter (whole seconds when the header is valid; undefined otherwise — the error is still thrown — so do not assume it is present). That refusal charged nothing. But while the database's connections are saturated, ANY route can answer 503 with Retry-After — do not assume from it that nothing moved (docs/API.md, the 503 row of Error Responses).

  • A wager route (trade, sell, retail ticket trade / cash-out): wait retryAfter seconds, then resend the SAME request with the SAME idempotencyKey. Never mint a new key for the retry.
  • Online only: a executeTrade / sellPosition sent WITHOUT idempotencyKey (optional there) must not be retried automatically — it may already have gone through. Reconcile it against the player's transaction history first. The retail ticket routes require a key, so this never applies there.

A 503 without Retry-After (a switched-off feature, an unreachable database) stays a plain EdgeAPIError.

import { EdgeServiceBusyError } from "@predigy/edge-sdk";

const key = crypto.randomUUID(); // one key per attempt, reused on every retry
let trade;
const player = client.withUser("your-player-id");
while (!trade) {
  try {
    trade = await player.executeTrade("mkt_3f9c1d2e4b5a", { side: "YES", amount: 100 }, { idempotencyKey: key });
  } catch (err) {
    if (!(err instanceof EdgeServiceBusyError)) throw err;
    await new Promise((r) => setTimeout(r, (err.retryAfter ?? 1) * 1000));
  }
}

API Reference

EdgeClient Methods

| Method | Description | |--------|-------------| | listMarkets(params?) | List markets with optional filters, including horse_meeting_id and horse_scope — see Horse racing below | | getMarket(id) | Get market by external ID | | getRaceDay() | Every horse-racing meeting you have open markets on, uncapped — see Horse racing below | | getDepth(id, { num_levels?, step_cents? }) | Simulated order-book depth, priced at the live b_effective (num_levels 0–50, step_cents 1–10) | | getHistory(id, { limit? }) | Trade history, newest first, with the prices after each trade (limit 1–200) | | createMarket(data) | Create a new market (admin) — see below | | getQuote(marketId, params) | Get a price quote | | executeTrade(marketId, params, { idempotencyKey? }) | Execute a trade; the key is sent as Idempotency-Key for replay-safe retries | | sellPosition(marketId, params, { idempotencyKey? }) | Sell contracts; same idempotencyKey option. params.min_avg_price (0.01–0.99) refuses the whole sell with 400 if the average fill would be below it | | getSellQuote(marketId, params) | Preview a sell's payout without executing (contracts is what would actually close) | | withUser(userId) | A client that acts as ONE player: X-Edge-User-Id on every request, sub-clients included; the original is unchanged. Without it the seven player-scoped calls are refused with 400 (DEMO operators excepted: there they share one account) | | getPortfolio() | Get user portfolio | | getPortfolioSummary() | The 5 headline portfolio figures, no positions list | | getBalanceBonusHistory({ limit?, offset? }) | The player's locked Balance Bonus credits and whether each is still pending (limit 1–500) | | getStats() | Your operator's dashboard statistics | | getRevenue() | Get revenue breakdown | | settleMarket(id, outcome) | Settle a market (admin) | | suspendMarket(id) | Suspend a market (admin) | | unsuspendMarket(id) | Reopen a suspended market (admin) | | rescheduleMarket(id, body) | Change a market's schedule after creation; omitted field = unchanged (admin) | | voidMarket(id) | Void a market: no side wins, every open online position refunded; retail tickets redeem at the cage (admin) | | getMarketPositions(id, { limit?, offset? }) | Every per-player position on a market, for wallet reconciliation; never credit an is_retail entry (limit 1–500) | | listAuditEvents({ after_id?, action?, resource_type?, actor_type?, limit?, offset? }) | Your audit trail — the reconciliation feed behind the MONEY webhooks (not generic webhook recovery: the feed. skip means a dropped feed webhook is never handed back here). after_id is a forward cursor (rows ABOVE that id, oldest first); after_id: 0 starts at the beginning and is a real cursor, not "unset". ⚠️ NOT complete on its own — settle, void and regrade rows normally land inside the money transaction, but a SETTLE OR VOID whose audit write fails inside its savepoint writes no row (regrade is unaffected), and the ~30s lag is a mitigation rather than a guarantee, so back it with a periodic full getMarketPositions re-read of open and recently-settled markets. Mounted un-prefixed only: a baseUrl ending in /v1 gives a 404 (7.0.0) | | getBalanceBonusConfig(params?) | Read Balance Bonus config (admin) | | updateBalanceBonusConfig(body) | Update Balance Bonus config (admin) | | getRiskControlsConfig() | Read your risk controls + clamp ranges (admin) | | updateRiskControlsConfig(body) | Update risk controls; null deletes a key (admin) | | getFees() | Read your fee terms, bounds and platform defaults (admin) | | updateFees(body) | Set your own fees; omit = leave alone, null = clear (admin) | | resetSandbox() | Reset sandbox data (demo mode only — 403 in production) | | createWebhook(params) | Register a webhook (admin) — the secret is shown once | | listWebhooks() | List webhooks | | getWebhook(id) | Get one webhook (no secret) | | updateWebhook(id, params) | Change URL / events / is_active / description; omitted field = unchanged (admin) | | deleteWebhook(id) | Deactivate a webhook (soft delete; updateWebhook(id, { is_active: true }) reverses it) (admin) | | rotateWebhookSecret(id) | Replace the signing secret; the new one is shown ONCE and the old one stops verifying immediately (admin) | | getWebhookDeliveries(id, { limit?, offset? }) | Delivery-attempt log, newest first (limit 1–200) | | testWebhook(id) | Send a signed test.ping to the endpoint and report delivered / failed | | listApiKeys() | Your API keys, revoked ones included; never a secret (admin) | | createApiKey({ name, scope }) | Mint a key — refused with 409 at 20 live keys (rotation order: docs/API.md, API keys — several per operator); the plaintext api_key is shown ONCE; store it, never log it (admin) | | revokeApiKey(keyId) | Revoke a key immediately; your last live ADMIN key is refused with 409 (admin) | | suspendSportsbookMarket(eventRef, { reason? }) | Mirror a fixed-odds suspension onto the linked market; a repeat is idempotent: true | | resumeSportsbookMarket(eventRef, { reason? }) | Reopen a mirrored market to the status its clock implies | | submitRegradeRequest(params) | Ask Predigy to regrade a RESOLVED market (admin) | | getRegradeRequest(id) / getRegradeAuditReport(id) | Status of a regrade request / its JSON audit report once executed | | healthCheck() | Health check |

Sub-Clients (SDK 2.x)

Grouped surfaces alongside the flat 1.x methods (upgrading is non-breaking):

| Sub-client | Methods | |------------|---------| | client.retail | mintTicket, executeRetailTrade, redeemTicket, cashoutTicket, getTicketStatus. ⚠️ executeRetailTrade and cashoutTicket take a REQUIRED third argument { idempotencyKey } — the server refuses a keyless retail wager or cash-out with 422. Mint one per cashier attempt and reuse it on every retry of that attempt, so the retry replays the original response instead of wagering — or paying — a second time | | client.parlay | createParlay, resolveParlayLeg | | client.compliance | 18 compliance report methods (exception report, daily transactions/results, wagering detail/summary, past-post, large wagers, structuring alerts, futures reconciliation, accrual recap, sport statistics, customer detail/summary, cutoff-enforcement log, operator config history, shift-close report, outstanding liability + its totals) + listRebateLedger, getRebatePeriod | | client.lp | listLPs, updateLPConfig, getDashboard, getActivity, getAnalytics (designation is Predigy-only — see CHANGELOG) | | client.feeds | getStatus, triggerSync, listEvents, configure, createManualEvent, listPending, importPending |

Horse racing

listMarkets caps limit at 100, and a busy race day holds more open horse-racing markets than that — so build a meeting tab bar from getRaceDay(), then list one meeting at a time:

const day = await client.getRaceDay(); // every meeting you have open markets on
for (const meeting of day.meetings) {
  const { markets } = await client.listMarkets({ horse_meeting_id: meeting.meeting_id });
  for (const market of markets) {
    if (market.horse) {                // null on non-horse and cross-track markets
      console.log(market.horse.race_number, market.horse.runners.map((r) => r.silk_seed));
    }
  }
}

horse_scope: "race" | "card" | "race_day" filters by market scope (any other value is a 400). getRaceDay is mounted only at /horse-racing/race-day, never under /v1. is_demo is a display flag, never a settlement signal.

Creating a market

await client.createMarket({
  title: "Lakers vs Celtics — Lakers Win",
  category: "NBA",
  // State the risk in DOLLARS. EDGE derives the engine's liquidity parameter
  // from this and the price the market opens at.
  max_exposure: 10000,
  // ⚠️ Required on every market (since 2026-09-06): when buying stops
  // (accept_in_play_trades: false) or in-play trading begins (true). It is
  // also the field the automatic cutoff starts from, with `trading_closes_at`
  // then driving `IN_PLAY` → `CLOSED`.
  event_start_time: new Date(Date.now() + 1 * 3600_000).toISOString(),
  // ⚠️ Required in practice: the server defaults `accept_in_play_trades` to
  // true, and an in-play market must declare its cutoff. Pass a FUTURE UTC
  // timestamp, or `accept_in_play_trades: false` for a pre-event-only market
  // (event_start_time is still required then).
  trading_closes_at: new Date(Date.now() + 4 * 3600_000).toISOString(),
});

max_exposure is a modelled maximum under EDGE's trading controls, not a contractual guarantee. It is mutually exclusive with b_base. Not every amount is expressible and which ones are depends on the opening price and the time to the event ($69.32–$6.93M at 50¢ and $391.21–$39.1M at 2¢ for a market created a day or more out, k = 1.0; 1.5x those inside 15 minutes); an out-of-range amount is refused with a 422 naming the nearest achievable figure, never silently adjusted.

Managing a market's schedule

// A postponed event: send only the fields that change. An omitted field is
// left alone; an explicit `null` is refused with 422 (there is no "clear").
const r = await client.rescheduleMarket("mkt_3f9c1d2e4b5a", {
  event_start_time: "2026-09-06T19:00:00Z",
  trading_closes_at: "2026-09-06T23:00:00Z",
});
// A SUSPENDED market whose stored start had passed reopens on a future start
// (`unsuspended: true`). Only a market still SUSPENDED needs reopening —
// calling unsuspendMarket on an OPEN market is a 409, not a no-op.
if (r.market_status === "SUSPENDED") await client.unsuspendMarket("mkt_3f9c1d2e4b5a");

// The event will never produce a result: refund every open online position instead of settling.
const v = await client.voidMarket("mkt_3f9c1d2e4b5a");
v.total_refunded; // "3842.500000" — an exact decimal STRING, not a number

License

Proprietary — see LICENSE. Copyright (c) 2026 Predigy Inc. All rights reserved. Use is permitted only under a separate written license agreement with Predigy Inc.