Lunar Shadow Labs feed
One WebSocket with every trade, launch, curve completion, migration, pool creation and liquidity change on 12 Solana venues, decoded into one JSON shape with Token-2022 risk flags.
Market data only. No signals, no advice, no profit claims (see Terms).
- Connect
- Filters
- Messages
- swap
- launch · curve_complete · migration
- pool_create · liquidity
- Mint flags
- mint_update
- Control messages, gaps, resume
- Venues
- Limits and performance
Connect
wss://feed.lunarshadowlabs.com/ws
Authorization: Bearer <your license key>
Your API key is your Whop license key: open whop.com/lunarshadow-b705 → Software → Your license keys (it is not on the order page). Browsers can't set WebSocket headers: use
the subprotocols solfeed.v1, key.<your key> instead. Never put the key in the
URL — that is refused. Your key allows 2 simultaneous connections.
Python
import asyncio, json, os, websockets # pip install websockets
async def main():
url = "wss://feed.lunarshadowlabs.com/ws?kinds=swap,launch&venues=pump,pumpswap"
headers = {"Authorization": f"Bearer {os.environ['LSL_KEY']}"}
async with websockets.connect(url, additional_headers=headers, compression=None, max_size=None) as ws:
async for raw in ws:
m = json.loads(raw)
if m["kind"] == "gap":
print("GAP", m["reason"], m.get("missed")) # the only signal that data was missed
elif m["kind"] == "swap":
print(m["venue"], "buy" if m["is_buy"] else "sell", m["base_mint"], m["price"])
asyncio.run(main())
TypeScript (Node 22+)
const ws = new WebSocket("wss://feed.lunarshadowlabs.com/ws?kinds=swap",
["solfeed.v1", `key.${process.env.LSL_KEY}`]);
ws.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.kind === "swap") console.log(m.venue, m.is_buy ? "buy" : "sell", m.base_mint, m.price);
};
Check your key's status any time on your account page.
Filters
The full stream is about 480 messages/second (~0.65 MB/s). Filters run on our side, so you only receive what you ask for:
?kinds=swap,launch,curve_complete,migration,pool_create,liquidity,mint_update?venues=pump,pumpswap,…(ids in Venues)?watch=<mint or pool>,<mint or pool>,…— a watchlist
Or send {"op":"subscribe","kinds":[…],"venues":[…],"mints":[…],"pools":[…]} at any time; it
replaces the current filter. With a filter, skipped seq numbers are normal.
Messages
Every message is one JSON object whose first fields are always v, seq, kind, venue, keys.
- Amounts are strings of raw integer units (they exceed JavaScript's safe integers). Scale with
the
*_decimalsfields.price(quote per whole base token) is the only float, ornullwhen an input is unknown. - Times are Unix microseconds:
t_ingest_us(we received the transaction),t_emit_us(we published the event). slot,sig,ix,innerlocate the event on chain;viais the outer program when the trade came through a router.commitmentisprocessed: events arrive before confirmation, so a rare fork can drop a transaction.
swap
{"v":1,"seq":184467,"kind":"swap","venue":"pumpswap","keys":["<base>","<quote>","<pool>"],
"stage":"executed","source":"geyser","commitment":"processed","slot":371234567,"sig":"5x…",
"ix":3,"inner":1,"t_ingest_us":1791262754000000,"t_emit_us":1791262754000025,"via":null,
"pool":"…","user":"…","base_mint":"…","quote_mint":"So11111111111111111111111111111111111111112",
"is_buy":true,"base_amount":"3500000000000","quote_amount":"125000000",
"base_decimals":6,"quote_decimals":9,"price":0.0000357,
"fees":{"lp":"…","protocol":"…","creator":"…"},"partial":false,"derived":false,
"base_flags":{…}|null,"quote_flags":{…}|null,"x":{…}}
- Base/quote: launchpads define them (the launched token is base). For AMMs the quote is the
first of USDC, USDT, SOL present in the pool, else the pool's second token (
x.quote_rule). is_buy= the user received base. Amounts are what the user paid and received.partial: only part of the order filled.derived: the program's logs were truncated by the runtime, so the amounts come from the pool's balance changes (rare).x: venue-specific extras (reserves after the trade, launchpadplatform, fee mode …). Additive only.
launch · curve_complete · migration
launch: mint, curve, creator, user,
name, symbol, uri, quote_mint, token_program,
flags. curve_complete: mint, curve, user,
quote_mint. migration: mint, from_curve,
to_venue, to_pool, base_amount, quote_amount (what the new
pool received), quote_mint.
pool_create · liquidity
pool_create: pool, creator, base/quote mints and decimals, initial
amounts (null when the venue creates pools empty), lp_mint, flags.
liquidity: pool, user, add, mints, amounts,
lp_amount.
Mint flags (base_flags / quote_flags)
null = not known yet — never a guess. Otherwise always program
(spl | token2022), decimals, supply,
mint_authority, freeze_authority (pubkey, or null = revoked), plus — only
when the mint carries it — transfer_fee (older/newer with bps,
max, epoch), transfer_hook, permanent_delegate,
paused, default_frozen, non_transferable,
interest_rate_bps, scaled_ui, close_authority, confidential,
metadata_pointer, permissioned_burn, and extensions (every Token-2022
extension type number present).
A permanent delegate can move anyone's tokens; a freeze authority can freeze them; a transfer fee or hook changes what a transfer delivers; a paused mint cannot move. Quote mints can carry these too.
mint_update
{"kind":"mint_update","venue":"token2022","mint":"…",
"changes":[{"field":"paused","old":false,"new":true}]}
A Token-2022 mint changed one of the fields above (not supply). sig may be null.
Control messages, gaps, resume
hello(server,epoch,build_id) on connect;heartbeatevery few seconds.gap:{"kind":"gap","seq":n,"missed":[a,b]|null,"reason":…}— the only signal that data was missed. Reasons:slow_client(you fell too far behind and we skipped ahead),resume_expired,source_reconnect(our upstream stream reconnected;missed: null),epoch_changed.- Resume: reconnect with
?since=<epoch>:<last seq>and you receive everything still in our buffer since then, or a gap saying what isn't.
Venues
| venue id | program |
|---|---|
pump | pump.fun bonding curve |
pumpswap | PumpSwap AMM |
launchlab | Raydium LaunchLab (StonkFun, bonk.fun, …) — x.platform names the launchpad |
meteora_dbc | Meteora Dynamic Bonding Curve (Believe, Bags, Jupiter Studio, …) — x.config identifies the launchpad |
raydium_v4, raydium_cpmm, raydium_clmm | Raydium AMM v4, CPMM, CLMM |
pancake_v3 | PancakeSwap v3 (Solana) |
orca | Orca Whirlpool |
meteora_dlmm, meteora_damm_v1, meteora_damm_v2 | Meteora DLMM, Dynamic AMM, DAMM v2 |
token2022 | not a venue: the source of mint_update events |
Limits and performance
- No credits, compute units or per-message charges: one flat price for the whole feed.
- One API key per license, 2 simultaneous connections; need more, buy another license. Sharing a key can get it revoked.
- Decode and publish on our origin: about 0.02–0.04 ms median after we receive a transaction. Live latency and uptime: status page.
- Schema changes are additive; a breaking change bumps
vand is announced first.