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

domain-quotes

v0.4.3

Published

Fast multi-currency domain quote checker library across registrars.

Readme

Domain Quotes

Build Test NPM License

Domain Quote is a lightweight TypeScript/JavaScript library to compute domain registration quotes across currencies with discounts and VAT, using curated datasets.

Includes:

  • Extension support based on unified registrar price list (OpenProvider/NIRA)
  • Currency conversion via remote exchange rates
  • VAT calculation with configurable rate
  • Flexible discount system with date ranges, extension/transaction filtering, and custom eligibility callbacks
  • Configurable markup to increase base prices before taxes/discounts
  • Extension normalization (.com and com are treated identically)
  • Clean ESM API with TypeScript types

Install

npm i domain-quotes

Usage

import { getDefaultQuote, DomainQuotes, DEFAULT_CONFIG } from 'domain-quotes';

// Quick quote (uses bundled defaults, no discounts configured by default)
const quote = await getDefaultQuote('com', 'USD');
// → { extension, currency, basePrice, discount, tax, totalPrice, symbol, domainTransaction }

// Extensions are normalized - leading dots are stripped
const quote2 = await getDefaultQuote('.com', 'USD'); // same as 'com'

// Custom config with discounts
const dq = new DomainQuotes({
  ...DEFAULT_CONFIG,
  discounts: {
    SAVE10: {
      rate: 0.1, // 10% discount
      extensions: ['com', 'net', 'org'],
      startAt: '2024-01-01T00:00:00Z',
      endAt: '2024-12-31T23:59:59Z',
    },
  },
});
const discounted = await dq.getQuote('com', 'USD', { discountCodes: ['SAVE10'] });

// Add a 15% markup before discounts/taxes
const withMarkup = new DomainQuotes({
  ...DEFAULT_CONFIG,
  markup: { type: 'percentage', value: 0.15 },
});
const quoteWithMarkup = await withMarkup.getQuote('com', 'USD');

Discounts

Discounts are configured via the discounts field in DomainQuoteConfig. Each discount can be filtered by:

  • Date range: startAt and endAt (ISO timestamps)
  • Extensions: List of eligible extensions (normalized, so .com and com are equivalent)
  • Transaction types: Optional list of transaction types (create, renew, restore, transfer)
  • Custom eligibility: Optional callback for complex eligibility logic
const dq = new DomainQuotes({
  ...DEFAULT_CONFIG,
  discounts: {
    // Basic discount
    WELCOME: {
      rate: 0.1,
      extensions: ['com', 'net'],
      startAt: '2024-01-01T00:00:00Z',
      endAt: '2024-12-31T23:59:59Z',
    },

    // Discount limited to specific transaction types
    NEWUSER: {
      rate: 0.2,
      extensions: ['com', 'net', 'org'],
      startAt: '2024-01-01T00:00:00Z',
      endAt: '2024-12-31T23:59:59Z',
      transactions: ['create'], // Only applies to new registrations
    },

    // Discount with custom eligibility callback
    BIGSPENDER: {
      rate: 0.25,
      extensions: ['com', 'net', 'org'],
      startAt: '2024-01-01T00:00:00Z',
      endAt: '2024-12-31T23:59:59Z',
      isEligible: (ctx) => ctx.basePrice >= 50, // Only if base price >= $50
    },

    // Async eligibility (e.g., check external service)
    VIP: {
      rate: 0.3,
      extensions: ['com'],
      startAt: '2024-01-01T00:00:00Z',
      endAt: '2024-12-31T23:59:59Z',
      isEligible: async (ctx) => {
        // Check if user is VIP via external service
        const isVip = await checkVipStatus(ctx.discountCode);
        return isVip;
      },
    },
  },
});

// Apply discounts
const quote = await dq.getQuote('com', 'USD', {
  discountCodes: ['WELCOME', 'NEWUSER'],
  discountPolicy: 'max', // default: use highest discount
});

// Stack multiple discounts
const stacked = await dq.getQuote('com', 'USD', {
  discountCodes: ['WELCOME', 'NEWUSER'],
  discountPolicy: 'stack', // sum all applicable discounts
});

Eligibility Callback Context

The isEligible callback receives a context object with:

interface DiscountEligibilityContext {
  extension: string;        // Normalized extension (e.g., 'com')
  currency: string;         // Currency code (e.g., 'USD')
  transaction: TransactionType; // Transaction type
  basePrice: number;        // Base price before discount
  discountCode: string;     // The discount code being evaluated
}

The callback is only invoked after all other criteria (date range, extension, transaction type) are satisfied. If the callback throws an error, the discount is skipped.

API

Functions

  • getDefaultQuote(extension, currency, options?): Promise<Quote>

    Computes a quote using bundled defaults (no discounts configured by default).

    const quote = await getDefaultQuote('com', 'USD');
    const withOptions = await getDefaultQuote('.ng', 'NGN', {
      discountCodes: ['SAVE10'],
      transaction: 'renew',
    });
  • normalizeExtension(extension: string): string

    Normalizes an extension by trimming whitespace, lowercasing, and removing leading dots.

    normalizeExtension('.COM')   // → 'com'
    normalizeExtension('..ng')   // → 'ng'
    normalizeExtension('  org ') // → 'org'
  • listSupportedExtensions(): string[]

    Returns all extensions with pricing data.

  • isSupportedExtension(extension: string): boolean

    Checks if an extension is supported (normalizes input).

  • listSupportedCurrencies(): string[]

    Returns supported currencies (default: ['USD', 'NGN']).

  • isSupportedCurrency(code: string): boolean

    Checks if a currency is supported (case-insensitive).

