Api

SSE Activity Stream

Real-time event streaming for droplit activity.

SSE 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

Requests must include X-Auth-Token generated using the normal signing flow.

Accept: text/event-stream
X-Auth-Token: <signed-token>

Important browser note

The native browser EventSource API does not support custom headers. For authenticated streams, use fetch + ReadableStream parsing (example below).

Event types

  1. faucet_activity: primary activity payload
  2. ping: keep-alive event

Payload shape

interface FaucetActivityItem {
	id: string;
	faucet_name: string;
	event_type: string;
	timestamp: string;
	details: Record<string, unknown>;
	metadata?: Record<string, unknown>;
}

Example: authenticated stream with fetch

interface FaucetActivityItem {
	id: string;
	faucet_name: string;
	event_type: string;
	timestamp: string;
	details: Record<string, unknown>;
	metadata?: Record<string, unknown>;
}

export async function connectActivityStream(
	apiBaseUrl: string,
	faucetName: string,
	authToken: string,
	onMessage: (activity: FaucetActivityItem) => void,
	onError: (error: unknown) => void,
	signal?: AbortSignal,
) {
	const requestPath = `/faucet/${faucetName}/activity-stream`;
	const response = await fetch(`${apiBaseUrl}${requestPath}`, {
		method: "GET",
		headers: {
			Accept: "text/event-stream",
			"X-Auth-Token": authToken,
		},
		signal,
	});

	if (!response.ok) {
		throw new Error(`SSE connect failed: ${response.status} ${response.statusText}`);
	}

	const reader = response.body?.getReader();
	if (!reader) {
		throw new Error("Missing stream reader");
	}

	const decoder = new TextDecoder();
	let buffer = "";

	try {
		while (true) {
			const { done, value } = await reader.read();
			if (done) break;

			buffer += decoder.decode(value, { stream: true });

			let splitIndex = buffer.indexOf("\n\n");
			while (splitIndex !== -1) {
				const rawEvent = buffer.slice(0, splitIndex);
				buffer = buffer.slice(splitIndex + 2);

				if (rawEvent.startsWith("event: faucet_activity")) {
					const dataLine = rawEvent
						.split("\n")
						.find((line) => line.startsWith("data: "));

					if (dataLine) {
						const json = dataLine.slice("data: ".length);
						try {
							onMessage(JSON.parse(json) as FaucetActivityItem);
						} catch (error) {
							onError(error);
						}
					}
				}

				splitIndex = buffer.indexOf("\n\n");
			}
		}
	} catch (error) {
		if (!signal?.aborted) {
			onError(error);
		}
	}
}

React usage sketch

import { useEffect } from "react";

useEffect(() => {
	if (!faucetName || !authToken) return;

	const abortController = new AbortController();

	connectActivityStream(
		process.env.NEXT_PUBLIC_DROPLIT_API_URL!,
		faucetName,
		authToken,
		(activity) => {
			// Handle event
		},
		(error) => {
			// Handle stream errors
		},
		abortController.signal,
	);

	return () => abortController.abort();
}, [faucetName, authToken]);