---
title: "Get your WebSocket stream token"
method: POST
path: "/stream/token"
tags: ["Streaming"]
---

# Get your WebSocket stream token

`POST /stream/token`

## Request

- Base URL: `https://madeonsol.com/api/v1`
- URL: `https://madeonsol.com/api/v1/stream/token`
- Auth: HTTP bearer

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).

## Response `200`

OK — { token, ws_url, expires_at: null, next_refresh_at: null, rotated, lifetime, dex_ws_url?, channels[], token_prices: { ... }, named_subscriptions: { ... } }

- object
  - `token` string
  - `expires_at` string, nullable: Always null — tokens do not expire, kept for wire compatibility
  - `next_refresh_at` string, nullable: Always null
  - `rotated` boolean: true when this call replaced a previous token (rotate:true was sent, or an existing token was minted fresh)
  - `lifetime` string: Human-readable explanation of the non-expiring token lifecycle
  - `ws_url` string, uri
  - `early_ws_url` string, uri: Present only for ULTRA/BUSINESS/ENTERPRISE when ShredPrism is activated. Use the same stream token at this separate early gateway.
  - `early_stream` object: Activation-gated ShredPrism early-stream discovery for ULTRA/BUSINESS/ENTERPRISE. Only implemented channels are advertised. Full wire contract: /docs/shredprism-stream.md and machine-readable channel schemas: /api/v1/asyncapi.json. Subscriptions support wallet/token/label filters, directions and exact raw instruction-amount rules plus opt-in lossless compact-v1 JSON; full JSON remains the default.
    - `channels` string[]
    - `subscribe_example` object
      - `type` 'subscribe'
      - `channels` string[]
    - `execution_status` 'unknown'
    - `coverage` string: Supported outer-instruction scope and intent-only amount semantics.
    - `note` string
  - `dex_ws_url` string, uri: Present for ULTRA/BUSINESS only — the DEX firehose socket (wss://madeonsol.com/ws/v1/dex-stream). Frames: `dex:trades` (one normalized trade per transaction, the signer's net effect; dedupe by tx_signature), in-band `dex:graduations` on mint-scoped subscriptions, and, since 2026-09-22, the opt-in per-pool view: subscribe with `legs: true` (or `"only"`) to receive per transaction one `dex:route` {signature, slot, signer, hop_count, leg_ids[], shape, decoded_complete, undecoded_program_ids[], liquidity_actions[], router_program, signer_net[], trade_mint} and one `dex:leg` per decoded pool swap {leg_id = `<signature>:<instruction_index>[.<inner_index>][#h<k>]` (stable live and on replay; `#h<k>` marks hop k >= 1 of ONE multi-hop instruction, which yields one leg per pool, those legs also carry hop_index and instruction_hop_count), program, venue, pool_model, pool, pool_kind, pool_authority, vault_in, vault_out, token_in/token_out {mint, amount_raw (integer string), decimals, amount_ui, native}, side, amount_basis, is_route_member; a leg decoded from a liquidity instruction whose balances read as a swap, e.g. Meteora DLMM rebalance_liquidity, also carries liquidity_instruction (the instruction name) and liquidity_instruction_kind (add | remove | create | migrate | rebalance): a liquidity position change, not a trade, so exclude it from trade analyses; normal swap legs carry neither key}, followed by the unchanged `dex:trades` frame exactly once (omitted with `legs: "only"`). Liquidity deposits/withdrawals are never a leg (listed on the route). The `subscribed` ack of a legs subscription carries `coverage.decodable_programs`; a route touching a program we do not decode says `decoded_complete: false` and names it. **Liquidity events (2026-09-23):** subscribe with `liquidity: true` (in addition to trades) or `"only"` (liquidity frames only; cannot be combined with `legs`) to receive `dex:liquidity` {id = `<signature>:<ix>[.<inner>]`, action pool_created | add | remove, program, venue, instruction, pool, pool_kind, pool_model, mints[{mint, amount_raw, decimals, side in|out}], lp_mint, owner, signer, amount_basis, reserves_before[] / reserves_after[] (raw vault balances), share_of_reserves (constant-product only), depth_effect, material (a removal of at least 25 % of reserves)}. Filters per subscription: token_mint(s), wallet(s), pool(s), dex, program (one of them is required with `"only"`), actions, min_share_of_reserves, material_only. Only liquidity instructions proven by a real transaction fixture are emitted (listed with `emits_events` in the ack's `liquidity_coverage`); other recognised ones appear on the route as `event: false`. Concentrated pools (CLMM, Whirlpool, DLMM) report share and depth as unknown; Meteora DAMM v1, Raydium LaunchLab and Moonshot are not emitted; there is no USD field and `min_usd` is rejected. Replay from the in-memory ring only; no durable resume (`not_reconstructable`). Full protocol in docs/api.md § DEX Firehose.
  - `usage` string
  - `subscribe_example` object
    - `type` string
    - `channels` string[]
  - `channels` string[]: The full registry of subscribable channel names
  - `token_prices` object: Details for the token:prices channel, the one channel requiring a subscribe filter
    - `subscribe_example` object
    - `mint_cap` integer, nullable: Max mints per connection for the caller's tier
    - `note` string
  - `rhc_token_prices` object: Details for the rhc:token_prices channel (address-scoped Robinhood Chain price ticks; filters.addresses required)
    - `subscribe_example` object
    - `address_cap` integer, nullable: Max addresses per connection for the caller's tier (counted across named subscriptions)
    - `coalesce_ms` integer: At most one tick per address per this many ms
    - `note` string
  - `named_subscriptions` object: Named subscriptions: several independent channel + filter sets on one socket (subscribe with sub_id; update / unsubscribe / list)
    - `subscribe_example` object
    - `max_per_connection` integer, nullable: Max subscriptions per connection for the caller's tier, the default one included (PRO 5, ULTRA 10, BUSINESS 20)
    - `note` string

## Other responses

- `401`: Missing or invalid API key, or an expired free-tier key. Authentication-stage error: no `X-RateLimit-*` headers, no `_rid`.
- `403`: Forbidden. One of: the key's tier is below this endpoint's or parameter's gate (`tier_required` with an `upgrade` block); the public demo key used outside its allow-list (`demo_key_endpoint_not_allowed`); an invalid or revoked key; an origin-restricted key called from a disallowed origin (`code` `origin_not_allowed` / `origin_required`). `X-RateLimit-*` headers are present only on `tier_required`.
- `429`: Too many requests. Every 429 carries `Retry-After`. A quota 429 (`rate_limit_exceeded`, with `X-RateLimit-*`), a heavy-query concurrency 429 (`error_kind` `concurrency`/`capacity`), or the per-IP authentication-failure block (30 failed auths/min; plain `error` + `retry_after_seconds`, no `X-RateLimit-*`).
- `500`: Unexpected server error from the route (`Internal server error`, with `_rid`). Safe to retry with backoff. A failed API-key lookup is a 503 `auth_backend_unavailable`, never a 500.
- `503`: The API-key or subscription lookup could not be answered (authentication stage). Retryable and not your fault: it is never counted as a failed authentication. Honour `Retry-After`; no `X-RateLimit-*`, no `_rid`.

---

[API](https://skmtc.dev/madeonsol/apis/madeonsol-api.md) · [All operations](https://skmtc.dev/madeonsol/apis/madeonsol-api/llms.txt) · [OpenAPI document](https://skmtc.dev/madeonsol/apis/madeonsol-api/revisions/d9531a28d83b?raw)
