Detailed explanation of how buylist prices are calculated including all modifiers, hotlist boosts, darklist penalties, and stock limitations.
Pricing Overview
The pricing engine calculates buylist prices through a multi-step process that starts with raw TCGPlayer market data and applies various modifiers based on store configuration. The system supports cash and credit pricing independently, with different rules for each payment type.
Prices are calculated per condition (NM, LP, MP, HP, DM), per language, and per printing type. The final price considers inventory levels, hotlist boosts, darklist penalties, availability delays, and stock limitations.
Step-by-Step Price Calculation
- Step 1: Input Validation
- Validates productId is provided
- Extracts printing, condition, language, and storeId from query parameters
- Defaults language to 'EN' if not provided
- Step 2: Store Configuration Lookup
- If storeId provided, finds UserModel by buylistConfig.storeId
- Extracts storeConfig from user.buylistConfig.config
- Gets storeCurrency (defaults to USD)
- If no store config found, returns raw TCGPlayer prices only (no final prices)
- Step 3: Product Lookup
- Fetches ProductModel by productId for categoryId, groupId, rarity, and EU price data
- Product data needed for game-specific pricing rules and availability delays
- Step 4: Raw Price Document Fetch
- Checks if store config enables EU prices (pricingOptions.euPrices)
- If EU enabled and product.euPrices exists: Uses product.euPrices array, finds matching printing (or Holofoil fallback for Foil), sets sourceCurrency='EUR'
- Otherwise: Queries PriceModel collection by productId and subTypeName (printing), sets sourceCurrency='USD'
- If printing='Foil' not found, tries 'Holofoil' as fallback
- Raw price doc contains: directLowPrice, lowPrice, midPrice, marketPrice, highPrice
- If no price doc found, defaults all values to 0
- Step 5: Currency Conversion
- If storeCurrency differs from sourceCurrency, fetches exchange rates
- USD rates: Fetches from exchangerate-api.com, cached in ExchangeRateModel (updated hourly)
- EUR rates: Fetches from exchangerate-api.com, cached in ExchangeRateEuroModel (updated hourly)
- Converts all price fields (directLowPrice, lowPrice, midPrice, marketPrice, highPrice) using exchange rate
- Rounds converted prices to 3 decimal places
- Step 6: Stock Limitations Check
- If storeConfig.stockLimitations.enabled is true:
- Fetches InventoryModel for productId matching condition, language, printing
- Calculates total quantity across all matching stock items
- If stopBuyingAtLimit=true and inventoryQuantity >= maxQuantityPerProduct: Returns zero prices for all conditions
- Otherwise: Checks reductionThresholds array, finds highest threshold where inventoryQuantity >= atQuantity
- Applies stockLimitationModifier = 1 - (reductionPercentage / 100)
- Example: If inventory=150 and threshold at 100 applies 10% reduction, modifier = 0.9
- Step 7: Availability Delay Check
- If pricingOptions.availabilityDelayDays is set (number > 0):
- Fetches GroupModel by groupId to get publishedOn date
- Calculates availableAfter = publishedOn + availabilityDelayDays
- If current date < availableAfter: Returns zero prices for all conditions (not buying yet)
- If current date >= availableAfter: Continues with normal pricing
- Step 8: Hotlist Boost Check
- Checks user.buylistConfig.hotlist array for matching productId
- If found and boostPercentage > 0: Sets hotlistBoostMultiplier = 1 + (boostPercentage / 100)
- Example: 10% boost = multiplier of 1.10
- Applied at final step after all other calculations
- Step 9: Darklist Penalty Check
- Checks user.buylistConfig.darklist array for matching productId
- If found and penaltyPercentage > 0: Sets darklistPenaltyMultiplier = 1 - (penaltyPercentage / 100)
- Example: 20% penalty = multiplier of 0.80
- Applied at final step after all other calculations
- Step 10: Game Configuration Check
- Checks if storeConfig.gameSettings[categoryId] exists and is enabled
- Checks if pricingOptions exists in game config
- If missing or disabled: Returns raw TCGPlayer prices only (no final cash/credit prices)
- Special case: Category 999 (Video Games) uses different pricing logic (see below)
- Step 11: Fallback Price Selection
- Selects base price from rawPriceDoc using fallback order:
- 1. primaryPriceType (default: 'market') → uses marketPrice
- 2. secondaryPriceType (default: 'low') → uses lowPrice
- 3. lastCallPriceType (default: 'mid') → uses midPrice
- 4. doomsdayPriceType (default: 'high') → uses highPrice
- Uses first non-zero price found, records usedFallbackLevel
- Applies fallback modifier based on level: primaryPriceModifier, secondaryPriceModifier, lastCallPriceModifier, or doomsdayPriceModifier
- Formula: chosenPrice × (1 + fallbackModifier / 100)
- Applies basePriceAdjustment (fixed amount added/subtracted)
- Result: finalBase price
- Step 12: Bulk Price Override Check
- If pricingOptions.bulkPriceRules array exists:
- Finds rule where: product.rarity matches rule.rarities array, language matches rule.languages (if specified), finalBase is within rule.minPrice/maxPrice range
- If match found:
- bulkCash = rule.cashMode === 'fixed' ? rule.cashValue : finalBase × (rule.cashValue / 100)
- bulkCredit = rule.creditMode === 'fixed' ? rule.creditValue : finalBase × (rule.creditValue / 100)
- If bulk override applied: Uses these prices for ALL enabled conditions, skips condition modifiers, applies stock/hotlist/darklist multipliers, returns early
- Step 13: Condition Modifiers
- Gets default condition modifiers from pricingOptions.conditionModifiers (default: NM=100%, LP=90%, MP=80%, HP=70%, DM=60%)
- Checks for set-specific overrides: If gameConfig.bulkCeilingSets exists and product.groupId matches, uses set.conditionDiscounts instead
- For each condition (NM, LP, MP, HP, DM):
- Checks if condition enabled in gameConfig.conditions
- If disabled: Sets price to 0
- If enabled: Calculates perCond[condition] = finalBase × (conditionModifier / 100)
- Applies rarity ceiling: If pricingOptions.perSetRarityCeilings[rarity] exists and price exceeds ceiling, caps to ceiling value
- Step 14: Language Modifiers
- Gets language modifiers from pricingOptions.languageModifiers (default: EN=100%)
- Checks if language enabled in gameConfig.languages
- If language disabled: Sets all condition prices to 0
- If enabled: Multiplies each condition price by (languageModifier / 100)
- Formula: perCond[condition] = perCond[condition] × (langModifier / 100)
- Step 15: Cash/Credit Price Ranges
- For each condition with non-zero base price:
- Cash Price: If pricingOptions.enableCash is true, searches cashPriceRanges array for range where baseVal >= range.min and baseVal <= range.max
- If range found: cashPrice = range.mode === 'fixed' ? range.fixedPrice : baseVal × (range.percentage / 100)
- If no range matches: cashPrice = 0 (not buying for cash)
- Credit Price: Same logic using creditPriceRanges and enableCredit flag
- Special case: If cashPrice < 0.001 but creditPrice >= 0.001, sets cashPrice = creditPrice
- Final prices rounded to 3 decimal places
- Step 16: Final Multipliers
- Applies all multipliers in sequence to final cash and credit prices:
- finalPrice = finalPrice × stockLimitationModifier × hotlistBoostMultiplier × darklistPenaltyMultiplier
- Rounds result to 3 decimal places
Video Games (Category 999) Special Logic
Video games use a completely different pricing system that reads from product.prices object instead of PriceModel:
- Conditions: Uses 'Loose', 'CIB', 'New', 'Graded', 'Box Only', 'Manual Only' instead of NM/LP/MP/HP/DM
- Price Source: Reads from product.prices object fields like 'loose-price', 'cib-price', 'new-price', etc.
- Price Parsing: Parses string values like "$xx.xx" into numeric values
- Cash/Credit Ranges: Uses cashPriceRanges and creditPriceRanges from gameSettings[999].pricingOptions
- Range Matching: Matches raw price against range min/max, calculates cash/credit using mode (fixed or percentage)
- Output: Returns fields like LooseFinalCash, LooseFinalCredit, CIBFinalCash, CIBFinalCredit, etc.
- Hotlist/Darklist: Still applies hotlistBoostMultiplier at final step
Price Range Configuration
Cash and credit prices are determined by matching the calculated base price against configured price ranges:
- Range Structure: Each range has min (or minPrice), max (or maxPrice, optional), mode ('fixed' or 'percentage'), and value
- Matching Logic: Finds first range where basePrice >= min and basePrice <= max (or max is null/undefined)
- Fixed Mode: Returns fixedPrice value directly (e.g., always pay $5 for cards in this range)
- Percentage Mode: Calculates basePrice × (percentage / 100) (e.g., pay 50% of market price)
- No Match: If base price doesn't match any range, final price is 0 (not buying)
- Independent Ranges: Cash and credit can have different ranges, enabling different pricing strategies
Pricing Calculation Examples
Example 1: Standard Card
Product: Lightning Bolt (TCGPlayer ID 12345), NM, EN, Normal printing
Raw marketPrice: $5.00
Store config: primaryPriceType='market', primaryPriceModifier=0%, basePriceAdjustment=0
Condition modifier: NM=100%
Language modifier: EN=100%
Cash range: min=$0, max=$100, percentage=50% → Cash = $5.00 × 50% = $2.50
Credit range: min=$0, max=$100, percentage=60% → Credit = $5.00 × 60% = $3.00
Final: NMFinalCash=$2.50, NMFinalCredit=$3.00
Example 2: With Hotlist Boost
Same card as above, but productId is in hotlist with 20% boost
Base calculations same: Cash=$2.50, Credit=$3.00
Apply hotlistBoostMultiplier: 1.20
Final: NMFinalCash=$2.50 × 1.20 = $3.00, NMFinalCredit=$3.00 × 1.20 = $3.60
Example 3: With Stock Limitation
Same card, but inventory has 150 copies (threshold at 100 applies 15% reduction)
stockLimitationModifier = 1 - 0.15 = 0.85
Base: Cash=$2.50, Credit=$3.00
Final: NMFinalCash=$2.50 × 0.85 = $2.125, NMFinalCredit=$3.00 × 0.85 = $2.55
Example 4: Fallback to Secondary Price
Product has marketPrice=0, lowPrice=$10.00
Uses secondaryPriceType='low' with secondaryPriceModifier=5%
finalBase = $10.00 × 1.05 = $10.50
Then applies condition/language/range logic as normal
usedFallbackLevel='secondary' in response
API Response Structure
{
"productId": 12345,
"price": {
"directLowPrice": 4.50,
"lowPrice": 5.00,
"midPrice": 5.50,
"marketPrice": 5.00,
"highPrice": 6.00,
"NMFinalCash": 2.50,
"NMFinalCredit": 3.00,
"LPFinalCash": 2.25,
"LPFinalCredit": 2.70,
"MPFinalCash": 2.00,
"MPFinalCredit": 2.40,
"HPFinalCash": 1.75,
"HPFinalCredit": 2.10,
"DMFinalCash": 1.50,
"DMFinalCredit": 1.80,
"usedFallbackLevel": "primary",
"inventoryQuantity": 45,
"stockLimitationApplied": false,
"stockLimitReached": false,
"bulkOverrideApplied": false,
"logs": ["[Step 1] Starting getPriceByProduct...", ...]
}
}
Debug Logs
The pricing endpoint includes detailed step-by-step logs in the response when debug mode is enabled. Each log entry shows:
- Step number and description
- Values being used (chosen price, modifiers, multipliers)
- Decisions made (which fallback level, which range matched, etc.)
- Final calculations at each stage
Logs are returned in price.logs array and can be used to troubleshoot pricing issues or understand why a price was calculated a certain way.