Skip to main content

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.

Updated 6 min readIntermediatecurl · TypeScript · PythonMarkdown
On this page
  1. Three signals, three questions
  2. Step 1: ask for a recommendation
  3. Step 2: check it against the live book
  4. Step 3: add a time dimension
  5. Combine the signals into one estimate
  6. When to reach for a different endpoint

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#

SignalQuestion it answersPlan
RecommendationWhat should I list or sell at right now, and how sure is that?Developer and up
NBBOWhat is the best executable ask and cash bid across every venue?Developer and up
Market analyticsWhat did each venue's asks and bids average over the last N hours?Pro and up
Price historyWhere 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']}")
Response 200GET /v1/recommendations/:marketHashName
Field reference
{
  "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"
}
A documented response with a clean two-sided book: strategy midpoint, no unstable reasons, high confidence.

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.

Node (SDK)
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);
Live
Try it live

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.

Node (SDK)
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));
The thresholds are a starting point. Tighten them for high-value items, where one thin quote can move the number a lot.
  • Never use aggregate.maxPrice or avgPrice from 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 source and confidence next 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

Run this guide against live data

Plans start at $50/month on the Developer tier. Want to try first? Ask in our Discord for a free 2-day Developer key.