Get your WebSocket stream token
Returns your stream token plus the WebSocket URL for the realtime stream. The token does not expire (since 2026-08-27): one token per user, the same value on every call, so multi-process clients can each call this at boot and nothing ever needs refreshing. It stops working only when the subscription lapses, or when you send {"rotate": true} in the body to replace it (the previous value keeps working for 60 s). expires_at / next_refresh_at are always null and kept only for wire compatibility. Present the token as Authorization: Bearer <token> on the handshake (preferred) or ?token= — it is masked in our access logs either way. ULTRA and BUSINESS responses also include dex_ws_url for the DEX firehose. PRO/ULTRA only.
After connecting, send {"type":"subscribe","channels":[...]} — nothing is delivered until you subscribe. Channels: kol:trades, kol:coordination (PRO+, user-scoped: fires of your own /kol/coordination/alerts rules), kol:first_touches (PRO+), deployer:alerts, wallet_tracker:events (ULTRA, subscriber-scoped), copytrade:signals (PRO+, user-scoped), price_alert:events (PRO+, user-scoped), sniper:deploys (ULTRA/BUSINESS/ENTERPRISE), token:graduations (PRO+ — every pump.fun graduation, tracked deployer or not), token:prices (PRO+, mint-scoped: filters.mints is required and capped per connection at 25 PRO / 100 ULTRA / 250 BUSINESS — a subscribe above the cap is rejected, never truncated; ~250 ms coalesced ticks, event token:price), token:locks (PRO+ — new Streamflow / Jupiter Lock / Bonfida lock or vesting contracts), token:fee_claims (PRO+ — pump.fun creator-fee distributions, social-handle claims and config changes), token:surges (PRO+ — token momentum fires: events token:surge for new tokens running vs their launch MC, tiers early/strong/breakout, and token:revival for ≥24 h-dormant tokens confirmed trading again; subscribe filters kinds[], tiers[], launchpads[], exclude_flags[], min_mc_usd/max_mc_usd, deployer_tier[]), and for Robinhood Chain rhc:kol_trades (PRO+), rhc:dex_trades (ULTRA+), rhc:dex_trades_unattributed (ULTRA+ — the additive complement to rhc:dex_trades: trades on a pool where no single side could be classified as the token, either because both sides are recognized quote/equity-quote assets like WETH/USDG, or, rarer, because neither is. Fully present in rhc_trades but omitted from rhc:dex_trades; subscribe to both for full chain coverage. Raw leg data only — token_in/token_out/amount_in/amount_out, no decimals/symbol enrichment, no buy/sell action), rhc:new_tokens (ULTRA+, one event per token the moment its symbol/name/decimals first resolve on chain, before its first trade), rhc:copytrade:signals (PRO+, user-scoped), rhc:price_alert:events (PRO+, user-scoped), rhc:kol:coordination (PRO+, user-scoped), rhc:kol:first_touches (PRO+, broadcast — ULTRA is required for the first-touch subscription endpoint, not for this channel) , rhc:token_locks (PRO+, broadcast: one event per token lock or vesting contract created on chain; withdrawals are not tracked) and rhc:token_prices (PRO+, address-scoped: filters.addresses is required and capped per connection at 25 PRO / 100 ULTRA / 250 BUSINESS (counted across named subscriptions), rejected above the cap, never truncated; event rhc:token_price: one snapshot per address on subscribe (snapshot: true, from the current price row), then ticks derived from the RHC trade feed at most once per address per 250 ms with the latest value, each carrying quality fresh | stale | unreliable and quality_reason (no_price, dust_trade, curve_live, state_quote_fallback, low_liquidity, price_age, mc_ceiling), price_source, source_pool, liquidity_usd, observed_at, price_age_ms; a stale or unreliable price is never delivered as fresh; state stream with no seq/id, a resume gets a fresh snapshot; under a named subscription every frame carries sub_id), rhc:lp_events (ULTRA+, RHC liquidity events: add / remove / pool_created on tracked Uniswap v2/v3/v4 pools with in_range + share + material; filters addresses, pools, dexes, actions, material_only, min_share), and the Phase 4 scoped channels, all PRO+ and all capped per connection at 25 PRO / 100 ULTRA / 250 BUSINESS tokens or wallets, each channel its own budget, rejected above the cap and never truncated: token:candles (mint-scoped, filters.mints required: live 1-minute candles, the stored row when the minute closes, optional in-progress minute with filters.updates: true), rhc:token_candles (address-scoped, filters.addresses required: the same for Robinhood Chain, plus revisions of a rewritten candle), token:risk (mint-scoped: risk-INPUT changes, mint or freeze authority revoked, transfer fee changed, first supply-inflation crossing; not a risk band or score stream), rhc:token_risk (address-scoped: a stored RHC risk verdict that changed at a sweep recheck), wallet:scores (wallet-scoped, filters.wallets required: deployer tier changes and KOL categorical state changes) and rhc:wallet_scores (wallet-scoped, filters.wallets with 0x addresses: RHC deployer tier changes). rhc:dev_activity (ULTRA+): the recorded developer of an RHC token buying or selling it (dev_buy / dev_sell only, never transfers; filters types / addresses / deployers / action; ring replay only); live since 2026-10-05. token:bundles / rhc:token_bundles (PRO+, broadcast): one token:bundle_cluster / rhc:token_bundle_cluster per NEW coordinated buy cluster on any token, launch or later (3+ distinct buyers in one slot / block, 3+ of them new to the token); durable resume; optional filters mints / addresses, min_wallets, min_new_wallets, phases, atomic_only. For the share of supply these wallets hold now, call /tokens/{mint}/bundle-share / /rhc/tokens/{address}/bundle-share. rhc:dex_trade / rhc:dex_trade_unattributed payloads carry additive enrichment since 2026-09-22: amount_in_raw / amount_out_raw as exact decimal strings (never floats), token + quote {address, symbol, decimals} (token_in_meta / token_out_meta on the unattributed channel), metadata_status known | pending | unknown, price_status fresh | stale | none with price_source and price_observed_at, mc_status (why mc_usd is null), side buy | sell only when a canonical quote defines the token, else null with side_reason; a KOL trade appears on both rhc:kol_trades and rhc:dex_trades with the same tx_hash + log_index and different ids. 33 channels total (17 Solana + 16 Robinhood Chain; the registry is services/shared/stream-channels.mjs and the connected frame echoes it). rhc:trades is accepted as a deprecated alias of rhc:dex_trades (older RHC SDKs sent it) — new code should subscribe to rhc:dex_trades. RHC channels ride the SAME socket and the same connection limit as the Solana channels — no second connection needed — but carry EVM-native fields (lowercase 0x addresses, eth_amount, tx_hash, block_number), so do not reuse a Solana deserializer unchanged.
Liquidity and lock lifecycle (Phase 3, 2026-09-23). rhc:lp_events delivers event rhc:lp_event (id rhc:lp_event:<tx_hash>:<log_index>) with action add | remove | pool_created, raw amount0/amount1 strings (null on v4, whose ModifyLiquidity reports no token amounts), the position range, in_range, active_liquidity_delta, active_share / active_share_reason, share_of_reserves (v2) and material (a removal of at least 25 % of reserves or active liquidity). in_range and the active fields are null (pool_state_unknown) when the pool's current tick was not known, never guessed; provider is usually a router or position manager, not the beneficial owner; there is no USD field. Invalid lp filters reject the channel rather than widening it. Durable resume from storage (up to 10,000 rows). On token:locks and rhc:token_locks, filters.lifecycle: true opts a subscription into lifecycle events (without it the channel is unchanged): Solana token:lock_claimed, token:lock_cancelled, token:lock_closed, token:lock_updated (change topup | extended | schedule_changed | recipient_changed), token:unlock_upcoming (next unlock within 24 h) and token:unlock_available (the unlock passed within 30 min: claimable per the schedule, NOT claimed); Robinhood Chain rhc:token_unlock_upcoming / rhc:token_unlock_available only, because RHC claims, extensions and cancels are not observable (withdrawals_tracked: false). Further lifecycle filters: events[], unlock_kinds[] (cliff, period, final, tranche), include_automatic_claims (default false: Streamflow keeper-cranked withdrawals, about 90 % of claim volume, are hidden unless set), and mints (Solana) / addresses (RHC). Ids are <event>:<event_key>, deduplicated durably server-side, and no event is emitted for history (backfills and reconciliation never alert).
Candles, risk inputs and wallet scores (Phase 4, 2026-09-23). All six channels are scoped only (no firehose on any tier). The cap counts the union across every named subscription on the connection, and each channel has its OWN budget: one PRO socket may watch 25 price mints, 25 candle mints and 25 risk mints at once. A missing or empty scope, a malformed entry or a scope above the cap rejects the channel (channels_rejected; codes candles_missing_filter, candles_invalid_scope, candles_over_cap, candles_invalid_filters, risk_scope_missing, risk_scope_invalid, risk_scope_over_cap, risk_invalid_filters, wallet_scores_missing_filter, wallet_scores_invalid_filter, wallet_scores_over_cap), an update that would make it invalid is refused whole (invalid_filters), and a downgrade above the new cap revokes it (channels_revoked). The generic filter keys (action, min_mc_usd, ...) never apply to these events. Their events enter the in-memory replay buffer only while a connected subscription watches the token or wallet, so a resume of a subscription holding them is served from storage; put them in their own named subscription. None of these event types can be registered as a webhook.
Candles. Events candle:closed (Solana) and rhc:candle_closed / rhc:candle_revised (RHC) carry the stored 1-minute row (bucket_start, bucket_end, closed_at, open/high/low/close price and MC, liquidity, supply, volumes, trades, buy/sell counts, dex, pool_address, final: true, source; RHC adds revision / revised_at). Id candle:<chain>:<token>:<bucket_start epoch s>, a revision appends :r<n>. With filters.updates: true the channel also sends candle:update / rhc:candle_update: the producer's in-progress minute, at most one per token per second, a state stream with no seq/id that is never replayed (replay_end.channels.<ch>.updates says mode: none, gap: state_stream), and with no snapshot of the open candle on subscribe (the first update comes with the next price move). A fully flat candle with no trades (o = h = l = c, 0 trades) is not emitted live and not replayed. Solana has no revised event: a rare merged rewrite of a minute is published under the same id as the first close. RHC replays only the latest revision of a candle. Frames arrive up to about 25 s after the minute ends. Durable resume from the candle tables for the subscription's tokens only, up to 10,000 rows.
Risk inputs. token:risk events risk:authority_changed (field mint_authority | freeze_authority | transfer_fee, before / after, observed_at, previous_observed_at) and risk:supply_inflated (level warn | danger at 0.5 % / 5 %, first crossing per level); rhc:token_risk event rhc:risk_verdict_changed (changed, before / after verdict, flags_added / flags_removed, checked_at, previous_checked_at, detection). These are changes of the risk INPUTS, never of the risk band or score, which moves with price and is not streamed (use GET /tokens/{mint}/risk). slot is null on the Solana events. The RHC score is higher = safer, the opposite of the Solana risk_score. RHC detection lag is NOT bounded to 24 h: the change happened somewhere in (previous_checked_at, checked_at], currently days to weeks apart (a full recheck cycle takes about 24 days, and only tokens with at least $500 liquidity are rechecked). On subscribe, on an update that changes the scope and on a resume, the server sends one state frame per token, risk:inputs / rhc:risk_verdict with snapshot: true and no id or seq; opt out with filters.risk_snapshot: false. Optional filters.risk_events narrows the event types. Ids <event>:<event_key>; durable resume from the event logs.
Wallet scores. wallet:scores events deployer:tier_changed (tier_before, tier_after, entered_ranking, stats) and kol:score_state_changed (only is_cold, is_heating_up, auto_strategy_tag); rhc:wallet_scores event rhc:deployer_tier_changed. filters.wallets is the scope key on BOTH chains (base58 wallets for Solana, 0x deployers for RHC; not filters.addresses, which means token addresses); optional filters.score_events. Every frame says when the value was RECOMPUTED, not when it changed: source live_write (a bond or deploy reclassified the deployer, real time), scheduled_recompute (the 6-hourly stats worker, the underlying stats can be hours older) or matview_refresh (the 10-minute KOL / 5-minute RHC recompute), with computed_at meaning that write, that run or that refresh. A Solana deployer entering a ranked tier from unranked counts as a tier change (tier_before: unranked, entered_ranking: true). No numeric or percentile score stream and no unscoped stream. Ids <event>:<event_key>; durable resume from the event logs.
User-scoped channels deliver only your own rule fires; the rest are broadcast.
Event ids, sequence numbers, resume and replay (Phase 1, 2026-09-22). Event frames carry seq and id; token:price ticks carry neither. seq is ONE ordinal per server process, shared by every channel and every customer and assigned before your filters run, so the seq values you receive have gaps in normal operation: a gap is not evidence of loss. id is stable: the same event has the same id live, re-sent from memory, rebuilt from storage or recovered after a bus reconnect, so delivery is at-least-once; dedupe by id. The connected frame carries the current seq, an instance id and the replay window.
Resume. Keep a cursor {instance, seq, ts} of the last frame your handler finished processing (it returned, or its promise settled) and reconnect with {type:subscribe, channels, resume:{instance, seq, ts}}. The subscribed ack echoes resume with accepted: true (accepted: false, reason: replay_in_progress plus a warning frame with that code when a replay is already running on the connection; its channels are still subscribed). Same instance and the cursor still inside the in-memory buffer (last 20,000 frames, about 6-8 minutes): frames are re-sent from memory (mode: ring, with their seq). Otherwise (server restart = instance_changed, or a longer absence = ring_truncated) each channel is rebuilt from storage from the cursor's ts (minus 60 s slack, deduped by id), bounded to 60 minutes, 2,000 rows per channel and 20,000 rows in total (rhc:dex_trades / rhc:dex_trades_unattributed: 10 minutes back and up to 10,000 rows, which can include trades that were never pushed live). Rebuilt frames carry seq: null, mode: durable, replayed: true, and partial: true + missing: [...] where a field is not stored. token:graduations and rhc:new_tokens have no stored source: whatever the memory buffer holds is re-sent, then the channel reports gap not_reconstructable. token:prices gets the current price row per watched mint once (snapshot: true, partial: true). A resume or legacy replay is ALWAYS bracketed by replay_start and replay_end, even with nothing to replay. replay_end carries sent, matched, complete, reason, retryable, retry_after_ms?, resume_ts_hint?, incomplete_channels?, last_seq, last_ts (maxima over frames sent; last_seq never above live_from_seq - 1), live_from_seq, mode, resume_reason, count (legacy, equals matched) and channels: {<channel>: {mode, sent, matched, complete, reason?, gap?, time_basis?, max_rows?, retryable?, retry_after_ms?, late_ingest_possible?, partial?, missing?, approximate?, since_ts?, truncated_at_ts?, gap_before_ts?}}; reasons are backpressure, instance_changed, ring_truncated, window_exceeded, row_cap, source_busy, source_error, late_ingest_possible, not_reconstructable.
Commit rule. Replayed frames do not move your saved cursor (a storage replay is sent channel by channel, not in one time order). On complete: true, commit {instance, seq: last_seq, ts: last_ts} (keep your previous value where one is null) and let live frames advance it again. On complete: false, retryable: true, keep the cursor you resumed from and resume again after retry_after_ms (from ts: resume_ts_hint ONLY when the reply carries one — the server emits it when every incomplete RETRYABLE channel was cut by row_cap (a channel with a FINAL gap, such as not_reconstructable on token:graduations or rhc:new_tokens, does not block the hint, because retrying could not fetch anything behind it); with a mixed retryable set (say one row_cap and one source_error channel) there is no hint and you resume from your pre-resume cursor, because the hint would step past the failed channel's unread range. Per-channel truncated_at_ts is always present for a client that tracks channels separately); duplicates are dropped by id. On complete: false, retryable: false the reported gaps are final (not_reconstructable, window_exceeded, ring_truncated, instance_changed): backfill them from REST, then commit as for complete. SDK behaviour: on a retryable incomplete recovery our SDKs resume again on the same socket after retry_after_ms (up to maxResumeRetries, default 5), using resume_ts_hint only when every incomplete retryable channel is row_cap. The budget is per connection and its counter resets on every (re)connect. When that budget is spent, the gap event says exhausted: true: the client stops asking on that connection, the committed cursor stays where it is, and the next reconnect resumes again. On a FINAL gap they emit a gap event first (channels, reason, permanent, the server's bounds and the RANGE that may be incomplete as skipped.from -> skipped.to) with advancedPastGap:true and source:"auto", then move the cursor past it. A count of missing events is never given because it cannot be known. That advance is the client library's own decision, not your approval: set onUnrecoverableGap:"stop" to keep the cursor instead, which stops the stream and emits fatal with the same gap, and acceptGap() then re-reports it with source:"manual" and continues. WATERMARK AND HOLDBACK: a row's id or timestamp is assigned when it is written, not when it commits, so a row with a lower id can still appear after a read passed it. The server therefore advances the committable watermark only across rows whose writing transaction had already finished when the read took its snapshot (xmin_blocked:true when it stops early), holds it 5 s behind now (holdback_ms) and re-scans the last 60 s (rescan_ms) on the next resume; a stuck long transaction cannot freeze it forever (capped at maxHoldbackMs, reported as holdback_capped_ms). While the cap is in force the channel reports holdback_ms: 300000 (the nominal 5000 stays as nominal_holdback_ms) and its guarantee states the real bound and says CAPPED: about 6 minutes (5 min + the 60 s re-scan), not the usual 65 s. Frames are still delivered as they are read; the holdback only limits what you may commit, and last_ts is held to the tightest safe_ts (per channel in replay_end.channels.<ch>.watermark {scheme, safe_ts, holdback_ms, nominal_holdback_ms?, holdback_capped_ms?, rescan_ms, xmin_blocked, gate_ms?, guarantee}, overall in replay_end.watermark_safe_ts). RECOVERY IS BOUNDED, NOT A COMPLETENESS GUARANTEE: with the id and ingest-time schemes a write whose transaction commits more than about 65 s after its id or timestamp was taken can be missed (about 6 minutes instead while holdback_capped_ms is set), and for rhc:dex_trades / rhc:dex_trades_unattributed (scheme producer_gate) recovery is bounded by the producer's 600 s freshness rule, so a trade already older than 600 s when it was ingested is not recoverable through this path at all. Each channel states its own bound in watermark.guarantee. Late writes: every storage read pages by an ingest-time column, a storage id or the producer's own freshness window, so a row written late is not skipped; a channel that could not rule that out would report complete: false, reason: late_ingest_possible, never complete: true. Load: recovery is limited server-wide; a resume waits up to 20 s for a slot, otherwise its storage-backed channels report source_busy with retry_after_ms (memory frames are still sent). A second replay request while one is still running gets only a replay_in_progress warning, no second replay_start / replay_end. Live frames that arrive during a replay are held (up to 5,000 frames or 4 MB per socket, counting each subscription's copy of a frame, so N overlapping named subscriptions use N slots per event; price ticks coalesced per mint and subscription) and delivered after replay_end, skipping ids already sent. A socket that cannot drain during a replay gets complete: false, reason: backpressure and a 4008 close; reconnect and resume. Frames can only be lost across a disconnect or a 4008 close, and the resume reply states what was and was not recovered. Legacy: replay_since_seq / replay_since_ts still work, from memory only; replay_start now reports replay_truncated: true with reason instance_changed or ring_truncated instead of replaying nothing silently. If the server's own event-bus connection drops, missed events are re-read from storage (up to 60 minutes, 5,000 rows per channel, 10,000 for the RHC DEX trade channels) and delivered with replayed: true, recovered: bus.
Entitlements. Every frame is checked against your current plan; after a downgrade, channels above it are dropped with a warning frame code: channels_revoked (also a token:prices mint set above the new cap). Revoking or rotating your API key deletes the stream token: open sockets close with 4001 within about a minute; mint a new one with the new key. Heartbeat pings every 15 s. Connection slots: PRO 2 · ULTRA 5 · BUSINESS 15 (see /stream/sessions to list or evict a slot).
Named subscriptions (Phase 2). A subscribe without sub_id is the connection's implicit default subscription: one channel set and one filter object for the whole socket (action: "buy" on it also drops deployer:alerts, whose payload has no action), exactly as before. A subscribe carrying sub_id (1-64 characters of A-Z a-z 0-9 _ . -) opens or extends an independent subscription with its OWN channels and filters: {type:subscribe, sub_id, channels, filters} is acked subscribed {sub_id, channels, filters, seq, instance, resume?}, {type:update, sub_id, filters} REPLACES that subscription's filters (updated {sub_id, filters}), {type:unsubscribe, sub_id} removes it (unsubscribed {sub_id}; with channels it drops only those; a bare {type:unsubscribe} still clears every subscription and now acks unsubscribed {channels: [], all: true}), and {type:list} answers subscriptions {list:[{sub_id, channels, filters}], count, max}. Every frame delivered under a named subscription carries sub_id; the default subscription's frames carry none. An event that matches several subscriptions is delivered once per matching subscription, each copy stamped with its sub_id, so dedupe by (sub_id, id). Filters of one subscription never affect another; tier gates, user scoping and revocation apply per subscription (channels_revoked names the sub_id). Refusals are warning frames with the sub_id and one of invalid_sub_id, too_many_subscriptions, unknown_sub_id, invalid_filters, channels_rejected, replay_in_progress. The cap counts every subscription on the connection, the default one included: PRO 5 · ULTRA 10 · BUSINESS 20 (named_subscriptions.max_per_connection in this response). token:prices mints are capped per CONNECTION across all subscriptions. Resume is per subscription: a subscribe with sub_id and resume replays only that subscription's channels through its own filters (its replay_start / replay_end carry the sub_id); a client holding several subscriptions re-sends each of them with the SAME cursor on reconnect, the server serves the replays one after another (resume.queued: true on the acks that wait; a repeat request for a sub_id that already has one running or queued is refused replay_in_progress) and holds live frames until the last replay_end; commit the cursor only after the last one, at the smallest last_seq / last_ts across them. The cursor itself stays per connection (seq / ts are global).
Request
- Base URL: https://madeonsol.com/api/v1
- URL: https://madeonsol.com/api/v1/stream/token
- Auth: HTTP bearer
Response
OK — { token, ws_url, expires_at: null, next_refresh_at: null, rotated, lifetime, dex_ws_url?, channels[], token_prices: { ... }, named_subscriptions: { ... } }
Changes
No recorded changes to this operation across all 1 revision of this API.