A token dashboard usually starts with a timer: every few seconds, fetch everything again. That works for one token on a quiet afternoon. It stops working when a user opens six panels on a busy launch, because every refresh re-reads risk, locks, flow and buyer data that did not change, spends quota, and still shows the price late.
This guide builds the other shape. The panel reads the token once, then listens to WebSocket channels that already exist and re-reads only the module an event made stale. The price moves as a live overlay without any REST call at all. Everything below runs against the MadeOnSol API with the terminal helper watchTokenIntelligence(), released in madeonsol-x402 4.3.0 on 2026-10-10.
If you want the architecture first, the trading terminal solution page has a diagram of the same flow.
The two building blocks
1. The include-scoped intelligence read. GET /api/v1/tokens/{mint}/intelligence?include=... returns only the modules you name. There is no default: you list the panels you render. Each module costs budget, and one read may name at most 5 modules with a total cost of 8:
| Module | Cost | What it is |
|---|
snapshot | 2 | Price and market snapshot |
risk | 2 | Risk inputs for the token |
buyer_quality | 2 | Who bought, scored |
flow | 3 | Buy and sell flow |
top_traders | 2 | Largest traders |
kol | 1 | Tracked KOL entry order |
locks | 1 | Token locks and vesting |
holder_count | 1 | Last complete holder census, timestamped |
holders | 3 | Excluded on Solana in this release; use /api/v1/tokens/{mint}/holders |
Every module comes back with its own status (ready, partial_history, unverified, unavailable or timeout), a machine reason, the source's own as_of and provenance. One failed module never zeroes another. Each read counts as one API call and needs a Pro or higher API key.
2. The main stream. wss://madeonsol.com/ws/v1/stream carries 31 channels (16 Solana, 15 Robinhood Chain). The ones a token panel cares about are token:prices, token:risk, token:locks, token:candles and, if you opt in, kol:trades. There is no token:intelligence channel. The helper combines the existing ones.
What triggers a re-read
The helper maps each module to the events that can actually change it:
snapshot: token:prices ticks. These are rendered as a separate live overlay and never trigger a REST read.
risk: token:risk input changes (risk:inputs, risk:authority_changed, risk:supply_inflated). This is a change in inputs, not a continuously recomputed score.
locks: token:locks lifecycle events (new lock, claimed, cancelled, closed, updated, unlock upcoming, unlock available). Streamflow keeper automatic claims are not requested.
flow, buyer_quality, top_traders: a closed one-minute candle on token:candles. That is a coarse trigger, not a trade tape.
kol: kol:trades, only when you pass includeKolBroadcast: true. That channel is a roster broadcast the server does not filter by mint, so it costs bandwidth on every panel that enables it.
holder_count: nothing. It is snapshot only, by design.
Invalidations are debounced (750 ms by default), each module has a minimum refresh interval (15 seconds by default, configurable from 1 second to 10 minutes), a refresh never exceeds 5 modules or cost 8, and a failed read is not retried automatically: the next event or your own refresh() call tries again. When the stream is quiet, the helper makes zero background reads.
Step 1: one panel in Node
Install the package and run this with a Pro key. The key stays on your server.
npm install [email protected]
MADEONSOL_API_KEY=msk_... SOLANA_MINT=<mint> node panel.mjs
// panel.mjs (Node 22+)
import { MadeOnSolREST } from "madeonsol-x402";
const client = new MadeOnSolREST({ apiKey: process.env.MADEONSOL_API_KEY });
const stream = client.stream(); // ONE shared socket for every panel
const view = client.watchTokenIntelligence(process.env.SOLANA_MINT, {
stream,
subId: "panel_main", // unique per panel on this socket
tier: "PRO", // your plan; the server stays authoritative
include: ["snapshot", "risk", "holder_count", "locks"], // cost 6 of 8
onChange(v) {
console.log("phase", v.phase, "stale", v.stale, "no_push", v.no_push, "incomplete", v.incomplete);
for (const [name, m] of Object.entries(v.snapshot?.modules ?? {})) {
console.log(" ", name, m.status, m.reason ?? "", m.as_of ?? "");
}
},
onLive(frame, module) {
if (module === "snapshot") console.log("price overlay", frame.data);
},
});
process.once("SIGINT", () => { view.dispose(); stream.close(); });
What happens in order: the helper sends a named subscribe on the shared socket, waits for the server ACK that names panel_main and every requested channel, and only then makes the first REST read. If the ACK is partial or refused (for example a channel your plan does not include), the view goes to phase: "degraded" with incomplete: true instead of pretending to be live.
view.no_push lists the modules that have no push source, here holder_count. Show them with their as_of so the user knows how old they are.
Step 2: several panels, one socket
Each panel is one named subscription on the same stream. Pro allows 5 named subscriptions per connection, Ultra 10 and Business 20. The price, candle and risk channels also cap how many mints one connection may watch (25 on Pro, 100 on Ultra, 250 on Business).
const panels = new Map();
function openPanel(mint) {
const subId = "panel_" + mint.slice(0, 8);
panels.set(mint, client.watchTokenIntelligence(mint, {
stream, subId, tier: "PRO",
include: ["snapshot", "risk", "flow"], // cost 7
onChange: (v) => render(mint, v),
}));
}
function closePanel(mint) {
panels.get(mint)?.dispose(); // unsubscribes this panel only
panels.delete(mint);
if (panels.size === 0) stream.close(); // last panel closes the socket
}
A subId that is already in use on the stream is refused, and so is default. That stops one widget from silently taking over another widget's filters.