> ## Documentation Index
> Fetch the complete documentation index at: https://assetpay.gg/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Price Feed (Redis)

> Tail the AssetPay price feed over Redis Streams instead of polling: connection, stream keys, event format, and snapshot-then-tail consumption.

## Overview

The AssetPay **price feed** is a low-latency stream 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.

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.

<Note>
  The feed requires separately provisioned credentials. If you only need your own priced view of the market (the same items `/secure/market?source=internal` returns, with your fee applied), the WebSocket [Market Updates](/docs/guides/websocket#market-updates) event covers that with just your API key.
</Note>

## Connection

<ParamField path="endpoint">`rediss://feed.assetpay.gg:6382` - TLS only; plaintext connections are rejected.</ParamField>

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.

<Note>
  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.
</Note>

Your ACL user is scoped to your own stream and snapshot. Reading anything else returns `NOPERM`.

## Streams & keys

AssetPay issues two key names alongside your credentials:

| Key                 | Type   | Contents                                 |
| ------------------- | ------ | ---------------------------------------- |
| `<stream>`          | Stream | Item and price changes, oldest to newest |
| `<stream>:snapshot` | Hash   | Current state (`itemId` → JSON block)    |

## Event format

Each stream entry has two fields: `type` and `data` (a JSON string).

<ResponseField name="type" type="string">
  `item.add`, `item.remove`, or `price.update`.
</ResponseField>

<ResponseField name="data" type="JSON string">
  The event payload - see below.
</ResponseField>

### `item.add`

A new item entered the pool (or its block was refreshed):

```json theme={null}
{
  "id": "0f9c...",
  "appid": 730,
  "assetId": "39482...",
  "name": "AK-47 | Redline (Field-Tested)",
  "marketHashName": "AK-47 | Redline (Field-Tested)",
  "iconUrl": "https://...",
  "tradable": true,
  "delivery": "instant",
  "price": "12.43",
  "updatedAt": "2026-06-15T12:00:00.000Z"
}
```

### `price.update`

An existing item's price changed:

```json theme={null}
{ "id": "0f9c...", "price": "12.10", "updatedAt": "2026-06-15T12:05:00.000Z" }
```

### `item.remove`

An item left the pool (sold or pulled):

```json theme={null}
{ "id": "0f9c..." }
```

Payloads may carry additional internal routing fields. Read the fields you need and ignore the rest.

## Consuming the feed

<Steps>
  <Step title="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.
  </Step>

  <Step title="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.
  </Step>

  <Step title="Tail the stream">
    `XREAD BLOCK 0 STREAMS <stream> <lastId>` in a loop, applying each event and advancing `lastId` to each entry's id.
  </Step>

  <Step title="Resync on disconnect">
    On reconnect, re-run from step 1. The snapshot is always current, so re-seeding reconciles any missed events.
  </Step>
</Steps>

### Example (Node.js / ioredis)

```typescript theme={null}
import Redis from "ioredis";
import { readFileSync } from "node:fs";

// The stream name AssetPay issued with your credentials.
const STREAM = "<YOUR_FEED_STREAM>";

const r = new Redis("rediss://<YOUR_FEED_USER>:<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
    }
  }
}
```

<Warning>
  Use a dedicated client for the blocking `XREAD ... BLOCK` loop - it ties up the connection. Don't reuse it for other commands.
</Warning>

## 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`.
* **The snapshot is the source of truth.** Membership is reconciled against it continuously, so an item present there is live even if you missed its `item.add`.
* **Credentials and the CA** are issued per integration - contact AssetPay to provision feed access.
