OmniTipper - Architectural Overview
Table of Contents
- System Overview
- Architecture Components
- Data Flow
- Database Schema
- Security Architecture
- External Integrations
- Deployment Architecture
- Key Design Decisions
- Technology Stack
System Overview
OmniTipper is a Discord-based cryptocurrency tipping bot that operates on the Base blockchain. The system enables users to tip, deposit, and withdraw various ERC20 tokens and native ETH through Discord commands, with off-chain transfers for tips and on-chain operations for deposits and withdrawals.
System Diagram
!System Architecture Diagram
Core Principles
- Off-chain tips: Fast, free internal transfers (database-only operations)
- On-chain operations: Deposits and withdrawals interact with the Base blockchain
- Multi-token support: Unlimited ERC20 tokens can be added dynamically
- Single deposit address: Each user has one address that accepts all tokens
- Automatic deposit monitoring: Periodic checks for new deposits with automatic sweeping
- Gas fee management: Automatic ETH funding for gas with tracking and accounting
Architecture Components
1. Bot Layer (src/bot.ts)
The main entry point that orchestrates all components:
- Discord Client: Manages connection to Discord Gateway
- Event Handlers: Processes
messageCreate events for commands
- Command Router: Routes commands to appropriate handlers
- Deposit Monitor: Periodic background task checking for deposits
- Balance Updater: Periodic task updating bot balances from chain
- Per-user Rate Limiter: Sliding-window throttling on every
! command (see
src/utils/rate-limiter.ts). Per-command limits are configured at the top of
bot.ts; unknown command keys fall back to a default bucket so newly added
commands cannot bypass throttling.
- Admin Command Context Guard: When both
ADMINGUILDID and
ADMINCHANNELID are set, !admin is only accepted in DMs to the bot or in
the configured guild channel (threads under it count). With either env var
unset, !admin works anywhere the user is otherwise authorized (legacy).
Key Responsibilities:
- Initialize all services on startup
- Handle Discord message events
- Coordinate deposit checking loop
- Apply rate limiting and admin context checks before dispatching commands
- Manage graceful shutdown
2. Command Handlers (src/commands/)
Modular command processors for user interactions:
User Commands
balances.ts: Display user balances for all supported tokens
tip.ts: Transfer tokens between users (off-chain)
withdraw.ts: Withdraw tokens to external addresses (on-chain)
deposit.ts: Generate/display deposit addresses
help.ts: Display help information
Admin Commands (admin.ts)
addcoin: Add new ERC20 token by contract address
addeth: Add native ETH token
botbalances: View bot's on-chain balances
userbalances: View specific user balances
accounting: Compare bot balances vs user balances (with gas fee accounting)
testprice: Test price conversion functionality
sweepeth: Sweep ETH from user deposit addresses
3. Service Layer (src/services/)
Core business logic services:
Web3Service (web3.ts)
Handles all blockchain interactions:
- Token Operations:
- Balance checking (native ETH and ERC20)
- Token transfers (from bot wallet and deposit addresses)
- Token information retrieval (symbol, name, decimals)
- Deposit Address Management:
- Generate new wallet addresses with private keys
- Transfer tokens from deposit addresses to bot wallet
- Automatic ETH funding for gas when needed
- ETH Sweeping:
- Sweep excess ETH from deposit addresses
- Leave reserve for gas fees
- Update bot balances after sweeping
- Optimization Features:
- JSON-RPC batch requests for balance checks
- Rate limiting to avoid RPS limits
- Gas price caching (10-second cache)
- Batch processing (100 requests per batch)
PriceService (price.ts)
Handles cryptocurrency price conversions:
- CoinMarketCap Integration: Fetches real-time prices
- USD Conversion: Converts USD amounts to token amounts
- Price Caching: 1-minute cache to reduce API calls
- Token Symbol Lookup: Uses database to get token symbols for API queries
MetricsService (metrics.ts)
Prometheus-compatible metrics collection:
- Command Metrics: Tracks command usage by type
- Transaction Metrics: Counts tips, withdrawals, deposits
- RPC Metrics: Tracks RPC calls, errors, and durations
- Gas Fee Metrics: Tracks gas fees by coin and operation type
- Performance Metrics: Tracks operation durations and latencies
- System Metrics: Active users, bot balances, uptime
- Deposit Monitor Metrics (chunking visibility):
omnitipperdepositmonitorbatchusers,
omnitipperdepositmonitoreligibleusers,
omnitipperdepositmonitorchunkedmode
- Discord Footprint Metrics:
omnitipperdiscordgateway_up (set by
ready/shardResume and shardDisconnect handlers),
omnitipperdiscordguilds, omnitipperdiscordguild_channels
Alert rules consuming these metrics live in
prometheus-alerts.yml; SLO targets and the metric ↔
rule mapping are in docs/monitoring/SLOs-and-alerts.md.
MetricsServer (metrics-server.ts)
HTTP server exposing Prometheus metrics:
- Metrics Endpoint:
/metrics - Prometheus format
- Health Endpoint:
/health - Health check
- Port: Configurable (default: 9090)
- Format: Prometheus text format (compatible with Grafana)
4. Database Layer (src/database/)
MongoDB-based data persistence:
Connection (connection.ts)
- Manages MongoDB connection lifecycle
- Handles connection errors and reconnection
- Honors optional
MONGODBMAXPOOL_SIZE (0 = driver default) for tuning
pool size under concurrent command load
Models (models.ts)
Database schema and operations:
Collections:
- Users: User accounts, balances, deposit addresses
- Coins: Supported tokens configuration
- BotBalances: Bot's on-chain balances
- DepositTracking: Last checked balances per user/coin
- GasFeeTracking: Recorded gas fees for accounting
- DepositMonitorState: Single-row round-robin cursor (
_id: 'default',
lastUserId) used when DEPOSITCHECKUSERBATCHSIZE > 0 so chunked scans
resume across restarts instead of always starting from user 0
Key Features:
- Encrypted private key storage (AES-256-GCM)
- BigInt precision preservation (stored as strings)
- Indexed queries for performance
- Atomic balance updates
5. Utility Layer (src/utils/)
Supporting utilities:
Encryption (encryption.ts)
- AES-256-GCM encryption for private keys
- Key derivation using scrypt
- Authentication tags for integrity
- Encrypted data detection
Logger (logger.ts)
- Multi-level logging (ERROR, WARN, INFO, DEBUG)
- Console and file output
- Date-stamped log files
- Separate error log file
- JSON formatting for objects
Environment (env.ts)
- Environment variable validation
- Required vs optional variable checking
- Type validation (e.g., encryption key length)
Rate Limiter (rate-limiter.ts)
- Per-user, per-command sliding-window limiter (in-memory)
- Includes a
default config bucket so unknown command keys still get throttled
- Periodic self-cleanup of expired entries to avoid unbounded growth
- Configured at the top of
src/bot.ts; reset hook available for tests
Admin Command Context (admin-command-context.ts)
- Parses optional
ADMINGUILDID + ADMINCHANNELID pair
isAdminCommandContextAllowed(...) returns true for DMs, true when the pair
is unset (legacy global behavior), and otherwise requires both guildId and
the channel id (parent channel for threads) to match
- Guards
!admin dispatch in src/bot.ts before the command runs
Operational scaling
These mechanisms are documented in detail in
docs/monitoring/SLOs-and-alerts.md,
docs/runbooks/high-availability.md,
and docs/architecture-tenancy.md.
- Chunked deposit monitor.
DEPOSITCHECKUSERBATCHSIZE = 0 scans every
user with a deposit address each tick (legacy behavior). > 0 switches to
round-robin chunks: each tick reads batchSize users starting after the
persisted cursor and advances the cursor on success. On retryable balance
errors the cursor is not advanced so the same chunk is retried next tick.
- Per-user rate limits. Sliding window per
(userId, command); default
bucket catches unknown commands. Limits live in src/bot.ts and can be
raised for power users without code changes elsewhere.
- Admin command scoping. When
ADMINGUILDID and ADMINCHANNELID are
both set, !admin is restricted to DMs or that single channel (and its
threads). Audit-friendly: admin actions concentrate in one logged channel.
- Discord HA constraint. Discord enforces one active gateway session per
bot token. The bot is not horizontally scaled across replicas; use
active/passive failover and rely on omnitipperdiscordgateway_up for
alerting.
- MongoDB pool tuning.
MONGODBMAXPOOL_SIZE lets ops raise the driver's
pool ceiling under concurrent command load.
Data Flow
1. Tip Flow (Off-chain)
!Tip Flow Diagram
No blockchain interaction - pure database operation
2. Deposit Flow (On-chain)
!Deposit Flow Diagram
3. Withdrawal Flow (On-chain)
!Withdrawal Flow Diagram
4. ERC20 Transfer from Deposit Address Flow
!ERC20 Transfer Flow Diagram
5. Price Conversion Flow
!Price Conversion Flow Diagram
Database Schema
!Database Schema Diagram
Users Collection
{
_id: string; // Discord user ID
username: string; // Discord username
balances: Record<string, string>; // coin symbol -> balance (string for precision)
depositAddress: string | null; // Single address for all tokens
depositPrivateKey: string | null; // Encrypted private key
createdAt: Date;
updatedAt: Date;
}
Indexes:
Coins Collection
{
_id: string; // Token symbol (e.g., "USDC", "ETH")
contractAddress: string | null; // ERC20 contract address (null for native ETH)
decimals: number; // Token decimals
name: string; // Token name
enabled: boolean; // Whether token is enabled
isNative: boolean; // true for native ETH
createdAt: Date;
}
Indexes:
_id (primary key)
contractAddress (for lookups)
enabled (for filtering)
BotBalances Collection
{
coin: string; // Token symbol
balance: string; // On-chain balance (string for precision)
updatedAt: Date;
}
Indexes:
DepositTracking Collection
{
_id: string; // "userId:coinSymbol"
userId: string; // Discord user ID
coin: string; // Token symbol
lastCheckedBalance: string; // Last checked balance
botFundedAmount: string; // ETH funded by bot (not user deposit)
updatedAt: Date;
}
Indexes:
_id (primary key)
userId, coin (compound index for queries)
GasFeeTracking Collection
{
_id: string; // Transaction hash or 'total'
type: 'funding' 'transfer';
amount: string; // Gas fee in ETH (string for precision)
transactionHash?: string; // Transaction hash
depositAddress?: string; // For funding operations
userId?: string; // For withdrawal operations
createdAt: Date;
}
Indexes:
_id (primary key)
type (for filtering by operation type)
createdAt (for time-based queries)
Security Architecture
1. Private Key Encryption
- Algorithm: AES-256-GCM
- Key Derivation: scrypt (from ENCRYPTION_KEY)
- Storage: Encrypted in database
- Access: Decrypted only when needed for transactions
- Key Management: Environment variable (minimum 32 characters)
2. Access Control
- Admin Commands: Restricted to users in
ADMINUSERIDS
- User Commands: Available to all users
- Validation: Command handlers check admin status before execution
3. Input Validation
- Amount Validation: Decimal precision checks
- Address Validation: Ethereum address format validation
- Command Parsing: Safe argument extraction
- Error Handling: Graceful error messages (no sensitive data exposure)
4. Rate Limiting
- RPC Calls: 100ms minimum interval (10 RPS max for batches)
- Batch Processing: 100 requests per batch with 200ms delays
- Gas Price Caching: 10-second cache to reduce calls
- Price Caching: 1-minute cache to reduce API calls
5. Balance Precision
- Storage: All balances stored as strings (preserves BigInt precision)
- Operations: BigInt arithmetic for calculations
- Display: Formatted with proper decimals
6. Transaction Safety
- Deposit Confirmation: System waits for on-chain confirmation (status=1) before crediting users.
- Refund Retry Logic: Failed withdrawals automatically retry refunds with exponential backoff (1s, 2s, 4s...) up to 5 times.
- Race Condition Prevention: Deposit checks are locked to prevent overlapping execution.
External Integrations
1. Discord API
- Library: discord.js v14
- Intents Required:
Guilds: Server information
GuildMessages: Server messages
MessageContent: Read message content (privileged)
DirectMessages: Receive DMs
DirectMessageReactions: DM reactions
GuildMembers: Server members (privileged, required for role-based tipping)
2. Base Blockchain
- RPC Provider: Configurable (default:
https://mainnet.base.org)
- Library: ethers.js v6
- Operations:
- Balance queries (
ethgetBalance, ethcall)
- Token transfers (
eth_sendTransaction)
- Contract interactions (ERC20 standard)
- Transaction receipts
3. CoinMarketCap API
- Endpoint:
/v2/cryptocurrency/quotes/latest
- Authentication: API key (header)
- Usage: Price conversion (USD ↔ tokens)
- Caching: 1-minute cache per token
- Rate Limiting: Handled by caching
4. MongoDB
- Version: MongoDB 7
- Connection: Configurable URL
- Collections: 5 collections (users, coins, botBalances, depositTracking, gasFeeTracking)
- Indexes: Optimized for common queries
Deployment Architecture
Container Architecture
!Container Architecture Diagram
Docker Compose Setup
┌─────────────────────────────────────┐
│ Docker Network │
│ │
│ ┌──────────────┐ ┌─────────────┐ │
│ │ Bot │ │ MongoDB │ │
│ │ Container │◄─┤ Container │ │
│ │ │ │ │ │
│ │ - Bun │ │ - MongoDB 7│ │
│ │ - Node.js │ │ - Volume │ │
│ │ - Source │ │ Mounted │ │
│ └──────┬───────┘ └─────────────┘ │
│ │ │
└─────────┼──────────────────────────┘
│
▼
┌──────────┐
│ Discord │
│ API │
└──────────┘
│
▼
┌──────────┐
│ Base │
│ RPC │
└──────────┘
│
▼
┌──────────┐
│CoinMarket│
│ Cap │
└──────────┘
Container Configuration
Bot Container:
- Base Image:
oven/bun:latest
- Ports: None (outbound only)
- Environment: All config via
.env file
- Restart Policy:
unless-stopped
- Dependencies: MongoDB container
MongoDB Container:
- Base Image:
mongo:7
- Port: 27017 (internal network)
- Volume: Persistent data storage
- Restart Policy:
unless-stopped
Environment Variables
Required:
DISCORDBOTTOKEN: Discord bot token
MONGODB_URL: MongoDB connection string
MONGODBDBNAME: Database name
WALLETPRIVATEKEY: Bot wallet private key
COINMARKETCAPAPIKEY: CoinMarketCap API key
ENCRYPTION_KEY: Encryption key (min 32 chars)
ADMINUSERIDS: Comma-separated Discord user IDs
Optional:
BASERPCURL: Base RPC endpoint (default: https://mainnet.base.org)
LOG_LEVEL: Logging level (default: INFO)
LOGTOFILE: Enable file logging (default: false)
LOG_DIR: Log directory (default: ./logs)
DEPOSITCHECKINTERVAL: Deposit check interval in ms (default: 60000)
METRICS_PORT: Metrics server port (default: 9090)
Key Design Decisions
1. Off-chain Tips
Decision: Tips are database-only operations (no blockchain transactions)
Rationale:
- Instant execution (no gas fees)
- No transaction costs for users
- Scales to high volume
- User balances tracked internally
Trade-offs:
- Requires trust in bot operator
- Funds are custodial until withdrawal
2. Single Deposit Address per User
Decision: One deposit address per user for all tokens
Rationale:
- Simpler user experience
- Easier to manage
- Single private key per user
- Works for all ERC20s and native ETH
Trade-offs:
- All tokens share the same address
- Requires ETH on address for gas (for ERC20 transfers)
3. Automatic ETH Funding
Decision: Bot automatically funds deposit addresses with ETH for gas
Rationale:
- Seamless user experience
- No need for users to manage gas
- ERC20 deposits work automatically
Trade-offs:
- Bot must maintain ETH reserves
- Requires tracking bot-funded amounts
- Gas fees reduce bot's ETH balance
4. ETH Sweeping
Decision: Automatically sweep excess ETH from deposit addresses
Rationale:
- Consolidates funds in bot wallet
- Reduces ETH fragmentation
- Leaves reserve for gas fees
Trade-offs:
- Additional transaction costs
- Must ensure enough ETH remains for gas
5. Batch Balance Checking
Decision: Use JSON-RPC batch requests for balance checks
Rationale:
- Reduces RPC calls dramatically
- Avoids rate limiting
- More efficient than individual calls
Trade-offs:
- More complex implementation
- Batch size limits (100 requests)
6. String-based Balance Storage
Decision: Store all balances as strings in database
Rationale:
- Preserves BigInt precision
- No floating-point errors
- Handles tokens with any decimal places
Trade-offs:
- Requires conversion for arithmetic
- Slightly more complex code
7. Gas Fee Tracking
Decision: Track all gas fees separately for accounting
Rationale:
- Accurate accounting reconciliation
- Can verify bot balances vs user balances
- Transparency for operations
Trade-offs:
- Additional database writes
- More complex accounting logic
8. Recursive Deposit Checking
Decision: Use recursive setTimeout instead of setInterval
Rationale:
- Prevents overlapping checks
- Ensures one check completes before next starts
- Better error handling
Trade-offs:
- Slightly more complex timing logic
- Potential delays if checks take long
Technology Stack
Runtime & Language
- Runtime: Bun (v1.2.17+)
- Language: TypeScript
- Package Manager: Bun
Core Dependencies
- discord.js: v14.14.1 - Discord API client
- ethers: v6.9.2 - Ethereum/Base blockchain interactions
- mongodb: v6.3.0 - MongoDB driver
- dotenv: v16.4.1 - Environment variable management
Infrastructure
- Docker: Containerization
- Docker Compose: Multi-container orchestration
- MongoDB: Database (v7)
External Services
- Discord: Bot platform
- Base Network: EVM-compatible blockchain
- CoinMarketCap: Price data API
Development Tools
- TypeScript: Type safety
- Bun: Fast JavaScript runtime and package manager
Performance Considerations
1. RPC Optimization
- Batch requests (100 per batch)
- Rate limiting (100ms between batches)
- Gas price caching (10 seconds)
- Balance check batching
2. Database Optimization
- Indexed queries
- Atomic updates
- Efficient data structures
- Minimal writes (batch updates where possible)
3. API Optimization
- Price caching (1 minute)
- Batch processing
- Error handling with retries
4. Memory Management
- No long-lived caches
- Efficient BigInt handling
- Proper cleanup on shutdown
Monitoring & Observability
Logging
- Levels: ERROR, WARN, INFO, DEBUG
- Outputs: Console and/or file
- Format: Structured with timestamps
- Rotation: Date-stamped log files
Metrics (Prometheus)
The bot exposes Prometheus-compatible metrics on an HTTP endpoint for integration with Grafana or other monitoring systems.
Metrics Endpoint
- URL:
http://localhost:9090/metrics (configurable via METRICS_PORT)
- Format: Prometheus text format
- Health Check:
http://localhost:9090/health
Available Metrics
Counters:
omnitippercommandstotal{command="..."} - Command execution counts
omnitippertipstotal - Total tips processed
omnitipperwithdrawalstotal - Total withdrawals
omnitipperdepositstotal - Total deposits detected
omnitipperrpccalls_total - RPC call count
omnitipperrpcerrors_total - RPC error count
omnitippererrorstotal{type="..."} - Error counts by type
omnitippergasfees_total{coin="..."} - Total gas fees by coin
omnitippergasfeesbytype_total{type="...",coin="..."} - Gas fees by operation type
omnitipperbalancechecks_total - Balance check count
omnitipperdepositchecks_total - Deposit check count
omnitipperdepositcheckerrorstotal - Deposit check errors
omnitipperpriceapicallstotal - Price API calls
omnitipperpriceapierrorstotal - Price API errors
omnitipperpriceapicachehits_total - Price API cache hits
Gauges:
omnitipperactiveusers - Current active user count
omnitipperbotbalance{coin="..."} - Bot balance per coin (in wei)
omnitipperuptimeseconds - Bot uptime
omnitipperbalancecheckdurationseconds - Average balance check duration
omnitipperdepositcheckdurationseconds - Average deposit check duration
Summaries:
omnitipperrpccalldurationseconds - RPC call duration with quantiles (p50, p95, p99)
Grafana Integration
- Configure Prometheus to scrape the metrics endpoint:
scrape_configs:
static_configs:
- targets: ['bot-container:9090']
- Import Grafana Dashboard or create custom dashboards using the available metrics
- Set up Alerts for:
- High error rates
- RPC call failures
- Balance discrepancies
- Deposit check failures
- Low bot balances
Key Metrics to Monitor
- Deposit check frequency and duration
- RPC call rates and error rates
- Transaction success rates (tips, withdrawals, deposits)
- Balance discrepancies
- Gas fee accumulation
- Error rates by type
- Active user count
- Bot balances per coin
- Price API performance
Health Checks
- Database connectivity
- Discord connection status
- RPC endpoint availability
- CoinMarketCap API status
- Metrics server availability (
/health endpoint)
Future Enhancements
Potential Improvements
- Multi-chain support: Extend to other EVM chains
- Transaction history: Track all user transactions
- Analytics dashboard: Admin interface for metrics
- Webhook notifications: Alternative to DMs
- Rate limiting: Per-user command rate limits
- Multi-signature wallet: Enhanced security
- Automated testing: Comprehensive test suite
- API endpoint: REST API for external integrations
Conclusion
OmniTipper is designed as a scalable, secure, and efficient cryptocurrency tipping bot. The architecture prioritizes:
- User Experience: Simple commands, automatic gas management
- Security: Encrypted private keys, input validation
- Performance: Batch operations, caching, rate limiting
- Reliability: Error handling, graceful shutdown, monitoring
- Maintainability: Modular design, clear separation of concerns
The system is production-ready and can handle high-volume operations while maintaining accuracy and security.