gRPC (Yellowstone)
Ultra-low latency streaming via Yellowstone gRPC. Subscribe to real-time Solana events including account changes, transaction confirmations, slot updates, and blocks with advanced filtering capabilities.
Overview
Yellowstone is a high-performance Solana validator plugin that exposes gRPC streaming via its Geyser plugin. FalconQ provides managed Yellowstone gRPC endpoints with persistent channels and automatic keepalives (30s ping). gRPC requires an All Access subscription, which allows up to 3 concurrent streams per account.
Endpoint
grpc.falconq.xyz:50051
Auth Header
x-token
Key Prefix
fq_grpc_live_...
Need earliest detection? Use FalconStream for shred-stage streaming — 15-100ms faster, with the same gRPC client and proto file.
Installation
npm install @grpc/grpc-js @grpc/proto-loader
You will also need the Yellowstone gRPC proto file. Download it from the yellowstone-grpc repository.
Connection Setup
const grpc = require('@grpc/grpc-js')
const protoLoader = require('@grpc/proto-loader')
const packageDefinition = protoLoader.loadSync('geyser.proto', {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
})
const geyser = grpc.loadPackageDefinition(packageDefinition)
const client = new geyser.Geyser(
'grpc.falconq.xyz:50051',
grpc.credentials.createInsecure(),
{
'grpc.keepalive_time_ms': 30000,
'grpc.keepalive_timeout_ms': 10000,
'grpc.keepalive_permit_without_calls': 1,
}
)
const metadata = new grpc.Metadata()
metadata.set('x-token', 'fq_grpc_live_YOUR_KEY')Available Methods
Besides the Subscribe stream, the endpoint exposes the standard Yellowstone unary calls:
| Method | Type | Description |
|---|---|---|
| Subscribe | Stream | Bidirectional stream of account, transaction, slot, and block updates |
| Ping | Unary | Connection health check |
| GetLatestBlockhash | Unary | Latest blockhash with last valid block height |
| GetBlockHeight | Unary | Current block height |
| GetSlot | Unary | Current slot |
| IsBlockhashValid | Unary | Check whether a blockhash is still valid |
| GetVersion | Unary | Yellowstone plugin version |
Subscribe Request Structure
A subscribe request defines filters for the types of updates you want. Each filter type (accounts, transactions, slots, blocks, blocksMeta) is optional. The first field in each filter is a client-assigned label that helps you identify which filter produced each update.
{
"slots": {},
"accounts": {},
"transactions": {},
"blocks": {},
"blocksMeta": {},
"accountsDataSlice": [],
"commitment": "confirmed"
}Account Streaming
Account Updates for a Program
Stream all account updates owned by a specific program. Use the owner field.
const request = {
slots: {},
accounts: {
pumpfun: {
owner: ['6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P'],
filters: [],
},
},
transactions: {},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
commitment: 'processed',
}
const stream = client.Subscribe(request, metadata)
stream.on('data', (update) => {
if (update.account) {
console.log('Account:', update.account.account.pubkey.toString())
console.log('Owner:', update.account.account.owner.toString())
console.log('Lamports:', update.account.account.lamports)
console.log('Slot:', update.account.slot)
}
})
stream.on('error', (err) => {
console.error('Stream error:', err)
})Account Updates for a Specific Address
Stream updates for a single account address using the account field.
const request = {
slots: {},
accounts: {
myAccount: {
account: ['9AnFgHoXFysVcuFFX7QztDmzuH8r5ZFvyP4sYwn1XTj9'],
},
},
transactions: {},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
commitment: 'confirmed',
}
const stream = client.Subscribe(request, metadata)
stream.on('data', (update) => {
if (update.account) {
const acc = update.account.account
console.log('Address:', acc.pubkey.toString())
console.log('Data length:', acc.data.length)
console.log('Lamports:', acc.lamports)
}
})Account Updates with memcmp Filters
Use memcmp filters to match specific byte sequences within account data. Useful for filtering by specific field values.
const request = {
slots: {},
accounts: {
raydiumSerumPools: {
account: [],
owner: ['675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'],
filters: [
{
memcmp: {
offset: '40',
base58: 'srmqPvymJeFKQ4zGQed1GFppgkRHL9kaELCbyksJtPX',
},
},
],
},
},
transactions: {},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
commitment: 'processed',
}
const stream = client.Subscribe(request, metadata)
stream.on('data', (update) => {
if (update.account) {
console.log('Raydium pool (Serum):', update.account.account.pubkey.toString())
}
})Transaction Streaming
Transaction Filter Structure
{
"transactions": {
"label": {
"vote": false,
"failed": false,
"signature": "",
"accountInclude": [],
"accountExclude": [],
"accountRequired": []
}
}
}accountInclude— Stream transactions involving these addresses (logical OR)accountExclude— Exclude transactions involving these addressesaccountRequired— Only include transactions that also involve these addresses (logical AND with accountInclude)vote/failed— Include or exclude vote/failed transactions
All Transactions of an Address
Stream all transactions involving a specific wallet, program, or token address.
const request = {
slots: {},
accounts: {},
transactions: {
walletTracker: {
vote: false,
failed: false,
accountInclude: [
'vines1vzrYbzLMRdu58ou5XTby4qAqVRLmqo36NKPTg'
],
},
},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
commitment: 'confirmed',
}
const stream = client.Subscribe(request, metadata)
stream.on('data', (update) => {
if (update.transaction) {
const tx = update.transaction.transaction
console.log('Signature:', tx.transaction?.signatures[0])
console.log('Slot:', tx.slot)
console.log('Failed:', tx.transaction?.meta?.err !== null)
}
})Transactions of Multiple Addresses
Track transactions across multiple wallets by specifying multiple addresses in accountInclude.
const request = {
slots: {},
accounts: {},
transactions: {
multiWallet: {
vote: false,
failed: false,
accountInclude: [
'HgQy5bqJd3GcjqakukhfMpqAfP62nTxGiqAqh4QtTuHF',
'8pQYy5peKKqKk34BvJBuuBAfakukTLsmT2MVSzijUgt1',
],
},
},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
commitment: 'confirmed',
}Transactions of a Token on a Specific DEX
Use accountRequired to filter token transactions only on a specific DEX (e.g., Raydium V4).
const request = {
slots: {},
accounts: {},
transactions: {
usdcOnRaydium: {
vote: false,
failed: false,
accountInclude: [
'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'
],
accountRequired: [
'675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8'
],
},
},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
commitment: 'confirmed',
}Transactions of a Liquidity Pool
Stream all transactions for a specific liquidity pool by specifying the pool address.
const request = {
slots: {},
accounts: {},
transactions: {
raydiumPool: {
vote: false,
failed: false,
accountInclude: [
'9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin'
],
},
},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
commitment: 'confirmed',
}Slot & Block Streaming
Slot Subscription
Subscribe to slot updates to track the latest confirmed slot in real time.
const request = {
slots: { slotFilter: {} },
accounts: {},
transactions: {},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
commitment: 'confirmed',
}
const stream = client.Subscribe(request, metadata)
stream.on('data', (update) => {
if (update.slot) {
console.log('Slot:', update.slot.slot)
console.log('Parent:', update.slot.parent)
console.log('Status:', update.slot.status)
}
})Block Streaming
Stream full blocks or block metadata as they are confirmed.
const request = {
slots: {},
accounts: {},
transactions: {},
blocks: {
blockFilter: {
accountInclude: [],
includeTransactions: true,
},
},
blocksMeta: {
metaFilter: {},
},
accountsDataSlice: [],
commitment: 'confirmed',
}
const stream = client.Subscribe(request, metadata)
stream.on('data', (update) => {
if (update.block) {
console.log('Block slot:', update.block.slot)
console.log('Transactions:', update.block.transactions?.length)
console.log('Blockhash:', update.block.blockhash)
}
if (update.blockMeta) {
console.log('BlockMeta slot:', update.blockMeta.slot)
console.log('Block time:', update.blockMeta.blockTime?.timestamp)
}
})Combined Subscriptions
Combine multiple filter types in a single subscribe request to receive accounts, transactions, and slot updates simultaneously.
const request = {
slots: { slotFilter: {} },
accounts: {
poolAccounts: {
account: ['9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin'],
},
},
transactions: {
poolTransactions: {
vote: false,
failed: false,
accountInclude: ['9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin'],
},
},
blocks: {},
blocksMeta: {},
accountsDataSlice: [],
commitment: 'confirmed',
}
const stream = client.Subscribe(request, metadata)
stream.on('data', (update) => {
if (update.slot) console.log('[SLOT]', update.slot.slot)
if (update.account) console.log('[ACCOUNT]', update.account.account.pubkey.toString())
if (update.transaction) console.log('[TX]', update.transaction.transaction.transaction?.signatures[0])
})Reconnection & Best Practices
Use persistent channels. FalconQ gRPC connections are persistent with 30s keepalives. Do not create new connections per subscription.
Implement reconnection with backoff. Handle stream errors and reconnect with exponential backoff. Replay from the last known slot to avoid missing data.
Use specific filters. Avoid subscribing to all data. Use accountInclude, owner, and memcmp to narrow your stream.
Gracefully close connections. Call stream.cancel() before exiting to properly drain the connection.
Replay slots on reconnect. When reconnecting, specify the last known slot in your subscribe request to replay any missed data.
let lastSlot = 0
function connectGrpc() {
const stream = client.Subscribe(request, metadata)
stream.on('data', (update) => {
if (update.slot) lastSlot = update.slot.slot
if (update.account) lastSlot = update.account.slot
if (update.transaction) lastSlot = update.transaction.slot
processUpdate(update)
})
stream.on('error', (err) => {
console.error('gRPC error:', err.message)
stream.cancel()
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000)
reconnectAttempts++
console.log('Reconnecting in', delay, 'ms from slot', lastSlot)
setTimeout(connectGrpc, delay)
})
stream.on('end', () => {
console.log('Stream ended, reconnecting...')
setTimeout(connectGrpc, 1000)
})
}
connectGrpc()