Kick awards 10 points per 5 minutes of watch time (20 for subscribers). moho earns
none of it, which was measured rather than assumed: nine minutes connected to a live
channel with 565 viewers, balance unchanged at 1270 the whole time.
The reason is structural. nobilis/src/backend/kick/socket.rs says it outright —
"Empty auth: all four are public, and this backend subscribes to nothing that is not".
Kick is told nothing about who is watching, so it has nothing to credit. The video path
is a signed CDN playlist fetched anonymously, so that earns nothing either.
How Kick actually counts a viewer
Found from a HAR of a logged-in session watching a live channel. Kick has moved off
Pusher onto Centrifugo and the new path is identified, which is the whole point.
There is no points call anywhere. Nothing client-side asks for them and nothing reports
watch time. It is all server-side, off the presence of an authenticated realtime
subscription.
Three steps:
1. Negotiate a connection.
POST https://web.kick.com/api/v1/realtime/connection
Authorization: Bearer <account token>
x-app-platform: web
{"client":{"id":"<kick user id>","type":"web"},
"capabilities":{"accepted_providers":[{"provider":"pusher"},{"provider":"centrifugo"}]}}
Note accepted_providers still lists pusher, which is presumably why the current
Pusher chat socket keeps working.
2. Get a subscription token for the livestream.
POST https://web.kick.com/api/v1/realtime/auth/channel
Authorization: Bearer <account token>
x-app-platform: web
{"channel":"private-livestream.<livestream id>"}
The channel is keyed by livestream id, not channel id — 126952428 in the capture,
which is the same id GET kick.com/current-viewers reports. It changes per broadcast,
so it has to be re-read when a channel goes live rather than cached with the channel.
3. Connect and subscribe. Centrifugo's own protocol over that websocket. The
connection response carried no connect token, only a URL, so the connection is
anonymous and the per-channel JWT is what identifies the viewer.
It is private-livestream.<id> that matters, not the chat channel — which is right,
since this is watch time rather than chat presence.
Scope
Agreed: authenticate for chat buffers that are open, and channels playing video
including picture-in-picture. Not every channel the account follows — a background
subscription to twenty channels is claiming to watch twenty streams at once.
Worth knowing before building
This changes what moho discloses. Today it reads Kick almost entirely anonymously; this
tells Kick, continuously, that this account is watching a given stream. That is the
trade for points and it was accepted deliberately, but it should stay scoped to what is
actually on screen.
It also means a second websocket client in the daemon: Centrifugo's protocol, not
Pusher's. tokio-tungstenite is already a dependency.
No credentials from the capture are recorded here.
Kick awards 10 points per 5 minutes of watch time (20 for subscribers). moho earns
none of it, which was measured rather than assumed: nine minutes connected to a live
channel with 565 viewers, balance unchanged at 1270 the whole time.
The reason is structural. `nobilis/src/backend/kick/socket.rs` says it outright —
"Empty auth: all four are public, and this backend subscribes to nothing that is not".
Kick is told nothing about who is watching, so it has nothing to credit. The video path
is a signed CDN playlist fetched anonymously, so that earns nothing either.
## How Kick actually counts a viewer
Found from a HAR of a logged-in session watching a live channel. Kick has **moved off
Pusher onto Centrifugo** and the new path is identified, which is the whole point.
There is no points call anywhere. Nothing client-side asks for them and nothing reports
watch time. It is all server-side, off the presence of an authenticated realtime
subscription.
Three steps:
**1. Negotiate a connection.**
```
POST https://web.kick.com/api/v1/realtime/connection
Authorization: Bearer <account token>
x-app-platform: web
{"client":{"id":"<kick user id>","type":"web"},
"capabilities":{"accepted_providers":[{"provider":"pusher"},{"provider":"centrifugo"}]}}
```
Answers with the websocket to use:
```
{"data":{"connections":[{"credentials":{"url":
"wss://realtime.us-west-2.platform.kick.com/connection/websocket"},
"provider":"centrifugo"}],"mode":"websocket"},"message":"success"}
```
Note `accepted_providers` still lists pusher, which is presumably why the current
Pusher chat socket keeps working.
**2. Get a subscription token for the livestream.**
```
POST https://web.kick.com/api/v1/realtime/auth/channel
Authorization: Bearer <account token>
x-app-platform: web
{"channel":"private-livestream.<livestream id>"}
```
Answers `{"data":{"token":"<JWT>"},"message":"success"}`.
The channel is keyed by **livestream id, not channel id** — `126952428` in the capture,
which is the same id `GET kick.com/current-viewers` reports. It changes per broadcast,
so it has to be re-read when a channel goes live rather than cached with the channel.
**3. Connect and subscribe.** Centrifugo's own protocol over that websocket. The
connection response carried no connect token, only a URL, so the connection is
anonymous and the per-channel JWT is what identifies the viewer.
It is `private-livestream.<id>` that matters, not the chat channel — which is right,
since this is watch time rather than chat presence.
## Scope
Agreed: authenticate for **chat buffers that are open, and channels playing video
including picture-in-picture**. Not every channel the account follows — a background
subscription to twenty channels is claiming to watch twenty streams at once.
## Worth knowing before building
This changes what moho discloses. Today it reads Kick almost entirely anonymously; this
tells Kick, continuously, that this account is watching a given stream. That is the
trade for points and it was accepted deliberately, but it should stay scoped to what is
actually on screen.
It also means a second websocket client in the daemon: Centrifugo's protocol, not
Pusher's. `tokio-tungstenite` is already a dependency.
No credentials from the capture are recorded here.
Not verified against live Kick: the Centrifugo endpoints are taken from a HAR of the site's own traffic, not from a run of this code. Test is to open a live channel and check the points plaque after ~6 minutes; the daemon log says either watching private-livestream.<id> or realtime refused: <reason>.
Finished
https://git.salastil.com/Salastil/nobilis/commit/e264ea6612a17d8c56c22877093367f8499483b3
https://git.salastil.com/Salastil/moho/commit/ed98fec95ed132b0d0ffaf7d00aa71286334da2a
Not verified against live Kick: the Centrifugo endpoints are taken from a HAR of the site's own traffic, not from a run of this code. Test is to open a live channel and check the points plaque after ~6 minutes; the daemon log says either `watching private-livestream.<id>` or `realtime refused: <reason>`.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Kick awards 10 points per 5 minutes of watch time (20 for subscribers). moho earns
none of it, which was measured rather than assumed: nine minutes connected to a live
channel with 565 viewers, balance unchanged at 1270 the whole time.
The reason is structural.
nobilis/src/backend/kick/socket.rssays it outright —"Empty auth: all four are public, and this backend subscribes to nothing that is not".
Kick is told nothing about who is watching, so it has nothing to credit. The video path
is a signed CDN playlist fetched anonymously, so that earns nothing either.
How Kick actually counts a viewer
Found from a HAR of a logged-in session watching a live channel. Kick has moved off
Pusher onto Centrifugo and the new path is identified, which is the whole point.
There is no points call anywhere. Nothing client-side asks for them and nothing reports
watch time. It is all server-side, off the presence of an authenticated realtime
subscription.
Three steps:
1. Negotiate a connection.
Answers with the websocket to use:
Note
accepted_providersstill lists pusher, which is presumably why the currentPusher chat socket keeps working.
2. Get a subscription token for the livestream.
Answers
{"data":{"token":"<JWT>"},"message":"success"}.The channel is keyed by livestream id, not channel id —
126952428in the capture,which is the same id
GET kick.com/current-viewersreports. It changes per broadcast,so it has to be re-read when a channel goes live rather than cached with the channel.
3. Connect and subscribe. Centrifugo's own protocol over that websocket. The
connection response carried no connect token, only a URL, so the connection is
anonymous and the per-channel JWT is what identifies the viewer.
It is
private-livestream.<id>that matters, not the chat channel — which is right,since this is watch time rather than chat presence.
Scope
Agreed: authenticate for chat buffers that are open, and channels playing video
including picture-in-picture. Not every channel the account follows — a background
subscription to twenty channels is claiming to watch twenty streams at once.
Worth knowing before building
This changes what moho discloses. Today it reads Kick almost entirely anonymously; this
tells Kick, continuously, that this account is watching a given stream. That is the
trade for points and it was accepted deliberately, but it should stay scoped to what is
actually on screen.
It also means a second websocket client in the daemon: Centrifugo's protocol, not
Pusher's.
tokio-tungsteniteis already a dependency.No credentials from the capture are recorded here.
Finished
e264ea6612ed98fec95eNot verified against live Kick: the Centrifugo endpoints are taken from a HAR of the site's own traffic, not from a run of this code. Test is to open a live channel and check the points plaque after ~6 minutes; the daemon log says either
watching private-livestream.<id>orrealtime refused: <reason>.