Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

What each release of this corpus changed, one release per tag. `Corrected` comes first in every release, because a fact that was wrong is what a returning reader has to see before anything else. The other sections are `Added`, `Revalidated`, and `Removed`, in that order, and a section with nothing in it is left out.

## 2026.08.3

### Corrected

- `reference/api.md` and `reference/api-streaming.md` say the template-buyoffer socket handler is wired: at the pinned 2.2.0 tag `new_template_buyoffer` broadcasts on `lognewtbuyo`, where both pages previously recorded the handler as defined but never called.
- `reference/atomicassets/tables.md` records that the `mediatype`/`info` synthesis is live on both hosted deployments, which now report 2.2.0 on `/health`, replacing the note that pinned mainnet to the pre-2.0 behavior.

### Revalidated

- The atomicassets-api baseline moves from an untagged main-branch state to tag `2.2.0`: of the twenty-one cited files, fifteen are byte-identical to the old pin and the rest change nothing cited except the socket wiring above; both line-range citations into the repo still point at byte-identical content.

## 2026.08.2

### Revalidated
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ The pages are read against these baselines:

- `atomicassets-contract` at `v2.0.0`
- `atomicmarket-contract` at `v2.0.0`
- `atomicassets-api` at its `main` branch state, which carries no release tag; the streaming and rate-limit pages pin commit `f6419858`
- `atomicassets-api` at tag `2.2.0`, commit `cf8bf323`, the version both hosted deployments report on `/health`
- `atomictools-contract` at commit `d89ce79e4`, which the deployed `atomictoolsx` ABI on WAX matches exactly
- `atomicassets-sdk` at main `80580c5` and `atomicmarket-sdk` at main `278bdfa`, both version 2.0.0
- `atomicassets-sdk` at tag `v2.1.1`, commit `5c70c62`, and `atomicmarket-sdk` at tag `v2.4.1`, commit `437300b`
- `@atomichub/vert` at `2.2.0`

WAX mainnet still runs the V1 `atomicassets` and `atomicmarket` contracts while WAX testnet and jungle4 run V2, so an action that exists only in V2 fails when it is sent to mainnet (`reference/validation.md`).
Expand Down
21 changes: 11 additions & 10 deletions reference/api-streaming.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
scope: atomicassets-api realtime surface - Socket.IO namespaces, events, room subscription, connection shape, and the absence of socket auth or app-layer limits
depends-on: []
key-modules:
- "atomicassets-api (main, f6419858): src/api/server.ts, src/api/utils.ts, src/api/notification.ts, src/api/namespaces/*/routes/*.ts"
- "atomicassets-api (2.2.0, cf8bf323): src/api/server.ts, src/api/utils.ts, src/api/notification.ts, src/api/namespaces/*/routes/*.ts"
---

# atomicassets-api realtime (Socket.IO)
Expand All @@ -11,7 +11,7 @@ key-modules:

The API attaches a Socket.IO server to the same HTTP server that serves the REST endpoints, so realtime and REST share one host and port. The server is constructed with `transports: ['websocket']`, `allowEIO3: true`, and `cors: {origin: '*'}`, so clients connect over the WebSocket transport only (HTTP long-polling is disabled) and both Engine.IO v3 and v4 clients are accepted from any origin. The Engine.IO path is the Socket.IO default `/socket.io`; realtime channels are Socket.IO namespaces, not URL routes. A client selects a channel by connecting to the namespace whose name matches the REST path plus the resource, for example `wss://wax.api.atomicassets.io/atomicmarket/v1/sales`. Use a Socket.IO v4 client (the server is `socket.io ^4.8.3`); pin the client to the server's major and force the WebSocket transport, because the default transport list starts with polling, which this server does not serve.

Source: `atomicassets-api (main, f6419858) src/api/server.ts` (`SocketServer`, `new Server(..., {transports: ['websocket'], allowEIO3: true, cors: {origin: '*'}})`), `package.json` (`socket.io`), `src/api/utils.ts` (`createSocketApiNamespace` maps namespace name to `<namespace path>/v1/<resource>`); live probe of `wss://wax.api.atomicassets.io` connecting to five namespaces over the WebSocket transport (all handshakes succeeded)
Source: `atomicassets-api (2.2.0, cf8bf323) src/api/server.ts` (`SocketServer`, `new Server(..., {transports: ['websocket'], allowEIO3: true, cors: {origin: '*'}})`), `package.json` (`socket.io`), `src/api/utils.ts` (`createSocketApiNamespace` maps namespace name to `<namespace path>/v1/<resource>`); live probe of `wss://wax.api.atomicassets.io` connecting to five namespaces over the WebSocket transport (all handshakes succeeded)

## Namespace and event catalog

