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.

Copy Bot Demo

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 authorisation.

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!