Building Real-Time Solana Apps with Helius Webhooks
**Category:** Development Tutorial | **Audience:** Intermediate Developers
**Reading Time:** 14 minutes | **Author:** Builderz Team
**Verified Packages:** helius-sdk@2.0.5, @solana/web3.js@1.98.4
What You'll Learn
Helius webhooks enable real-time notifications for on-chain Solana events, eliminating the need for constant polling. This tutorial teaches you to create, configure, and handle webhooks for transaction monitoring, NFT tracking, and DeFi event detection. By the end, you'll have a production-ready webhook system processing blockchain events in real-time.
Key Takeaways:
- Set up enhanced and raw webhooks for different use cases
- Monitor up to 100,000 addresses per webhook
- Handle webhook payloads securely with authentication
- Build notification systems for transfers, swaps, and NFT events
- Implement retry logic and error handling
TL;DR
Helius webhooks deliver Solana events to your server in real-time:
- **Enhanced Webhooks**: Parsed, human-readable transaction data
- **Raw Webhooks**: Full transaction details for custom parsing
- **Discord Webhooks**: Direct Discord channel notifications
- **Scale**: Up to 100,000 addresses per webhook
import { createHelius } from 'helius-sdk';
const helius = createHelius({ apiKey: 'your-api-key' });
// Create webhook in 3 lines
const webhook = await helius.webhooks.create({
webhookURL: 'https://your-server.com/webhook',
transactionTypes: ['TRANSFER', 'SWAP'],
accountAddresses: ['wallet-address-to-monitor'],
webhookType: 'enhanced',
});Prerequisites
Before starting, ensure you have:
- **Helius API Key** from [dashboard.helius.dev](https://dashboard.helius.dev)
- **Node.js 18+** installed
- **Public HTTPS endpoint** for receiving webhooks
- Understanding of **Solana transactions**
- Basic **Express.js** or Next.js knowledge
# Create project and install dependencies
mkdir helius-webhooks-demo && cd helius-webhooks-demo
npm init -y
npm install helius-sdk expressCore Concepts
Webhook Types
Helius offers three webhook types for different use cases:
| Type | Data Format | Best For |
|------|-------------|----------|
| enhanced | Parsed, human-readable | NFTs, Jupiter swaps, SPL transfers |
| raw | Full transaction JSON | Custom parsing, DeFi protocols |
| discord | Formatted message | Team notifications |
Each type is available for both mainnet and devnet (add Devnet suffix for devnet, e.g., enhancedDevnet).
Transaction Types
Over 100 transaction types are supported. Common ones include:
const commonTransactionTypes = [
'TRANSFER', // SOL/token transfers
'SWAP', // Jupiter, Raydium swaps
'NFT_SALE', // Marketplace sales
'NFT_MINT', // New NFT mints
'NFT_LISTING', // Marketplace listings
'STAKE', // Staking operations
'LOAN', // Lending protocol events
'BURN', // Token burns
'CREATE_MERKLE_TREE', // Compressed NFT tree creation
'COMPRESSED_NFT_MINT' // cNFT minting
];**Note**: Enhanced parsing currently works best for NFT, Jupiter, and SPL transactions. For DeFi protocols, use raw webhooks.
Project Setup
Project Structure
helius-webhooks-demo/
├── src/
│ ├── index.ts # Express server
│ ├── webhook-handler.ts # Webhook processing logic
│ ├── webhook-setup.ts # Webhook creation/management
│ └── types.ts # TypeScript interfaces
├── package.json
└── tsconfig.jsonInitial Configuration
// src/types.ts
export interface WebhookPayload {
type: string;
timestamp: string;
data: EnhancedTransaction[];
}
export interface EnhancedTransaction {
signature: string;
type: string;
source: string;
fee: number;
feePayer: string;
slot: number;
timestamp: number;
nativeTransfers?: NativeTransfer[];
tokenTransfers?: TokenTransfer[];
events?: TransactionEvent;
}
export interface NativeTransfer {
fromUserAccount: string;
toUserAccount: string;
amount: number;
}
export interface TokenTransfer {
fromUserAccount: string;
toUserAccount: string;
fromTokenAccount: string;
toTokenAccount: string;
tokenAmount: number;
mint: string;
tokenStandard: string;
}
export interface TransactionEvent {
nft?: NFTEvent;
swap?: SwapEvent;
}
export interface NFTEvent {
type: string;
nfts: Array<{ mint: string; tokenStandard: string }>;
buyer?: string;
seller?: string;
amount?: number;
}
export interface SwapEvent {
nativeInput?: { account: string; amount: number };
nativeOutput?: { account: string; amount: number };
tokenInputs?: TokenIO[];
tokenOutputs?: TokenIO[];
}
export interface TokenIO {
userAccount: string;
tokenAccount: string;
mint: string;
rawTokenAmount: { tokenAmount: string; decimals: number };
}Step 1: Create the Express Server
Set up a secure webhook receiver with authentication.
// src/index.ts
import express, { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import { handleWebhook } from './webhook-handler';
import type { WebhookPayload } from './types';
const app = express();
const PORT = process.env.PORT || 3000;
// Your secret auth header (set when creating webhook)
const WEBHOOK_AUTH_SECRET = process.env.WEBHOOK_AUTH_SECRET || 'your-secret-key';
// Middleware to verify webhook authenticity
function verifyWebhookAuth(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers['authorization'];
if (!authHeader || authHeader !== WEBHOOK_AUTH_SECRET) {
console.warn('Unauthorized webhook attempt:', req.ip);
return res.status(401).json({ error: 'Unauthorized' });
}
next();
}
// Parse JSON body
app.use(express.json({ limit: '10mb' }));
// Health check endpoint
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Webhook endpoint
app.post('/webhook', verifyWebhookAuth, async (req: Request, res: Response) => {
try {
const payload: WebhookPayload = req.body;
// Acknowledge receipt immediately (Helius expects quick response)
res.status(200).json({ received: true });
// Process asynchronously
await handleWebhook(payload);
} catch (error) {
console.error('Webhook processing error:', error);
// Still return 200 to prevent retries for processing errors
res.status(200).json({ received: true, error: 'Processing failed' });
}
});
app.listen(PORT, () => {
console.log(`Webhook server running on port ${PORT}`);
});Importance of Quick Response
Helius expects a response within 5-10 seconds. Always acknowledge immediately and process asynchronously:
// WRONG - Processing before responding
app.post('/webhook', async (req, res) => {
await processComplexLogic(req.body); // May take 30+ seconds
res.json({ ok: true }); // Too late!
});
// CORRECT - Respond first, process later
app.post('/webhook', async (req, res) => {
res.json({ ok: true }); // Immediate response
await processComplexLogic(req.body); // Async processing
});Step 2: Implement Webhook Handlers
Process different transaction types with specific logic.
// src/webhook-handler.ts
import type {
WebhookPayload,
EnhancedTransaction,
NativeTransfer,
TokenTransfer,
} from './types';
export async function handleWebhook(payload: WebhookPayload): Promise<void> {
console.log(`Processing ${payload.data.length} transactions`);
for (const tx of payload.data) {
try {
await routeTransaction(tx);
} catch (error) {
console.error(`Error processing tx ${tx.signature}:`, error);
}
}
}
async function routeTransaction(tx: EnhancedTransaction): Promise<void> {
switch (tx.type) {
case 'TRANSFER':
await handleTransfer(tx);
break;
case 'SWAP':
await handleSwap(tx);
break;
case 'NFT_SALE':
await handleNFTSale(tx);
break;
case 'NFT_MINT':
await handleNFTMint(tx);
break;
default:
console.log(`Unhandled transaction type: ${tx.type}`);
}
}
async function handleTransfer(tx: EnhancedTransaction): Promise<void> {
// Handle SOL transfers
if (tx.nativeTransfers && tx.nativeTransfers.length > 0) {
for (const transfer of tx.nativeTransfers) {
const solAmount = transfer.amount / 1e9; // Lamports to SOL
console.log(
`SOL Transfer: ${transfer.fromUserAccount} -> ${transfer.toUserAccount}: ${solAmount} SOL`
);
// Alert for large transfers
if (solAmount > 100) {
await sendAlert({
type: 'LARGE_TRANSFER',
amount: solAmount,
from: transfer.fromUserAccount,
to: transfer.toUserAccount,
signature: tx.signature,
});
}
}
}
// Handle token transfers
if (tx.tokenTransfers && tx.tokenTransfers.length > 0) {
for (const transfer of tx.tokenTransfers) {
console.log(
`Token Transfer: ${transfer.mint} - ${transfer.tokenAmount} tokens`
);
// Track specific token movements
await trackTokenTransfer(transfer, tx.signature);
}
}
}
async function handleSwap(tx: EnhancedTransaction): Promise<void> {
const swapEvent = tx.events?.swap;
if (!swapEvent) {
console.log('Swap event without details');
return;
}
// Extract swap details
const inputToken = swapEvent.tokenInputs?.[0];
const outputToken = swapEvent.tokenOutputs?.[0];
const nativeIn = swapEvent.nativeInput;
const nativeOut = swapEvent.nativeOutput;
console.log('Swap detected:', {
signature: tx.signature,
input: inputToken
? `${inputToken.rawTokenAmount.tokenAmount} of ${inputToken.mint}`
: nativeIn
? `${nativeIn.amount / 1e9} SOL`
: 'unknown',
output: outputToken
? `${outputToken.rawTokenAmount.tokenAmount} of ${outputToken.mint}`
: nativeOut
? `${nativeOut.amount / 1e9} SOL`
: 'unknown',
});
// Track trading volume
await updateTradingMetrics(tx);
}
async function handleNFTSale(tx: EnhancedTransaction): Promise<void> {
const nftEvent = tx.events?.nft;
if (!nftEvent) return;
console.log('NFT Sale:', {
signature: tx.signature,
nfts: nftEvent.nfts.map((n) => n.mint),
seller: nftEvent.seller,
buyer: nftEvent.buyer,
amount: nftEvent.amount ? nftEvent.amount / 1e9 : 'unknown',
});
// Send notification for tracked collections
for (const nft of nftEvent.nfts) {
await checkTrackedCollection(nft.mint, nftEvent, tx.signature);
}
}
async function handleNFTMint(tx: EnhancedTransaction): Promise<void> {
const nftEvent = tx.events?.nft;
if (!nftEvent) return;
console.log('NFT Mint:', {
signature: tx.signature,
nfts: nftEvent.nfts.length,
minter: tx.feePayer,
});
// Track collection minting activity
await updateMintingMetrics(tx);
}
// Helper functions (implement based on your needs)
async function sendAlert(alert: object): Promise<void> {
console.log('ALERT:', alert);
// Implement: Discord, Telegram, email, etc.
}
async function trackTokenTransfer(transfer: TokenTransfer, signature: string): Promise<void> {
// Implement: Store in database, update balances, etc.
}
async function updateTradingMetrics(tx: EnhancedTransaction): Promise<void> {
// Implement: Trading volume tracking
}
async function checkTrackedCollection(
mint: string,
event: object,
signature: string
): Promise<void> {
// Implement: Collection-specific notifications
}
async function updateMintingMetrics(tx: EnhancedTransaction): Promise<void> {
// Implement: Mint tracking
}Step 3: Create and Manage Webhooks
Use the Helius SDK to programmatically manage webhooks.
// src/webhook-setup.ts
import { createHelius } from 'helius-sdk';
const helius = createHelius({ apiKey: process.env.HELIUS_API_KEY! });
export async function createMonitoringWebhook(
addresses: string[],
transactionTypes: string[] = ['TRANSFER', 'SWAP', 'NFT_SALE']
): Promise<string> {
try {
const webhook = await helius.webhooks.create({
webhookURL: process.env.WEBHOOK_URL!,
transactionTypes: transactionTypes as any,
accountAddresses: addresses,
webhookType: 'enhanced',
authHeader: process.env.WEBHOOK_AUTH_SECRET,
});
console.log('Webhook created:', webhook.webhookID);
return webhook.webhookID;
} catch (error) {
console.error('Failed to create webhook:', error);
throw error;
}
}
export async function updateWebhookAddresses(
webhookId: string,
newAddresses: string[]
): Promise<void> {
try {
// Get current webhook
const current = await helius.webhooks.get(webhookId);
// Merge addresses (up to 100k limit)
const allAddresses = [
...new Set([...current.accountAddresses, ...newAddresses]),
].slice(0, 100000);
// Update webhook
await helius.webhooks.update(webhookId, {
accountAddresses: allAddresses,
});
console.log(`Webhook ${webhookId} updated with ${allAddresses.length} addresses`);
} catch (error) {
console.error('Failed to update webhook:', error);
throw error;
}
}
export async function listAllWebhooks(): Promise<void> {
try {
const webhooks = await helius.webhooks.getAll();
console.log(`Found ${webhooks.length} webhooks:`);
for (const webhook of webhooks) {
console.log({
id: webhook.webhookID,
url: webhook.webhookURL,
types: webhook.transactionTypes,
addressCount: webhook.accountAddresses.length,
webhookType: webhook.webhookType,
});
}
} catch (error) {
console.error('Failed to list webhooks:', error);
}
}
export async function deleteWebhook(webhookId: string): Promise<void> {
try {
await helius.webhooks.delete(webhookId);
console.log(`Webhook ${webhookId} deleted`);
} catch (error) {
console.error('Failed to delete webhook:', error);
throw error;
}
}Creating Webhooks for Different Use Cases
// Monitor a wallet for all activity
const walletWebhook = await createMonitoringWebhook(
['YourWalletAddress'],
['TRANSFER', 'SWAP', 'NFT_SALE', 'NFT_MINT']
);
// Monitor an NFT collection (all token accounts)
const collectionWebhook = await helius.webhooks.create({
webhookURL: process.env.WEBHOOK_URL!,
transactionTypes: ['NFT_SALE', 'NFT_LISTING', 'NFT_BID'],
accountAddressOwners: ['CollectionCreatorAddress'], // Monitor by owner
webhookType: 'enhanced',
authHeader: process.env.WEBHOOK_AUTH_SECRET,
});
// Monitor a DeFi pool with raw data
const defiWebhook = await helius.webhooks.create({
webhookURL: process.env.WEBHOOK_URL!,
transactionTypes: ['ANY'], // All transactions
accountAddresses: ['PoolProgramAddress'],
webhookType: 'raw', // Get full transaction data
authHeader: process.env.WEBHOOK_AUTH_SECRET,
});Step 4: Error Handling and Retries
Helius retries failed webhooks automatically, but implement your own safeguards.
// src/webhook-handler.ts (additions)
import { Queue } from 'bullmq'; // Or your preferred queue
const webhookQueue = new Queue('webhooks', {
connection: { host: 'localhost', port: 6379 },
});
// Add retry logic with exponential backoff
export async function handleWebhookWithRetry(
payload: WebhookPayload,
attempt: number = 1
): Promise<void> {
const MAX_ATTEMPTS = 3;
try {
await handleWebhook(payload);
} catch (error) {
console.error(`Attempt ${attempt} failed:`, error);
if (attempt < MAX_ATTEMPTS) {
// Queue for retry with exponential backoff
await webhookQueue.add(
'retry',
{ payload, attempt: attempt + 1 },
{ delay: Math.pow(2, attempt) * 1000 } // 2s, 4s, 8s
);
} else {
// Log to dead letter queue
await logFailedWebhook(payload, error);
}
}
}
async function logFailedWebhook(payload: WebhookPayload, error: unknown): Promise<void> {
console.error('Webhook permanently failed:', {
timestamp: payload.timestamp,
transactionCount: payload.data.length,
error: error instanceof Error ? error.message : 'Unknown error',
});
// Store in database for manual review
}Idempotency
Handle duplicate deliveries gracefully:
const processedSignatures = new Set<string>();
async function handleWebhookIdempotent(payload: WebhookPayload): Promise<void> {
for (const tx of payload.data) {
// Skip already processed transactions
if (processedSignatures.has(tx.signature)) {
console.log(`Skipping duplicate: ${tx.signature}`);
continue;
}
// Mark as processing
processedSignatures.add(tx.signature);
try {
await routeTransaction(tx);
} catch (error) {
// Remove from set on failure to allow retry
processedSignatures.delete(tx.signature);
throw error;
}
}
}For production, use Redis or a database for processed signature tracking.
Common Pitfalls
Slow Response Times
Problem: Helius times out waiting for response.
// WRONG
app.post('/webhook', async (req, res) => {
await heavyDatabaseOperation(req.body);
res.json({ ok: true });
});
// CORRECT
app.post('/webhook', (req, res) => {
res.json({ ok: true }); // Respond immediately
heavyDatabaseOperation(req.body).catch(console.error);
});Missing Authentication
Problem: Unauthorized requests processed.
// Always verify the auth header you set when creating the webhook
if (req.headers['authorization'] !== YOUR_SECRET) {
return res.status(401).end();
}Ignoring Payload Size
Problem: Large payloads crash server.
// Set appropriate limits
app.use(express.json({ limit: '50mb' }));
// Handle memory efficiently for large batches
for (const tx of payload.data) {
await processTransaction(tx);
// Don't accumulate all in memory
}Not Handling All Transaction Types
Problem: Unhandled types silently fail.
// Log unhandled types for review
default:
console.warn(`Unhandled type: ${tx.type}`, tx.signature);
await logUnhandledType(tx);Production Deployment
Environment Variables
# .env
HELIUS_API_KEY=your-helius-api-key
WEBHOOK_URL=https://your-domain.com/webhook
WEBHOOK_AUTH_SECRET=your-secret-auth-header-value
PORT=3000
REDIS_URL=redis://localhost:6379Deployment with ngrok (Development)
# Expose local server for testing
ngrok http 3000
# Use the ngrok URL when creating webhooks
WEBHOOK_URL=https://abc123.ngrok.io/webhookProduction Checklist
- [ ] HTTPS endpoint with valid SSL certificate
- [ ] Authentication header verification
- [ ] Response time under 5 seconds
- [ ] Error logging and monitoring
- [ ] Idempotency for duplicate handling
- [ ] Queue-based processing for reliability
- [ ] Health check endpoint
- [ ] Rate limiting for abuse prevention
Next Steps & Resources
Advanced Use Cases
- **Portfolio Tracking**: Monitor multiple wallets with balance updates
- **Trading Bots**: React to swap events in real-time
- **NFT Sniping**: Detect listings instantly for arbitrage
- **Alerting Systems**: Telegram/Discord notifications for whale movements
Official Resources
- [Helius Documentation](https://docs.helius.dev/)
- [Helius SDK on GitHub](https://github.com/helius-labs/helius-sdk)
- [Helius Dashboard](https://dashboard.helius.dev)
Related Builderz Content
- [Compressed NFTs with DAS API](/blog/compressed-nfts-das-api-guide)
- [Jupiter Token Swap Tutorial](/blog/jupiter-token-swap-tutorial)
- [Solana Development Services](/services)
Summary
Helius webhooks provide the foundation for real-time Solana applications:
- **Choose webhook type**: Enhanced for parsed data, raw for full control
- **Secure your endpoint**: Always use authentication headers
- **Respond quickly**: Acknowledge within 5 seconds, process async
- **Handle failures**: Implement idempotency and retry logic
- **Monitor at scale**: Up to 100k addresses per webhook
With webhooks handling the event delivery, you can focus on building features instead of polling infrastructure.
Built with expertise from Builderz - 32+ Solana projects with real-time webhook integrations.



