Перейти к основному контенту
Loading market data…
SoccerverseSoccerverse

These developer docs are available in English only. View in English

Realtime Events (SSE)

Live change notifications over Server-Sent Events: what to subscribe to, what an event looks like, and how to keep a site up to date without polling.

The events stream tells your code when something in Soccerverse has changed — a new block, a club, a player, a transfer bid — so you can reload just that data instead of polling on a timer. It is standard Server-Sent Events (SSE): one long-lived GET request, no API key.

An event is a doorbell, not a delivery. It says what changed and never carries the new values. When one arrives, fetch the fresh data from the /v1 API, for example https://play.soccerverse.com/v1/club/50.

This page covers the hosted service on play.soccerverse.com. If you run your own node, see Running your own node.

Quick start

Run this in a terminal:

curl -N 'https://play.soccerverse.com/events?topics=chain:block'

-N turns off curl's buffering so each line shows up as it arrives. Press Ctrl-C to stop. Every second or two you get one view.ready event, one per new game block. This is the start of a real capture:

retry: 2000

id: 7838391252ce922b:1790657443067-0
event: cursor
data: {"epoch":"7838391252ce922b","id":"1790657443067-0","schema":"sv.events.v1"}

id: 7838391252ce922b:1790657445164-0
event: view.ready
data: {"data":{"current":{"blockHash":"121ce40e43955a92892fc9525328fab55ec357522c4f4f46004b1d282d1c19e3","height":94634870,"name":"gsp","ready":true},"polygonHeight":94634869,"previous":{"blockHash":"b1659cf5cbbdaaf45719b5bf265faaf44811704e2a2e057f3fa5f2f780c626fc","height":94634869,"name":"gsp","ready":true},"transition":"forward"},"id":"1790657445164-0","observedAt":"2026-09-29T04:50:45.164362Z","schema":"sv.events.v1","source":{"blockHash":"121ce40e43955a92892fc9525328fab55ec357522c4f4f46004b1d282d1c19e3","height":94634870,"name":"gsp","ready":true},"topics":["chain:block","source:gsp"],"type":"view.ready"}

