Complete business logic documentation for the tradeit.gg pricing engine — every formula, threshold, and decision path
The Pricing Manager runs as a NestJS service that determines prices for every tradeable item (CS2/TF2/Rust skins). It produces these outputs:
22100 = $221.00.We'll follow this item through every step of the pricing pipeline to show how each calculation works with real numbers.
Think of the Pricing Manager as a shopkeeper who needs to decide three things for every item on the shelf:
The system recalculates all prices periodically by running through a 13-step pipeline. Each step adds more information or applies adjustments based on market conditions, inventory levels, and risk signals.
How the 4 prices relate to each other:
Before calculating any price, we need raw ingredients from 4 sources. Think of it like a chef gathering ingredients before cooking:
Data flows into the pricing engine:
For our AK-47 Redline: Buff says buy=$18, sell=$20 with 35 listings. The aggregator shows prices on 4 markets (Steam $23, CSFloat $22, Buff $21, Halo $21.50). Our bots hold 15 units. Monthly: 40 withdrawals, 35 deposits.
Fetches from the buff_prices_scraper table (appId = 730). Provides:
| Field | Description |
|---|---|
buffBuy | Highest buy order price on Buff |
buffSell | Lowest sell listing price on Buff |
buffBuyAmount | Number of buy orders |
buffSellAmount | Number of sell listings |
isDoppler | Whether this item is a Doppler knife variant |
HTTP GET to external aggregator. Returns per-item array of market prices from: Steam, CSFloat, Buff163, Halo, and others. Each entry has: market, price, quantity, volume.
From the tradeit database item_stocks table:
| Field | Description |
|---|---|
botStock | Items held by our bots |
containerBotStock | Items in container bots |
tradableContainerBotStock | Tradeable items in container bots |
totalTradableContainerBotStock | Total tradeable container bot items (all containers) |
reservedItemsStock | Reserved items total |
tradeableReservedItemsStock | Reserved items that are tradeable |
lockedReservedItemsStock | Reserved items that are locked |
tempReservedItemsStock | Temporarily reserved |
userListingsStock | Items listed by users (not ours) |
currentStock is calculated as:
| Period | Fields |
|---|---|
| Monthly | monthlyInAmount, monthlyOutAmount, monthlyInUniqueAmount, monthlyOutUniqueAmount, monthlyInAvgPrice, monthlyOutAvgPrice |
| Weekly | weeklyInAmount, weeklyOutAmount, weeklyInAvgPrice, weeklyOutAvgPrice |
| Daily | dailyInAmount, dailyOutAmount, dailyInAvgPrice, dailyOutAvgPrice |
"In" = items deposited to us (users sell). "Out" = items withdrawn from us (users buy). "Unique" = distinct users.
Calculated from hourly snapshots stored in item_prices_snapshot (up to 100 per item):
| Field | Description |
|---|---|
avg7DStablePrice | Average stable price over last 7 days |
prevMonthAvg7DStablePrice | Average stable price from 30-37 days ago |
median7DStablePrice | Median stable price over 7 days |
median30DStablePrice | Median stable price over 30 days |
median7DMaxStock | Median wantedMaxStock over 7 days |
median30DMaxStock | Median wantedMaxStock over 30 days |
The stable price is the foundation of all other prices. It smooths out short-term market noise and manipulation by averaging across markets and time. Without it, a single marketplace spike or a low-liquidity item could cause our prices to swing wildly.
Imagine you're checking the price of an item across 5 different stores. One store has it at $25, another at $22, one sketchy store says $50, and two say $21. You wouldn't use $50 as your reference — you'd filter out the outlier and average the rest.
That's exactly what stable price does: it collects prices from multiple marketplaces (Steam, CSFloat, Buff, Halo), filters out the top 30% of prices (outliers), weights by quantity (a store with 100 units is more reliable than one with 3), and then smooths this over 30 days of snapshots.
Stable Price Pipeline:
Our AK-47 Redline has prices from 4 markets: Steam ($23), CSFloat ($22), Buff ($21), Halo ($21.50). After filtering and weighting, livePrice = 2,300. The 30-day snapshot mean is 2,200, so stablePrice = 2,210. The 7-day average (2,150) is within 30% of stable, so no spike protection triggers.
Keep only markets where: price > 0, quantity > 2, market != 'tradeit_gg'
Keep markets where: price ≤ 70th percentile OR (Steam AND price > 0)
For items with stablePrice > 1000 ($10): if price moved >30% from historical avg, use avg7DStablePrice instead.
steamApiPrice(2 * maxPrice + stablePrice) / 3buffApiPrice * 1.5buffBuy * 1.50Different price tiers need different margins. A $0.10 skin and a $500 knife carry very different risk profiles. Pricing groups assign each item a margin (our spread between buy and sell) and a group tier used for popularity ranking. This ensures we don't apply the same profit logic to cheap commodity skins and rare expensive items.
Just like a jewelry store has higher markups than a supermarket, we apply different margins at different price points. A $0.50 sticker might have a 150% margin (we pay $0.20, sell for $0.50) while a $500 knife might only have 30% (we pay $385, sell for $500). The pricing group table maps price ranges to margins.
Our AK-47 at stablePrice $22.10 falls into marginGroup 12 with margin 0.75. This means the player trade price will be roughly botPrice / 1.75 — about 57% of the bot price. The group number (12) also determines the rank multiplier cap (1.15 since it's below 30).
Items are assigned a margin and marginGroup based on their stable price. The pricing_groups table defines price tiers.
Lookup: find the last row where minPrice ≤ stablePrice. That row's margin and group are assigned.
Base prices are the starting point before any stock or market adjustments. Popular items (high market quantity and high withdrawal rate) get a higher multiplier because they're easier to resell. Unpopular items get a lower multiplier because they carry more risk of sitting in inventory. This ranking happens within each price tier so a popular cheap skin doesn't compete with an unpopular expensive one.
Not all $22 items are equal. An AK-47 Redline that 40 people buy per month is much safer to hold than a niche sticker with the same price but only 2 buyers. The system ranks items by popularity within their price tier and gives popular items a bonus multiplier (up to 1.5x) and unpopular ones a penalty (down to 0.9x).
The bot price is what we'd pay to acquire it from bots. The player price is what we'd pay a user — always lower than bot price (the gap is the margin, set by the pricing group).
Popularity ranking within a price group:
Our AK-47 Redline is in marginGroup 12 with margin 0.75. It's a popular item (popularity = 150 * 40 = 6,000), ranking high in its group. It gets rankMultiplier = 1.05.
baseBotTradePrice = ceil(2,210 * 1.05) = 2,321
basePlayerTradePrice = floor(2,321 / 1.75) = 1,326
So before any adjustments, we'd pay bots $23.21 and users $13.26 for this item.
Items grouped by marginGroup, sorted by popularity (descending). Rank goes from 1 (least) to N (most popular).
| MarginGroup | Cap Multiplier |
|---|---|
| < 30 | 1.15 (High) |
| ≥ 30 | 1.5 (Extreme) |
Stock targets determine how many of each item we want to hold. Holding too much ties up capital and exposes us to price drops. Holding too little means we can't fulfill trades. The system calculates a target stock level from trading activity, then applies 5 protection layers to prevent overstocking due to manipulation, market share concerns, or demand spikes.
Think of stock targets like a grocery store deciding how many bananas to keep on the shelf. Too many and they go bad (price drops). Too few and customers leave empty-handed. The system looks at how fast items move (in and out) and sets a target. Then 5 safety checks run to make sure we're not being tricked into holding too much.
Stock Protection Funnel: each layer can only reduce, never increase
For our AK-47: monthlyOut=40, weeklyOut=12, monthlyIn=35, sumQuantity=150. Default max stock calculates to ~38. No protections trigger (price isn't cheap, unique ratio is healthy, market share is fine), except in/out ratio is slightly unfavorable — capping at wantedMaxStock = 30. With currentStock=18, we have room: maxDeposit = 30 - 18 = 12 more units.
The raw estimate based on how fast items move in and out. Averages multiple signals (monthly out, unique users, weekly trend, monthly in) to get a balanced view rather than relying on any single metric.
How many should we hold? The system asks 5 questions and averages the answers: How many sold last month? How many unique buyers? What's the weekly trend extrapolated? Does the unique-buyer count seem suspiciously low? How many are coming in? The average gives a starting target before safety caps.
Components: [monthlyOut=40, uniqueOut=30, weeklyTrend=48, monthlyIn=35]. Average = (40+30+48+35)/4 = 38.25. sumQuantity=150 > 10 so minimum is 1. defaultMaxStock = 38.
Raw estimate of how many units to hold, before protection caps.
monthlyOutAmountmonthlyOutUniqueAmountweeklyOut * 4 (capped to monthlyOut if weekly/monthly > 0.8, or if weekly/monthly > 0.3 AND unique ratio < 0.3)monthlyOutUniqueAmount if unique/monthly ratio < 0.2The default max stock can be fooled by whales, price manipulation, or market anomalies. Each protection catches a specific failure mode. They run independently and the most restrictive one wins (min of all), ensuring that if ANY signal says "danger", we reduce our stock target.
Each protection asks a specific question:
For our AK-47 (defaultMaxStock=38): Unique ratio is 30/40=0.75 (healthy, >0.3) → no anomaly. Market share: 38/150=25% (under 50%) → OK. Price $22 is not <$5 → no low capacity. No demand spike vs median → OK. In/out ratio: slightly unfavorable at 0.7 → caps to 38*0.75=29, rounded to 30.
Each protection caps the value. Final wantedMaxStock = ceil(min(all 5)).
If monthlyOut > 20 AND sumQuantity < 400 AND uniqueRatio < 0.3 AND buffSellAmount < 50: cap at monthlyOutUniqueAmount
defaultMaxStock/sumQuantity > 0.5: cap at sumQuantity * 0.5stablePrice > 25000 AND ratio > 0.3: cap at sumQuantity * 0.3If defaultMaxStock > 100 AND stablePrice < 500: multiply by wantedMaxStockLowCapacityRatio (admin config)
If dms > 20 AND dms/median30DMaxStock > 2 AND (price doubled OR unique ratio < 0.3): cap at median30DMaxStock * 1.2
| globalRatio | Multiply by |
|---|---|
| 0.6 – 0.8 | 0.75 |
| 0.4 – 0.6 | 0.50 |
| 0 – 0.4 | 0.25 |
| ≥ 0.8 | 1.0 (no change) |
Only when: stablePrice > 2000 AND (totalInflow > 50 OR currentStock > 30)
For very cheap, very high-demand items where we're already over target: instead of aggressively dumping, allow a small controlled buffer (5% extra). This prevents constant churn of blocking then unblocking deposits for items that move fast.
Our AK-47 at $22.10 does NOT qualify for buffer (needs stablePrice < $1 AND wantedMaxStock > 200). Buffer is only for cheap, high-volume items like $0.03 stickers where we might hold 500+ units.
Allows controlled overstocking for cheap high-demand items. ALL conditions:
currentStock > wantedMaxStock AND stablePrice < 100 AND wantedMaxStock > 200 AND weeklyOut > 20 AND sumQuantity > 1000neededStock = min(ceil(avg(40/4, 30/4, 12) * 1.45), 30) = min(ceil(11.08 * 1.45), 30) = min(17, 30) = 17
dailyMaxStock = max(30/4, 1) = 8 (accept max 8 per day)
maxDeposit = 30 - 18 = 12 (room for 12 more units)
Base prices assume a static world. Adjustments layer on real-time signals: inventory levels, market data quality, price trends, and anomalies. Each adjustment runs independently and produces a multiplier. They're combined conservatively in step 8: bot takes the most aggressive increase, player takes the most aggressive decrease.
Base prices are like the MSRP on a car. But the actual price you pay depends on supply and demand, the dealer's inventory, market conditions, and whether the model is being discontinued. Adjustments are these real-world factors.
There are 9 independent adjustments, each watching a different signal. Some affect what we pay bots, some affect what we pay users, some affect both. At the end, they're combined by taking the most aggressive value — if any one signal says "danger", we act on it.
The 9 Adjustments at a glance:
BOT = affects bot price PLAYER = affects player price BLOCK = can fully block deposits
Nine independent adjustments. Each produces a multiplier for bot and/or player price.
The core inventory balancer. If we have too few of an item, raise prices to attract deposits and slow withdrawals. If too many, lower prices to push items out and discourage new deposits. The asymmetry (4.5% vs 10%) is intentional: we're more aggressive at reducing player prices when overstocked than increasing bot prices when understocked.
This is the most important single adjustment. It measures "are we under or over our target stock?" and translates that into a price nudge. The further from balanced, the bigger the nudge. At -1 (full overstock), we aggressively discount. At +1 (completely empty), we pay a premium to attract items.
Deficit Scale:
Our AK-47: currentStock=18, neededStock=20. We're slightly understocked.
deficit = (20 - 18) / 20 = 0.10 (10% understocked)
Bot change: 10% * 0.10 = +0.01 (1% increase to attract items)
Player change: 4.5% * 0.10 = +0.0045 (0.45% increase, gentle)
Result: slightly higher prices to encourage deposits and slow withdrawals.
maxStock > 0 AND adj stock > maxStock AND stablePrice < 2000maxStock = 0 OR tradeableStock > maxStock(neededStock - currentStock) / neededStock-min(overstockDiff / stockRange, 1) where stockRange = max(maxStock - currentStock + neededStock + 1, 1)Scaling: If maxStock < 4: deficit *= maxStock / 4
| Overstocked (deficit < 0) | Understocked (deficit > 0) | |
|---|---|---|
| BOT | 4.5% * deficit (small decrease) | 10% * deficit (big increase) |
| PLAYER | 10% * deficit (big decrease) | 4.5% * deficit (small increase) |
For items priced at $0.03-$0.50, percentage adjustments round to zero and have no effect. This adds a fixed cent deduction (1-3 cents) that actually moves the needle on cheap items.
Our AK-47 at $22.10 is not cheap (stablePrice > $1). Deficit is positive (understocked). Result: 0 cents deducted. This only matters for sub-$1 overstocked items.
| Condition | Cents |
|---|---|
| deficit between -0.5 and -1 | +1 |
| deficit = -1 (full overstock) | +2 |
| stablePrice < 100 | +1 additional |
Prevents someone from dumping a large quantity of one item in a single day. If today's deposits already hit 25% of our desired max stock, block further deposits until tomorrow.
AK-47: wantedMaxStock=30 (>30? No). Even if it were, dailyMaxStock=8 and suppose only 3 deposited today. Result: not blocked. Would only trigger for high-stock items getting flooded.
A wide spread between Buff buy and sell orders signals an illiquid or manipulated item. If the buy/sell gap is large, it means nobody agrees on the price and we could get stuck holding it. This caps our buy price or blocks deposits entirely for risky items.
AK-47: buffBuy=1,800, buffSell=2,000. Spread = 2000/1800 = 1.11. At stablePrice $22 (2,210 cents, > $10), the threshold for wide spread is 1.5. Since 1.11 < 1.5: no change. If spread were 1.6 (e.g., buy=$18, sell=$29), it would cap our player price.
| # | Condition | Action |
|---|---|---|
| 1 | (no buff data) AND stablePrice > 200000 | DUMP (-1) |
| 2 | buffBuy*2 < stablePrice AND stablePrice > 15000 | DUMP (-1) |
| 3 | buffSell = 0 | No change |
| 4 | buffBuy = 0 AND sumQuantity > 30 | No change |
| 5 | buffBuy = 0 AND sumQuantity ≤ 30 | DUMP (-1) |
| 6 | stablePrice > 100000 AND spread > 1.3 | Cap at buffBuy*1.75 |
| 7 | stablePrice > 1000 AND spread > 1.5 | Cap at buffBuy*1.75 |
Few Buff sell listings means the item is hard to price accurately and hard to resell. If we can't verify fair market value with enough data points, reduce exposure by capping what we pay or blocking deposits.
AK-47: buffSellAmount=35 (≥10), sumQuantity=150 (≥20). Neither dump condition met. buffRisk = 1,326 / (1,800*1.75) = 0.42 (< 1.3). No change. This fires for rare items with <10 Buff listings.
| # | Condition | Action |
|---|---|---|
| 1 | stablePrice > 1000 AND buffSellAmount < 10 AND sumQuantity < 20 | DUMP |
| 2 | buffSell=0 AND buffBuy=0 | No change |
| 3 | sumQuantity < 30 AND buffRisk > 1.3 | Cap at buff*1.75 |
Compares what we actually paid/received in real trades vs our configured base prices. If we're consistently paying more than our bot price to acquire items, we need to raise it. If we're paying users far more than what items actually sell for, we need to lower the player price. Reality-check against our own trading data.
Suppose our AK-47 has monthlyInAvgPrice=2,400 and baseBotPrice=2,321. Is 2,400 > 2,321*1.03 (2,391)? Yes → botChange = +0.004. For player side, monthlyOutAvgPrice=1,200, and 1,200*2=2,400 is not < 1,326, so no player change.
The stable price is smoothed and lags behind sudden market moves. If the live market price crashes 20%+ below our stable price, we're paying too much. This reduces our buy price to match reality. Conversely, if our stable price is lagging behind a genuine price increase, it boosts the bot price so we don't miss acquisition opportunities.
AK-47: stablePrice=2,210, livePrice=2,300, avg7D=2,150. live/stable = 2,300/2,210 = 1.04 (not < 0.8). stable/max(avg7D,prevMonth) = 2,210/2,150 = 1.03 (not < 0.8 either). No change. This fires when live price crashes — e.g., if a major sale event drops live to $15 while stable is still $22.
Only when adjustByLiveToStablePriceRatio = '1'.
Catches items where our stable price has drifted far above where it was historically. This can happen through gradual manipulation or stale data. By comparing current stable price against the 30-day median and Buff data, we detect items that are priced unrealistically high and reduce or block deposits before we overpay.
AK-47: stablePrice=2,210, buffBuy=1,800. Is stable/buff > 2? 2,210/1,800 = 1.23 (no, < 2). Not triggered. This fires for items where stable is 2x+ Buff — a strong sign our price is inflated vs the real market.
Triggers when: stablePrice > 300 AND stablePrice/buffBuy > 2 AND median30D > 1 AND (stable/avg7D > 1.3 OR stable/prevMonth > 2)
If block flag ON: -1. Otherwise: -(1 - min(median30D/stablePrice, 1))
Detects sudden price spikes that could be market manipulation (pump and dump). If either the live or stable price jumped 30%+ above the 7-day average, block all deposits immediately. Better to miss a legitimate spike briefly than to accept items at an inflated price.
AK-47: stablePrice=2,210, avg7D=2,150. Is stable > avg7D*1.3 (2,795)? No. Is live (2,300) > 2,795? No. Not blocked. If someone manipulated the market and stable jumped to $30 while avg7D was still $22, this would immediately block deposits.
Multiple adjustments may fire simultaneously. The combining logic reflects our risk posture: for bot prices (what we pay to acquire), be aggressive — take the biggest increase. For player prices (what we pay users), be conservative — take the biggest reduction. One red flag is enough to lower what we pay.
How adjustments combine:
The asymmetry is deliberate. For buying (bot price), we want to be competitive — if any signal says we should pay more to acquire, we do. For paying users (player price), we're paranoid — if even one signal says this item is risky, we reduce what we pay. Better to miss a few deposits than to overpay on a manipulated item.
AK-47 adjustment results: byDeficitBot=+0.01, byMonthlyBot=+0.004, byAvg7DBot=0. Bot: MAX = +0.01
byDeficitPlayer=+0.005, all other player adjustments=0 (healthy item!). Player: MIN = +0.005 (the only non-zero value, which is actually positive — slight price increase since we're understocked)
This AK-47 is a healthy, popular item — most protections don't fire. That's the normal case.
Zero values excluded. If all zero, result is 0.
This is where everything comes together. Base prices are multiplied by the combined adjustments and admin markups to produce the 4 final prices that users and bots see. Each price has its own formula and purpose.
Now all the data and adjustments merge into 4 final numbers. The store price has an interesting twist: if we're massively overstocked on an item AND the market price is reasonable, we switch to liquidation mode — undercutting the cheapest market listing to move inventory fast, even if it means lower margins.
Store Price Decision Tree:
Our AK-47: botStock=15 (not > 20), so liquidation does NOT trigger. Normal mode:
storePrice = max(round(2,210 * 1.01 * 1.0 * 1.05), 2,100) = max(2,344, 2,100) = 2,344 ($23.44)
Bot price: floor(2,321 * 1.01 * 1.05) = 2,461 ($24.61)
Player price: floor(1,326 * 1.005) = 1,332 ($13.32)
Instant sell: round(1,332 / 1.75 * 0.85) = 647 ($6.47)
botStock > 20adjustedMaxDeposit < -30minPrice ≥ stablePrice*0.8 OR stablePrice - minPrice ≤ 10Adjustment values are pulled automatically from the calculators above. Each price card has its own inputs.
Safety nets that run after all price calculations. They catch edge cases the formulas miss: Doppler knives with unreliable market data, price changes too small to matter (avoids constant micro-fluctuations), and the hard rule that player price must never exceed bot price (or we'd lose money on every trade).
Even after all the math, we run 3 safety checks before publishing prices. Think of them as quality control on the assembly line:
Protection chain (runs in order):
AK-47 is not a Doppler → skip. New bot price 2,461 vs previous ~2,450 → change is 0.4% < 1% → keep old price. Player 1,332 ≤ 2,461*0.97 (2,387)? Yes → OK. The 1% filter is the most commonly triggered protection — it keeps prices stable between runs.
Only for isDoppler = true:
finalBot = 0: set to stablePrice * 1.75finalPlayer = 0: set to floor(playerPrice / (1 + margin))buffSellAmount < 15: player = min(buffBuy, buffSell, player/1.75) * 1.75buffBuy = 0 OR buffSellAmount < 5: player = 0 (block deposits)If price changed < 1% from previous run: keep old price. Applies to bot, player, and store separately.
Bot+Player: protectDopplers → protectMinorChanges → protectPlayerBotDiff
Store: protectStorePriceMinorChanges (separate chain)
Runtime-configurable flags and multipliers that control the aggressiveness of various adjustments. These can be changed in the admin panel without redeploying code, allowing quick response to market conditions.
These are the knobs operators can turn without touching code. They fall into 3 categories:
In a market crash, an operator might turn on the safety switches and reduce markups. During stable periods, they can relax the dials to be more competitive.
| Key | Type | Effect | Used In |
|---|---|---|---|
tradePriceMarkup | "0.05" | Markup on bot trade prices | Final bot price |
storePriceMarkup | "0.05" | Markup on store prices | Store price |
instantSellPriceAdjustPercent | "85" | % of base for instant sell | Instant sell |
blockDepositByPrevMonthPrice | "1"/"0" | Full block on prev-month anomaly | Adj #8 |
adjustByLiveToStablePriceRatio | "1"/"0" | Live-to-stable crash protection | Adj #7 |
blockDepositByAvg7DStablePrice | "1"/"0" | Spike deposit blocking | Adj #9 |
liveToStableModifier | "0.5" | Aggressiveness of stable-to-min adj | Adj #7B |
avg7DStableBotPriceModifier | "0.5" | Aggressiveness of 7d bot boost | Adj #7 bot |
decreaseDepositPriceForCheapItems | "1"/"0" | Cent deduction for cheap items | Final player |
weeklyInOutRatioWeight | "0.5" | Weekly vs monthly weight | Stock prot #5 |
wantedMaxStockLowCapacityRatio | "0.3" | Cheap item stock cap multiplier | Stock prot #3 |
If you take away one thing from this document, it's this: the system is designed to be paranoid about overpaying and aggressive about acquiring. It uses multiple overlapping safety nets at different time horizons, trusts Buff as the closest thing to ground truth, and always prefers action (reduce price, block deposits) over inaction when a signal looks bad. The philosophy is: we can always re-open deposits tomorrow, but we can't un-buy an overpriced item.
Generated from pricing-manager source code. All formulas match the TypeScript implementation.