bolthub logobolthub
Guides

Streaming Endpoints (SSE)

Publish and consume live Server-Sent-Events endpoints behind a bolthub L402 paywall.

What a streaming endpoint is

A streaming endpoint answers with Content-Type: text/event-stream and keeps the connection open, pushing Server-Sent Events as they happen: live trades, liquidation feeds, alert streams. The body has no end, so everything that assumes "request, then a complete response" needs different handling. bolthub supports this end to end: the gateway proxies the live body without buffering, and the API Hub's Try It renders a live event viewer.

Publishing a streaming endpoint

Flip Streaming endpoint (SSE) on the endpoint's settings page in the dashboard. With the flag on:

  • The gateway passes 2xx text/event-stream responses through unbuffered, chunk by chunk, instead of applying its standard 30-second response timeout.
  • The gateway injects : keep-alive comment frames every 15 seconds so quiet streams survive idle-connection cutoffs along the path.
  • The API Hub shows an SSE badge on the endpoint, emits curl -N snippets, and the Try It panel renders a live event viewer instead of a response body.

Pricing models for streams

A stream is priced as a connection, never per event:

ModelStream semantics
per_requestOne payment buys one connection, up to the gateway's lifetime caps. Reconnecting is a new payment.
time_passThe pass window is the stream lifetime; clients holding a session token reconnect free within the window. The recommended model for feeds meant to be monitored.
token_bucketOne connection debits one request from the bucket.
meteredOne connection deducts one unit from the deposit.
per_kbNot supported. Per-KB billing measures a complete body's size, and a live stream has no final size. The API rejects the combination, and the gateway refuses to serve it.

When a paid window ends mid-stream, the gateway sends a terminal frame before closing:

event: payment_required
data: {"reason": "time_pass_expired"}

Two per-endpoint limits are available through the API (maxStreamSeconds, idleTimeoutSeconds) for capping a single stream's lifetime and detecting a silent origin. Both are optional; the gateway's defaults apply otherwise.

Origin requirements

Your origin must flush the response headers immediately on connect, ideally followed by a comment frame (: hello) or an initial event. If the first byte only leaves your server when the first real event fires, every client sits in "waiting for headers" until it times out, and health and sample capture will report the endpoint as failing. Send something within a second of the connection opening.

Consuming a streaming endpoint

The L402 flow is unchanged: the first request answers 402 Payment Required with a Lightning invoice, and the retry with the L402 token gets the live stream.

curl

The -N flag disables curl's output buffering so events print as they arrive:

curl -iN "https://acme.gw.bolthub.ai/v1/live-feed"
# pay the invoice from the 402, then retry with the token:
curl -N -H 'Authorization: L402 <macaroon>:<preimage>' \
  "https://acme.gw.bolthub.ai/v1/live-feed"

Python

L402Auth is an httpx auth adapter: it pays the 402 and never buffers the post-payment body, so httpx.stream keeps streaming. Disable the read timeout for the live body:

import httpx
from bolthub import L402Auth, LndWallet

wallet = LndWallet(host="https://your-node:8080", macaroon="...")
auth = L402Auth(wallet, budget_sats=10_000)

timeout = httpx.Timeout(10.0, read=None)
with httpx.Client(timeout=timeout) as http:
    with http.stream("GET", "https://acme.gw.bolthub.ai/v1/live-feed", auth=auth) as resp:
        for line in resp.iter_lines():
            if line:
                print(line)

TypeScript

@bolthub/pay 0.8+ adds a streaming request option: the client timeout then bounds only the time to response headers, never the body read. Read the body incrementally and stop the stream by aborting a signal you pass in:

import { L402Client, NwcWallet } from "@bolthub/pay";

const wallet = new NwcWallet(nwcConnection);
const client = new L402Client({ wallet, budgetSats: 10_000 });

const ctrl = new AbortController();
const resp = await client.get("https://acme.gw.bolthub.ai/v1/live-feed", {
  streaming: true,
  signal: ctrl.signal,
});

const reader = resp.body!.getReader();
const decoder = new TextDecoder();
for (;;) {
  const { done, value } = await reader.read();
  if (done) break;
  process.stdout.write(decoder.decode(value, { stream: true }));
}
// ctrl.abort() from anywhere closes the stream cleanly.

Plain EventSource cannot send the Authorization header, so it only works for free streaming endpoints. For paid streams use fetch with a reader as above.

From an agent (MCP)

@bolthub/mcp consumes streams two ways. For a one-off taste, call_api takes stream_events/stream_seconds and returns a bounded window of live events (one payment per window). For continuous monitoring, open_stream pays once and holds the connection while read_stream returns new events for free; wait_seconds makes it block until the next event arrives. See the MCP reference for the tool details.

What arrives on the wire

Standard SSE frames, separated by a blank line:

: keep-alive

event: liquidation
data: {"symbol": "BTCUSDT", "side": "SELL", "size": 0.42, "price": 64210.5}
  • Lines starting with : are comments; the gateway's keep-alives look like this. Ignore them, but treat their arrival as proof the connection is alive.
  • A frame's data: can span multiple lines; concatenate them with newlines.
  • The terminal event: payment_required frame means the paid window ended. Reconnecting starts a new L402 flow (and a new payment for per_request pricing).

Try It in the API Hub

The Hub's Try It on a streaming endpoint opens a live viewer after payment: events scroll in as they arrive, with a Stop button and a cap of 100 events or 60 seconds, whichever comes first. The cap only bounds the viewer. Your payment bought the full stream, so consume it with curl -N or an SDK for the unbounded feed. A quiet stream is normal for event-driven feeds; the keep-alive comments hold the connection open between events.

The Sample Data panel on streaming endpoints shows the first captured events of the live stream, labeled as a stream prefix rather than a complete response.