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

*Build with the API · Intermediate · Updated 2026-09-25*

"What is my inventory worth?" has at least three honest answers. What it would cost to buy the same items again today, what you would receive if you sold everything right now, and something in between that assumes you are willing to wait. Most inventory calculators give you the first number, summed from one marketplace's lowest listings, and call it value. That number is rarely what you could actually get.

This guide builds all three from live data. You bring the list of items and quantities you hold; SkinPricer prices them in batches of up to 100 per request with [`GET /v1/aggregations/min-prices`](/api-docs/aggregations) and [`GET /v1/aggregations/max-orders`](/api-docs/max-orders), then adds liquidity and a suggested price for the holdings that matter most. All prices are integer USD cents.

> **Bring your own item list:** The API prices items by `market_hash_name`. It does not read Steam inventories. Export or maintain your holdings yourself (a spreadsheet, your own database, or your trading records) and pass the names and quantities in.

## The three numbers worth reporting

| Value | Built from | Answers |
| --- | --- | --- |
| Mark-to-market | Lowest fresh ask per item | What it would cost to rebuy the inventory today. An upper bound, not cash. |
| Liquidation value | Highest fresh cash bid per item, capped by bid depth | What you could receive by selling into standing buy orders now, before fees. |
| Patient value | Suggested list price plus time-to-sell | What you could realize by listing and waiting, with how long that takes. |

The gap between the first two is the price of immediacy. On liquid rifle skins it is usually small; on rare knives and gloves with few buy orders it can be large. Reporting both, instead of one headline, is what makes a valuation trustworthy.

## Step 1: batch the lowest asks

Send up to 100 market hash names per request as a comma-separated `marketHashNames` list. Each item comes back with a `prices` array holding the lowest ask on every market that lists it, with `offers` (listings behind that price) and an `is_stale` flag. Items with no sell-side data are simply absent from the response, so match results by name rather than by position. Both aggregation endpoints are included from the Developer plan up.

**curl**

```bash
export SKINPRICER_API_KEY="sp_live_•••"   # from your account's API keys page

curl -s -G "https://pricing.skinpricer.com/v1/aggregations/min-prices" \
  --data-urlencode "marketHashNames=AK-47 | Redline (Field-Tested),AWP | Asiimov (Field-Tested)" \
  -H "Authorization: ApiKey $SKINPRICER_API_KEY"
```

**Node (SDK)**

```ts
// npm i @skinpricer/sdk
import {
  SkinpricerClient,
  chunk,
  formatUsd,
  type ItemMaxOrders,
  type ItemMinPrices,
} from "@skinpricer/sdk";

const client = new SkinpricerClient({
  apiKey: process.env.SKINPRICER_API_KEY!,
  authScheme: "ApiKey",
});

interface Holding {
  marketHashName: string;
  quantity: number;
}

const holdings: Holding[] = [
  { marketHashName: "AK-47 | Redline (Field-Tested)", quantity: 3 },
  { marketHashName: "AWP | Asiimov (Field-Tested)", quantity: 1 },
];

const asks = new Map<string, ItemMinPrices>();
const bids = new Map<string, ItemMaxOrders>();

for (const batch of chunk(holdings.map((h) => h.marketHashName), 100)) {
  const [minRes, maxRes] = await Promise.all([
    client.aggregations.minPrices({ marketHashNames: batch }),
    client.aggregations.maxOrders({ marketHashNames: batch }),
  ]);
  for (const item of minRes.items) asks.set(item.market_hash_name, item);
  for (const item of maxRes.items) bids.set(item.market_hash_name, item);
}
```

**Python**

