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:
connected: ownership is verified and the stream is readyping: keep-alive eventfaucet_activity: primary activity payloaderror: 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;
}