How to estimate what a CS2 skin is really worth
Estimate what a CS2 skin is really worth: combine NBBO, recent history, and a suggested list price with confidence scores instead of one market's number.
On this page
There is no single price for a CS2 skin. There is the cheapest listing somewhere, the highest buy order somewhere else, yesterday's average on a third venue, and a Steam price in wallet credit. A fair value is a judgment built from those signals, and the most useful thing an API can do is give you the signals plus an honest statement of how much to trust each one.
This guide combines three SkinPricer reads into one estimate: a suggested price from GET /v1/recommendations/:marketHashName, the live executable book from GET /v1/nbbo, and a time dimension from market analytics and price history. Each step adds evidence, and the final function tells you which evidence it used.
Three signals, three questions#
| Signal | Question it answers | Plan |
|---|---|---|
| Recommendation | What should I list or sell at right now, and how sure is that? | Developer and up |
| NBBO | What is the best executable ask and cash bid across every venue? | Developer and up |
| Market analytics | What did each venue's asks and bids average over the last N hours? | Pro and up |
| Price history | Where has the typical ask been over days or weeks? | Pro and up |
Step 1: ask for a recommendation#
The recommendation endpoint returns one suggested_price along with the strategy that produced it, a confidence and a liquidity_score (both 0 to 1), and unstable_reasons explaining anything that weakened the estimate. Unlike most pricing reads, this response is snake_case. The default best bid behind it only counts cash-executable venues, the same rule the NBBO uses.
export SKINPRICER_API_KEY="sp_live_•••" # from your account's API keys page
curl -s "https://pricing.skinpricer.com/v1/recommendations/AK-47%20%7C%20Redline%20(Field-Tested)" \
-H "Authorization: ApiKey $SKINPRICER_API_KEY"// npm i @skinpricer/sdk
import { SkinpricerClient, formatUsd } from "@skinpricer/sdk";
const client = new SkinpricerClient({
apiKey: process.env.SKINPRICER_API_KEY!,
authScheme: "ApiKey",
});
const name = "AK-47 | Redline (Field-Tested)";
const rec = await client.recommendations.get(name);
if (rec.suggested_price === null) {
console.log("No hard price:", rec.strategy, rec.unstable_reasons.join(", "));
} else {
console.log("Suggested:", formatUsd(rec.suggested_price), "via", rec.strategy);
console.log("Confidence:", rec.confidence, "| liquidity:", rec.liquidity_score);
}import os
from urllib.parse import quote
import requests
name = "AK-47 | Redline (Field-Tested)"
res = requests.get(
f"https://pricing.skinpricer.com/v1/recommendations/{quote(name, safe='')}",
headers={"Authorization": f"ApiKey {os.environ['SKINPRICER_API_KEY']}"},
timeout=10,
)
res.raise_for_status()
rec = res.json()
if rec["suggested_price"] is None:
print("No hard price:", rec["strategy"], rec["unstable_reasons"])
else:
print(f"Suggested {rec['suggested_price'] / 100:.2f} USD via {rec['strategy']}"){
"canonical_item_id": "cmlofca920lka01yozajhixt3",
"market_hash_name": "Glock-18 | Water Elemental (Factory New)",
"suggested_price": 6470,
"strategy": "midpoint",
"confidence": 0.96,
"liquidity_score": 0.98,
"unstable_reasons": [],
"best_ask": {
"market": "csfloat",
"price": 6500,
"updated_at": "2026-06-23T21:52:27.427Z",
"is_stale": false
},
"best_bid": {
"market": "buff163",
"price": 6440,
"updated_at": "2026-06-23T21:59:48.717Z",
"is_stale": false
},
"spread_bps": 93,
"market_count": 10,
"fresh_market_count": 9,
"listing_count": 710,
"buy_order_count": 88,
"total_bid_quantity": 128,
"calculated_at": "2026-06-23T22:07:15.477Z"
}The strategy tells you how the number was built. midpoint means both sides were fresh and the suggestion sits halfway between the best ask and best bid. best_ask_fallback and best_bid_fallback mean only one fresh side existed. The stale_ strategies mean the inputs were old. crossed_best_ask means the best bid nets the seller more than the best ask even after estimated fees, so the suggestion falls back to the ask. Two strategies return suggested_price: null: no_data when nothing is quoted, and insufficient_support when the top of the book rests on too little independent evidence to price.
no_sell_quotes/no_buy_orders: one side of the book is empty.stale_top_of_book: the best ask or best bid is older than the freshness window.low_fresh_market_coverage: fewer than two venues have fresh quotes.thin_sell_liquidity/thin_buy_liquidity: few listings or buy orders back the price.insufficient_independent_support: the top of the book rests on a handful of listings or orders from too few venues, or on a thin quote no other venue comes close to.wide_spread: the best ask sits more than 15% above the best bid.crossed_market: the bid nets more than the ask after fees, which usually means one leg is out of date.
Step 2: check it against the live book#
The recommendation is built from the same quotes as the NBBO, but reading the NBBO directly gives you the fee-aware view as well. The midpoint of the raw best ask and best bid is a reasonable display value; the executionAdjusted bid tells you what a seller would actually receive. If the two disagree sharply, trust the adjusted book for any decision that moves money.
const quote = await client.nbbo.get({ marketHashName: name });
const liveMid =
quote.bestAsk && quote.bestBid && !quote.bestAsk.isStale && !quote.bestBid.isStale
? Math.round((quote.bestAsk.price + quote.bestBid.price) / 2)
: null;
const netBid = quote.executionAdjusted?.bestBid?.price ?? null;
console.log("Live mid:", liveMid, "| seller nets:", netBid, "| confidence:", quote.confidence);The current NBBO for AK-47 | Redline (Field-Tested), fetched live.
Step 3: add a time dimension#
A snapshot can catch a market at a strange moment. Two Pro-plan reads add history. Market analytics returns, per venue, the current ask and bid plus rolling averages, minimums, and maximums over a window of 1 to 168 hours (windowHours, default 24); its response is snake_case and its averages can be fractional cents. Price history returns time buckets; with interval=DAY_1 over the last week, the bucket medianPrice values give you a stable reference for the typical ask.
const weekAgo = new Date(Date.now() - 7 * 24 * 3600 * 1000);
const [analytics, history] = await Promise.all([
client.marketAnalytics.get(name, { windowHours: 72 }),
client.history.get(name, { from: weekAgo, interval: "DAY_1" }),
]);
for (const row of analytics.markets) {
if (row.sell_avg_price !== null) {
console.log(row.market, "72h avg ask:", Math.round(row.sell_avg_price));
}
}
const medians = history.history
.map((bucket) => bucket.medianPrice)
.filter((p): p is number => p !== null)
.sort((a, b) => a - b);
const weeklyMedian = medians[Math.floor(medians.length / 2)] ?? null;
console.log("Median of daily median asks:", weeklyMedian);curl -s "https://pricing.skinpricer.com/v1/market-analytics/AK-47%20%7C%20Redline%20(Field-Tested)?windowHours=72" \
-H "Authorization: ApiKey $SKINPRICER_API_KEY"Combine the signals into one estimate#
Put the steps together as a waterfall: take the strongest signal that is available and trustworthy, and record which one you used. That way a valuation can always explain itself, which matters when a user asks why their skin is worth what you say.
type FairValue = { cents: number; source: "recommendation" | "live-mid" | "weekly-median" } | null;
function fairValue(
rec: { suggested_price: number | null; confidence: number; unstable_reasons: string[] },
liveMid: number | null,
weeklyMedian: number | null,
): FairValue {
if (rec.suggested_price !== null && rec.confidence >= 0.7 && rec.unstable_reasons.length === 0) {
return { cents: rec.suggested_price, source: "recommendation" };
}
if (liveMid !== null) return { cents: liveMid, source: "live-mid" };
if (weeklyMedian !== null) return { cents: weeklyMedian, source: "weekly-median" };
return null; // not enough evidence: say so rather than guessing
}
console.log(fairValue(rec, liveMid, weeklyMedian));- Never use
aggregate.maxPriceoravgPricefrom the pricing snapshot as a value. They describe asks, and one optimistic listing skews both. - Keep Steam wallet prices out of cash valuations unless you label them. The Steam vs cash guide explains why.
- Store the
sourceandconfidencenext to every value you persist. A number without provenance cannot be audited later.
When to reach for a different endpoint#
Valuing a whole inventory rather than one item? The inventory valuation guide batches lowest asks and highest bids for up to 100 items per call. To know how long a skin would take to sell at your estimate, add liquidity and time-to-sell. For longer charts behind the weekly median, see CS2 price history.
Endpoints used in this guide
Plans start at $50/month on the Developer tier. Want to try first? Ask in our Discord for a free 2-day Developer key.
Related guides
- Prices & quotesIntermediate
Steam Market vs cash-market prices: how to compare them correctly
Steam Market prices are wallet credit, not cash. Learn how to compare Steam and cash-market CS2 prices after fees, using execution-adjusted quotes.
GET /v1/nbboGET /v1/pricing/:marketHashName5 min read - Build with the APIIntermediate
How to value a CS2 inventory at live market prices
Value a CS2 inventory at live market prices: batch lowest asks and highest buy orders, add liquidity, and report a realistic liquidation value.
GET /v1/aggregations/min-pricesGET /v1/aggregations/max-orders6 min read - History & backtestingBeginner
How to get CS2 skin price history at any interval
Fetch CS2 skin price history in 10-minute, hourly, or daily buckets, across all markets or split per market, ready for charts, alerts, and analysis.
GET /v1/pricing/:marketHashName/historyGET /v1/pricing/:marketHashName/history/markets6 min read - Liquidity & depthIntermediate
How to check CS2 skin liquidity and time-to-sell
Check how liquid a CS2 skin is and how long it takes to sell, per market and across markets, one item or thousands at once. With runnable API code.
GET /v1/liquidity/:marketHashNameGET /v1/liquidity/summary/:marketHashName6 min read