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.
On this page
"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 and GET /v1/aggregations/max-orders, then adds liquidity and a suggested price for the holdings that matter most. All prices are integer USD cents.
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.
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"// 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);
}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)}"){
"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
}
]
}
]
}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.
{
"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
}
]
}
]
}// 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(", ")}`);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 (up to 100 names per call, Pro plan) and fetch a suggested list price with GET /v1/recommendations/:marketHashName. 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.
// 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"}`);
}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 goes deeper than a batch valuation can. The liquidity guide 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 builds on the same batch endpoints. The min-prices API reference documents both aggregation endpoints in full.
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
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.
GET /v1/recommendations/:marketHashNameGET /v1/nbbo6 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 - Build with the APIIntermediate
How to build a CS2 price alert bot for Discord
Build a Discord bot that alerts on CS2 price drops and spread changes, with batched polling, rate-limit headers, and 429 backoff. Full runnable code.
GET /v1/aggregations/min-pricesGET /v1/nbbo6 min read