Skip to main content

How to check CS2 market data freshness before you trust a price

Check CS2 market data freshness before you trust a price: per-market ingest status, latency percentiles, stale flags, and marketplace health.

Updated 7 min readBeginnercurl · TypeScript · PythonMarkdown
On this page
  1. Level 1: freshness on every quote
  2. Level 2: is each marketplace updating right now?
  3. Level 3: how reliable has each marketplace been?
  4. The keyless view: market health
  5. Put it together: a trust gate
  6. When to reach for a different endpoint

Every price you read is a claim about the past. A marketplace published it at some moment, a collector picked it up a little later, and by the time your code acts on it the listing may be gone. Most of the time the gap is seconds and it does not matter. The expensive cases are the ones where a marketplace quietly stopped updating an hour ago and its last prices still look perfectly normal.

SkinPricer exposes freshness at three levels so you can catch that before it costs money: on every quote, per marketplace right now, and per marketplace over the last day. This guide explains each signal, which endpoint serves it, and how to combine them into a simple trust gate.

Level 1: freshness on every quote#

Each quote the pricing endpoints return carries its own timing. These fields are documented in full on the Freshness & timestamps page; the ones that matter most are:

FieldMeaning
fetchedAtWhen SkinPricer last fetched this market's quote.
ingestedAtWhen the quote reached SkinPricer's price store, shortly after fetchedAt. Judge staleness by fetchedAt or isStale, not this.
ingestLatencyMsThe delay between the two, in milliseconds: SkinPricer's processing delay, not the age of the price.
isStaleTrue when fetchedAt is more than 30 minutes old (a uniform window across markets). On snake_case endpoints it is is_stale.
freshMarketCountOn NBBO and pricing responses: how many of the contributing markets are still fresh.

Stale quotes are still returned, deliberately. On a thinly traded item a 40-minute-old ask may be the only price that exists, and hiding it would be worse than labelling it. Treat isStale as a soft signal: fine for display with a label, not fine as one leg of a trade. Also remember that null is not zero. A missing bid means no one is bidding, and the Nulls & partial data page lists every case where a field is null rather than measured.

Level 2: is each marketplace updating right now?#

Per-quote flags tell you about one price. GET /v1/status/freshness tells you about a whole marketplace: for each market it returns delay_seconds since its most recent ingested data, a status, and an overall_status roll-up across all markets. It is key-gated (Developer plan and up) and its payload is snake_case.

StatusRule
healthyLast data 5 minutes ago or less.
degradedBetween 5 and 15 minutes.
staleMore than 15 minutes.
overall_statusstale if more than half the markets are stale; degraded if any market is stale or more than 30% are degraded; otherwise healthy.
export SKINPRICER_API_KEY="sp_live_•••"   # from your account's API keys page

curl -s "https://pricing.skinpricer.com/v1/status/freshness" \
  -H "Authorization: ApiKey $SKINPRICER_API_KEY"
// npm i @skinpricer/sdk
import { SkinpricerClient } from "@skinpricer/sdk";

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

const freshness = await client.status.freshness();
console.log(`Overall: ${freshness.overall_status} at ${freshness.checked_at}`);

for (const m of freshness.markets) {
  if (m.status !== "healthy") {
    console.log(`${m.market} is ${m.status}: last data ${m.delay_seconds}s ago`);
  }
}
Response 200GET /v1/status/freshness
Field reference
{
  "markets": [
    {
      "market": "dmarket",
      "delay_seconds": 0,
      "last_update": "2026-06-23T22:00:00.000Z",
      "status": "healthy"
    },
    {
      "market": "steamcommunity",
      "delay_seconds": 0,
      "last_update": "2026-06-23T22:00:00.000Z",
      "status": "healthy"
    }
  ],
  "overall_status": "healthy",
  "checked_at": "2026-06-23T22:04:00.118Z"
}
The documented response: one row per market plus the roll-up.

Level 3: how reliable has each marketplace been?#

A market can be healthy this minute and still be flaky. GET /v1/status/market-latency summarizes the last 24 hours per market: average, p95, and p99 fetch-to-ingest latency in milliseconds, the number of fetches, the share that errored (error_rate, 0 to 1), and the share that breached the latency target (slo_breach_rate, 0 to 1). A market with a high p99 or a rising breach rate is one to down-weight even when its latest quote looks fresh.

