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

tcgpriser

v0.7.0

Published

Typed Node.js/browser client for the tcgpriser.se API: Pokémon TCG price data, catalog, shop matches and bargains.

Readme

tcgpriser

A typed Node.js / browser client for the tcgpriser.se API: Pokémon TCG catalog data, price history, shop listings and bargains for shops tracked in Sweden.

Features

  • 🎯 Fully typed, straight off the API's own OpenAPI spec
  • Async/await on every method, no callbacks
  • 🛠️ Full IntelliSense for every method and response field
  • 📦 Zero runtime dependencies, built on the standard fetch API, ships as ESM and CJS
  • 🔄 Types stay in sync with the API: yarn generate:types regenerates them from a live instance

Installation

npm install tcgpriser

or with yarn

yarn add tcgpriser

Needs Node 18+ for global fetch, or pass your own fetch implementation (see Options).

Usage

import { TcgPriser } from 'tcgpriser';

const tcgpriser = new TcgPriser();

const card = await tcgpriser.cards.get('mega-evolution-ascended-heroes-fezandipiti-ex');
console.log(card.name, card.expansion?.name);

// Content (name, images, expansion, ...) and pricing (retailPrice, lowestShopOffer, ...) are
// separate, differently-cached calls — see "Pricing" below.
const pricing = await tcgpriser.cards.pricing('mega-evolution-ascended-heroes-fezandipiti-ex');
console.log(pricing.retailPrice, pricing.lowestShopOffer?.shop.name);

const { data: bargains } = await tcgpriser.bargains.list({ type: 'card' });

Public methods need no token. A handful of premium methods do, see Authentication.

Cards

await tcgpriser.cards.list({ search: 'pikachu', limit: 10 });
await tcgpriser.cards.get('mega-evolution-ascended-heroes-fezandipiti-ex'); // id or technicalName
await tcgpriser.cards.matches('fezandipiti-ex', { inStock: true });

Sealed products

Booster boxes, ETBs, tins and the like. Single cards live under cards, not here.

await tcgpriser.products.list({ search: 'booster box' });
await tcgpriser.products.get('scarlet-violet-booster-pack');
await tcgpriser.products.matches('scarlet-violet-booster-pack');

Expansions

await tcgpriser.expansions.list();
await tcgpriser.expansions.products('eng-scarlet-violet-journey-together'); // { expansion, cards, sealed } — content only

Pricing

list()/get()/expansions.products() all return catalog content only — name, images, brand, expansion, rarity. Pricing (retailPrice, estimatedValue, lowestShopOffer, referencePriceSnapshotsByProvider) is a separate, shorter-cached call: content changes on an admin edit or catalog import, pricing refreshes daily, so each is cached at the TTL its own freshness supports.

await tcgpriser.cards.pricing('fezandipiti-ex'); // id or technicalName
await tcgpriser.products.pricing('scarlet-violet-booster-pack');

// Batch form, up to 200 ids at once — ids only, not technicalNames.
const { data: cards } = await tcgpriser.cards.list({ search: 'pikachu' });
await tcgpriser.cards.pricingBatch(cards.map((card) => card.id));

Shops

await tcgpriser.shops.list({ active: true });
await tcgpriser.shops.get('alphaspel');
await tcgpriser.shopMatches.forShop('alphaspel'); // everything currently listed there

Price stats and bargains

await tcgpriser.priceStats.daily({ technicalName: 'scarlet-violet-booster-pack' });
await tcgpriser.priceStats.estimatedValues({ expansion: 'eng-scarlet-violet-journey-together' });
await tcgpriser.bargains.list({ type: 'sealed' }); // 'sealed' | 'card' | 'all'

Pack rates

await tcgpriser.packRates.list();
await tcgpriser.packRates.get(expansionId);

Available Methods

cards

