- Sequencer integration strategy (core competitive advantage) - High-performance parsing architecture (< 5ms target) - Multi-hop arbitrage detection algorithms - Execution strategies (front-running, batching) - Dynamic gas optimization for maximum profit - Comprehensive risk management (slippage, circuit breaker, position sizing) - Profitability metrics and tracking - Complete system architecture with performance targets Key Features: - Sequencer front-running (100-500ms advantage) - Multi-protocol coverage (13+ DEXs) - Sub-50ms end-to-end latency target - 85%+ success rate target - Batch execution for gas savings - Flashbots integration option Profitability Targets: - Conservative: 18 ETH/month (180% monthly ROI) - Moderate: 141 ETH/month (705% monthly ROI) - Optimistic: 456 ETH/month (912% monthly ROI) Implementation: 8-week roadmap with clear phases 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
51 KiB
V2 Profitability Plan
Executive Summary
This document outlines the complete architecture for a profitable MEV bot that leverages Arbitrum sequencer access to execute arbitrage trades ahead of the chain. The sequencer reader/parser is the core competitive advantage, providing visibility into pending transactions before they're confirmed on-chain.
Key Profitability Drivers:
- Sequencer Front-Running: Execute before price-moving transactions hit the chain
- Multi-Protocol Coverage: Capture arbitrage across 13+ DEX protocols
- Fast Parsing: Sub-millisecond transaction parsing and validation
- Batch Execution: Combine multiple opportunities to save gas
- Dynamic Gas Optimization: Maximize net profit through intelligent gas pricing
- Risk Management: Protect capital with slippage limits and circuit breakers
Target Metrics:
- Latency: < 50ms from sequencer event to execution decision
- Success Rate: > 85% of executed trades profitable
- Average Profit: > 0.05 ETH per trade (after gas)
- Daily Volume: 50-200 trades per day
- ROI: > 20% monthly on deployed capital
1. Sequencer Integration Strategy
1.1 Core Advantage: Reading Pending Transactions
Arbitrum Sequencer Access:
Sequencer Feed (WebSocket)
↓
Pending Transaction Stream
↓
Parse ALL Swap Events (before on-chain)
↓
Calculate Price Impact
↓
Detect Arbitrage Opportunity
↓
Execute Front-Run Transaction
↓
Capture Profit
Why This Matters:
- Traditional bots read from the mempool AFTER transactions are broadcast
- We read from the sequencer BEFORE transactions are finalized
- This gives us a time advantage of 100-500ms
- In MEV, milliseconds = millions
1.2 Sequencer Reader Architecture
// pkg/sequencer/reader.go
type SequencerReader struct {
wsClient *websocket.Conn
parsers *parsers.Factory
validator *validation.Validator
cache *cache.PoolCache
arbDetector *arbitrage.Detector
executor *execution.Executor
metrics *metrics.Collector
// Performance critical
parseLatency prometheus.Histogram
detectLatency prometheus.Histogram
executeLatency prometheus.Histogram
}
func (r *SequencerReader) Start(ctx context.Context) error {
// Subscribe to Arbitrum sequencer WebSocket feed
err := r.wsClient.Subscribe(ctx, "newPendingTransactions")
if err != nil {
return fmt.Errorf("sequencer subscription failed: %w", err)
}
// Process pending transactions in real-time
for {
select {
case tx := <-r.wsClient.Transactions():
// CRITICAL PATH: Every nanosecond counts
go r.ProcessPendingTx(ctx, tx)
case <-ctx.Done():
return ctx.Err()
}
}
}
func (r *SequencerReader) ProcessPendingTx(ctx context.Context, tx *types.Transaction) {
startTime := time.Now()
// Step 1: Parse transaction (< 5ms target)
events, err := r.parsers.ParseTransaction(tx)
if err != nil {
r.metrics.ParseErrors.Inc()
return
}
r.parseLatency.Observe(time.Since(startTime).Seconds())
// Step 2: Validate events (< 2ms target)
validEvents := r.validator.FilterValid(events)
if len(validEvents) == 0 {
return
}
// Step 3: Calculate price impact (< 3ms target)
priceImpacts := r.calculatePriceImpacts(validEvents)
// Step 4: Detect arbitrage (< 10ms target)
opportunities := r.arbDetector.FindOpportunities(priceImpacts)
r.detectLatency.Observe(time.Since(startTime).Seconds())
// Step 5: Execute profitable trades (< 30ms target)
for _, opp := range opportunities {
if opp.NetProfit.Cmp(r.minProfitThreshold) > 0 {
r.executor.ExecuteFrontRun(ctx, opp, tx)
}
}
r.executeLatency.Observe(time.Since(startTime).Seconds())
// TOTAL TARGET: < 50ms from sequencer event to execution
}
1.3 Sequencer Connection Management
// pkg/sequencer/connection.go
type ConnectionManager struct {
primaryURL string
backupURLs []string
currentConn *websocket.Conn
reconnectBackoff time.Duration
maxReconnectDelay time.Duration
}
func (cm *ConnectionManager) MaintainConnection(ctx context.Context) error {
for {
conn, err := cm.connectWithBackoff(ctx)
if err != nil {
return err
}
cm.currentConn = conn
// Monitor connection health
if err := cm.monitorConnection(ctx, conn); err != nil {
log.Error().Err(err).Msg("Connection lost, reconnecting...")
continue
}
}
}
func (cm *ConnectionManager) connectWithBackoff(ctx context.Context) (*websocket.Conn, error) {
backoff := cm.reconnectBackoff
for {
// Try primary endpoint
conn, err := websocket.Dial(cm.primaryURL)
if err == nil {
log.Info().Msg("Connected to primary sequencer endpoint")
return conn, nil
}
// Try backup endpoints
for _, backupURL := range cm.backupURLs {
conn, err := websocket.Dial(backupURL)
if err == nil {
log.Info().Str("endpoint", backupURL).Msg("Connected to backup sequencer endpoint")
return conn, nil
}
}
// Exponential backoff
select {
case <-time.After(backoff):
backoff = min(backoff*2, cm.maxReconnectDelay)
case <-ctx.Done():
return nil, ctx.Err()
}
}
}
1.4 Sequencer Data Validation
CRITICAL: Sequencer data is unconfirmed and may be reorganized. We MUST validate:
// pkg/sequencer/validator.go
type SequencerDataValidator struct {
cache *cache.PoolCache
}
func (v *SequencerDataValidator) ValidatePendingTx(tx *types.Transaction) error {
// 1. Validate transaction structure
if tx.To() == nil {
return errors.New("transaction has no recipient")
}
// 2. Validate gas price is reasonable
if tx.GasPrice().Cmp(maxReasonableGas) > 0 {
return errors.New("gas price unreasonably high")
}
// 3. Validate nonce progression
if !v.isNonceValid(tx) {
return errors.New("invalid nonce")
}
// 4. Validate signature
if !v.isSignatureValid(tx) {
return errors.New("invalid signature")
}
return nil
}
func (v *SequencerDataValidator) ValidateSwapEvent(event *SwapEvent) error {
// 1. Validate pool exists in cache
pool := v.cache.GetByAddress(event.PoolAddress)
if pool == nil {
return errors.New("unknown pool")
}
// 2. Validate tokens match pool
if pool.Token0 != event.Token0 || pool.Token1 != event.Token1 {
return errors.New("token mismatch")
}
// 3. Validate amounts are non-zero
if event.Amount0.Cmp(big.NewInt(0)) == 0 && event.Amount1.Cmp(big.NewInt(0)) == 0 {
return errors.New("zero amounts")
}
// 4. Validate amounts are reasonable (not dust, not astronomical)
if !v.isAmountReasonable(event.Amount0, pool.Token0Decimals) {
return errors.New("unreasonable amount0")
}
if !v.isAmountReasonable(event.Amount1, pool.Token1Decimals) {
return errors.New("unreasonable amount1")
}
return nil
}
2. Fast Parsing for Profitability
2.1 Parsing Speed Requirements
Why Speed Matters:
- If we take 100ms to parse, another bot executes first
- If we take 50ms, we capture the profit
- Target: < 5ms per transaction parse
2.2 Optimized Parser Architecture
// pkg/parsers/optimized_factory.go
type OptimizedFactory struct {
// Pre-compiled ABI parsers (cached at startup)
uniswapV2ABI abi.ABI
uniswapV3ABI abi.ABI
curveABI abi.ABI
balancerV2ABI abi.ABI
kyberABI abi.ABI
camelotABI abi.ABI
// Protocol router cache (address -> protocol type)
routerCache map[common.Address]ProtocolType
routerCacheMutex sync.RWMutex
// Pool cache for fast lookups
poolCache *cache.PoolCache
}
func (f *OptimizedFactory) ParseTransaction(tx *types.Transaction) ([]*SwapEvent, error) {
// Fast path: Check if this is a known router
f.routerCacheMutex.RLock()
protocol, exists := f.routerCache[*tx.To()]
f.routerCacheMutex.RUnlock()
if exists {
// Use cached protocol parser (FAST)
return f.parseWithProtocol(tx, protocol)
}
// Slow path: Identify protocol from transaction data
protocol = f.identifyProtocol(tx)
if protocol == ProtocolUnknown {
return nil, ErrUnknownProtocol
}
// Cache for next time
f.routerCacheMutex.Lock()
f.routerCache[*tx.To()] = protocol
f.routerCacheMutex.Unlock()
return f.parseWithProtocol(tx, protocol)
}
func (f *OptimizedFactory) parseWithProtocol(tx *types.Transaction, protocol ProtocolType) ([]*SwapEvent, error) {
// Route to optimized protocol-specific parser
switch protocol {
case ProtocolUniswapV2:
return f.parseUniswapV2(tx) // Optimized for UniV2
case ProtocolUniswapV3:
return f.parseUniswapV3(tx) // Optimized for UniV3
case ProtocolCurve:
return f.parseCurve(tx) // Optimized for Curve
// ... other protocols
default:
return nil, ErrUnknownProtocol
}
}
2.3 Parallel Parsing for Batches
// pkg/parsers/parallel.go
type ParallelParser struct {
factory *OptimizedFactory
workerPool *WorkerPool
}
func (p *ParallelParser) ParseBatch(txs []*types.Transaction) ([]*SwapEvent, error) {
results := make(chan *ParseResult, len(txs))
// Fan-out: Distribute transactions to worker pool
for _, tx := range txs {
tx := tx // Capture for goroutine
p.workerPool.Submit(func() {
events, err := p.factory.ParseTransaction(tx)
results <- &ParseResult{Events: events, Err: err}
})
}
// Fan-in: Collect results
var allEvents []*SwapEvent
for i := 0; i < len(txs); i++ {
result := <-results
if result.Err == nil {
allEvents = append(allEvents, result.Events...)
}
}
return allEvents, nil
}
3. Arbitrage Detection Algorithms
3.1 Multi-Hop Path Finding
// pkg/arbitrage/pathfinder.go
type PathFinder struct {
marketGraph *MarketGraph
maxHops int // Typically 2-4 hops
minProfit *big.Float
}
type ArbitragePath struct {
Hops []*Hop
InputToken common.Address
OutputToken common.Address
InputAmount *big.Int
OutputAmount *big.Int
GrossProfit *big.Float
GasCost *big.Int
NetProfit *big.Float
}
type Hop struct {
Pool *PoolState
Protocol ProtocolType
TokenIn common.Address
TokenOut common.Address
AmountIn *big.Int
AmountOut *big.Int
}
func (pf *PathFinder) FindArbitragePaths(priceImpact *PriceImpact) []*ArbitragePath {
// Starting token and amount from price impact
startToken := priceImpact.TokenOut
startAmount := priceImpact.AmountOut
var paths []*ArbitragePath
// BFS/DFS to find circular paths back to start token
pf.searchPaths(startToken, startToken, startAmount, []*Hop{}, &paths)
// Filter paths by profitability
var profitablePaths []*ArbitragePath
for _, path := range paths {
if path.NetProfit.Cmp(pf.minProfit) > 0 {
profitablePaths = append(profitablePaths, path)
}
}
// Sort by net profit (highest first)
sort.Slice(profitablePaths, func(i, j int) bool {
return profitablePaths[i].NetProfit.Cmp(profitablePaths[j].NetProfit) > 0
})
return profitablePaths
}
func (pf *PathFinder) searchPaths(
currentToken common.Address,
targetToken common.Address,
currentAmount *big.Int,
currentPath []*Hop,
results *[]*ArbitragePath,
) {
// Max depth reached
if len(currentPath) >= pf.maxHops {
return
}
// Get all pools with currentToken
pools := pf.marketGraph.GetPoolsWithToken(currentToken)
for _, pool := range pools {
// Determine output token
var outputToken common.Address
if pool.Token0 == currentToken {
outputToken = pool.Token1
} else {
outputToken = pool.Token0
}
// Calculate output amount
outputAmount := pf.calculateSwapOutput(pool, currentToken, outputToken, currentAmount)
if outputAmount.Cmp(big.NewInt(0)) == 0 {
continue // Insufficient liquidity
}
// Create hop
hop := &Hop{
Pool: pool,
Protocol: pool.Protocol,
TokenIn: currentToken,
TokenOut: outputToken,
AmountIn: currentAmount,
AmountOut: outputAmount,
}
// Add to path
newPath := append(currentPath, hop)
// Check if we've returned to target token (arbitrage loop complete)
if outputToken == targetToken && len(newPath) >= 2 {
// Calculate profitability
path := pf.buildArbitragePath(newPath)
if path != nil {
*results = append(*results, path)
}
continue // Don't recurse further
}
// Recurse to find more hops
pf.searchPaths(outputToken, targetToken, outputAmount, newPath, results)
}
}
3.2 Profitability Calculation
// pkg/arbitrage/profitability.go
type ProfitCalculator struct {
gasOracle *GasOracle
ethPrice *big.Float // ETH price in USD
}
func (pc *ProfitCalculator) CalculateNetProfit(path *ArbitragePath) *big.Float {
// 1. Calculate gross profit (output - input, in same token)
inputAmountFloat := new(big.Float).SetInt(path.InputAmount)
outputAmountFloat := new(big.Float).SetInt(path.OutputAmount)
// Scale to 18 decimals for comparison
inputScaled := pc.scaleToDecimals(inputAmountFloat, 18)
outputScaled := pc.scaleToDecimals(outputAmountFloat, 18)
grossProfit := new(big.Float).Sub(outputScaled, inputScaled)
// 2. Estimate gas cost
gasCost := pc.estimateGasCost(path)
gasCostFloat := new(big.Float).SetInt(gasCost)
// 3. Calculate net profit
netProfit := new(big.Float).Sub(grossProfit, gasCostFloat)
return netProfit
}
func (pc *ProfitCalculator) estimateGasCost(path *ArbitragePath) *big.Int {
// Base transaction cost
baseCost := big.NewInt(21000)
// Per-hop cost (varies by protocol)
var totalGas = big.NewInt(0)
totalGas.Add(totalGas, baseCost)
for _, hop := range path.Hops {
var hopGas *big.Int
switch hop.Protocol {
case ProtocolUniswapV2:
hopGas = big.NewInt(100000) // ~100k gas per UniV2 swap
case ProtocolUniswapV3:
hopGas = big.NewInt(150000) // ~150k gas per UniV3 swap
case ProtocolCurve:
hopGas = big.NewInt(120000) // ~120k gas per Curve swap
case ProtocolBalancerV2:
hopGas = big.NewInt(130000) // ~130k gas per Balancer swap
default:
hopGas = big.NewInt(120000) // Default estimate
}
totalGas.Add(totalGas, hopGas)
}
// Get current gas price from oracle
gasPrice := pc.gasOracle.GetCurrentGasPrice()
// Calculate total cost
gasCost := new(big.Int).Mul(totalGas, gasPrice)
return gasCost
}
3.3 Real-Time Arbitrage Detection
// pkg/arbitrage/detector.go
type Detector struct {
pathFinder *PathFinder
profitCalc *ProfitCalculator
marketGraph *MarketGraph
minProfitUSD float64 // Minimum profit in USD
// Performance metrics
detectLatency prometheus.Histogram
opportunitiesFound prometheus.Counter
}
func (d *Detector) FindOpportunities(priceImpacts []*PriceImpact) []*ArbitragePath {
startTime := time.Now()
defer func() {
d.detectLatency.Observe(time.Since(startTime).Seconds())
}()
var allPaths []*ArbitragePath
// Check each price impact for arbitrage opportunities
for _, impact := range priceImpacts {
// Find all possible arbitrage paths
paths := d.pathFinder.FindArbitragePaths(impact)
// Calculate profitability for each path
for _, path := range paths {
netProfit := d.profitCalc.CalculateNetProfit(path)
path.NetProfit = netProfit
// Filter by minimum profit
minProfitFloat := big.NewFloat(d.minProfitUSD)
if netProfit.Cmp(minProfitFloat) > 0 {
allPaths = append(allPaths, path)
d.opportunitiesFound.Inc()
}
}
}
return allPaths
}
4. Execution Strategies
4.1 Front-Running Execution
// pkg/execution/frontrun.go
type FrontRunner struct {
client *ethclient.Client
signer types.Signer
privateKey *ecdsa.PrivateKey
flashbots *FlashbotsClient // Optional: MEV protection
maxGasPrice *big.Int
gasPremium float64 // Multiplier above target tx gas price
}
func (fr *FrontRunner) ExecuteFrontRun(
ctx context.Context,
opportunity *ArbitragePath,
targetTx *types.Transaction,
) (*types.Transaction, error) {
// 1. Build arbitrage transaction
arbTx, err := fr.buildArbitrageTx(opportunity)
if err != nil {
return nil, fmt.Errorf("failed to build arbitrage tx: %w", err)
}
// 2. Set gas price higher than target transaction
targetGasPrice := targetTx.GasPrice()
frontRunGasPrice := fr.calculateFrontRunGasPrice(targetGasPrice)
if frontRunGasPrice.Cmp(fr.maxGasPrice) > 0 {
return nil, errors.New("required gas price exceeds maximum")
}
arbTx.GasPrice = frontRunGasPrice
// 3. Sign transaction
signedTx, err := types.SignTx(arbTx, fr.signer, fr.privateKey)
if err != nil {
return nil, fmt.Errorf("failed to sign tx: %w", err)
}
// 4. Submit to network (choose path based on configuration)
if fr.flashbots != nil {
// Use Flashbots to avoid frontrunning ourselves
return fr.flashbots.SendBundle(ctx, []*types.Transaction{signedTx, targetTx})
} else {
// Direct submission to mempool
err = fr.client.SendTransaction(ctx, signedTx)
if err != nil {
return nil, fmt.Errorf("failed to send tx: %w", err)
}
return signedTx, nil
}
}
func (fr *FrontRunner) calculateFrontRunGasPrice(targetGasPrice *big.Int) *big.Int {
// Add premium to ensure we're ahead
premium := new(big.Float).SetFloat64(fr.gasPremium)
targetFloat := new(big.Float).SetInt(targetGasPrice)
frontRunFloat := new(big.Float).Mul(targetFloat, premium)
frontRunGasPrice, _ := frontRunFloat.Int(nil)
return frontRunGasPrice
}
4.2 Batch Execution
// pkg/execution/batch.go
type BatchExecutor struct {
multicallContract common.Address
client *ethclient.Client
signer types.Signer
privateKey *ecdsa.PrivateKey
}
// Multicall contract interface
type Multicall interface {
Aggregate(calls []Call) (*big.Int, []byte, error)
}
type Call struct {
Target common.Address
CallData []byte
}
func (be *BatchExecutor) ExecuteBatch(
ctx context.Context,
opportunities []*ArbitragePath,
) (*types.Transaction, error) {
// 1. Build multicall data
calls := make([]Call, 0, len(opportunities))
for _, opp := range opportunities {
callData, err := be.buildSwapCallData(opp)
if err != nil {
continue // Skip this opportunity
}
calls = append(calls, Call{
Target: opp.Hops[0].Pool.Address,
CallData: callData,
})
}
if len(calls) == 0 {
return nil, errors.New("no valid calls to batch")
}
// 2. Encode multicall transaction
multicallABI, _ := abi.JSON(strings.NewReader(MulticallABI))
multicallData, err := multicallABI.Pack("aggregate", calls)
if err != nil {
return nil, fmt.Errorf("failed to pack multicall: %w", err)
}
// 3. Build transaction
nonce, err := be.client.PendingNonceAt(ctx, crypto.PubkeyToAddress(be.privateKey.PublicKey))
if err != nil {
return nil, err
}
gasPrice, err := be.client.SuggestGasPrice(ctx)
if err != nil {
return nil, err
}
tx := types.NewTransaction(
nonce,
be.multicallContract,
big.NewInt(0), // No ETH sent
2000000, // Gas limit (adjust based on batch size)
gasPrice,
multicallData,
)
// 4. Sign and send
signedTx, err := types.SignTx(tx, be.signer, be.privateKey)
if err != nil {
return nil, err
}
err = be.client.SendTransaction(ctx, signedTx)
if err != nil {
return nil, err
}
return signedTx, nil
}
4.3 Smart Execution Router
// pkg/execution/router.go
type ExecutionRouter struct {
frontRunner *FrontRunner
batchExecutor *BatchExecutor
batchThreshold int // Min opportunities to batch
batchWindow time.Duration // Time to wait for batching
}
func (er *ExecutionRouter) Execute(
ctx context.Context,
opportunities []*ArbitragePath,
targetTx *types.Transaction,
) error {
// Decision: Single vs Batch execution
if len(opportunities) == 1 {
// Single opportunity: Front-run immediately
_, err := er.frontRunner.ExecuteFrontRun(ctx, opportunities[0], targetTx)
return err
}
if len(opportunities) >= er.batchThreshold {
// Multiple opportunities: Batch for gas savings
_, err := er.batchExecutor.ExecuteBatch(ctx, opportunities)
return err
}
// Between 1 and threshold: Wait for batch window
select {
case <-time.After(er.batchWindow):
// Execute what we have
if len(opportunities) > 1 {
_, err := er.batchExecutor.ExecuteBatch(ctx, opportunities)
return err
}
_, err := er.frontRunner.ExecuteFrontRun(ctx, opportunities[0], targetTx)
return err
case <-ctx.Done():
return ctx.Err()
}
}
5. Gas Optimization for Maximum Profit
5.1 Dynamic Gas Pricing
// pkg/gas/oracle.go
type GasOracle struct {
client *ethclient.Client
historicalPrices []HistoricalGasPrice
// Gas price strategy
strategy GasStrategy
}
type GasStrategy int
const (
StrategyFast GasStrategy = iota // Pay more to execute faster
StrategyOptimal // Balance speed vs cost
StrategyEconomy // Minimize gas cost
)
type HistoricalGasPrice struct {
Timestamp time.Time
GasPrice *big.Int
BaseFee *big.Int
}
func (go *GasOracle) GetOptimalGasPrice(opportunity *ArbitragePath) *big.Int {
// Get current network gas prices
currentGasPrice, _ := go.client.SuggestGasPrice(context.Background())
// Calculate maximum gas price we can pay while remaining profitable
maxProfitableGasPrice := go.calculateMaxProfitableGasPrice(opportunity)
// Apply strategy
switch go.strategy {
case StrategyFast:
// Use higher percentile (90th) to ensure quick inclusion
return go.calculatePercentileGasPrice(90)
case StrategyOptimal:
// Use median gas price, capped at max profitable
medianGas := go.calculatePercentileGasPrice(50)
if medianGas.Cmp(maxProfitableGasPrice) > 0 {
return maxProfitableGasPrice
}
return medianGas
case StrategyEconomy:
// Use 25th percentile, capped at max profitable
lowGas := go.calculatePercentileGasPrice(25)
if lowGas.Cmp(maxProfitableGasPrice) > 0 {
return maxProfitableGasPrice
}
return lowGas
default:
return currentGasPrice
}
}
func (go *GasOracle) calculateMaxProfitableGasPrice(opportunity *ArbitragePath) *big.Int {
// Given net profit and gas usage, what's the max gas price?
// netProfit = grossProfit - (gasUsed * gasPrice)
// gasPrice = (grossProfit - netProfit) / gasUsed
grossProfit := opportunity.GrossProfit
minAcceptableProfit := big.NewFloat(0.01) // 0.01 ETH minimum
profitForGas := new(big.Float).Sub(grossProfit, minAcceptableProfit)
gasUsed := new(big.Float).SetInt64(int64(opportunity.EstimatedGas))
maxGasPrice := new(big.Float).Quo(profitForGas, gasUsed)
maxGasPriceInt, _ := maxGasPrice.Int(nil)
return maxGasPriceInt
}
5.2 Gas Estimation
// pkg/gas/estimator.go
type GasEstimator struct {
client *ethclient.Client
// Cached estimates by protocol and hop count
estimateCache map[string]uint64
cacheMutex sync.RWMutex
}
func (ge *GasEstimator) EstimateArbitrageGas(path *ArbitragePath) (uint64, error) {
// Generate cache key
cacheKey := ge.generateCacheKey(path)
// Check cache
ge.cacheMutex.RLock()
if cached, exists := ge.estimateCache[cacheKey]; exists {
ge.cacheMutex.RUnlock()
return cached, nil
}
ge.cacheMutex.RUnlock()
// Build transaction
tx, err := ge.buildArbitrageTx(path)
if err != nil {
return 0, err
}
// Estimate gas
estimatedGas, err := ge.client.EstimateGas(context.Background(), ethereum.CallMsg{
From: crypto.PubkeyToAddress(ge.privateKey.PublicKey),
To: tx.To(),
Gas: 0,
GasPrice: big.NewInt(0),
Value: big.NewInt(0),
Data: tx.Data(),
})
if err != nil {
return 0, err
}
// Add safety margin (10%)
estimatedGas = estimatedGas * 110 / 100
// Cache for future use
ge.cacheMutex.Lock()
ge.estimateCache[cacheKey] = estimatedGas
ge.cacheMutex.Unlock()
return estimatedGas, nil
}
6. Risk Management
6.1 Slippage Protection
// pkg/execution/slippage.go
type SlippageProtector struct {
maxSlippage float64 // e.g., 0.005 = 0.5%
}
func (sp *SlippageProtector) CalculateMinOutput(
expectedOutput *big.Int,
) *big.Int {
// Calculate minimum acceptable output with slippage
expectedFloat := new(big.Float).SetInt(expectedOutput)
slippageFactor := 1.0 - sp.maxSlippage
minOutputFloat := new(big.Float).Mul(
expectedFloat,
big.NewFloat(slippageFactor),
)
minOutput, _ := minOutputFloat.Int(nil)
return minOutput
}
func (sp *SlippageProtector) ValidateExecution(
expectedProfit *big.Float,
actualProfit *big.Float,
) error {
// Check if actual profit is within acceptable slippage
expectedFloat := expectedProfit
slippageFactor := big.NewFloat(1.0 - sp.maxSlippage)
minAcceptableProfit := new(big.Float).Mul(expectedFloat, slippageFactor)
if actualProfit.Cmp(minAcceptableProfit) < 0 {
return fmt.Errorf(
"slippage too high: expected %s, got %s",
expectedProfit.String(),
actualProfit.String(),
)
}
return nil
}
6.2 Circuit Breaker
// pkg/safety/circuit_breaker.go
type CircuitBreaker struct {
maxLossPerHour *big.Float
maxFailuresPerMin int
// State tracking
hourlyLoss *big.Float
recentFailures []time.Time
// Circuit state
state CircuitState
stateMutex sync.RWMutex
resetAfter time.Duration
}
type CircuitState int
const (
StateClosed CircuitState = iota // Normal operation
StateOpen // Circuit tripped, blocking trades
StateHalfOpen // Testing if safe to resume
)
func (cb *CircuitBreaker) AllowExecution() bool {
cb.stateMutex.RLock()
defer cb.stateMutex.RUnlock()
return cb.state == StateClosed || cb.state == StateHalfOpen
}
func (cb *CircuitBreaker) RecordFailure(loss *big.Float) {
cb.stateMutex.Lock()
defer cb.stateMutex.Unlock()
// Track recent failures
cb.recentFailures = append(cb.recentFailures, time.Now())
// Add to hourly loss
cb.hourlyLoss.Add(cb.hourlyLoss, loss)
// Check if we should trip the circuit
if cb.shouldTrip() {
cb.state = StateOpen
log.Error().Msg("Circuit breaker TRIPPED - halting trading")
// Schedule automatic reset
time.AfterFunc(cb.resetAfter, cb.attemptReset)
}
}
func (cb *CircuitBreaker) shouldTrip() bool {
// Check hourly loss threshold
if cb.hourlyLoss.Cmp(cb.maxLossPerHour) > 0 {
return true
}
// Check failure rate
oneMinAgo := time.Now().Add(-1 * time.Minute)
recentFailureCount := 0
for _, t := range cb.recentFailures {
if t.After(oneMinAgo) {
recentFailureCount++
}
}
if recentFailureCount >= cb.maxFailuresPerMin {
return true
}
return false
}
func (cb *CircuitBreaker) attemptReset() {
cb.stateMutex.Lock()
defer cb.stateMutex.Unlock()
// Move to half-open state (allow limited testing)
cb.state = StateHalfOpen
log.Info().Msg("Circuit breaker entering HALF-OPEN state")
// Reset counters
cb.hourlyLoss = big.NewFloat(0)
cb.recentFailures = []time.Time{}
}
6.3 Position Sizing
// pkg/safety/position_sizer.go
type PositionSizer struct {
totalCapital *big.Float
maxPositionSize float64 // e.g., 0.1 = 10% of capital
maxConcurrentTrades int
currentPositions []*Position
positionsMutex sync.RWMutex
}
type Position struct {
ID string
Size *big.Float
EntryTime time.Time
ExitTime *time.Time
Profit *big.Float
}
func (ps *PositionSizer) CalculatePositionSize(opportunity *ArbitragePath) *big.Int {
ps.positionsMutex.RLock()
defer ps.positionsMutex.RUnlock()
// Calculate maximum allowed position size
maxSize := new(big.Float).Mul(
ps.totalCapital,
big.NewFloat(ps.maxPositionSize),
)
// Check if we have room for another position
if len(ps.currentPositions) >= ps.maxConcurrentTrades {
return big.NewInt(0) // No room for more trades
}
// Use the smaller of: max position size or opportunity input amount
opportunitySize := new(big.Float).SetInt(opportunity.InputAmount)
var positionSize *big.Float
if opportunitySize.Cmp(maxSize) < 0 {
positionSize = opportunitySize
} else {
positionSize = maxSize
}
positionSizeInt, _ := positionSize.Int(nil)
return positionSizeInt
}
7. Profitability Metrics and Tracking
7.1 Metrics Collection
// pkg/metrics/profitability.go
type ProfitabilityMetrics struct {
// Prometheus metrics
totalProfit prometheus.Counter
totalLoss prometheus.Counter
tradesExecuted prometheus.Counter
tradesProfitable prometheus.Counter
tradesUnprofitable prometheus.Counter
averageProfit prometheus.Gauge
averageLoss prometheus.Gauge
successRate prometheus.Gauge
gasCostTotal prometheus.Counter
netProfitTotal prometheus.Counter
// Profit by protocol
profitByProtocol map[ProtocolType]prometheus.Counter
// Database for detailed tracking
db *sql.DB
}
func (pm *ProfitabilityMetrics) RecordTrade(trade *CompletedTrade) error {
// Update Prometheus metrics
if trade.NetProfit.Cmp(big.NewFloat(0)) > 0 {
pm.totalProfit.Add(trade.NetProfit.Float64())
pm.tradesProfitable.Inc()
} else {
loss := new(big.Float).Abs(trade.NetProfit)
pm.totalLoss.Add(loss.Float64())
pm.tradesUnprofitable.Inc()
}
pm.tradesExecuted.Inc()
pm.gasCostTotal.Add(trade.GasCost.Float64())
pm.netProfitTotal.Add(trade.NetProfit.Float64())
// Calculate success rate
total := pm.tradesProfitable.Value() + pm.tradesUnprofitable.Value()
if total > 0 {
successRate := pm.tradesProfitable.Value() / total
pm.successRate.Set(successRate)
}
// Update per-protocol metrics
for _, hop := range trade.Path.Hops {
pm.profitByProtocol[hop.Protocol].Add(trade.NetProfit.Float64())
}
// Store in database for historical analysis
return pm.storeTrade(trade)
}
func (pm *ProfitabilityMetrics) storeTrade(trade *CompletedTrade) error {
_, err := pm.db.Exec(`
INSERT INTO trades (
tx_hash,
timestamp,
path,
gross_profit,
gas_cost,
net_profit,
success
) VALUES (?, ?, ?, ?, ?, ?, ?)
`,
trade.TxHash.Hex(),
trade.Timestamp,
trade.Path.String(),
trade.GrossProfit.String(),
trade.GasCost.String(),
trade.NetProfit.String(),
trade.Success,
)
return err
}
7.2 Real-Time Profitability Dashboard
// pkg/metrics/dashboard.go
type Dashboard struct {
metrics *ProfitabilityMetrics
}
func (d *Dashboard) GetCurrentStats() *DashboardStats {
return &DashboardStats{
TotalTrades: d.metrics.tradesExecuted.Value(),
ProfitableTrades: d.metrics.tradesProfitable.Value(),
FailedTrades: d.metrics.tradesUnprofitable.Value(),
SuccessRate: d.metrics.successRate.Value(),
TotalProfit: d.metrics.totalProfit.Value(),
TotalLoss: d.metrics.totalLoss.Value(),
TotalGasCost: d.metrics.gasCostTotal.Value(),
NetProfit: d.metrics.netProfitTotal.Value(),
AverageProfit: d.metrics.averageProfit.Value(),
AverageLoss: d.metrics.averageLoss.Value(),
ProfitByProtocol: d.getProfitByProtocol(),
}
}
type DashboardStats struct {
TotalTrades float64
ProfitableTrades float64
FailedTrades float64
SuccessRate float64
TotalProfit float64
TotalLoss float64
TotalGasCost float64
NetProfit float64
AverageProfit float64
AverageLoss float64
ProfitByProtocol map[ProtocolType]float64
}
7.3 Performance Analysis
// pkg/analytics/performance.go
type PerformanceAnalyzer struct {
db *sql.DB
}
func (pa *PerformanceAnalyzer) AnalyzeDaily() (*DailyReport, error) {
// Query database for today's trades
rows, err := pa.db.Query(`
SELECT
COUNT(*) as total_trades,
SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as profitable,
SUM(gross_profit) as gross_profit,
SUM(gas_cost) as gas_cost,
SUM(net_profit) as net_profit,
AVG(net_profit) as avg_profit,
MAX(net_profit) as max_profit,
MIN(net_profit) as min_profit
FROM trades
WHERE DATE(timestamp) = CURDATE()
`)
if err != nil {
return nil, err
}
defer rows.Close()
var report DailyReport
rows.Next()
rows.Scan(
&report.TotalTrades,
&report.ProfitableTrades,
&report.GrossProfit,
&report.GasCost,
&report.NetProfit,
&report.AvgProfit,
&report.MaxProfit,
&report.MinProfit,
)
report.SuccessRate = float64(report.ProfitableTrades) / float64(report.TotalTrades)
report.ROI = report.NetProfit / report.TotalCapitalDeployed
return &report, nil
}
type DailyReport struct {
TotalTrades int
ProfitableTrades int
SuccessRate float64
GrossProfit float64
GasCost float64
NetProfit float64
AvgProfit float64
MaxProfit float64
MinProfit float64
TotalCapitalDeployed float64
ROI float64
}
8. Complete System Architecture
8.1 High-Level Flow
┌─────────────────────────────────────────────────────────────────┐
│ ARBITRUM SEQUENCER │
│ (Pending Transactions) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ SEQUENCER READER │
│ • WebSocket connection │
│ • Connection health monitoring │
│ • Automatic reconnection │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ TRANSACTION PARSER │
│ • Protocol identification (< 1ms) │
│ • Protocol-specific parsing (< 5ms) │
│ • Parallel batch processing │
│ • ABI caching │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ VALIDATOR │
│ • Validate swap events │
│ • Check pool cache │
│ • Verify amounts (non-zero, reasonable) │
│ • Validate tokens (no zero addresses) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ PRICE IMPACT CALCULATOR │
│ • Calculate price change from swap │
│ • Update pool reserves │
│ • Proper decimal handling │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ ARBITRAGE DETECTOR │
│ • Multi-hop path finding (BFS/DFS) │
│ • Calculate gross profit │
│ • Estimate gas cost │
│ • Calculate net profit │
│ • Filter by minimum profit │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ RISK MANAGEMENT │
│ • Position sizing │
│ • Slippage protection │
│ • Circuit breaker check │
│ • Max concurrent positions │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ EXECUTION ROUTER │
│ • Single vs batch decision │
│ • Gas optimization │
│ • Transaction building │
│ • Signing │
└────────────────────────────┬────────────────────────────────────┘
│
┌──────────┴──────────┐
│ │
▼ ▼
┌────────────────────┐ ┌────────────────────┐
│ FRONT-RUNNER │ │ BATCH EXECUTOR │
│ │ │ │
│ • Higher gas price │ │ • Multicall │
│ • Single trade │ │ • Multiple trades │
│ • Flashbots option │ │ • Gas savings │
└──────────┬─────────┘ └─────────┬──────────┘
│ │
└──────────┬───────────┘
│
▼
┌──────────────────────────┐
│ ETHEREUM NETWORK │
│ (Transaction Submitted) │
└─────────────┬────────────┘
│
▼
┌──────────────────────────┐
│ METRICS COLLECTOR │
│ │
│ • Record trade │
│ • Calculate profit │
│ • Update dashboard │
│ • Store in database │
└──────────────────────────┘
8.2 Performance Targets
Latency Targets:
Sequencer to Parse: < 5ms
Parse to Validate: < 2ms
Validate to Detect: < 10ms
Detect to Execute: < 30ms
Total (End-to-End): < 50ms
Throughput Targets:
Transactions per second: 1000+
Concurrent parsing: 100+ goroutines
Batch size: 10-50 trades
Profitability Targets:
Success rate: > 85%
Minimum profit per trade: > 0.05 ETH
Daily trades: 50-200
Monthly ROI: > 20%
Max drawdown: < 5%
8.3 Configuration
# config/production.yaml
sequencer:
primary_url: "wss://arb1.arbitrum.io/feed"
backup_urls:
- "wss://arbitrum-one.publicnode.com"
- "wss://arb-mainnet.g.alchemy.com/v2/YOUR_KEY"
reconnect_backoff: 1s
max_reconnect_delay: 60s
parsing:
worker_pool_size: 100
batch_size: 50
timeout: 5ms
arbitrage:
min_profit_usd: 50.0
max_hops: 4
path_finding_timeout: 10ms
execution:
strategy: "smart" # frontrun, batch, or smart
batch_threshold: 3
batch_window: 100ms
max_gas_price: "500000000000" # 500 gwei
gas_premium: 1.1 # 10% above target tx
gas:
strategy: "optimal" # fast, optimal, or economy
safety_margin: 1.1 # 10% buffer on estimates
risk:
max_slippage: 0.005 # 0.5%
max_position_pct: 0.1 # 10% of capital
max_concurrent_trades: 5
max_loss_per_hour: 1.0 # 1 ETH
max_failures_per_min: 10
circuit_breaker_reset: 1h
monitoring:
metrics_port: 9090
dashboard_port: 8080
log_level: "info"
enable_profiling: true
9. Implementation Roadmap
Phase 1: Sequencer Integration (Week 1-2)
Tasks:
- Set up Arbitrum sequencer WebSocket connection
- Implement connection health monitoring
- Build automatic reconnection logic
- Create pending transaction stream processor
- Add latency monitoring (sequencer → parser)
- Test with live sequencer feed
- Validate transaction ordering
Success Criteria:
- Stable connection for 24+ hours
- < 1ms latency from sequencer event to parser
- Automatic recovery from disconnections
- Zero missed transactions
Phase 2: High-Performance Parsing (Week 2-3)
Tasks:
- Implement optimized parser factory
- Build protocol identification cache
- Create parallel parsing worker pool
- Add per-protocol optimized parsers
- Implement decimal handling (critical)
- Add parsing validation
- Benchmark parsing performance
Success Criteria:
- < 5ms per transaction parse
- 100% accuracy on all supported protocols
- Proper decimal scaling
- No zero addresses
- Handle 1000+ tx/sec
Phase 3: Arbitrage Detection (Week 3-4)
Tasks:
- Build market graph from pool cache
- Implement BFS/DFS path finding
- Create profitability calculator
- Add gas cost estimator
- Implement profit filtering
- Optimize path finding performance
- Test with historical data
Success Criteria:
- < 10ms path finding
- Find all 2-4 hop opportunities
- Accurate profit calculations
- Gas estimates within 10% of actual
Phase 4: Execution Engine (Week 4-5)
Tasks:
- Build front-run executor
- Implement batch executor
- Create smart execution router
- Add gas optimization
- Integrate Flashbots (optional)
- Build transaction monitoring
- Add execution metrics
Success Criteria:
- < 30ms execution decision
- Successful front-running
- Gas optimization working
- Transaction monitoring
- Flashbots integration (if used)
Phase 5: Risk Management (Week 5-6)
Tasks:
- Implement slippage protection
- Build circuit breaker
- Add position sizing
- Create risk monitoring dashboard
- Test failure scenarios
- Add automated alerts
Success Criteria:
- Slippage protection working
- Circuit breaker trips on losses
- Position limits enforced
- Alert system functional
Phase 6: Metrics & Monitoring (Week 6-7)
Tasks:
- Set up Prometheus metrics
- Build profitability tracker
- Create real-time dashboard
- Add database for trade history
- Implement performance analyzer
- Build daily reports
Success Criteria:
- All metrics collected
- Dashboard shows real-time data
- Trade history stored
- Daily reports generated
Phase 7: Testing & Optimization (Week 7-8)
Tasks:
- End-to-end testing with testnet
- Performance optimization
- Load testing (1000+ tx/sec)
- Chaos testing (failure scenarios)
- Production deployment preparation
- Documentation
Success Criteria:
- < 50ms end-to-end latency
- 85%+ success rate in testing
- Passes all load tests
- Handles failure scenarios
- Ready for production
10. Profitability Projections
Conservative Scenario
Assumptions:
- 50 trades per day
- 60% success rate
- Average profit per successful trade: 0.05 ETH
- Average loss per failed trade: 0.02 ETH
- Average gas cost: 0.01 ETH per trade
Daily Calculation:
- Successful trades: 30 * 0.05 ETH = 1.5 ETH
- Failed trades: 20 * -0.02 ETH = -0.4 ETH
- Gas costs: 50 * 0.01 ETH = -0.5 ETH
- Net daily profit: 0.6 ETH
Monthly: 0.6 ETH * 30 = 18 ETH
Yearly: 18 ETH * 12 = 216 ETH
With 10 ETH capital deployed:
Monthly ROI: 180%
Yearly ROI: 2160%
Moderate Scenario
Assumptions:
- 100 trades per day
- 75% success rate
- Average profit per successful trade: 0.08 ETH
- Average loss per failed trade: 0.02 ETH
- Average gas cost: 0.008 ETH per trade (batch optimization)
Daily Calculation:
- Successful trades: 75 * 0.08 ETH = 6.0 ETH
- Failed trades: 25 * -0.02 ETH = -0.5 ETH
- Gas costs: 100 * 0.008 ETH = -0.8 ETH
- Net daily profit: 4.7 ETH
Monthly: 4.7 ETH * 30 = 141 ETH
Yearly: 141 ETH * 12 = 1692 ETH
With 20 ETH capital deployed:
Monthly ROI: 705%
Yearly ROI: 8460%
Optimistic Scenario
Assumptions:
- 200 trades per day
- 85% success rate
- Average profit per successful trade: 0.1 ETH
- Average loss per failed trade: 0.02 ETH
- Average gas cost: 0.006 ETH per trade (aggressive batching)
Daily Calculation:
- Successful trades: 170 * 0.1 ETH = 17.0 ETH
- Failed trades: 30 * -0.02 ETH = -0.6 ETH
- Gas costs: 200 * 0.006 ETH = -1.2 ETH
- Net daily profit: 15.2 ETH
Monthly: 15.2 ETH * 30 = 456 ETH
Yearly: 456 ETH * 12 = 5472 ETH
With 50 ETH capital deployed:
Monthly ROI: 912%
Yearly ROI: 10944%
Note: These are theoretical projections. Actual results will vary based on market conditions, competition, execution quality, and unforeseen factors.
11. Key Success Factors
1. Speed is Everything
- 50ms end-to-end latency is the target
- Every millisecond counts in MEV
- Optimize at every layer (network, parsing, detection, execution)
2. Sequencer Access is the Moat
- Direct sequencer feed = competitive advantage
- Reading pending transactions before they're on-chain
- 100-500ms time advantage over traditional bots
3. Accurate Parsing is Critical
- Wrong parsing = wrong profit calculations = losses
- Protocol-specific parsers for accuracy
- Proper decimal handling (non-negotiable)
- Validate everything
4. Gas Optimization Directly Impacts Profit
- High gas prices eat into profits
- Batching saves gas
- Smart gas pricing (don't overpay)
- Flashbots for MEV protection
5. Risk Management Protects Capital
- Slippage protection prevents bad trades
- Circuit breaker prevents cascading losses
- Position sizing limits exposure
- Monitor everything
6. Continuous Optimization
- Monitor metrics constantly
- Analyze what works and what doesn't
- Optimize slow paths
- Add new protocols as they launch
12. Next Steps
- Review and approve this plan
- Set up development environment
- Begin Phase 1: Sequencer Integration
- Follow task breakdown from 07_TASK_BREAKDOWN.md
- Create feature branches:
feature/v2/sequencer/<task-id> - Implement with 100% test coverage
- Deploy to testnet for validation
- Deploy to production with monitoring
This is the complete profitability plan. The sequencer reader/parser is the foundation that enables everything else. Let's build it.