Class

  • new DomainQuotes(config: DomainQuoteConfig)

    Creates a quote calculator with custom configuration.

    const dq = new DomainQuotes({
      ...DEFAULT_CONFIG,
      vatRate: 0.2,
      supportedCurrencies: ['USD', 'NGN', 'EUR', 'GBP'],
      discounts: { /* ... */ },
    });
    
    const quote = await dq.getQuote('com', 'EUR', options);

Constants

  • DEFAULT_CONFIG: DomainQuoteConfig

    The default configuration with remote pricing data, 7.5% VAT, and no discounts.

  • DEFAULT_VAT_RATE = 0.075 (7.5%)

Types

type TransactionType = 'create' | 'renew' | 'restore' | 'transfer';
type DiscountPolicy = 'stack' | 'max';
type MarkupType = 'percentage' | 'fixedUsd';

interface GetQuoteOptions {
  discountCodes?: string[];          // Discount codes to apply (case-insensitive)
  now?: number | Date;               // Override current time for testing
  discountPolicy?: DiscountPolicy;   // 'max' (default) or 'stack'
  transaction?: TransactionType;     // default: 'create'
}

interface Quote {
  extension: string;                 // Normalized extension
  currency: string;                  // Currency code
  basePrice: number;                 // Price before discount
  discount: number;                  // Total discount amount
  tax: number;                       // Tax amount
  totalPrice: number;                // Final price (basePrice - discount + tax)
  symbol: string;                    // Currency symbol
  domainTransaction: TransactionType; // Transaction type
}

interface Markup {
  type: MarkupType;                  // 'percentage' or 'fixedUsd'
  value: number;                     // 0.2 = +20%, or fixed USD amount
}

interface DiscountEligibilityContext {
  extension: string;
  currency: string;
  transaction: TransactionType;
  basePrice: number;
  discountCode: string;
}

type DiscountEligibilityCallback =
  (context: DiscountEligibilityContext) => boolean | Promise<boolean>;

interface DiscountConfig {
  rate: number;                      // e.g., 0.1 for 10%
  extensions: string[];              // Eligible extensions (normalized)
  startAt: string;                   // ISO timestamp
  endAt: string;                     // ISO timestamp
  transactions?: TransactionType[];  // Limit to specific transaction types
  isEligible?: DiscountEligibilityCallback; // Custom eligibility logic
}

interface ExchangeRateData {
  countryCode: string;
  currencyName: string;
  currencySymbol: string;
  currencyCode: string;
  exchangeRate: number;
  inverseRate: number;
}

type PriceEntry = number | Record<string, number>;
type PriceTable = Record<string, PriceEntry>;

interface DomainQuoteConfig {
  createPrices: PriceTable;                    // Base prices for create
  renewPrices?: PriceTable;                    // Optional prices for renew
  restorePrices?: PriceTable;                  // Optional prices for restore
  transferPrices?: PriceTable;                 // Optional prices for transfer
  exchangeRates: ExchangeRateData[];           // Currency conversion data
  vatRate: number;                             // VAT rate (e.g., 0.075 for 7.5%)
  discounts: Record<string, DiscountConfig>;   // Discount configurations
  markup?: Markup;                             // Optional markup
  supportedCurrencies?: string[];              // Allowed currencies (default: ['USD', 'NGN'])
}

Errors

  • UnsupportedExtensionError - code: 'ERR_UNSUPPORTED_EXTENSION'
  • UnsupportedCurrencyError - code: 'ERR_UNSUPPORTED_CURRENCY'
import { UnsupportedExtensionError, UnsupportedCurrencyError } from 'domain-quotes';

try {
  await getDefaultQuote('invalid-tld', 'USD');
} catch (err) {
  if (err instanceof UnsupportedExtensionError) {
    console.log(err.code); // 'ERR_UNSUPPORTED_EXTENSION'
  }
}

Notes

  • Rounding: All monetary values are rounded to 2 decimal places at each step.

  • VAT: A single VAT rate is applied to the subtotal (base price - discount). Default is 7.5%.

  • Extension normalization: Leading dots are stripped and extensions are lowercased. .COM, ..com, and com are all equivalent.

  • Discount order: The isEligible callback is only called after date range, extension, and transaction type checks pass.

  • Remote data: Price and exchange-rate data are fetched at import time from:

    • Prices: https://raw.githubusercontent.com/namewiz/registrar-pricelist/refs/heads/main/data/unified-create-prices.csv
    • Exchange rates: https://raw.githubusercontent.com/namewiz/registrar-pricelist/refs/heads/main/data/exchange-rates.json

    These are cached in-memory for the life of the process.

Testing

npm test

The test suite uses Node's built-in node:test runner and builds the library first.