Each event is three lines: id, event (the event's name) and data (JSON). The first two blocks are set-up: retry: 2000 tells the client to wait 2 seconds before reconnecting, and cursor is a bookmark of your place in the stream. That is the whole protocol. The rest of this page is what to subscribe to and how to keep the connection healthy.

Base URL and access

  • Endpoint: https://play.soccerverse.com/events
  • GET only, with no authentication and no API key.
  • The response is 200 with Content-Type: text/event-stream, and it stays open until you close it.
  • Browsers on other websites are currently blocked, see Use it in a browser. Servers and scripts are not affected.
MethodPathPurposeMain query/path parameters
GET/eventsThe live stream.topics optional comma-separated list. Leave it out to receive everything.

Pitfalls:

  • Some clients, for example Python's default urllib, get a Cloudflare 403 unless they send a User-Agent header. Set your own, such as my-app/1.0.
  • HEAD and POST return 405. Only GET works.

Use it from Node.js

Browsers on other websites cannot open the stream (see below), so the dependable way to add live updates to your own site is to keep one stream open on your server. When something you care about changes, your server reloads the data and updates your visitors however you like: its own SSE endpoint, a WebSocket, or a short poll of your server.

This example watches club 50. It needs Node.js 22.12 or newer and one small package, which gives Node the same EventSource that browsers have:

npm install eventsource

Save this as watch.mjs:

import { EventSource } from 'eventsource';

async function refresh() {
  try {
    const res = await fetch('https://play.soccerverse.com/v1/club/50');
    const club = await res.json();
    // Do something with `club` here, such as updating your page or database.
    console.log('club 50 reloaded');
  } catch (err) {
    console.error('refresh failed:', err.message);
  }
}

function connect() {
  const url = 'https://play.soccerverse.com/events?topics=club:50,clubs,cache:reset';
  const es = new EventSource(url);

  // Load when the stream opens. This also reloads after every reconnect.
  es.addEventListener('open', refresh);

  // Reload if club 50 is named. Events that name no clubs (a reset or a full
  // rebuild) or whose list was cut short also mean "reload".
  const onChange = (e) => {
    const { data } = JSON.parse(e.data);
    if (!data.clubIds || data.clubIds.includes(50) || data.truncated) refresh();
  };
  for (const type of ['resource.changed', 'view.ready', 'chain.reorg']) {
    es.addEventListener(type, onChange);
  }

  // The server cannot replay what you missed: start again from scratch.
  es.addEventListener('resync-required', () => {
    es.close();
    connect();
  });

  // Network blips reconnect by themselves. If the server refused the
  // connection (429 or 503), the stream stays closed: try again later.
  es.addEventListener('error', () => {
    if (es.readyState === EventSource.CLOSED) setTimeout(connect, 30_000);
  });
}

connect();
setInterval(refresh, 60_000); // safety net, and keeps Node alive while the stream reconnects

Run it with node watch.mjs. What it does:

  • It loads club 50 when the stream opens (at the start and after every reconnect), again whenever club:50 or clubs says club 50 may have changed, and once a minute as a safety net. The cache:reset topic means "reload everything you show", see Topics.
  • Watching more than one club or player? Add their topics to the URL (plus clubs or players) and read data.clubIds or data.playerIds to see which ones changed, so you reload only those. If the list is missing or truncated is true, reload all of them.
  • To see it react while testing, add chain:block to the topics. You will get a reload every second or two.

Pitfalls:

  • es.onmessage never fires. Every Soccerverse event has a name, and onmessage only receives events without one. Register each name with addEventListener, as above.
  • The eventsource package does not keep Node running while it waits to reconnect. A script that only holds the stream open would quietly exit after the first dropped connection. The setInterval line prevents that, and a web server would too.

Use it in a browser

Currently this only works from Soccerverse's own game apps, such as https://play.soccerverse.com. The stream sends the CORS headers browsers need to those addresses and to no other website. On any other website, including soccerverse.com, the browser refuses the connection: open never fires, readyState goes straight to 2 (closed), and the console shows a CORS error. Servers, scripts and tools like curl are not affected.

For a page on your own website you have two options:

  • Poll /v1 from the page. Unlike the stream, /v1 and /datacentre allow requests from any website.
  • Hold the stream on your server. Run the Node.js example there and pass changes on to your pages.

Topics

Pick what you want to hear about with ?topics=a,b,c. Names are exact. A trailing :* matches a whole family, for example share:club:50:*. Leave topics out (or use *) to receive every event. That is handy for a minute of exploring, but in real code subscribe only to what you show.

TopicFires when
chain:blockA new game block was processed, every second or two. Arrives as view.ready; the height is in data.current.height.
club:{id}, for example club:50That club changed. Reload /v1/club/{id}. Bids and some other changes arrive only on clubs, so listen to both.
clubsOne or more clubs changed. Some changes, such as bids, arrive only here; the ids are in data.clubIds. No clubIds means any club may have changed.
player:{id} and playersOne player, or a batch of players, changed. The ids are in data.playerIds. Reload /v1/player/{id}.
user:{name}:profile and usersA user's profile changed. Reload /v1/user/{name}.
share:club:{id}:orders and share:club:{id}:tradesInfluence orders or trades for that club. share:player:{id}:orders and :trades work the same way for a player. Also subscribe to market:shares, see below.
auctionsA transfer auction changed: a bid, a new listing or an ending. The player ids are in data.playerIds.
messagesNew in-game news, such as transfers and results.
leaderboardThe rich list was recalculated. This one is chatty.
proposalsA governance proposal or vote changed.
graph:xaya-stats, graph:sv-subgraph, graph:democrit-svThat public subgraph indexed newer data.
cache:resetThe chain reorganised, or the game node recovered from catching up. Reload everything you show.

More topics exist, for example matchday, jobs, transfers and params. Exploring with no topics shows what is being published.

Good to know:

  • No single club topic covers everything. Some changes, such as a transfer bid that ties up a club's money, are announced only on clubs, never on club:{id}. Others, such as a club's wages, player values and ratings moving after one of its players changes, can arrive only on club:{id}. To catch every change to club 50, listen to club:50 and clubs, and reload when data.clubIds is missing, contains 50, or data.truncated is true, as the example above does.
  • clubIds is missing about once a day, when every club's wages, player values and ratings are recalculated. That arrives as a clubs event whose data is just {"full":true}.
  • When a lot changes at once, such as a matchday, you get one players (or users) event instead of player:{id} (or user:{name}:profile) events. It lists what changed: data.playerIds for players and data.names for users.
  • In a busy block, the per-share topics (such as share:club:{id}:orders) can be replaced by a single market:shares event. If you watch order books or trades, also subscribe to market:shares and reload them when it arrives. Its data has no ids on an ordinary trade, so do not rely on it.
  • User names with anything other than ASCII letters, digits, ., _, - and ~ (but not a leading ~) must be encoded: ~ followed by the unpadded base64url of the UTF-8 name. For example José becomes user:~Sm9zw6k:profile.

Pitfalls:

  • A well-formed topic that nobody publishes is accepted and simply never fires. If nothing arrives, check the spelling against the table.
  • Topic names start with a lowercase letter, so Club:50 is rejected with 400. Invalid topics fail the whole connection, not just that one topic.

Event format

A real event, captured the moment club 20808 changed:

id: 7dee42ad5f405260:1790657569212-0
event: resource.changed
data: {"data":{"clubIds":[20808],"count":1},"id":"1790657569212-0","observedAt":"2026-09-29T04:52:49.212312Z","schema":"sv.events.v1","source":{"blockHash":"c2bf2b962ee243f10d335d18adfb3a4f33f0f5a1e70b5839f467053fa02fd151","checkpoint":94634952,"checkpointKind":"gspHeight","height":94634952,"name":"datacentre.clubs","ready":true},"topics":["club:20808"],"type":"resource.changed"}

The data line is JSON. Formatted for reading:

{
  "data": { "clubIds": [20808], "count": 1 },
  "id": "1790657569212-0",
  "observedAt": "2026-09-29T04:52:49.212312Z",
  "schema": "sv.events.v1",
  "source": {
    "blockHash": "c2bf2b962ee243f10d335d18adfb3a4f33f0f5a1e70b5839f467053fa02fd151",
    "checkpoint": 94634952,
    "checkpointKind": "gspHeight",
    "height": 94634952,
    "name": "datacentre.clubs",
    "ready": true
  },
  "topics": ["club:20808"],
  "type": "resource.changed"
}
FieldMeaning
typeThe event name. Same as the event: line.
topicsThe topics this event was published on.
dataA small JSON object, usually the ids that changed (clubIds, playerIds) or a count. Never new values. Its shape depends on the topic. If the id list is missing, or truncated is true because the list was cut at 500 ids, reload everything you show for that topic.
sourceWhich part of the system reported the change and the block height it has reached. You can ignore it.
observedAtWhen the server noticed the change, in UTC. This is not the block time.
idThe event's place in the stream. Treat it as opaque. When you reconnect, send the full id: line (<epoch>:<id>) instead of this shorter one, see Staying connected.
schemaAlways sv.events.v1.

The events you will see:

Event nameMeaning
resource.changedSomething you can fetch has changed. The topic says what.
view.readyA new block was processed (chain:block), or the game node recovered (cache:reset).
chain.reorgThe newest blocks were replaced. Anything you show may have changed: reload everything.
source.degradedThe game node behind the API is catching up, so data can lag until it recovers.
cursorKeep-alive and bookmark. Nothing to do.
resync-requiredThe server cannot replay what you missed. See Staying connected.
drainingThis server is being restarted. The connection closes and your client reconnects by itself. Nothing to do.

Events are delivered at least once, so the same event can occasionally arrive twice. Reloading twice is harmless.

Staying connected

Browsers and the eventsource package do most of this for you:

  • Reconnecting. If the connection drops, the client waits 2 seconds (the retry: 2000 line) and reconnects. It sends the value of the last id: line it saw in a Last-Event-ID header, and the server replays everything after it. If you write your own client, send that header yourself, with the full value of the last id: line, for example 7838391252ce922b:1790657443067-0, not the shorter id inside the JSON. The server rejects that one with 400.
  • Keep-alive. On a quiet stream the server sends a cursor event every 15 seconds. If a whole minute passes with nothing at all, treat the connection as dead: close it, open a new one and reload.
  • Replay window. The server can catch you up on roughly the last 15 minutes. Come back later than that and it cannot.

When the server cannot catch you up it sends resync-required and closes the stream. This is a real one, captured by reconnecting with a made-up Last-Event-ID:

event: resync-required
data: {"epoch":"7838391252ce922b","latest":"1790657581008-0","oldestAvailable":"1790617290105-0","reason":"epoch_mismatch","requested":"1790657443067-0","schema":"sv.events.v1","snapshotRequired":true}

The reason field says why; the fix is always the same:

  1. Close the stream yourself. The event has no id on purpose, so a browser that reconnects on its own would ask for the same impossible position again and again.
  2. Open a new stream. Without a Last-Event-ID it starts from now.
  3. Once it is open, reload everything you show.

The example above does exactly this. You will see resync-required now and then because play.soccerverse.com is served by several machines, and a reconnect can land on one that cannot continue your stream. Clients that keep cookies (browsers do) usually stay on the same machine, so it is rare in a browser. A plain Node.js script keeps no cookies and will see it more often.

Fair use and limits

  • One stream per app, not one per visitor. Limits are per IP address. Open the stream from your server and share it.
  • Subscribe only to what you show. At most 64 topics per connection.
  • Limits per IP address. At most 50 open streams and 5 new connections per second (bursts of up to 10). Beyond that the server answers 429. When it is at capacity it answers 503 with Retry-After: 30.
  • Back off. After a 429 or 503, wait at least 30 seconds before you try again, and never retry in a tight loop. The example above waits 30 seconds.
  • Reload politely. Events can arrive in bursts. If several arrive together, wait a second and reload once. Reloads are ordinary API calls, and /v1 and /datacentre share a limit of 20 requests per second (bursts of 40) per IP address.
  • Topics are public. Anyone can listen to any topic. Events are hints: they carry ids, counts and block numbers, not game data.

Events or polling?

Compared onPoll /v1Use events
Works from a browser on your own siteYesNot yet, see above
Time to see a changeUp to one polling intervalUsually a few seconds
Requests when nothing has changedOne per intervalAlmost none
EffortA timer and a fetchA stream plus reconnect handling

Start with polling if a page only needs to refresh now and then. Use events when you want changes within seconds, or want to stop polling. Either way the data comes from /v1: events only tell you when to ask. Even with events, keep a slow reload (about once a minute) as a safety net.

Running your own node

Everything above is the hosted service on play.soccerverse.com. A bare GSP node, which is what you get when you run your own, has none of the hosted extras: no /events, no /v1 and no Datacentre REST API. It has one helpful method instead: the JSON-RPC call waitforchange, a long poll that returns when the newest block changes. The hosted /v1/gsp proxy does not allow it. On the hosted service use /events.

The loop:

  1. Call getnullstate. Its blockhash is your starting hash.
  2. Call waitforchange with that hash. It waits until the newest block is a different one and returns its hash. If nothing changes it returns the same hash after the node's own timeout (5 seconds by default), so compare the answer with what you sent and call again.
  3. When the hash is new, read what you need with the get_* methods (see GSP JSON-RPC), then go back to step 2 with the new hash.
# Use your own node's JSON-RPC address.
curl -sS http://localhost:8600/ \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"waitforchange","params":["<last block hash>"]}'

The result is the block hash as a plain string, not the usual state envelope:

{
  "id": 1,
  "jsonrpc": "2.0",
  "result": "20bb58a2bfde318045c44525048db7bf8daac4703af789c09b618d6da380b9db"
}

Наши партнеры