Skip to main content
The live market is the set of cheapest current listings across connected marketplaces, kept warm and repriced for the viewer. It has two surfaces:
  • GET /market — a snapshot of the feed, cheapest first.
  • GET /market/live — a WebSocket that pushes price deltas in real time.
Use them together: load a snapshot, open the socket, then apply each delta on top of the snapshot. Prices on both surfaces already include the viewer’s fee, so you render them as-is.

Snapshot

GET /market returns the curated feed sorted cheapest first.
const { items, total } = await api<{ items: MarketListing[]; total: number }>(
  "/market?limit=100",
  { method: "GET" },
  "user_42",
);
page
integer
default:"1"
1-based page number.
limit
integer
default:"100"
Listings per page, 1–500, or -1 to return the whole feed in one response.
Each entry’s price is a decimal number with the viewer’s fee already applied, and id is the buyable listing id you pass to the trade endpoints. See Data models for the full MarketListing shape.

Live stream

wss://api.skinshark.gg/market/live?scope=watchlist
The socket pushes a delta whenever a listing in scope appears, reprices, or is removed. It carries no snapshot of its own — pair it with GET /market to establish initial state.

Scopes

scope
string
default:"all"
all or watchlist.
ScopeContentsPayload
watchlistThe curated feed GET /market returnsFull listing (icon, rarity, reference fields)
allEvery listing event across the market (firehose)Lighter — type and iconUrl are omitted; high volume
Pick watchlist to track the same set GET /market serves. Pick all only if you want the raw firehose and can absorb the volume.

Authentication

The live socket uses the same JWT ticket as the WebSocket event stream — the ticket carries a market scope alongside deposit and trade, so one ticket works for both sockets.
1

Mint a ticket

Call POST /auth/ws-token with On-Behalf-Of set to the sub-user you’re acting as.
const { token } = await api<{ token: string; expiresIn: string }>(
  "/auth/ws-token",
  { method: "POST" },
  "user_42",
);
2

Open the socket

Pass the token as a bearer.<jwt> subprotocol (preferred — query strings leak into logs) or as ?token=:
const ws = new WebSocket(
  "wss://api.skinshark.gg/market/live?scope=watchlist",
  [`bearer.${token}`],
);
3

Wait for the ready frame

On a successful upgrade the server sends:
{ "event": "market.ready", "data": { "scope": "watchlist" }, "ts": 1714678920000 }

Protocol

  • Read-only. Sending any frame closes the socket with 4002 Read-only.
  • Heartbeats. The server sends WS pings; reply with pongs (most clients do this automatically).
  • Frame shape. Every push is { event, data, ts }, where ts is Unix epoch milliseconds.

Events

type LiveEvent =
  | { event: "market.ready"; data: { scope: "all" | "watchlist" }; ts: number }
  | { event: "market.new"; data: MarketListing; ts: number }
  | { event: "market.update"; data: MarketListing; ts: number }
  | { event: "market.remove"; data: { listingId: string; itemId?: string }; ts: number };
Treat market.new and market.update identically — upsert the listing keyed by data.id. On market.remove, drop the listing by data.listingId.
const listings = new Map<string, MarketListing>();

ws.addEventListener("message", (raw) => {
  const evt = JSON.parse(raw.data.toString()) as LiveEvent;
  switch (evt.event) {
    case "market.new":
    case "market.update":
      listings.set(evt.data.id, evt.data);
      break;
    case "market.remove":
      listings.delete(evt.data.listingId);
      break;
  }
});

Keeping a view in sync

1

Open the socket and buffer

Open scope=watchlist and buffer deltas until the snapshot loads, so nothing is lost in the gap.
2

Load the snapshot

Page through GET /market into your map keyed by id.
3

Apply buffered and live deltas

Replay the buffered deltas, then keep applying new ones. An update for a listing you don’t have is just an insert; a remove for one you don’t have is a no-op.
Prices are snapshotted at connect time using the acting sub-user’s fee tier. A mid-session fee change takes effect on the next reconnect.

Close codes

CodeReasonCause
4001Missing tokenNo ticket in the subprotocol or ?token=
4001Invalid tokenBad signature, expired, or wrong purpose
4002Read-onlyClient sent a frame after upgrade
4003Missing market scopeTicket lacks the market scope
4008Too many connectionsOver the per-user socket limit
A sub-user may hold up to 5 concurrent live sockets. For the reconnect pattern (mint a fresh ticket on 4001, back off on network blips), see the WebSocket guide.