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 (
.comandcomare treated identically) - Clean ESM API with TypeScript types
npm i domain-quotesimport { 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 are configured via the discounts field in DomainQuoteConfig. Each discount can be filtered by:
- Date range:
startAtandendAt(ISO timestamps) - Extensions: List of eligible extensions (normalized, so
.comandcomare 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
});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.
-
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): stringNormalizes 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): booleanChecks if an extension is supported (normalizes input).
-
listSupportedCurrencies(): string[]Returns supported currencies (default:
['USD', 'NGN']). -
isSupportedCurrency(code: string): booleanChecks if a currency is supported (case-insensitive).
-
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);
-
DEFAULT_CONFIG: DomainQuoteConfigThe default configuration with remote pricing data, 7.5% VAT, and no discounts.
-
DEFAULT_VAT_RATE=0.075(7.5%)
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'])
}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'
}
}-
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, andcomare all equivalent. -
Discount order: The
isEligiblecallback 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.
- Prices:
npm testThe test suite uses Node's built-in node:test runner and builds the library first.