Skip to main content

Building Solana Copy Trading Bot with gRPC Streams

A comprehensive guide to building a high-performance Solana trading bot that leverages gRPC streams for real-time copy trading using Bitquery CoreCast.

Table of Contents


Output

The final result of this project would appear as the one given below.

Architecture Overview

This Solana trading bot implements a streaming architecture for copy trading:

┌─────────────────────┐
│ Solana Blockchain │
│ (DEX Trades) │
└──────────┬──────────┘


┌─────────────────────────────────┐
│ Bitquery CoreCast gRPC Stream │ ← Real-time data streaming
│ docs.bitquery.io/docs/grpc/ │
└──────────┬──────────────────────┘


┌─────────────────────┐
│ gRPC Client │ ← @grpc/grpc-js
│ (CoreCast Proto) │
└──────────┬──────────┘


┌─────────────────────┐
│ Event Handler │ ← Trade filtering & strategy
└──────────┬──────────┘


┌─────────────────────┐
│ Jupiter Swap API │ ← Optimal trade execution
└──────────┬──────────┘


┌─────────────────────┐
│ Solana Transaction │ ← On-chain execution
└─────────────────────┘

Learn more about Bitquery CoreCast architecture.


Bitquery CoreCast Integration

Bitquery CoreCast provides real-time blockchain data streaming via gRPC for Solana trading bots.

// index.js - Loading Protocol Buffers
const { loadPackageDefination } = require('bitquery-corecast-proto');
const packageDefinition = loadPackageDefination();
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
const solanaCorecast = protoDescriptor.solana_corecast;

Reference: Protobuf Loading Documentation


Streaming Real-Time Solana DEX Trades

Initializing the gRPC Client

The bot connects to Bitquery's CoreCast server using the given below code snippet:

// index.js - Client Initialization
function initializeClient() {
client = new solanaCorecast.CoreCast(
config.server.address, // corecast.bitquery.io
grpc.credentials.createSsl()
);

metadata = new grpc.Metadata();
metadata.add('authorization', config.server.authorization);
}

Refer to this document for any issues related to authorization.

Creating the Stream

Multiple Solana stream topics are available:

// index.js - Stream Creation
function startStream() {
const request = createRequest();
let stream;

switch (config.stream.type) {
case 'dex_trades': // Real-time DEX trades
stream = client.DexTrades(request, metadata);
break;
case 'dex_orders': // Order book updates
stream = client.DexOrders(request, metadata);
break;
case 'transactions': // All transactions
stream = client.Transactions(request, metadata);
break;
// ... more stream types
}
}

Handling Stream Events

The bot processes incoming trade messages in real-time:

// index.js - Event Handler
stream.on('data', async (message) => {
if (message.Trade) {
// Extract trade data from protobuf message
const marketAddress = toBase58(message.Trade.Market?.MarketAddress);
const inputMint = toBase58(message.Trade.Buy?.Currency?.MintAddress);
const outputMint = toBase58(message.Trade.Sell?.Currency?.MintAddress);
const buyAmount = message.Trade.Buy?.Amount;

// Apply trading strategy
if (approveTrade(buyAmount)) {
await executeTrades({ inputMint, outputMint, marketAddress, buyAmount });
}
}
});

stream.on('error', (error) => {
console.error('Stream error:', error);
});

stream.on('end', () => {
console.log('Stream ended');
});

Trade Execution with Jupiter API

Native SOL Conversion

Jupiter requires wrapped SOL (wSOL) instead of native SOL for trading. Our bot handles this conversion:

// trade.js - SOL Mint Conversion
const NATIVE_SOL_MINT = '11111111111111111111111111111111';
const WRAPPED_SOL_MINT = 'So11111111111111111111111111111111111111112';

const convertedInputMint = inputMint === NATIVE_SOL_MINT
? WRAPPED_SOL_MINT
: inputMint;

Fetching Swap Quotes

We use Jupiter's aggregation API to find optimal swap routes:

// trade.js - Quote Fetching
const jupiter = createJupiterApiClient({
basePath: 'https://quote-api.jup.ag/v6'
});

