Keep your market catalog in sync without polling
If you keep your own copy of AGG’s events and markets, for search, alerts or your own listing pages, you need to know when a market is listed, changes or goes away. Polling List venue markets sorted byupdatedAt gets you
part of the way, at the cost of a full page read every few seconds. It also misses changes that do
not touch the row itself. A market that joins or leaves a matched cluster
keeps its updatedAt, so a poller never sees it become comparable across venues.
The catalog feed tells you what changed instead.
How it works
There are two halves, and you use both:- The
catalogWebSocket channel pushes acatalog_changeframe for every event or market that is created, changes or is deleted. It is live only: nothing is replayed after a reconnect. GET /catalog/changesreturns the same items from any point in the last 24 hours, oldest first, paged by cursor. This is how you fill the gap while you were away.
stream_interrupted that lists catalog. The SDK’s
watchCatalog does all of this for you.
Items are state, not history
Each item carries an entity’s current state, not a single edit. Two edits close together can arrive as one item, and the overlap means you will see some items twice. Both are fine if you apply an item only when it is newer than what you hold for that(entity, id):
A delete carries only the id: no snapshot and
venue: null. Deletes are sent even when you
filter by venue, because the server may no longer know the venue. Drop the id if you hold it and
ignore it otherwise.
Matching fields
Every snapshot says where the entity sits in its matched cluster:
Anchors can change. When a cluster gains or loses a member, every affected entity gets a new
item, and the anchor may move to another member. Store each entity under its own
id and treat
clusterAnchorId as a pointer you update, never as a key. These fields follow your app’s
venue settings: a member whose anchor is on a venue your app hides is reported unmatched, the
same way list endpoints show it as its own row. Market snapshots also carry
outcomes[].id, which is the id the orderbook socket takes.
Snapshots carry the fields that identify and place an entity: titles, status, dates, categories,
outcomes and matching. Prices and volume are not catalog changes. Read those from
market data.
Quick start (SDK)
watchCatalog subscribes, catches up, de-duplicates and re-runs the catch-up after reconnects.
onChange is called once per newer state of an entity.
The handle has
stop(), lastChangedAt, resync(since) and ready. Persist lastChangedAt
somewhere durable once the changes before it are stored, and pass it back as since when your
process restarts. An older position is always safe: it only re-reads more. Without socket,
watchCatalog opens its own connection and stop() closes it. To share a connection you already
have, pass it as socket. On a backend, give the client your apiKey and the connection
authenticates with it. See Server connections.
First sync
Before your first watch, you hold nothing. Note the time, list everything once with List venue events and List venue markets, then startwatchCatalog with
since set to the time you noted. Changes made while you were listing arrive through the
catch-up.
React
useCatalogFeed from @agg-build/hooks runs watchCatalog for the life of the component, on
its own connection (the client needs wsUrl). It takes the same options plus enabled, and
returns { status, lastChangedAt, error, resync }. status is idle, syncing, live,
error or resync_required.
since, such as the position you loaded at startup. Changing since, venues,
entities or matchedOnly restarts the feed; changing the callbacks does not. After
resync_required, re-list and call the returned resync(startedAt).
Most catalogs belong on a server. Use the hook for live UI, such as a “new markets” list, not as
your system of record.
Raw protocol
Any language works. Connect, subscribe, catch up, then apply frames. The sameisNewer check
makes the overlap harmless.
1. Subscribe. Open wss://ws.agg.market/ws?appId=YOUR_APP_ID. From a backend, send your key
in the x-app-api-key handshake header and no Origin. After connected, send:
{ "type": "subscribed", "channel": "catalog", "id": "cat-1" }. Hold any
catalog_change frames that arrive until the catch-up finishes. Frame shapes are in
WebSocket Protocol.
2. Catch up. Page from your saved position minus 60 seconds until hasMore is false.
Repeat multi-value parameters (venues=a&venues=b); a comma list is rejected.
nextCursor is opaque and always set. A page can hold fewer than limit items, or none, while hasMore is
still true, so stop on hasMore, not on an empty page. limit is 1 to 500 and defaults
to 200. Full parameters are in List catalog changes.
3. Go live. Apply the held frames with the same check, then apply each new frame as it
arrives. Save the newest changedAt you applied.
4. Repeat the catch-up from your saved position minus 60 seconds after every reconnect and
every stream_interrupted that lists catalog. Keep holding frames while it runs.
A Python backend, for example (httpx and websockets 14+). load_position() returns your saved
changedAt as epoch seconds, and apply_if_newer() is the check above plus your write:
heartbeat at least every 30 seconds, so they never wait longer than that.
Reconnect with backoff when the socket closes, as in
Reconnection. The new connection runs the catch-up again.
Resync after 24 hours
GET /catalog/changes keeps 24 hours. A since, or a cursor, older than that returns 410:
onResyncRequired followed by handle.resync(startedAt);
isCatalogResyncRequired(err) recognizes the error from getCatalogChanges. Keep the frames that
arrive while you re-list; the catch-up after resync covers them anyway.
Limits and timing
- 24-hour window. Resume positions older than that need a resync.
- No replay on the socket. Frames sent while you were disconnected are not resent. The REST catch-up is the only way to fill a gap.
- Latency. A frame follows a recorded change within a few seconds. How soon a new listing is recorded depends on how often AGG discovers that venue’s catalog: typically 30 to 60 seconds after the venue lists a market.
- Your app’s settings apply. Venues and categories disabled for your app are left out of both halves, except deletes, which only carry an id.
- Settings changes are not replayed. Disabling a venue or category for your app hides future items; it does not send deletes for what you already hold. Re-list after changing them.
- Market categories. A market’s
categoriesare its event’s. When only the event’s categories change, the event gets a new item and its markets do not; read categories from the event when they matter. - Filters. Up to 64 venues per subscribe. Subscribing again on the same connection replaces
your filter. There is no server-side matched-only filter; the SDK’s
matchedOnlyfilters on the client. - Rate limits.
GET /catalog/changeshas its own Catalog feed group in Rate limits, separate from Discovery. A catch-up runs once per reconnect, not on a timer: at current volume, expect roughly 40 pages of 500 items for every hour you were disconnected. Once you are on the feed, you should not need to pollGET /venue-eventsorGET /venue-marketsfor new listings. - Connections. The
catalogchannel works on browser and API-key connections. Backends connect withx-app-api-keyand noOrigin; the connection limits are in WebSocket limits.
Related
WebSocket Protocol
catalog subscribe, ack and catalog_change frames.List catalog changes
REST catch-up parameters and responses.
Matched Clusters
What
matched and the cluster anchor mean on list responses.Building market views
Turning the events and markets you hold into screens.