Raw Yellowstone gRPC vs a decoded WebSocket
Yellowstone (Geyser) gRPC gives you raw Solana transactions as fast as a node sees them. Turning those into "this wallet bought this token on this pool for this much" is a separate job. This guide lists what that job involves, and when doing it yourself makes more sense than using a decoded feed like ours.
What decoding involves
Our origin subscribes to Yellowstone gRPC for the programs we cover and does the following for every transaction. If you parse raw gRPC yourself, this is your list:
- Per-program decoders. Each venue logs trades differently: Anchor
emit_cpievents, instruction data, or balance changes. Twelve venues means twelve decoders, and each program's event layout can change when it is upgraded. - Inner instructions. Many trades arrive through routers and aggregators, so the swap is an inner
instruction. We report the outer program as
via. - Duplicates. Some programs emit two events for one swap (Meteora DLMM emits a current event and a legacy twin). You need to publish one.
- Truncated logs. The runtime truncates long logs, so a trade's event can be missing. We fall
back to the pool's balance changes and mark the event
derived: true. - Base and quote. Pools list tokens in arbitrary order. We orient every pool the same way (the
first of USDC, USDT, SOL is the quote) so
is_buyandpricemean the same thing on every venue. - Token-2022. Transfer fees change what a user actually receives, and mint extensions matter for risk. We read each mint and attach its flags to every event (details).
- Raw units and precision. Amounts exceed JavaScript's safe integer range, so we send them as strings with the decimals alongside.
- Reconnects. gRPC streams drop. Every event carries a sequence number; you can resume from
where you left off, and a
gapmessage tells you when something could not be replayed.
The result: one JSON shape
A PumpSwap trade and a Raydium CLMM trade arrive in the same shape. Here is one swap from our feed:
{
"v": 1,
"seq": 13619,
"kind": "swap",
"venue": "pumpswap",
"keys": [
"7XSrFStYppauv9dvrPBqMXqJarQJo4QFSDsUp7Dmpump",
"So11111111111111111111111111111111111111112",
"6tv1HzMFM93HHPZds7kyWZFuUEgVAnNkAgT1d5Dg7Gw5",
"jMrze5nuUXTTWraMGWDnqzLED5JWvQygLByH86hpwJ1"
],
"stage": "executed",
"source": "geyser",
"commitment": "processed",
"slot": 454291681,
"sig": "3AqTMkaFsKzWjn7BgzFyC2dZZYdS4wksvHB67VYBnt5KmU3GbKK2tfcTKAqrD7PoBqBL3AMJjKt79E3Jm7EaUVKW",
"ix": 0,
"inner": 26,
"t_ingest_us": 1791394580178034,
"t_emit_us": 1791394580178087,
"via": "DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH",
"pool": "6tv1HzMFM93HHPZds7kyWZFuUEgVAnNkAgT1d5Dg7Gw5",
"user": "jMrze5nuUXTTWraMGWDnqzLED5JWvQygLByH86hpwJ1",
"base_mint": "7XSrFStYppauv9dvrPBqMXqJarQJo4QFSDsUp7Dmpump",
"quote_mint": "So11111111111111111111111111111111111111112",
"is_buy": true,
"base_amount": "328124895560",
"quote_amount": "210414078",
"base_decimals": 6,
"quote_decimals": 9,
"price": 6.412621561094693e-07,
"fees": {
"lp": "415839",
"protocol": "103960",
"coin_creator": "1975231",
"cashback": "0",
"buyback": "51980",
"holder_rewards": "1975231"
},
"partial": false,
"derived": false,
"base_flags": {
"program": "token2022",
"decimals": 6,
"supply": "960096786441637",
"mint_authority": null,
"freeze_authority": null,
"metadata_pointer": "7XSrFStYppauv9dvrPBqMXqJarQJo4QFSDsUp7Dmpump",
"extensions": [
18,
19
]
},
"quote_flags": null,
"x": {
"pool_quote_amount_in": "207919048",
"pool_quote_amount_in_with_lp_fee": "208334887",
"pool_base_token_reserves": "167125465510533",
"pool_quote_token_reserves": "88191182513",
"virtual_quote_reserves": "17501328902",
"ix_name": "buy"
}
}
Captured from our live feed on 2026-10-07.
The same fields (pool, user, base_mint, quote_mint,
is_buy, amounts, decimals, price, fees) appear for every venue; venue-specific
extras go in x.
import asyncio, json, os, websockets # pip install websockets
URL = "wss://feed.lunarshadowlabs.com/ws?kinds=swap"
HEADERS = {"Authorization": f"Bearer {os.environ['LSL_KEY']}"}
async def main():
async with websockets.connect(URL, additional_headers=HEADERS, max_size=None) as ws:
async for raw in ws:
m = json.loads(raw)
if m["kind"] == "swap":
print(m["venue"], "buy" if m["is_buy"] else "sell", m["base_mint"], m["price"])
asyncio.run(main())
When to parse raw gRPC yourself
- You need programs we don't decode (see the venue list), or non-DEX activity.
- You need the full transaction or account data, not the decoded event.
- You need
confirmedorfinalizedcommitment. We publish atprocessed, before confirmation. - You already run Yellowstone and have decoders you trust.
When a decoded feed is simpler
- You want DEX trades, launches, migrations, pool creations and liquidity changes, already decoded, across many venues.
- You would rather not maintain decoders as programs upgrade.
- You want to filter server-side: by event kind, venue, mint, pool or wallet. The full stream is about 480 messages per second, about 0.65 MB/s, so filtering cuts most of the traffic.
Latency and uptime as we observe them are published on our status page.
Try the feed
One WebSocket, 12 Solana venues, one flat price.
Start free trial Read the docs
3-day free trial, then $49/month per license plus applicable tax, charged automatically every month until you cancel. Cancel online any time before the trial ends and you pay nothing. Starting a trial means you agree to the Terms. Market data only, no signals or advice.