# Live events

Everything the site shows changes live. One Server-Sent Events stream, `GET /api/events`, carries:

- each new block;
- the sales;
- every listing and offer change;
- the floors;
- the marketplace's activity;
- affiliate stats;
- the wallet transfers the site broadcast;
- the BTC/USD price;
- the chat.

A page opens one stream and updates in place. A script can do the same with any SSE client.

`GET /api/events`

Server-Sent Events: a `snapshot` (the state) on connect, then `status`, `backfill`, `block`, `supply`, `reorg`, `snapshot` and `sales` events, `mempool` (an invalidation whenever what is pending changes: `{version, keys: {sales, transfers, extractions, rows, addresses, outpoints}, counts: {sales, transfers, extractions}}`; read `/api/pending`, or the answer you show, again when a key you care about is set), the chat's `chat` (`{type: "message", topic, message, thread?}`, a reply with its root's new counts in `thread`, `{type: "delete", ids, threads, topics}`, `threads` the roots whose replies went, or `{type: "topic", topic, archived}`) and `profile` (`{address, display_name}`) events while chat is on, the marketplace's `listing`, `offer`, `market`, `activity`, `affiliate` and `transfer` (a wallet transfer broadcast here changed status: `{version, count, keys: {addresses}, changes}`) events while it is on, and a `ping` every 15 s. Limited to 2,048 open streams, 8 from one IPv4 address or IPv6 /64 and 32 from one IPv6 /48. When every place is taken, a newcomer takes the place of the oldest stream of the network holding the most (at least two more than the newcomer's): that stream gets `retry: 60000` and an `evicted` event (`{reason, retry_ms}`), then ends; reconnect after `retry_ms` with `last_event_id`.

#### Query parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | no | Events per list in the snapshot, 1-1000. Default: `500`. (min 1, max 1000) |
| `last_event_id` | integer | no | Resume after this event id (what `Last-Event-ID` says; for a client that reopens the stream itself). Unknown or too old: a `snapshot` instead. (min 0) |

#### Example request

```bash title="cURL"
curl 'https://raresats.net/api/events'
```
```js title="JavaScript"
const response = await fetch('https://raresats.net/api/events');
const data = await response.json();
```
```python title="Python"
import requests

data = requests.get("https://raresats.net/api/events").json()
```

#### Response `200`

An event stream.

Content type: `text/event-stream`.

#### Errors

`{error}` (a message, no code):

| Status | When |
| --- | --- |
| 503 | Too many open streams from your network (8 from an IPv4 address or IPv6 /64, 32 from an IPv6 /48), or every place taken and no network holding more than yours. |

## Frames

Each frame has three fields, and `data` is one line of JSON:

```
id: 1790950000000042
event: listing
data: {"version":7,"changes":[…],"listings":[…]}
```

- **Ids.** Ids increase strictly, across server restarts too. `ping`, `hello` and the `retry:` line carry none, so `Last-Event-ID` always names a real event.
- **Ping.** A `ping` (`{"time": <ms>}`) comes every 15 s. A stream silent for longer than about 45 s is dead: open it again.
- **Hello.** `hello` is the first frame of every connection: `{"boot": <ms>, "resumed": <bool>, "replayed": <n>, "last_id": <id>}`.

## Resuming

Send the id of the last event you handled, either in the `Last-Event-ID` header (browsers do this on their own reconnects) or as `?last_event_id=`.

- **Replayed.** If the server still holds every event after it, it sends them in order (`hello.resumed: true`) and then goes on live. No `snapshot` is sent.
- **Not replayed.** If the server no longer holds them, it sends a `snapshot` (`hello.resumed: false`). The id may be from before a restart, or too old for the server's buffer of the last 2,000 events or 4 MiB. Treat this as a gap: read again whatever you show.

A snapshot frame's id is the newest event's id when it was sent, so resuming from it replays only what came after.

- **Taken down.** The replay never brings back what a chat admin removed. A deleted message's frame is dropped (and a quote of it emptied); a removed or changed display name drops the address's earlier `profile` frames and is written into its earlier messages. The removals themselves (`chat` `delete`, `profile`) are replayed in their place.

## Limits

A server holds at most 2,048 streams: 8 from one IPv4 address or IPv6 /64, and 32 from one IPv6 /48. Over your network's share, the stream is refused with a 503. When every place is taken, a newcomer takes the place of the oldest stream of the network holding the most (if it holds at least two more than the newcomer's). That stream gets `retry: 60000` and an `evicted` event, `{"reason", "retry_ms"}`, then ends: reconnect after `retry_ms`, resuming from your last id.

## Event types