| Method | Description | |---|---| | list(params) | Search or list cards | | get(id) | Fetch one card by id or technicalName | | matches(id, params) | Current shop listings matched to this card | | prices(id, params) 🔒 | Individual marketplace sale records | | referencePrices(id, params) 🔒 | Cardmarket / TCGplayer / eBay / Tradera price history | | livePricing(id) 🔒 | Pricing computed fresh for this request |

products

| Method | Description | |---|---| | list(params) | Search or list sealed products | | get(id) | Fetch one product by id or technicalName | | matches(id, params) | Current shop listings matched to this product | | prices(id, params) 🔒 | Individual marketplace sale records | | referencePrices(id, params) 🔒 | Cardmarket / TCGplayer / Tradera price history | | livePricing(id) 🔒 | Pricing computed fresh for this request |

expansions

| Method | Description | |---|---| | list() | Every expansion | | products(technicalName) | Every card and sealed product in one expansion | | livePricing(technicalName) 🔒 | Fresh pricing for every item in one expansion |

shops

| Method | Description | |---|---| | list(params) | Every tracked shop | | get(id) | Fetch one shop by id or technicalName |

shopMatches

| Method | Description | |---|---| | list(params) | Every current match across every shop | | forShop(shop, params) | Everything currently listed at one shop | | shopStats(params) | Match counts per shop |

shopMatchStats 🔒

| Method | Description | |---|---| | forProduct(id, params) | One product's price history, broken out per shop | | forShop(shop, params) | One shop's price history, broken out per product | | compare(params) | One product's price at every shop that carries it |

priceStats

| Method | Description | |---|---| | daily(params) | Daily average price history | | estimatedValues(params) | Current estimated market value | | topProducts(params) | Items ranked by shop availability | | product(id) 🔒 | Daily history, estimate and variant summary for one product | | productFull(id) 🔒 | product() plus the item's current shop matches | | productDaily(id, params) 🔒 | Daily history for one product, custom window | | productDailyLast30(id) 🔒 | Daily history, fixed to the last 30 days | | productEstimatedValue(id) 🔒 | Current estimated value only | | productByVariant(id, params) 🔒 | Price stats per card condition/grade | | productDailyByVariant(id, params) 🔒 | Daily history for one condition/grade |

bargains

| Method | Description | |---|---| | list(params) | Current listings priced below their reference price | | search(params) 🔒 | Same, with real pagination and filters |

packRates

| Method | Description | |---|---| | list() | Pull-rate odds for every expansion that has them | | get(expansionId) | Pull-rate odds for one expansion |

shopUrls 🔒

| Method | Description | |---|---| | submit(params) | Submit a shop URL for scraping | | assignProduct(id, params) | Manually assign (or clear) the product a URL resolves to |

stats

| Method | Description | |---|---| | platform() | Platform-wide overview counts |

🔒 = premium, needs an API token. See below.

Authentication

Public methods work with no setup. Premium methods (marked 🔒 above) need a Premium subscriber's API token, generated from tcgpriser.se/account/api-token. Unlike the site's own session login, which is OAuth-based and can't be driven headlessly, the API token is a long-lived, revocable secret made specifically for scripts and other programmatic callers. Generate it once from your account page and pass it in:

const tcgpriser = new TcgPriser(myApiToken); // shorthand for { authToken: myApiToken }
await tcgpriser.cards.livePricing('fezandipiti-ex');

Or set no default and pass a token per call, which fits better when one client instance serves requests for many different signed-in users:

const tcgpriser = new TcgPriser();
await tcgpriser.cards.livePricing('fezandipiti-ex', { authToken: requestUserApiToken });

A missing or invalid token gets 401 unauthorized. A valid token without an active subscription gets 403 premiumRequired. Both come back as TcgPriserError:

import { TcgPriser, TcgPriserError } from 'tcgpriser';

try {
  await tcgpriser.cards.get('does-not-exist');
} catch (error) {
  if (error instanceof TcgPriserError) {
    console.log(error.statusCode, error.code, error.message); // 404 'notFound' 'Card not found'
  }
}

