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:

MethodTypeDescription
SubscribeStreamBidirectional stream of account, transaction, slot, and block updates
PingUnaryConnection health check
GetLatestBlockhashUnaryLatest blockhash with last valid block height
GetBlockHeightUnaryCurrent block height
GetSlotUnaryCurrent slot
IsBlockhashValidUnaryCheck whether a blockhash is still valid
GetVersionUnaryYellowstone 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 addresses
  • accountRequired — 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

1.

Use persistent channels. FalconQ gRPC connections are persistent with 30s keepalives. Do not create new connections per subscription.

2.

Implement reconnection with backoff. Handle stream errors and reconnect with exponential backoff. Replay from the last known slot to avoid missing data.

3.

Use specific filters. Avoid subscribing to all data. Use accountInclude, owner, and memcmp to narrow your stream.

4.

Gracefully close connections. Call stream.cancel() before exiting to properly drain the connection.

5.

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