Kick: no watch time is earned, because nothing says we are watching #206

Closed
opened 2026-09-12 14:59:05 -04:00 by Salastil · 1 comment
Owner

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.

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.
Salastil added the feature label 2026-09-12 14:59:05 -04:00
Author
Owner

Finished

e264ea6612
ed98fec95e

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>`.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#206