Expand All @@ -30,40 +30,41 @@ Each namespace name is the configured namespace path (`/atomicassets`, `/atomicm
| `/atomicmarket/v1/auctions` | `new_auction` | `lognewauct` | `new_auctions` |
| `/atomicmarket/v1/auctions` | `new_bid` | `auctionbid` | `new_bids` |
| `/atomicmarket/v1/buyoffers` | `new_buyoffer` | `lognewbuyo` | `new_buyoffers` |
| `/atomicmarket/v1/template_buyoffers` | `new_template_buyoffer` | `lognewtbuyo` | `new_template_buyoffers` |

Every listed namespace also emits `fork` (payload `{block_num}`) to the whole namespace when the connected reader rolls back a microfork, so a consumer holding optimistic state can invalidate everything at or after that block. The `back` and `update` asset events are emitted only when the `atomicassets` namespace is configured with `socket_features.asset_update: true`; the reference config ships it `false`, so a default deployment broadcasts `new_asset`, `burn`, and `fork` on the assets namespace and nothing else. The `atomictools` namespace registers no socket handler, so it exposes no realtime events even though its REST namespace exists.

Source: `atomicassets-api (main, f6419858) src/api/namespaces/atomicassets/routes/assets.ts` (`new_asset`/`burn`/`back`/`update`/`fork`, `asset_update` gate), `.../atomicassets/routes/offers.ts` (`create`), `.../atomicassets/routes/transfers.ts` (`new_transfer`), `.../atomicmarket/routes/sales.ts` (`new_sale`/`purchased_sale`), `.../atomicmarket/routes/auctions.ts` (`new_auction`/`new_bid`), `.../atomicmarket/routes/buyoffers.ts` (`new_buyoffer`), `.../atomicmarket/index.ts` (`socket()` wires sales/auctions/buyoffers), `.../atomictools/index.ts` (empty `socket()`), `config/server.config.example.json` (`socket_features.asset_update: false`)
Source: `atomicassets-api (2.2.0, cf8bf323) src/api/namespaces/atomicassets/routes/assets.ts` (`new_asset`/`burn`/`back`/`update`/`fork`, `asset_update` gate), `.../atomicassets/routes/offers.ts` (`create`), `.../atomicassets/routes/transfers.ts` (`new_transfer`), `.../atomicmarket/routes/sales.ts` (`new_sale`/`purchased_sale`), `.../atomicmarket/routes/auctions.ts` (`new_auction`/`new_bid`), `.../atomicmarket/routes/buyoffers.ts` (`new_buyoffer`), `.../atomicmarket/index.ts` (`socket()` wires sales/auctions/buyoffers/template-buyoffers), `.../atomictools/index.ts` (empty `socket()`), `config/server.config.example.json` (`socket_features.asset_update: false`)

## What triggers a broadcast

Socket events are not driven by the API's own database writes. The filler's notifier publishes each batch of matching action traces and table deltas to a Redis pub/sub channel named `eosio-contract-api:<chain>:<reader>:api`; the API subscribes to that channel through `ApiNotificationReceiver`, and each socket route registered an `onData(<channel>, ...)` listener that queries the affected rows and emits the socket event. A deployment therefore broadcasts realtime events only when its API process shares Redis with a running filler whose reader is configured to publish notifications; an API pointed at a database with no live notifier serves REST normally but stays silent on every namespace. Connecting to a namespace succeeds regardless, because the handshake is independent of whether any notifier is publishing.

Source: `atomicassets-api (main, f6419858) src/api/notification.ts` (`ApiNotificationReceiver`, channel `eosio-contract-api:<chain>:<reader>:api`), `src/api/namespaces/*/index.ts` (`socket()` constructs the receiver from `args.connected_reader` and registers the per-resource `onData` listeners); live probe held five namespaces open for 30 and 50 second windows and observed no events, consistent with connectivity being independent of notifier traffic
Source: `atomicassets-api (2.2.0, cf8bf323) src/api/notification.ts` (`ApiNotificationReceiver`, channel `eosio-contract-api:<chain>:<reader>:api`), `src/api/namespaces/*/index.ts` (`socket()` constructs the receiver from `args.connected_reader` and registers the per-resource `onData` listeners); live probe held five namespaces open for 30 and 50 second windows and observed no events, consistent with connectivity being independent of notifier traffic

## Market namespaces gate events behind opt-in rooms; asset and offer namespaces do not

The three `atomicmarket` namespaces broadcast their entity events only to Socket.IO rooms, and a fresh connection joins none of them. A client opts in by emitting a `subscribe` event whose payload sets each wanted room to a truthy value; the same handler leaves any room whose flag is absent or falsy, so `subscribe` is the full membership list on every call, not an additive toggle. The room names are `new_sales` and `purchased_sales` on `/atomicmarket/v1/sales`, `new_auctions` and `new_bids` on `/atomicmarket/v1/auctions`, and `new_buyoffers` on `/atomicmarket/v1/buyoffers`. Without a `subscribe` the market namespaces deliver only `fork`, which is broadcast namespace-wide. The `/atomicassets/v1/assets` and `/atomicassets/v1/offers` namespaces use no rooms: every subscriber receives every event the moment it connects, with no `subscribe` step.
The four `atomicmarket` namespaces broadcast their entity events only to Socket.IO rooms, and a fresh connection joins none of them. A client opts in by emitting a `subscribe` event whose payload sets each wanted room to a truthy value; the same handler leaves any room whose flag is absent or falsy, so `subscribe` is the full membership list on every call, not an additive toggle. The room names are `new_sales` and `purchased_sales` on `/atomicmarket/v1/sales`, `new_auctions` and `new_bids` on `/atomicmarket/v1/auctions`, `new_buyoffers` on `/atomicmarket/v1/buyoffers`, and `new_template_buyoffers` on `/atomicmarket/v1/template_buyoffers`. Without a `subscribe` the market namespaces deliver only `fork`, which is broadcast namespace-wide. The `/atomicassets/v1/assets` and `/atomicassets/v1/offers` namespaces use no rooms: every subscriber receives every event the moment it connects, with no `subscribe` step.

Source: `atomicassets-api (main, f6419858) src/api/namespaces/atomicmarket/routes/sales.ts`, `.../auctions.ts`, `.../buyoffers.ts` (each `namespace.on('connection', ...)` with a `subscribe` handler over a fixed `availableRooms` list; entity events use `namespace.in('<room>').emit(...)` while `fork` uses `namespace.emit(...)`), `.../atomicassets/routes/assets.ts` and `.../offers.ts` (no connection handler, all events via `namespace.emit(...)`)
Source: `atomicassets-api (2.2.0, cf8bf323) src/api/namespaces/atomicmarket/routes/sales.ts`, `.../auctions.ts`, `.../buyoffers.ts`, `.../template-buyoffers.ts` (each `namespace.on('connection', ...)` with a `subscribe` handler over a fixed `availableRooms` list; entity events use `namespace.in('<room>').emit(...)` while `fork` uses `namespace.emit(...)`), `.../atomicassets/routes/assets.ts` and `.../offers.ts` (no connection handler, all events via `namespace.emit(...)`)

## Transfers ride the offers namespace

There is no `/v1/transfers` Socket.IO namespace. The transfers socket handler registers on `<namespace path>/v1/offers`, the same namespace the offers handler uses, so `logtransfer` broadcasts arrive as the `new_transfer` event on `/atomicassets/v1/offers` alongside the offer `create` event. A consumer that wants transfer events must connect to the offers namespace and listen for `new_transfer`; connecting to a `/v1/transfers` namespace name yields a valid but permanently silent connection.

Source: `atomicassets-api (main, f6419858) src/api/namespaces/atomicassets/routes/transfers.ts` (`createSocketApiNamespace(this.server, this.core.path + '/v1/offers')`), `.../offers.ts` (same namespace path)
Source: `atomicassets-api (2.2.0, cf8bf323) src/api/namespaces/atomicassets/routes/transfers.ts` (`createSocketApiNamespace(this.server, this.core.path + '/v1/offers')`), `.../offers.ts` (same namespace path)

## Template buyoffers emit no socket events at this commit

`reference/api.md` records that template-buyoffer socket notifications fire only for new offers, never for cancellation or fulfillment. The source is consistent with and narrower than that: the `templateBuyofferSockets` handler emits `new_template_buyoffer` only on `lognewtbuyo` and has no cancel or fulfill branch, but at this commit that handler is defined and never called. The `atomicmarket` namespace's `socket()` wires only the sales, auction, and buyoffer handlers, so `/atomicmarket/v1/template_buyoffers` accepts connections and its `new_template_buyoffers` room exists in the dead code, yet no template-buyoffer event is broadcast. Track template-buyoffer state changes by polling `/atomicmarket/v1/template_buyoffers` rather than over the socket. Re-check this when the notifier wiring changes, since it is a one-line registration away from going live.
`reference/api.md` records that template-buyoffer socket notifications fire only for new offers, never for cancellation or fulfillment. The source matches: the `atomicmarket` namespace's `socket()` registers `templateBuyofferSockets` beside the sales, auction, and buyoffer handlers, and that handler emits `new_template_buyoffer` (room `new_template_buyoffers`) only on `lognewtbuyo`, with no cancel or fulfill branch. Track cancellation and fulfillment by polling `/atomicmarket/v1/template_buyoffers` rather than over the socket.

Source: `atomicassets-api (main, f6419858) src/api/namespaces/atomicmarket/routes/template-buyoffers.ts` (`templateBuyofferSockets` defined, emits only on `lognewtbuyo`), `src/api/namespaces/atomicmarket/index.ts` (`socket()` calls `salesSockets`, `auctionSockets`, `buyofferSockets` only; no reference to `templateBuyofferSockets` anywhere in the tree)
Source: `atomicassets-api (2.2.0, cf8bf323) src/api/namespaces/atomicmarket/routes/template-buyoffers.ts:127-170` (`templateBuyofferSockets`, emits only on `lognewtbuyo`), `src/api/namespaces/atomicmarket/index.ts:187-190` (`socket()` registers all four handlers); live `/health` reads of `wax.api.atomicassets.io` and `test.wax.api.atomicassets.io`, both reporting `2.2.0`

## Socket connections carry no app-layer auth or rate limit

The Socket.IO server registers no connection or handshake middleware, so namespace connections are unauthenticated and uncapped by the application: any client may open any namespace and, on the market namespaces, `subscribe` to any room. The express-rate-limit middleware documented in `reference/api.md` is mounted on the REST namespace paths and never sees the WebSocket upgrade, which Socket.IO intercepts on the HTTP server ahead of express, so REST rate limiting does not bound socket connections or event volume. Any connection ceiling a consumer hits in practice comes from a fronting proxy or CDN, not the API. Treat the reference deployment's actual socket exposure as operator- and edge-specific, not a software guarantee.

Source: `atomicassets-api (main, f6419858) src/api/server.ts` (`SocketServer` constructs `new Server(...)` with no `io.use(...)` and no per-namespace auth; the express `limiter` is mounted per REST namespace path in `src/api/namespaces/*/index.ts`, not on the Socket.IO server)
Source: `atomicassets-api (2.2.0, cf8bf323) src/api/server.ts` (`SocketServer` constructs `new Server(...)` with no `io.use(...)` and no per-namespace auth; the express `limiter` is mounted per REST namespace path in `src/api/namespaces/*/index.ts`, not on the Socket.IO server)

## Consumption example

Expand Down
4 changes: 3 additions & 1 deletion reference/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ Source: live probes of `https://wax.api.atomicassets.io/atomicmarket/v1/royaltie

## Template buyoffers keep all lifecycle states

AtomicMarket template buyoffers in the atomicassets-api follow a three-state lifecycle: `lognewtbuyo` inserts a row in state 0 (LISTED), `canceltbuyo` flips it to 1 (CANCELED), and `fulfilltbuyo` flips it to 2 (SOLD), setting the seller and inserting the fulfilled asset rows. Rows are never deleted or archived: no maintenance job cleans up CANCELED or SOLD offers, so they persist indefinitely as state markers. The `/v1/template_buyoffers` endpoint applies no state filter by default: without an explicit `state` query parameter it returns offers in all three states, so clients that only want active offers must pass `state=0`. No socket notifications are broadcast for template buyoffers at the pinned commit: the socket handler for new offers exists in the source but is never wired into the `atomicmarket` namespace, and cancellation and fulfillment have no handler at all (`reference/api-streaming.md`). Poll the endpoint rather than waiting on socket events. The filler and API state enums both encode LISTED=0, CANCELED=1, SOLD=2 and map 1:1.
AtomicMarket template buyoffers in the atomicassets-api follow a three-state lifecycle: `lognewtbuyo` inserts a row in state 0 (LISTED), `canceltbuyo` flips it to 1 (CANCELED), and `fulfilltbuyo` flips it to 2 (SOLD), setting the seller and inserting the fulfilled asset rows. Rows are never deleted or archived: no maintenance job cleans up CANCELED or SOLD offers, so they persist indefinitely as state markers. The `/v1/template_buyoffers` endpoint applies no state filter by default: without an explicit `state` query parameter it returns offers in all three states, so clients that only want active offers must pass `state=0`. Template buyoffers broadcast one socket event at the pinned tag: `templateBuyofferSockets` is wired into the `atomicmarket` namespace and emits `new_template_buyoffer` on `lognewtbuyo`, while cancellation and fulfillment have no handler at all (`reference/api-streaming.md`). Poll the endpoint for any state change past creation. Both hosted deployments report `2.2.0` on `/health`, so the wiring is live there.

Source (socket wiring and deployment version): `atomicassets-api (2.2.0, cf8bf323) src/api/namespaces/atomicmarket/index.ts:187-190` (`socket()` registers `templateBuyofferSockets`); live `GET /health` on `wax.api.atomicassets.io` and `test.wax.api.atomicassets.io`, both reporting `2.2.0` The filler and API state enums both encode LISTED=0, CANCELED=1, SOLD=2 and map 1:1.

## The `state` field means something different on each listing endpoint

Expand Down
Loading
Loading