Node (SDK)
const latency = await client.status.marketLatency();

const shaky = latency.markets
  .filter((m) => m.error_rate > 0.05 || m.slo_breach_rate > 0.25)
  .map((m) => `${m.market} (p95 ${Math.round(m.p95_latency_ms / 1000)}s)`);

console.log(`Over the last ${latency.window_hours}h, watch: ${shaky.join(", ") || "none"}`);
The thresholds are examples. Pick yours from how your strategy tolerates delay.
Response 200GET /v1/status/market-latency
Field reference
{
  "markets": [
    {
      "market": "buff163",
      "avg_latency_ms": 51439,
      "p95_latency_ms": 69525,
      "p99_latency_ms": 75350,
      "fetch_count": 30003561,
      "error_rate": 0,
      "slo_breach_rate": 0.28
    },
    {
      "market": "dmarket",
      "avg_latency_ms": 839,
      "p95_latency_ms": 1021,
      "p99_latency_ms": 16686,
      "fetch_count": 3078604,
      "error_rate": 0,
      "slo_breach_rate": 0
    }
  ],
  "window_hours": 24,
  "measured_at": "2026-06-23T09:08:55.013Z"
}

The keyless view: market health#

GET /v1/markets/health is the one endpoint that needs no API key, which makes it ideal for status pages and quick checks. It is served from api.skinpricer.com and limited by IP (roughly 60 requests a minute). For every integrated marketplace it returns:

  • status: live, warn, or stale, judged against that market's own expected refresh cadence (a market that normally updates every few minutes is held to a tighter bar than a slow one).
  • lastEventAt: the time of the latest ingested event, or null if there has not been one.
  • freshnessCoveragePct: the share (0 to 100) of that market's served listings fetched within its expected refresh window. null when the market serves no rows. It complements status, which only tracks the ingest heartbeat.
  • healthBars: 48 half-hour buckets covering the last 24 hours, most recent last, where 2 is live, 1 degraded, and 0 down.
Python
import requests

# No API key needed for market health.
res = requests.get("https://api.skinpricer.com/v1/markets/health", timeout=10)
res.raise_for_status()

for market in res.json():
    coverage = market.get("freshnessCoveragePct")
    down_last_day = market["healthBars"].count(0)
    print(
        f"{market['id']:<16} {market['status']:<6}",
        f"coverage={coverage if coverage is not None else 'n/a'}%",
        f"down buckets={down_last_day}",
    )
Live
Try it live

Fetch live market health right now. No key involved.

Put it together: a trust gate#

A practical pattern is to refresh the market-level signals once a minute on your server, keep a set of markets you currently trust, and check each quote against that set before acting on it.

Node (SDK)
import { SkinpricerClient } from "@skinpricer/sdk";

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

interface HealthRow {
  id: string;
  status: "live" | "warn" | "stale";
  freshnessCoveragePct: number | null;
}

/** Markets that are healthy now and whose listings are mostly fresh. */
export async function trustedMarkets(): Promise<Set<string>> {
  const [freshness, healthRes] = await Promise.all([
    client.status.freshness(),
    fetch("https://api.skinpricer.com/v1/markets/health"), // keyless
  ]);
  const health = (await healthRes.json()) as HealthRow[];

  const healthyNow = new Set(
    freshness.markets.filter((m) => m.status === "healthy").map((m) => m.market),
  );
  return new Set(
    health
      .filter((h) => h.status === "live" && healthyNow.has(h.id))
      .filter((h) => h.freshnessCoveragePct === null || h.freshnessCoveragePct >= 80)
      .map((h) => h.id),
  );
}

/** Act only on quotes that are fresh and come from a trusted market. */
export function canUse(
  quote: { market: string; isStale: boolean },
  trusted: Set<string>,
): boolean {
  return !quote.isStale && trusted.has(quote.market);
}
Cache the trusted set for a minute. The freshness read counts against your plan's budget; market health is keyless and does not.

When to reach for a different endpoint#

Freshness checks guard the numbers; they do not produce them. For a single item's best prices with per-side staleness built in, read the best bid and ask guide. If you alert on prices, the price alert bot guide shows where these checks belong in a polling loop. And if you are showing prices to visitors, the comparison site guide turns these same signals into status badges.

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.