A policy-aware, risk-bounded NFT arbitrage system that buys the lowest listing and sells into the best collection offer on OpenSea (Base), using private transactions and survival-weighted execution to minimize inventory risk.
- Risk-First Design: Multi-layered protection with survival probability gating, daily drawdown caps, and per-trade loss limits
- Private Transactions: Uses Alchemy's private transaction endpoint to minimize MEV exposure
- Adaptive Scanning: Scan frequency adjusts based on market volatility (survival λ)
- Bounded Failover: Automatically tries alternative bids if primary sell fails
- 721C Compliant: Respects creator royalties and uses approved conduits
- Comprehensive Observability: Hourly Slack reports, persistent state tracking, detailed skip reasons
src/
├── index.ts # Main entry point
├── worker.ts # Main worker loop with adaptive scanning
├── config.ts # Configuration validation
├── types.ts # TypeScript type definitions
├── feeds/
│ ├── opensea.ts # OpenSea API client
│ └── reservoir.ts # Reservoir API client (discovery + prices)
├── strategy/
│ ├── collectionOfferArb.ts # Arbitrage strategy logic
│ ├── survival.ts # Survival probability model (λ calculation)
│ └── risk.ts # Drawdown tracking & risk management
├── execution/
│ ├── exec.ts # Trade execution with failover
│ └── approval.ts # ERC-721 approval management
├── state/
│ └── manager.ts # Persistent state (trades, PnL, approvals)
├── report/
│ └── hourly.ts # Slack reporting with blocks
└── utils/
└── logger.ts # Structured logging
- Node.js 20+
- An Alchemy account (Base RPC + private transactions)
- OpenSea API key
- Slack webhook URL
- A dedicated wallet with ETH on Base
# Clone the repository
git clone <repo-url>
cd base-721c-arbitrage-bot
# Install dependencies
npm install
# Copy and configure environment
cp .env.example .env
# Edit .env with your credentialsKey environment variables (see .env.example for full list):
# Required
RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
PRIVATE_KEY=your_wallet_private_key
OPENSEA_API_KEY=your_opensea_key
SLACK_WEBHOOK_URL=your_slack_webhook
ALCHEMY_API_KEY=your_alchemy_key
# Risk parameters
MIN_PROFIT_USD=10 # Minimum profit per trade
MAX_DAILY_DRAWDOWN_ETH=0.2 # Halt if daily loss exceeds this
MAX_SINGLE_TRADE_LOSS_ETH=0.05 # Max acceptable loss per trade
# Collection discovery
SLUGS= # Leave empty for auto-discovery
MIN_DAILY_VOLUME_USD=10000 # Minimum 24h volume filter
MAX_COLLECTIONS=20 # Max collections to monitor# Development mode
npm run dev
# Production build
npm run build
npm start- Push code to GitHub
- Connect repository to Render
- Create a new Worker service
- Set environment variables in Render UI (secrets)
- Add persistent disk:
/var/data/state(1GB) - Deploy
The render.yaml file contains the full deployment configuration.
- Discovery: Auto-discover Base collections via Reservoir (or use manual slugs)
- Quote Ingestion: Fetch lowest listing + best collection offer from OpenSea
- Analysis: Calculate fees, royalties, gas, and net profit
- Survival Check: Gate trades with pSurvive ≥ 0.85 (configurable)
- Risk Check: Verify drawdown and single-trade loss limits
- Execution: Buy via private tx → Sell with failover to top-N bids
- Reporting: Log trade, update PnL, send hourly Slack summaries
The bot tracks bid vanish events in a 15-minute rolling window to calculate arrival rate λ:
pSurvive = exp(-λ × executionTime)
- Cold start (first 30 min): Uses pessimistic prior λ = 0.08
- Warm: Exponentially-weighted average of recent vanish events
- Trades gated at pSurvive ≥ 0.85 (default)
If primary sell fails:
- Fetch top N collection bids (default: 3)
- For each bid:
- Rebuild fulfillment data
- Estimate gas dynamically
- Check if net profit ≥ -MAX_SINGLE_TRADE_LOSS_ETH
- Accept first successful sell within timeout (30s default)
- Alert operator if inventory stuck after exhausting all options
All state is persisted to disk for restart safety:
state/
├── trades.csv # All executed trades
├── pnl-YYYY-MM-DD.json # Daily PnL summaries
└── approvals.json # ERC-721 approval cache
Automatically sent every hour (only if trades executed):
- Total trades, wins, losses
- Win rate %
- Net PnL (ETH)
- Top and bottom fills
- Drawdown breach: Trading halted
- Inventory stuck: Failed to sell after buy
- High λ: Survival rate deteriorating
- API errors: >5 4xx errors in 5 minutes
- Pre-trade: Survival probability, profit threshold, gas caps
- During trade: Balance checks, approval validation, expiry checks
- Post-trade: Drawdown tracking, single-trade loss limits
- Emergency: Auto-halt on breach, manual reset required
Daily PnL = Σ(realized net from receipts)
If Daily PnL < -MAX_DAILY_DRAWDOWN_ETH:
→ Halt trading
→ Send Slack alert
→ Require manual interventionnpm testTest coverage includes:
- Profit calculations
- Survival model math
- Gas estimation
- Pagination logic
Set --dry-run flag or mock sendPrivateTransaction to test without executing real trades:
// In exec.ts
if (process.env.DRY_RUN === 'true') {
return mockExecutionResult();
}Start with minimal configuration:
MIN_PROFIT_USD=15 # High threshold
MAX_DAILY_DRAWDOWN_ETH=0.05 # Low exposureMonitor for 24h before scaling up.
✅ Healthy indicators:
- Hourly Slack reports arriving
state/trades.csvupdating- No 4xx/5xx error loops in logs
- Survival λ < 0.1
- No reports for 2+ hours
- Repeated sell failures (>3 consecutive)
- λ > 0.1 for 10+ minutes
- Gas price consistently at MAX_GAS_GWEI
Possible causes:
- Profit threshold too high for current market
- Gas prices above MAX_GAS_GWEI
- Survival threshold too strict
- No collections meeting volume filter
Actions:
- Check logs for skip reasons
- Lower MIN_PROFIT_USD or increase MAX_GAS_GWEI
- Reduce SURVIVAL_MIN_PSURVE to 0.80
- Lower MIN_DAILY_VOLUME_USD
Actions:
- Review
state/pnl-*.jsonto identify losing patterns - Adjust MIN_PROFIT_USD upward
- Reduce MAX_SINGLE_TRADE_LOSS_ETH
- Increase SURVIVAL_MIN_PSURVE
- Manually reset halt after adjustments:
worker.risk.resetHalt()
Causes:
- All failover bids vanished
- Collection offers dried up
- Gas estimation failures
Actions:
- Check OpenSea UI for stuck NFT
- Manually list at floor - 2% via OpenSea
- Consider adding relist fallback (P2 feature)
Actions:
- Reduce MAX_COLLECTIONS
- Increase scan interval (adjust survival λ threshold)
- Enable request caching
- Contact API provider for limit increase
For more aggressive trading:
MIN_PROFIT_USD=5
SURVIVAL_MIN_PSURVE=0.80
FAILOVER_MAX_BIDS=5For conservative, high-quality fills:
MIN_PROFIT_USD=15
SURVIVAL_MIN_PSURVE=0.90
FAILOVER_MAX_BIDS=2
ENABLE_CROSS_VALIDATION_HARD_GATE=true- Use a dedicated hot wallet with minimal balances
- Never commit PRIVATE_KEY to version control
- Rotate keys periodically
- Monitor wallet address for unexpected activity
- Store all secrets in Render environment (not in code)
- Use read-only keys where possible
- Enable IP whitelisting if supported
- Bot respects OpenSea conduit addresses
- No fee circumvention attempts
- Royalties calculated and paid per collection metadata
- No blocked operators used
✅ Single collection support
✅ Survival gating
✅ Failover logic
✅ Slack reporting
✅ Risk management
- Multi-collection with pagination
- Per-collection fee/royalty overrides
- Enhanced alerting (Lambda threshold breach)
- Cross-validation hard gate toggle
- Relist fallback (floor - ε for 15 min)
- Per-collection survival tracking
- Concurrent trade execution
- Daily summary reports
- Backtesting module
- Dynamic sizing based on confidence
- Cross-chain support (Optimism, Arbitrum)
- ML-based survival prediction
All logs are JSON-formatted for easy parsing:
# View recent errors
grep '"level":"ERROR"' logs.txt | tail -20
# Track specific collection
grep '"slug":"milady"' logs.txt
# Monitor survival lambda
grep 'pSurvive' logs.txt# View today's trades
cat state/trades.csv | grep "$(date +%Y-%m-%d)"
# Check current PnL
cat state/pnl-$(date +%Y-%m-%d).json | jq '.realized'
# List approved collections
cat state/approvals.json | jq 'keys'# Via environment
kill -SIGTERM <process_id>
# Via Render UI
Click "Restart Service"
# Manual halt reset (in code)
worker.risk.resetHalt()- Fork the repository
- Create a feature branch
- Write tests for new functionality
- Ensure all tests pass:
npm test - Submit pull request with detailed description
MIT
This software is provided for educational purposes. Use at your own risk. The authors are not responsible for any financial losses incurred through the use of this bot. Always test thoroughly with small amounts before scaling up.
For issues, questions, or feature requests:
- Open a GitHub issue
- Check existing documentation
- Review logs for error messages
Built according to the PRD specifications with:
- OpenSea Seaport 1.6 protocol
- Alchemy private transactions
- Reservoir market data
- Risk-first design principles