Summary
Clients currently poll the transaction history endpoint to see updates. This issue replaces polling with a persistent Server-Sent Events (SSE) stream per wallet: the server pushes every new event matching the wallet's subscriptions in real time, guarantees at-least-once delivery by tracking the last acknowledged cursor per connection, supports reconnection with cursor-based replay so no events are missed during a disconnect, and enforces backpressure so a slow client cannot exhaust server memory.
Scope
1. Subscription registry
POST /subscriptions accepts a JWT-authenticated request with a topics array (e.g. ['key_buy', 'key_sell', 'follower_added']) and returns a subscriptionId
- Subscriptions are stored in Redis as a sorted set per wallet:
subscriptions:{walletAddress} → set of (subscriptionId, topics[])
- A wallet may hold at most 5 concurrent subscriptions; a 6th returns 409
subscription_limit_reached
- Subscriptions expire after 24 hours of inactivity (no connected SSE client); a background worker prunes expired entries every 5 minutes
2. SSE connection and event fan-out
GET /subscriptions/:subscriptionId/stream upgrades to an SSE connection; requires the same JWT that created the subscription
- On connect, the server registers the connection in an in-process connection map keyed by
subscriptionId
- When a new event is ingested by the event pipeline, the fan-out worker looks up all subscriptions whose
topics include the event type and pushes the serialised event to every connected SSE client matching the event's wallet
- Fan-out must not block the ingestion pipeline — publish to a per-subscription in-process queue and drain asynchronously
- Each SSE message includes:
id: {eventCursor}, event: {topic}, data: {JSON payload}
3. Cursor-based replay and at-least-once delivery
- The
eventCursor is an opaque string encoding (walletAddress, topic, createdAt, eventId) — monotonically increasing per wallet
- On reconnect, the client sends
Last-Event-ID: {cursor} in the request header; the server replays all events after that cursor from the database before switching to live fan-out
- Store the last acknowledged cursor per subscription in Redis; update it each time an event is successfully written to the SSE stream
- If a client disconnects without acknowledging, events since the last stored cursor are replayed on the next reconnect (at-least-once guarantee)
- Replay is capped at 1000 events; if the gap exceeds this, return an
X-Replay-Truncated: true header and start from the oldest available event in the window
4. Backpressure and slow-client protection
- Each SSE connection has an in-process event queue capped at 500 messages
- If the queue reaches 500, stop accepting new events for that connection and emit a warn log
sse_queue_full with subscriptionId and queueDepth
- If the queue remains full for more than 30 seconds, forcibly close the connection with SSE comment
# connection closed: slow client and mark the subscription as THROTTLED in Redis
- A
THROTTLED subscription cannot reconnect for 60 seconds (return 429 with Retry-After)
5. Heartbeat and connection hygiene
- Send an SSE comment (
:) heartbeat every 15 seconds to keep the connection alive through proxies
- Track connection count per wallet in Redis; enforce a maximum of 2 concurrent SSE connections per wallet (3rd connection returns 429
too_many_connections)
- On server shutdown, send
event: server_closing to all open connections before closing, giving clients time to reconnect to another instance
6. Integration tests
- Subscribe to
key_buy events, ingest a buy event, assert the SSE stream receives the event within 200ms
- Disconnect mid-stream and reconnect with the last cursor, assert only missed events are replayed (no duplicates)
- Ingest 1100 events while disconnected, reconnect, assert replay is capped at 1000 and
X-Replay-Truncated: true is set
- Saturate the per-connection queue to 500 and hold it for 31 seconds, assert the connection is forcibly closed and the subscription is
THROTTLED
- Open 3 concurrent SSE connections for the same wallet, assert the 3rd returns 429
- Attempt to create a 6th subscription for the same wallet, assert 409
Acceptance Criteria
ETA: 24 hours
Coordinate on Telegram
Summary
Clients currently poll the transaction history endpoint to see updates. This issue replaces polling with a persistent Server-Sent Events (SSE) stream per wallet: the server pushes every new event matching the wallet's subscriptions in real time, guarantees at-least-once delivery by tracking the last acknowledged cursor per connection, supports reconnection with cursor-based replay so no events are missed during a disconnect, and enforces backpressure so a slow client cannot exhaust server memory.
Scope
1. Subscription registry
POST /subscriptionsaccepts a JWT-authenticated request with atopicsarray (e.g.['key_buy', 'key_sell', 'follower_added']) and returns asubscriptionIdsubscriptions:{walletAddress}→ set of(subscriptionId, topics[])subscription_limit_reached2. SSE connection and event fan-out
GET /subscriptions/:subscriptionId/streamupgrades to an SSE connection; requires the same JWT that created the subscriptionsubscriptionIdtopicsinclude the event type and pushes the serialised event to every connected SSE client matching the event's walletid: {eventCursor},event: {topic},data: {JSON payload}3. Cursor-based replay and at-least-once delivery
eventCursoris an opaque string encoding(walletAddress, topic, createdAt, eventId)— monotonically increasing per walletLast-Event-ID: {cursor}in the request header; the server replays all events after that cursor from the database before switching to live fan-outX-Replay-Truncated: trueheader and start from the oldest available event in the window4. Backpressure and slow-client protection
sse_queue_fullwithsubscriptionIdandqueueDepth# connection closed: slow clientand mark the subscription asTHROTTLEDin RedisTHROTTLEDsubscription cannot reconnect for 60 seconds (return 429 withRetry-After)5. Heartbeat and connection hygiene
:) heartbeat every 15 seconds to keep the connection alive through proxiestoo_many_connections)event: server_closingto all open connections before closing, giving clients time to reconnect to another instance6. Integration tests
key_buyevents, ingest a buy event, assert the SSE stream receives the event within 200msX-Replay-Truncated: trueis setTHROTTLEDAcceptance Criteria
POST /subscriptionscreates a subscription with topic filtering and enforces the 5-subscription limitGET /subscriptions/:id/streamopens an SSE stream and delivers matching events in real timeX-Replay-Truncated: trueheaderTHROTTLEDsubscriptions blocked for 60 seconds with 429 andRetry-Afterserver_closingevent sent to all connections on graceful shutdownETA: 24 hours
Coordinate on Telegram