# 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.

*Prices & quotes · Intermediate · Updated 2026-09-25*

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`](/api-docs/recommend), the live executable book from [`GET /v1/nbbo`](/api-docs/nbbo), and a time dimension from [market analytics](/api-docs/market-analytics) and [price history](/api-docs/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.

**curl**

```bash
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"
```

**Node (SDK)**

```ts
// 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);
}
```

**Python**

```python
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 200** (`GET /v1/recommendations/:marketHashName`)

```json
{
  "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.

> **A withheld price is an answer, not an error:** When the book is too thin to support a number, the API returns `null` rather than guessing. Surface that to your users as "not enough data" instead of falling back to a single listing, which is exactly the number an attacker can move with one cheap order.

## 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.

```ts
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);
```

## Step 3: add a time dimension

A snapshot can catch a market at a strange moment. Two Pro-plan reads add history. [Market analytics](/api-docs/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](/api-docs/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.

**Node (SDK)**

```ts
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**

```bash
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.

```ts
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](/resources/steam-vs-cash-market-prices) 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](/resources/cs2-inventory-value) 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](/resources/cs2-skin-liquidity). For longer charts behind the weekly median, see [CS2 price history](/resources/cs2-price-history).

## Endpoints used

- [GET /v1/recommendations/:marketHashName](/api-docs/recommend.md)
- [GET /v1/nbbo](/api-docs/nbbo.md)
- [GET /v1/market-analytics/:marketHashName](/api-docs/market-analytics.md)
- [GET /v1/pricing/:marketHashName/history](/api-docs/history.md)

## Related guides

- [Steam Market vs cash-market prices: how to compare them correctly](/resources/steam-vs-cash-market-prices.md)
- [How to value a CS2 inventory at live market prices](/resources/cs2-inventory-value.md)
- [How to get CS2 skin price history at any interval](/resources/cs2-price-history.md)
- [How to check CS2 skin liquidity and time-to-sell](/resources/cs2-skin-liquidity.md)