| Event | When | Data |
|---|---|---|
| `hello` | first frame of a connection | `{boot, resumed, replayed, last_id}` |
| `snapshot` | on connect when not resumed; after the server rebuilds its window | what `GET /api/state` answers |
| `status` | the index's reachability, mode or tip changed | what `GET /api/status` answers |
| `backfill` | the server reads past blocks | `{done, total, …}` |
| `block` | new blocks read | `{tip, status, supply, transfers, extractions}`: the new notable transfers and extractions only |
| `supply` | the supply was read at a new tip | `{supply, status}` |
| `reorg` | blocks were replaced | `{from_height, common_height, removed: {transfers, extractions}, tip}`: the ids of the items dropped |
| `sales` | the sales changed | `{available, generation, seq, count, as_of, first_time, stats}`. Read the new sales with `GET /api/sales?after=<seq>` while `generation` is the one you hold; on a new `generation` read them all again |
| `listing` | listings changed state | see [Listing events](#listing-events) |
| `offer` | offers changed state | see [Offer events](#offer-events) |
| `market` | a row's floor or listed count changed | `{version, floors: {<row>: {floor_unit, listed, listed_sats, contains?, contains_min_price?} \| null}}`: only the rows that changed, as [floors](https://raresats.net/docs/get-floors.md) gives them; `null` means nothing is listed there now, nor in a bundle |
| `activity` | new marketplace activity | `{version, events: [ActivityEvent] \| null, keys: {rows, addresses}}`: `events` as `GET /api/market/activity` lists them, oldest first; `null` over 25 |
| `affiliate` | an affiliate registered (optional), or a purchase it referred was made or settled | `{version, keys: {addresses}}`: read `GET /api/market/affiliates/{address}` again |
| `transfer` | a wallet transfer the site broadcast went pending, confirmed, was replaced (sped up, cancelled), conflicted, dropped or was refused (rejected) | see [Transfer events](#transfer-events) |
| `price` | the BTC/USD price changed (read every 5 minutes while a page is open) | `{current: {usd, time}}` |
| `chat` | a message or reply was posted, or messages were deleted | `{type: "message", message, thread?}` or `{type: "delete", ids, threads}` |
| `profile` | a display name changed | `{address, display_name}` |
| `ping` | every 15 s | `{time}` |
| `evicted` | the stream is ended to make room (see [Limits](#limits)) | `{reason, retry_ms}` |

## Listing events

```
{"version": 7, "count": 1,
 "keys": {"rows": [rows], "sellers": [addresses]},
 "changes": [{"id", "outpoint", "status", "row", "under": [rows], "seller_address", "price", "unit_price", "updated_at"}] | null,
 "listings": [Listing] | null}
```

- **What a change says.** Each change gives the listing's new `status`: `active`, `pending`, `sold`, `cancelled` or `moved`.
  - `under` lists every row the listing is shown under: its own, the rarer sub-categories', the label rows' and those it holds some sats of in a bundle. Use it to tell whether a list you show is affected.
- **Full objects.** `listings` holds the full [Listing objects](https://raresats.net/docs/listing-object.md), exactly as `GET /api/market/listings/{id}` answers.
- **Large bursts.** A block that settles many listings, or a large batch listed, can change many at once. One event keeps its size down in two steps:
  - **Over 25 listings:** `listings` is `null`, and the changes are all still there.
  - **Over 100:** `changes` is `null` too, and `keys` (every row and seller touched) and `count` remain.

  In either case, read again once the lists you show that the keys name.
- **Transitions.** Every transition is a change, whichever route, block or mempool check made it:
  - **Listed or repriced.** A listing is created, or a new one replaces the old one: the old id is `cancelled` and the new one `active`.
  - **Cancelled.**
  - **Bought.** The listing goes `pending` with its `txid`, then `sold` once confirmed. If the purchase leaves the mempool unconfirmed it goes back to `active`.
  - **Moved** (`moved`). Its UTXO was spent without paying the listing.
  - **Signature public.** The listing's signature became public.
  - **Placed under a new row.**

## Offer events

```
{"version": 8, "count": 1,
 "keys": {"rows": [rows], "addresses": [sellers and bidders]},
 "changes": [{"id", "outpoint", "status", "row", "seller_address", "bidder_payment_address", "bidder_ordinals_address", "price", "updated_at"}] | null,
 "offers": [Offer] | null,
 "best": {"<outpoint>": Offer | null} | null}
```

- **Statuses.** Each change gives the offer's new `status`: `active`, `pending`, `accepted`, `cancelled`, `expired` or `invalid`. The full [Offer](https://raresats.net/docs/offer-object.md) gives an invalid offer's `reason`.
- **`best`.** `best` holds the best active offer of each outpoint the changes touched, as `GET /api/market/offers/best` answers it, or `null` when none is left. It is what a listing's `best_offer` shows.
- **Large bursts.** As for listings: over 25 changes `offers` is `null`, and over 100 `changes` is `null` too. `best` is left out (`null`) above 200 outpoints.

An accepted offer also moves the UTXO's listing: a `listing` event says it is `pending`, then `sold` with `sold_via: "offer"`.

## Transfer events

```
{"version": 9, "count": 1,
 "keys": {"addresses": [addresses]},
 "changes": [{"id", "txid", "kind", "status", "updated_at", "addresses": [addresses]}] | null}
```

- **When.** A transfer built on the site (extract, send, consolidate, speed up, cancel, …) is broadcast (`pending`), and then settles: `confirmed` in a block, `replaced` by its speed-up or cancel, `conflicted` (spent by another transaction), `dropped` (left the mempool), `unknown` (the broadcast's answer was lost) or `rejected` (bitcoind refused it: never sent).
- **`addresses`.** Every address whose page shows it: the owner's payment and ordinals addresses and every address it pays, the marketplace's fee address left out. Read `GET /api/market/transfers?address=<a>` again for an address you show.
- **Large bursts.** Over 100 changes, `changes` is `null` and `keys` remains.

Transactions that move tracked sats also show in the `mempool` event while pending; this one covers every transfer the site broadcast, plain bitcoin too.

## What never goes down the stream

Events carry only what the GET endpoints already answer, from the same public forms. They never include:

- a signature;
- a PSBT;
- an offer's plan;
- a script;
- a purchase's reservation.

## Bursts and load

- **Coalescing.** The server gathers a market burst for 250 ms and sends one event of each type.
- **Reading again.** A client should do the same: wait a moment after an event and read once what it shows. Never send one request per change.
- **Hidden pages.** A hidden page may skip its reads and read once when it shows again.