```python
import os
import requests

HEADERS = {"Authorization": f"ApiKey {os.environ['SKINPRICER_API_KEY']}"}
names = ["AK-47 | Redline (Field-Tested)", "AWP | Asiimov (Field-Tested)"]

asks, bids = {}, {}
for i in range(0, len(names), 100):
    params = {"marketHashNames": ",".join(names[i : i + 100])}
    for path, store in (("min-prices", asks), ("max-orders", bids)):
        res = requests.get(
            f"https://pricing.skinpricer.com/v1/aggregations/{path}",
            params=params,
            headers=HEADERS,
            timeout=10,
        )
        res.raise_for_status()
        for item in res.json()["items"]:
            store[item["market_hash_name"]] = item

print(f"asks for {len(asks)} of {len(names)} items, buy orders for {len(bids)}")
```

*Market hash names never contain commas, so a comma-separated list is safe. A request where none of the names resolve returns 400.*

**Response 200** (`GET /v1/aggregations/min-prices`)

```json
{
  "items": [
    {
      "canonical_item_id": "cmlofca920lka01yozajhixt3",
      "market_hash_name": "Glock-18 | Water Elemental (Factory New)",
      "prices": [
        {
          "market_name": "csfloat",
          "offers": 132,
          "price": 6500,
          "updated_at": "2026-06-23T21:52:27.427Z",
          "is_stale": false,
          "ingested_at": "2026-06-23T21:55:56.923Z",
          "ingest_latency_ms": 209496
        },
        {
          "market_name": "skinport",
          "offers": 40,
          "price": 6516,
          "updated_at": "2026-06-23T21:51:53.531Z",
          "is_stale": false,
          "ingested_at": "2026-06-23T21:54:30.775Z",
          "ingest_latency_ms": 157244
        }
      ]
    }
  ]
}
```

*The documented min-prices response. Note the snake_case fields: the aggregation endpoints predate the camelCase convention.*

## Step 2: batch the highest cash bids

The max-orders endpoint is the bid-side mirror: same request, but each item carries an `orders` array with the highest buy order per market and a `total_quantity` that approximates how many units that order will absorb. Two filters turn it into a liquidation number you can defend. First, keep only venues whose bids pay cash you can withdraw; a Steam Community Market buy order pays in wallet credit. Second, never count more units at a bid than it can absorb.

**Response 200** (`GET /v1/aggregations/max-orders`)

```json
{
  "items": [
    {
      "canonical_item_id": "cmlofca920lka01yozajhixt3",
      "market_hash_name": "Glock-18 | Water Elemental (Factory New)",
      "orders": [
        {
          "market_name": "buff163",
          "offers": 47,
          "price": 6440,
          "updated_at": "2026-06-23T21:59:48.717Z",
          "is_stale": false,
          "total_quantity": 47,
          "ingested_at": "2026-06-23T21:59:48.000Z",
          "ingest_latency_ms": 0
        }
      ]
    }
  ]
}
```

```ts
// Venues whose buy orders pay out cash. Adjust to the accounts you actually hold.
const CASH_VENUES = new Set(["buff163", "csfloat", "skinport", "dmarket", "marketcsgo", "waxpeer"]);

function lowestFreshAsk(item: ItemMinPrices | undefined): number | null {
  const fresh = (item?.prices ?? []).filter((p) => p.is_stale !== true);
  return fresh.length > 0 ? Math.min(...fresh.map((p) => p.price)) : null;
}

function bestCashBid(item: ItemMaxOrders | undefined): { price: number; units: number } | null {
  const cash = (item?.orders ?? []).filter((o) => !o.is_stale && CASH_VENUES.has(o.market_name));
  if (cash.length === 0) return null;
  const best = cash.reduce((a, b) => (b.price > a.price ? b : a));
  return { price: best.price, units: best.total_quantity ?? best.offers };
}

let markToMarketCents = 0;
let liquidationCents = 0;
const unpriced: string[] = [];

for (const h of holdings) {
  const ask = lowestFreshAsk(asks.get(h.marketHashName));
  const bid = bestCashBid(bids.get(h.marketHashName));
  if (ask === null && bid === null) {
    unpriced.push(h.marketHashName);
    continue;
  }
  if (ask !== null) markToMarketCents += ask * h.quantity;
  if (bid !== null) liquidationCents += bid.price * Math.min(h.quantity, bid.units);
}

console.log(`Mark-to-market: ${formatUsd(markToMarketCents)}`);
console.log(`Liquidation (top cash bids, before fees): ${formatUsd(liquidationCents)}`);
if (unpriced.length > 0) console.log(`No live data for: ${unpriced.join(", ")}`);
```

