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 #
curl 'https://raresats.net/api/events'const response = await fetch('https://raresats.net/api/events');
const data = await response.json();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,helloand theretry:line carry none, soLast-Event-IDalways 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.
hellois 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. Nosnapshotis 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
profileframes and is written into its earlier messages. The removals themselves (chatdelete,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 |
offer | offers changed state | see 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 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 |
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) | {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,cancelledormoved.underlists 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.
listingsholds the full Listing objects, exactly asGET /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:
listingsisnull, and the changes are all still there. - Over 100:
changesisnulltoo, andkeys(every row and seller touched) andcountremain.
- Over 25 listings:
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,expiredorinvalid. The full Offer gives an invalid offer'sreason. best.bestholds the best active offer of each outpoint the changes touched, asGET /api/market/offers/bestanswers it, ornullwhen none is left. It is what a listing'sbest_offershows.- Large bursts. As for listings: over 25 changes
offersisnull, and over 100changesisnulltoo.bestis 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:confirmedin a block,replacedby its speed-up or cancel,conflicted(spent by another transaction),dropped(left the mempool),unknown(the broadcast's answer was lost) orrejected(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. ReadGET /api/market/transfers?address=<a>again for an address you show.- Large bursts. Over 100 changes,
changesisnullandkeysremains.
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.