const quoteReq = {
inputMint: convertedInputMint,
outputMint: convertedOutputMint,
amount: amountInRaw,
slippageBps: slippageBps.toString(),
onlyDirectRoutes: false // Allow indirect routes for better liquidity
};

const quote = await jupiter.quoteGet(quoteReq);

Creating and Sending Transactions

Once we have a quote, we build and execute the swap transaction:

// trade.js - Transaction Execution
const swapReq = {
quoteResponse: quote,
userPublicKey: wallet.publicKey.toString(),
wrapAndUnwrapSOL: true, // Handle SOL wrapping automatically
asLegacyTransaction: true
};

const swapRes = await jupiter.swapPost({ swapRequest: swapReq });

// Deserialize and sign transaction
const txBuf = Buffer.from(swapRes.swapTransaction, 'base64');
const tx = Transaction.from(txBuf);
tx.sign([wallet]);

// Send to Solana network
const txSig = await connection.sendRawTransaction(
tx.serialize(),
{ skipPreflight: true, maxRetries: 3 }
);

// Wait for confirmation
await connection.confirmTransaction(txSig, 'confirmed');

Code Walkthrough

Helper Functions

Base58 Encoding

Solana addresses are encoded in base58. We convert byte arrays to base58 strings:

// index.js - Base58 Conversion
function toBase58(bytes) {
if (!bytes || bytes.length === 0) return 'undefined';
try {
return bs58.encode(bytes);
} catch (error) {
return 'invalid_address';
}
}

Configuration Management

The bot supports hot-reloading of configuration without restart:

// index.js - Config Watching
fs.watch('./config.yaml', (eventType, filename) => {
if (eventType === 'change') {
clearTimeout(watchTimeout);
watchTimeout = setTimeout(() => {
reloadAndRestart();
}, 300); // Debounce rapid changes
}
});

Trading Strategy

Implement your copy trading logic in the approveTrade() function:

// index.js - Trade Approval Logic
function approveTrade(buyAmount) {
// Example: Only approve large trades
if (buyAmount > 100 * 1000000000) {
console.log('Approving large trade:', buyAmount);
return true;
}
return false;
}

Strategy Ideas:

  • Volume-based filtering
  • Token whitelist/blacklist
  • Risk management (max position size)
  • Cooldown periods
  • Multi-signal confirmation

Configuration & Filtering

Stream Configuration

Configure which on-chain activity to monitor:

# config.yaml
stream:
type: "dex_trades" # Real-time DEX trades

Available Stream Types:

Filters

Use filtering options to target specific trades:

# config.yaml
filters:
traders: # Copy trades from specific addresses
- "HV1KXxWFaSeriyFvXyx48FqG9BoFbfinB8njCJonqP7K"

programs: # Filter by DEX programs
- "..."

pool: # Filter by liquidity pools
- "..."

signers: # Filter by transaction signers
- "..."
// index.js - Request Builder
function createRequest() {
const request = {};

if (config.filters.traders?.length > 0) {
request.trader = { addresses: config.filters.traders };
}

if (config.filters.programs?.length > 0) {
request.program = { addresses: config.filters.programs };
}

// ... more filter types

return request;
}

Best Practices

1. Error Handling

Implement comprehensive error handling for network failures:

// trade.js - Error Handling
try {
const quote = await jupiter.quoteGet(quoteReq);
} catch (error) {
console.error('Jupiter API error:', error.response?.data || error.message);
// Implement retry logic or fallback
}

2. Rate Limiting

To avoid being rate-limited by Jupiter:

// Add delays between trades
await new Promise(resolve => setTimeout(resolve, 1000));

3. Monitoring

Log all trade executions for analysis:

console.log('✅ Copy trade executed!', {
tx: txSig,
inputMint,
outputMint,
amount: buyAmount
});

4. Security

  • Keep API keys in secrets.json (never commit)
  • Use separate trading wallet
  • Set maximum trade amounts
  • Implement stop-loss mechanisms

5. Testing

Start with small amounts:

// Reduce trade amount for testing
amountInRaw: (buyAmount / 100).toString() // 1% of original

Additional Resources

Bitquery Documentation

External APIs

Get Started

Sign up for Bitquery CoreCast and start building your Solana copy trading bot today!