*Liquidation here is conservative: units beyond the top bid's depth are left out rather than assumed to sell at the same price.*

> **Do not sum wallet bids into cash value:** Steam Market buy orders often sit well above cash-market bids because they pay in wallet credit, which cannot be withdrawn. Mixing them into a liquidation total overstates what the inventory is worth in money. Filter by venue, and report wallet value separately if your users care about it.

## Step 3: add liquidity and a suggested price for the big holdings

A few items usually carry most of an inventory's value, and those are the ones where timing matters. For them, score time-to-sell with [`POST /v1/liquidity/batch`](/api-docs/liquidity-batch) (up to 100 names per call, Pro plan) and fetch a suggested list price with [`GET /v1/recommendations/:marketHashName`](/api-docs/recommend). The recommendation withholds a hard price (`suggested_price` is null, with a strategy such as `insufficient_support`) when the supporting quotes are too thin or unstable. Treat that as a signal, not a gap to fill with a guess.

```ts
// Rank holdings by their mark-to-market contribution and enrich the top 20.
const top = holdings
  .map((h) => ({ h, value: (lowestFreshAsk(asks.get(h.marketHashName)) ?? 0) * h.quantity }))
  .sort((a, b) => b.value - a.value)
  .slice(0, 20)
  .map(({ h }) => h.marketHashName);

const { results } = await client.liquidity.batch({ marketHashNames: top });
for (const r of results) {
  const rec = await client.recommendations.get(r.marketHashName);
  const price = rec.suggested_price === null ? "withheld" : formatUsd(rec.suggested_price);
  const eta = r.medianTimeToSellHours === null ? "no estimate" : `~${r.medianTimeToSellHours}h`;
  console.log(`${r.marketHashName}: list at ${price} (${rec.strategy}), sells in ${eta} on ${r.bestMarketToList ?? "n/a"}`);
}
```

*Recommendations are one request per item, so enrich the holdings that move the total instead of the whole list.*

## Step 4: report it honestly

- Show mark-to-market and liquidation side by side, and label which is which. The spread between them is information, not noise.
- List items with no live data separately instead of valuing them at zero. Missing data is unknown value, not no value.
- Show fees as their own line. Liquidation here is before seller commission and withdrawal costs, which differ by venue.
- Date-stamp the valuation. Quotes are live, and a report without a time is a report you cannot compare tomorrow.
- Budget your calls. Two batch requests per 100 items prices the whole list; per-item enrichment is where rate limits bite, so cache it for your refresh interval.

## When to reach for a different endpoint

If you need the reasoning behind a single item's worth, the [fair value guide](/resources/cs2-skin-fair-value) goes deeper than a batch valuation can. The [liquidity guide](/resources/cs2-skin-liquidity) explains every time-to-sell field used in step 3. And if you want to be told when a holding's price moves instead of re-running the valuation, the [price alert bot guide](/resources/cs2-price-alert-discord-bot) builds on the same batch endpoints. The [min-prices API reference](/api-docs/aggregations) documents both aggregation endpoints in full.

## Endpoints used

- [GET /v1/aggregations/min-prices](/api-docs/aggregations.md)
- [GET /v1/aggregations/max-orders](/api-docs/max-orders.md)
- [POST /v1/liquidity/batch](/api-docs/liquidity-batch.md)
- [GET /v1/recommendations/:marketHashName](/api-docs/recommend.md)

## Related guides

- [How to estimate what a CS2 skin is really worth](/resources/cs2-skin-fair-value.md)
- [How to check CS2 skin liquidity and time-to-sell](/resources/cs2-skin-liquidity.md)
- [How to build a CS2 price alert bot for Discord](/resources/cs2-price-alert-discord-bot.md)
