Skip to main content

Overview

The AssetPay price feed is a low-latency firehose of item and price changes, delivered over Redis Streams at feed.assetpay.gg. Instead of polling the REST API, you tail a stream and react to each change as it happens - items entering the pool, items leaving, and price updates per marketplace. Each consumer reads a snapshot of current state once, then tails the stream for incremental changes. If you fall behind or reconnect, you re-read the snapshot and resume - no events are lost.

Connection

endpoint
rediss://feed.assetpay.gg:6382 - TLS only; plaintext connections are rejected.
The feed is TLS-encrypted with a private CA. To connect you need:
  • The CA certificate (ca.crt) - AssetPay provides this; your client must trust it.
  • An ACL username + password - AssetPay issues these per integration.
  • SNI / servername set to feed.assetpay.gg when validating the certificate.
The feed is a raw Redis (TCP) endpoint, so feed.assetpay.gg resolves directly to the origin - it is not proxied like the HTTP API.

Access tiers

ACL userSeesUse case
marketYour own per-market stream only (autoseller:items:feed:<marketplace>)A single-marketplace tenant - you never see another market’s prices
partnerThe consolidated stream, all marketplaces in one blockA first-party consumer aggregating every market

Streams & keys

KeyTypeContents
autoseller:items:feedStreamConsolidated - every item with all marketplace prices
autoseller:items:feed:snapshotHashCurrent state for the consolidated stream (itemId → block)
autoseller:items:feed:<marketplace>StreamPer-marketplace stream
autoseller:items:feed:<marketplace>:snapshotHashCurrent state for that marketplace
Marketplaces: white_market, waxpeer, shadowpay, csgoempire, market_csgo, assetpay. Only priced markets emit data (today that is white_market).

Event format

Each stream entry has two fields: type and data (a JSON string).
type
string
item.add, item.remove, or price.update.
data
JSON string
The event payload - see below.

item.add

A new item entered the pool (or its block was refreshed). On a per-market stream the payload is flattened to that market’s price:
{
  "id": "0f9c...",
  "appid": 730,
  "assetId": "39482...",
  "name": "AK-47 | Redline (Field-Tested)",
  "marketHashName": "AK-47 | Redline (Field-Tested)",
  "iconUrl": "https://...",
  "tradable": true,
  "delivery": "instant",
  "marketplace": "white_market",
  "price": "12.43",
  "updatedAt": "2026-06-15T12:00:00.000Z"
}
On the consolidated stream, prices are nested per marketplace:
{
  "id": "0f9c...",
  "appid": 730,
  "name": "AK-47 | Redline (Field-Tested)",
  "prices": {
    "white_market": { "price": "12.43", "updatedAt": "2026-06-15T12:00:00.000Z" }
  }
}

price.update

An existing item’s price changed on one marketplace:
{ "id": "0f9c...", "marketplace": "white_market", "price": "12.10", "updatedAt": "2026-06-15T12:05:00.000Z" }

item.remove

An item left the pool (sold or pulled):
{ "id": "0f9c..." }

Consuming the feed

1

Capture the stream position

Record the latest stream id (XREVRANGE <stream> + - COUNT 1) before seeding, so anything added during seeding replays from the stream - no gap.
2

Seed from the snapshot

HGETALL the snapshot hash to load current state in one round trip. Each field is an item id; each value is the JSON block.
3

Tail the stream

XREAD BLOCK 0 STREAMS <stream> <lastId> in a loop, applying each event and advancing lastId to each entry’s id.
4

Resync on disconnect

On reconnect, re-run from step 1. The snapshot is always current, so re-seeding reconciles any missed events.

Example (Node.js / ioredis)

import Redis from "ioredis";
import { readFileSync } from "node:fs";

const STREAM = "autoseller:items:feed:white_market";

const r = new Redis("rediss://market:<YOUR_FEED_PASSWORD>@feed.assetpay.gg:6382", {
  tls: { ca: readFileSync("ca.crt"), servername: "feed.assetpay.gg" },
});

// 1. capture position  2. seed snapshot  3. tail
const top = await r.xrevrange(STREAM, "+", "-", "COUNT", 1);
let lastId = top[0]?.[0] ?? "0";

const snapshot = await r.hgetall(`${STREAM}:snapshot`);
for (const [, json] of Object.entries(snapshot)) {
  upsert(JSON.parse(json)); // your handler
}

while (true) {
  const res = await r.xread("COUNT", 200, "BLOCK", 0, "STREAMS", STREAM, lastId);
  for (const [, entries] of res ?? []) {
    for (const [id, fields] of entries) {
      lastId = id;
      const type = fields[fields.indexOf("type") + 1];
      const data = JSON.parse(fields[fields.indexOf("data") + 1]);
      handle(type, data); // item.add | item.remove | price.update
    }
  }
}
Use a dedicated client for the blocking XREAD ... BLOCK loop - it ties up the connection. Don’t reuse it for other commands.

Notes

  • Prices are decimal-dollar strings (e.g. "12.43"), not cents - parse carefully to avoid float rounding.
  • Streams are length-capped (~100k entries, trimmed automatically) - always seed from the snapshot rather than reading the stream from 0.
  • Credentials and the CA are issued per integration - contact AssetPay to provision feed access.