Api

WebSocket Activity Stream

Authenticated real-time WebSocket events for droplit activity.

WebSocket Activity Stream

Use the activity stream endpoint to receive real-time droplit events such as taps, pushes, deposits, and sync activity.

Endpoint

GET /faucet/{faucetName}/activity-stream

Replace {faucetName} with your droplit name.

Authentication

The HTTP request upgrades to a WebSocket, then the SDK's Peer performs the BRC-103/104 handshake over that connection. Every protected route uses this mutual-authentication protocol.

Event types

After authentication, the server sends:

  1. connected: ownership is verified and the stream is ready
  2. ping: keep-alive event
  3. faucet_activity: primary activity payload
  4. error: authentication, authorization, or stream failure

Payload shape

type ActivityStreamMessage =
	| { type: "connected"; data: { message: string } }
	| { type: "ping"; data: { time: string } }
	| { type: "faucet_activity"; data: FaucetActivityItem }
	| { type: "error"; status: number; error: string };

After the Peer handshake, send one authenticated general message (for example, {"type":"subscribe"}) so the server can verify faucet ownership. The current server then sends plain JSON connected, ping, faucet_activity, and error frames on that same authenticated WebSocket. They are not Peer envelopes. Route frames with messageType to Peer and frames with type to the activity handler. Do not treat an unauthenticated activity frame as proof of ownership.

Browser transport

The SDK defines the transport interface while Peer owns all signing, nonce, certificate, and session logic:

import {
	Peer,
	type AuthMessage,
	type Transport,
	type WalletInterface,
} from "@bsv/sdk";

class WebSocketTransport implements Transport {
	private pending: Array<{
		message: string;
		resolve: () => void;
	}> = [];

	constructor(private socket: WebSocket, private onEvent: (event: ActivityStreamMessage) => void) {
		socket.addEventListener("open", () => {
			for (const item of this.pending.splice(0)) {
				socket.send(item.message);
				item.resolve();
			}
		});
	}

	async send(message: AuthMessage): Promise<void> {
		const serialized = JSON.stringify(message);
		if (this.socket.readyState === WebSocket.OPEN) {
			this.socket.send(serialized);
			return;
		}

		await new Promise<void>((resolve) => {
			this.pending.push({ message: serialized, resolve });
		});
	}

	async onData(
		callback: (message: AuthMessage) => Promise<void>,
	): Promise<void> {
		this.socket.addEventListener("message", (event) => {
			const message = JSON.parse(String(event.data));
   if ("type" in message) this.onEvent(message);
   else void callback(message as AuthMessage);
		});
	}
}

export async function connectActivityStream(
	apiBaseUrl: string,
	faucetName: string,
	wallet: WalletInterface,
	onEvent: (event: ActivityStreamMessage) => void,
): Promise<WebSocket> {
	const url = new URL(`/faucet/${faucetName}/activity-stream`, apiBaseUrl);
	url.protocol = url.protocol === "https:" ? "wss:" : "ws:";

	const socket = new WebSocket(url);
 let authenticated = false;
 const peer = new Peer(wallet, new WebSocketTransport(socket, event => {
  if (authenticated || event.type === "error") onEvent(event);
 }));

	await peer.ready;
	await peer.getAuthenticatedSession();
 authenticated = true;
 await peer.toPeer(Array.from(new TextEncoder().encode('{"type":"subscribe"}')));
	return socket;
}