Options

new TcgPriser(myApiToken);  // shorthand for { authToken: myApiToken }
new TcgPriser();            // no token, public methods only

new TcgPriser({
  authToken: myApiToken,

  // Local dev, self-hosting or tests only. Leave this out for normal use.
  advanced: {
    baseUrl: 'https://api.tcgpriser.se', // default; point at a local dev server instead
    headers: { 'User-Agent': 'my-app/1.0' },
    fetch: myCustomFetch, // defaults to global fetch (Node 18+)
  },
});

Types

Every response type is exported from the package root:

import type { Card, SealedProduct, Bargain, ItemStats } from 'tcgpriser';

If you need the raw generated schema instead, it's exported too:

import type { components } from 'tcgpriser';

type CardSchema = components['schemas']['CardWithPricing'];

Most types in tcgpriser are direct aliases onto that generated schema, so a field in your code and a field in the API docs are the same field, always.

Images

imageUrl, logoUrl and symbolUrl fields point at tcgpriser.se's own CDN, which is sized for tcgpriser.se's own traffic, not for hotlinking from other sites and apps. For best performance, rehost these images on your own storage/CDN and cache them there instead of linking to them directly — one less hop, tuned to your own traffic and geography, and no dependency on infrastructure that isn't yours.

A simple way to do this: fetch the image once, save it under the URL's path (e.g. products/eng-scarlet-violet-booster-pack.png) as a stable local key, serve it from your own storage from then on, and periodically re-fetch (a nightly job is plenty) using a conditional GET so you only pay for images that actually changed:

import { mkdir, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';

const etags = new Map<string, string>(); // persist this however you persist anything else

async function rehostImage(imageUrl: string, cacheDir: string): Promise<string> {
  const key = new URL(imageUrl).pathname.replace(/^\/[^/]+\//, ''); // "products/....webp"
  const localPath = join(cacheDir, key);
  const knownEtag = etags.get(key);

  const res = await fetch(imageUrl, { headers: knownEtag ? { 'If-None-Match': knownEtag } : {} });
  if (res.status === 304) return localPath; // unchanged since last sync

  if (!res.ok) throw new Error(`Failed to fetch ${imageUrl}: ${res.status}`);
  await mkdir(dirname(localPath), { recursive: true });
  await writeFile(localPath, Buffer.from(await res.arrayBuffer()));

  const etag = res.headers.get('etag');
  if (etag) etags.set(key, etag);
  return localPath;
}

Swap the fs/mkdir/writeFile calls for your own storage's SDK (S3, R2, Cloudflare Images, ...) if you're not caching to local disk. See examples/rehost-images.ts for a runnable version of this against a live tcgpriser response.

Scripts

Build

yarn build

Bundles to dist/ as ESM, CJS and .d.ts with tsup.

Regenerate types

yarn generate:types

Regenerates src/generated/openapi.d.ts from a live API instance (defaults to http://localhost:5000, the local dev server). Committed to the repo, not gitignored. Regenerate it, review the diff, commit it, same as pris-tabell-ui does for its own generated types.

Test

yarn test

test/http.test.ts runs against a mocked fetch. test/live.test.ts hits a real API instance and skips itself if nothing answers at TCGPRISER_TEST_BASE_URL (default http://localhost:5000), so the suite stays green with no server running.

Example

yarn example

Runs examples/basic.ts against a local dev API. Set TCGPRISER_AUTH_TOKEN to see the premium call succeed instead of the expected 401.

yarn example:rehost-images

Runs examples/rehost-images.ts — the "Images" section's rehosting pattern against a handful of real product images. Run it twice to see the second pass come back as 304s.

Scope

Covers the API's full documented surface: public catalog, price and bargain reads, plus the premium endpoints above. Not covered: the admin/scraper/auth surface, which isn't part of any published contract.

License

MIT