Real-time blockchain webhook notification service for Tempo with self-indexing architecture and Polar billing integration.
- β Self-indexing - Zero external API costs (no IndexSupply needed)
- β Direct Tempo RPC integration - Free public endpoints
- β Sub-second webhook latency (~500-700ms)
- β Advanced filtering - Amount, address, memo pattern matching
- β Battle-tested retry logic - Exponential backoff
- β Reorg protection - Handles blockchain reorganizations
- β Polar billing integration - Subscription management via webhooks
- β Multi-tenancy - Organization-based isolation
- β Bootstrap-friendly - $12-15/month infrastructure cost
- Rust 1.75 or higher
- PostgreSQL 16+
- Redis 7+
- NATS Server (for message queue)
- Tempo RPC access (free public endpoints)
# Clone the repository
git clone https://github.com/Emengkeng/tempo-webhook.git
cd tempo-webhook
# Copy environment file
cp .env.example .env
# Edit .env with your configuration
nano .env# Generate JWT secret
openssl rand -hex 32
# Generate API key encryption key
openssl rand -hex 32Add these to your .env file.
# Install sqlx-cli
cargo install sqlx-cli --no-default-features --features postgres
# Run migrations
sqlx migrate runMake sure you have Redis and NATS running:
# Redis (using Docker)
docker run -d -p 6379:6379 redis:7
# NATS (using Docker)
docker run -d -p 4222:4222 nats:latest# Development
cargo run
# Production
cargo build --release
./target/release/tempo-webhooksAll API requests require an API key in the header:
X-API-Key: tempo_live_xxxxxxxxxxxxxcurl -X POST https://api.tempohooks.com/api/v1/subscriptions \
-H "X-API-Key: tempo_live_xxxxx" \
-H "Content-Type: application/json" \
-d '{
"type": "TRANSFER",
"network": "mainnet",
"address": "0x20c0000000000000000000000000000000000001",
"webhook_url": "https://myapp.com/webhooks",
"webhook_secret": "whsec_xxxxx",
"filters": [
{"type": "amount_min", "value": "1000000"}
]
}'curl https://api.tempohooks.com/api/v1/subscriptions \
-H "X-API-Key: tempo_live_xxxxx"curl https://api.tempohooks.com/api/v1/webhooks/logs?subscription_id=sub_abc123 \
-H "X-API-Key: tempo_live_xxxxx"Verify webhook signatures in your application:
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const [timestampPart, hashPart] = signature.split(',');
const timestamp = timestampPart.split('=')[1];
const hash = hashPart.split('=')[1];
// Prevent replay attacks (5 minute window)
if (Date.now() / 1000 - timestamp > 300) {
return false;
}
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const expectedHash = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(hash),
Buffer.from(expectedHash)
);
}# Install Fly CLI
brew install flyctl
# Login
flyctl auth login
# Deploy
flyctl deploy
# Set secrets
flyctl secrets set DATABASE_URL="postgres://..."
flyctl secrets set REDIS_URL="redis://..."
flyctl secrets set JWT_SECRET="..."
# ... etc# Build
docker build -t tempo-webhooks .
# Run
docker run -p 8080:8080 \
-e DATABASE_URL="postgres://..." \
-e REDIS_URL="redis://..." \
tempo-webhooksBootstrap budget (monthly):
| Service | Provider | Cost |
|---|---|---|
| PostgreSQL | Render.com | $7 |
| Redis | Upstash | $0 (free tier) |
| App Hosting | Fly.io | $5 |
| Domain | Cloudflare | $1 |
| Total | $13/month |
# Run all tests
cargo test
# Run with logs
RUST_LOG=debug cargo test
# Run specific test
cargo test test_webhook_deliverySee .env.example for all required environment variables.
DATABASE_URL- PostgreSQL connection stringREDIS_URL- Redis connection stringTEMPO_MAINNET_WS- Tempo mainnet WebSocket URLTEMPO_MAINNET_HTTP- Tempo mainnet HTTP RPC URLJWT_SECRET- Secret for JWT tokensAPI_KEY_ENCRYPTION_KEY- Secret for API key encryptionPOLAR_SECRET_KEY- Polar API keyPOLAR_WEBHOOK_SECRET- Polar webhook secret
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Tempo Blockchain β
β (wss://rpc.tempo.xyz) β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β WebSocket
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β WebSocket Listener β
β (Real-time block notifications) β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Self-Indexer β
β (Selective indexing + Event parsing) β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Event Matcher β
β (Apply filters + Match subscriptions) β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Webhook Dispatcher β
β (HMAC signatures + Retry logic) β
ββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Applications β
β (Receive webhooks) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Contributions welcome! Please open an issue or PR.