
Copy Trading
- 201 installs
- 257 repo stars
- Updated June 24, 2026
- agiprolabs/claude-trading-skills
copy-trading is a Claude Code skill for wallet evaluation, monitoring, and copy-trade strategy design that replicates profitable Solana wallets' DEX trades with independent risk controls.
About
copy-trading is a Claude Code skill for building a Solana copy-trading system that finds profitable on-chain wallets, evaluates whether their edge is real, monitors them in real time, and replicates trades with proportional sizing. It defines a six-stage pipeline plus quantitative wallet thresholds and independent risk limits. A developer uses it when building an agent that follows other wallets' DEX swaps. It covers discovery sources, evaluation metrics, and MEV/latency tradeoffs.
- Six-stage pipeline: discover, evaluate, filter, monitor, execute, risk-manage copied wallets
- Quantitative wallet thresholds (>=55% win rate, profit factor >=1.5, <30% bot probability)
- Independent risk controls with per-wallet, per-trade, and daily/weekly loss limits
Copy Trading by the numbers
- 201 all-time installs (skills.sh)
- Ranked #466 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
copy-trading capabilities & compatibility
Needs Solana RPC / streaming (Yellowstone gRPC, Helius) and a funded wallet; monitoring infra costs vary by provider.
- Capabilities
- copy trading · wallet evaluation · wallet monitoring · trade execution · risk management
- Works with
- chrome
- Use cases
- trading · data analysis · orchestration
- Runs
- Runs locally
- Pricing
- Bring your own API key
What copy-trading says it does
Wallet evaluation, monitoring, and copy-trade strategy design for Solana DEX trading
Copy trading is the practice of monitoring one or more wallets that have demonstrated consistent profitability and replicating their trades in your own wallet.
Copy trades require independent risk controls that do not depend on the copied wallet's behavior.
npx skills add https://github.com/agiprolabs/claude-trading-skills --skill copy-tradingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 201 |
|---|---|
| repo stars | ★ 257 |
| Last updated | June 24, 2026 |
| Repository | agiprolabs/claude-trading-skills ↗ |
What it does
Discover, evaluate, monitor, and replicate profitable Solana wallets with independent risk controls.
Who is it for?
Building a Solana copy-trading agent that discovers, scores, monitors, and mirrors profitable wallets.
Skip if: TradFi platform copy-trading on delayed platform-reported P&L rather than on-chain transactions.
When should I use this skill?
You want to follow and replicate on-chain wallet trades on Solana DEXes.
What you get
A vetted copy-list with real-time monitoring, proportional sizing, and independent stop/loss limits.
By the numbers
- Six-stage copy-trade pipeline
- Win-rate minimum 55%, profit factor minimum 1.5
- Max allocation per wallet 10-20% of portfolio
Files
Copy Trading
Wallet evaluation, monitoring, and copy-trade strategy design for Solana DEX trading. Identify profitable wallets on-chain, evaluate whether their edge is real and replicable, monitor their activity in real time, and execute proportionally-sized trades with independent risk controls.
What Copy Trading Means on Solana
Copy trading is the practice of monitoring one or more wallets that have demonstrated consistent profitability and replicating their trades in your own wallet. On Solana, every DEX swap is publicly visible on-chain within seconds, making it technically feasible to detect and follow any wallet's activity.
How It Differs from TradFi Copy Trading
| Dimension | TradFi (eToro, etc.) | Solana On-Chain |
|---|---|---|
| Data source | Platform-reported P&L | Verifiable on-chain transactions |
| Latency | Minutes to hours | Seconds (websocket) to sub-second (gRPC) |
| Front-running risk | Low | High (MEV bots, sandwich attacks) |
| Trade cost | Commissions + spread | Gas + slippage + priority fees |
| Capacity | High (large-cap equities) | Low (micro-cap tokens have thin liquidity) |
| Signal decay | Slow | Fast (PumpFun tokens move in minutes) |
| Transparency | Partial (delayed reporting) | Full (every transaction is public) |
The core tradeoff: Solana provides perfect transparency but introduces execution risk. The wallet you copy got a price that no longer exists by the time you trade.
The Copy-Trade Pipeline
Stage 1 — Discovery
Find wallets with strong track records. Sources include:
- SolanaTracker Top Traders:
GET /top-traders/{token}returns the highest-PnL wallets for any token - Birdeye Trader Rankings: wallet-level P&L leaderboards by token or globally
- On-chain leaderboards: community-built dashboards (GMGN, Cielo, Arkham)
- Social signals: wallets shared on Twitter/X or Telegram alpha groups
- Your own analysis: run
token-holder-analysison a token that performed well, then profile the top holders
See references/wallet_discovery.md for detailed source documentation and scoring methodology.
Stage 2 — Evaluation
Every discovered wallet must pass quantitative evaluation before it enters a copy list. Use the wallet-profiling skill for deep behavioral analysis, then apply copy-trade-specific criteria.
Minimum thresholds:
| Metric | Minimum | Why |
|---|---|---|
| Trade count (30d) | >= 50 | Statistical significance |
| Win rate | >= 55% | Edge above random |
| Profit factor | >= 1.5 | Wins meaningfully exceed losses |
| Last active | Within 7 days | Still trading, not abandoned |
| Distinct tokens traded | >= 10 | Not a one-token wonder |
| Max single-trade % of total PnL | < 40% | Not reliant on one lucky hit |
| Bot probability | < 30% | Human-like timing patterns |
Run scripts/evaluate_wallet.py for a comprehensive copy-trade suitability assessment.
Stage 3 — Filtering
After evaluation, apply additional filters:
- Consistency check: rolling 7-day win rate should not swing below 40% in any period
- Style compatibility: understand whether the wallet is a sniper, scalper, or swing trader — your infrastructure must match their speed
- Size compatibility: if they trade 500 SOL per position and you have 10 SOL total, proportional sizing may be too small to cover fees
- Sybil check: use the
sybil-detectionskill to verify the wallet is not part of a wash-trading cluster
Stage 4 — Monitoring
Once a wallet passes evaluation and filtering, set up real-time monitoring.
Monitoring approaches (fastest to simplest):
1. Yellowstone gRPC: sub-second latency, streams all transactions for subscribed wallets 2. Helius Enhanced WebSocket: near-real-time with parsed transaction data 3. Polling via RPC: getSignaturesForAddress every 5-10 seconds — simple but slower
See references/execution_strategy.md for implementation details on each approach.
Stage 5 — Execution
When a monitored wallet executes a swap:
1. Detect the transaction (via monitoring infrastructure) 2. Parse the trade: token address, direction (buy/sell), size 3. Validate the token: check liquidity, holder distribution, honeypot risk 4. Size the position: proportional to your portfolio, not theirs 5. Execute via Jupiter aggregator with appropriate slippage tolerance 6. Record the copy trade with attribution to the source wallet
Stage 6 — Risk Management
Copy trades require independent risk controls that do not depend on the copied wallet's behavior.
See references/risk_framework.md for the complete framework.
Key limits:
| Control | Recommended Value | Purpose |
|---|---|---|
| Max allocation per wallet | 10-20% of portfolio | Diversification across signal sources |
| Max concurrent copy positions | 3-5 | Prevent overexposure |
| Per-trade stop loss | -15% to -25% | Independent downside protection |
| Daily copy-trade loss limit | -5% of portfolio | Circuit breaker |
| Weekly copy-trade loss limit | -10% of portfolio | Longer-term circuit breaker |
Wallet Scoring for Copy Suitability
Composite score from 0-100 based on weighted criteria:
copy_score = (
trade_count_score * 0.15 +
win_rate_score * 0.20 +
profit_factor_score * 0.25 +
consistency_score * 0.20 +
recency_score * 0.10 +
human_probability_score * 0.10
)Component calculations:
- Trade count score:
min(trade_count / 200, 1.0) * 100— maxes out at 200 trades - Win rate score:
max((win_rate - 0.40) / 0.30, 0) * 100— scaled from 40% to 70% - Profit factor score:
min((pf - 1.0) / 3.0, 1.0) * 100— scaled from 1.0 to 4.0 - Consistency score:
(1.0 - std_dev_of_rolling_win_rate) * 100— lower variance = higher score - Recency score:
max(1.0 - days_since_last_trade / 14, 0) * 100— decays over 14 days - Human probability score:
(1.0 - bot_probability) * 100
Interpretation:
| Score | Rating | Action |
|---|---|---|
| 80-100 | Excellent | Strong copy-trade candidate |
| 60-79 | Good | Suitable with monitoring |
| 40-59 | Marginal | Proceed with caution, reduce allocation |
| 0-39 | Poor | Do not copy |
Position Sizing for Copy Trades
Three approaches, from simplest to most nuanced:
Fixed Amount
Use a constant SOL amount per copy trade (e.g., 0.5 SOL). Simple but ignores the wallet's conviction level.
Proportional
Match the copied wallet's allocation as a percentage of their estimated portfolio:
your_size = (their_trade_size / their_estimated_portfolio) * your_portfolioRequires estimating their total portfolio, which can be imprecise.
Confidence-Scaled
Base amount multiplied by your confidence in the wallet:
your_size = base_amount * (copy_score / 100) * conviction_multiplierWhere conviction_multiplier is higher for wallets with longer track records.
Anti-Patterns to Avoid
Copying Bots
Bots have sub-second execution and often use MEV strategies. You cannot match their latency. By the time you detect their trade, the opportunity is gone or you become the exit liquidity. Use the bot probability score to filter these out.
Survivorship Bias
A wallet with 1000% returns from one PumpFun token is not necessarily skilled. Look for wallets with consistent performance across many tokens, not outlier wins. The max-single-trade-PnL filter catches this.
Blind Following
Never copy a trade without understanding what the token is. At minimum, run basic safety checks: liquidity depth, holder concentration, contract verification. A 2-second check can prevent buying a honeypot.
No Independent Exits
The copied wallet may have information you do not have. They may exit for reasons unrelated to the trade. Always maintain your own stop loss. Never rely solely on mirroring their exit.
Correlation Risk
If you copy 5 wallets and they all buy the same token, you have 5x the intended exposure. Track aggregate position across all copy sources and enforce portfolio-level limits.
Ignoring Capacity
A wallet profiting on tokens with $50K daily volume cannot be copied at scale. If your trade is 10% of daily volume, you will move the price against yourself.
Integration with Other Skills
| Skill | How It Integrates |
|---|---|
wallet-profiling | Deep behavioral analysis of candidate wallets |
sybil-detection | Verify wallet is not part of a wash-trading ring |
token-holder-analysis | Safety check tokens before copying a buy |
liquidity-analysis | Verify sufficient liquidity to enter/exit |
helius-api | WebSocket monitoring and transaction parsing |
jupiter-api / jupiter-swap | Trade execution via aggregator |
slippage-modeling | Estimate execution cost of the copy trade |
position-sizing | Portfolio-aware sizing for copy positions |
risk-management | Portfolio-level risk controls |
Files
References
references/wallet_discovery.md— Sources and methods for finding copy-trade candidatesreferences/execution_strategy.md— Monitoring infrastructure and execution approachesreferences/risk_framework.md— Portfolio-level risk controls for copy trading
Scripts
scripts/evaluate_wallet.py— Comprehensive copy-trade suitability scoring for a walletscripts/monitor_wallet.py— Real-time wallet transaction monitoring with trade alerts
Copy Trade Execution Strategy
Monitoring infrastructure, timing considerations, position sizing, and exit strategies for copy trading on Solana.
Monitoring Approaches
Option 1 — Polling via RPC (Simplest)
Poll getSignaturesForAddress at regular intervals to detect new transactions.
- Latency: 5-15 seconds depending on poll interval
- Complexity: Low — standard HTTP requests
- Cost: Free with any RPC endpoint
- Best for: Swing trader wallets where seconds do not matter
import httpx
import time
def poll_wallet(rpc_url: str, wallet: str, interval: int = 10) -> None:
"""Poll for new transactions every `interval` seconds."""
last_sig = None
while True:
payload = {
"jsonrpc": "2.0", "id": 1,
"method": "getSignaturesForAddress",
"params": [wallet, {"limit": 5}]
}
resp = httpx.post(rpc_url, json=payload)
sigs = resp.json().get("result", [])
if sigs and sigs[0]["signature"] != last_sig:
for sig in sigs:
if sig["signature"] == last_sig:
break
print(f"New tx: {sig['signature']}")
last_sig = sigs[0]["signature"]
time.sleep(interval)Option 2 — Helius Enhanced WebSocket (Recommended)
Helius provides parsed transaction data via WebSocket with near-real-time delivery.
- Latency: 1-3 seconds after on-chain confirmation
- Complexity: Medium — WebSocket connection management
- Cost: Free tier includes WebSocket access (limited connections)
- Best for: Day traders and scalpers
import asyncio
import json
import websockets
async def monitor_helius(api_key: str, wallet: str) -> None:
"""Monitor wallet via Helius enhanced WebSocket."""
uri = f"wss://atlas-mainnet.helius-rpc.com/?api-key={api_key}"
async with websockets.connect(uri) as ws:
subscribe = {
"jsonrpc": "2.0", "id": 1,
"method": "transactionSubscribe",
"params": [{
"accountInclude": [wallet]
}, {
"commitment": "confirmed",
"encoding": "jsonParsed",
"transactionDetails": "full"
}]
}
await ws.send(json.dumps(subscribe))
while True:
msg = json.loads(await ws.recv())
if "params" in msg:
handle_transaction(msg["params"]["result"])Option 3 — Yellowstone gRPC (Lowest Latency)
Direct gRPC stream from a validator. Sub-second latency.
- Latency: < 500ms from block production
- Complexity: High — requires gRPC client, protobuf parsing
- Cost: Requires a premium RPC provider (Helius, Triton, or self-hosted)
- Best for: Sniper wallets where sub-second execution matters
gRPC setup is beyond the scope of this skill. See your RPC provider's documentation for Yellowstone gRPC configuration.
Timing Strategies
Immediate Copy
Execute the copy trade as fast as possible after detection.
- When to use: Copying scalpers on tokens with rapid price movement
- Risk: Higher slippage, potential sandwich attacks, may buy the top
- Implementation: WebSocket or gRPC monitoring with pre-built Jupiter swap transactions
Delayed Copy (Confirmation Wait)
Wait for the transaction to confirm (1-2 slots) before executing.
- When to use: Copying swing traders, tokens with stable liquidity
- Risk: Price may have moved, but you have confirmation the trade succeeded
- Implementation: Polling or WebSocket with a short delay before execution
Filtered Copy
Detect the trade, run safety checks on the token, then decide whether to copy.
- When to use: All copy trading (recommended default)
- Checks before copying:
1. Token liquidity > minimum threshold (e.g., $50K) 2. Holder concentration not excessive (top 10 holders < 50%) 3. Token is not on a known scam list 4. Your aggregate exposure to this token < maximum per-token limit 5. Daily copy-trade loss limit not exceeded
def should_copy_trade(
token_address: str,
trade_size_sol: float,
portfolio_state: dict,
) -> tuple[bool, str]:
"""Decide whether to copy a detected trade."""
# Check daily loss limit
if portfolio_state["daily_copy_loss"] < -portfolio_state["daily_limit"]:
return False, "Daily copy-trade loss limit reached"
# Check per-token exposure
existing = portfolio_state["positions"].get(token_address, 0)
if existing + trade_size_sol > portfolio_state["max_per_token"]:
return False, "Per-token exposure limit reached"
# Check concurrent position count
if len(portfolio_state["positions"]) >= portfolio_state["max_positions"]:
return False, "Maximum concurrent positions reached"
return True, "Trade approved"Position Sizing
Fixed Amount
Allocate a constant SOL amount per copy trade regardless of the source wallet's size.
def fixed_size(base_amount: float = 0.5) -> float:
"""Fixed position size per copy trade."""
return base_amountPros: Simple, predictable risk per trade. Cons: Ignores the source wallet's conviction level.
Proportional
Match the copied wallet's allocation as a fraction of their estimated portfolio.
def proportional_size(
their_trade_size: float,
their_portfolio_estimate: float,
your_portfolio: float,
) -> float:
"""Scale position proportionally to the source wallet."""
their_fraction = their_trade_size / max(their_portfolio_estimate, 1.0)
return their_fraction * your_portfolioPros: Mirrors conviction level. Cons: Requires estimating the source wallet's total portfolio, which is imprecise.
Confidence-Scaled
Base amount adjusted by your confidence in the wallet.
def confidence_scaled_size(
base_amount: float,
copy_score: float,
max_multiplier: float = 2.0,
) -> float:
"""Scale position by copy-trade suitability score."""
multiplier = min(copy_score / 100.0, 1.0) * max_multiplier
return base_amount * multiplierPros: More capital to higher-conviction wallets. Cons: Requires maintaining accurate copy scores.
Exit Strategies
Mirror Exit
Exit when the copied wallet exits. Requires continued monitoring of the wallet after entry.
- Pros: Leverages the wallet's exit timing intelligence
- Cons: Adds monitoring complexity; if monitoring fails, you have no exit signal
- Implementation: Same monitoring infrastructure, watching for sell transactions on your held tokens
Independent Exit
Use your own stop loss and take profit levels, ignoring the copied wallet's exit.
- Recommended defaults:
- Stop loss: -20% from entry
- Take profit: +50% from entry (or trailing stop at -15% from peak)
- Pros: Simple, does not depend on continued monitoring
- Cons: May exit too early or too late relative to the wallet
Hybrid (Recommended)
Mirror the wallet's exit but maintain an independent safety stop loss.
def hybrid_exit_check(
entry_price: float,
current_price: float,
wallet_exited: bool,
stop_loss_pct: float = -0.20,
) -> tuple[bool, str]:
"""Check if position should be exited."""
pnl_pct = (current_price - entry_price) / entry_price
if pnl_pct <= stop_loss_pct:
return True, f"Stop loss hit: {pnl_pct:.1%}"
if wallet_exited:
return True, "Mirroring wallet exit"
return False, "Hold"Pre-Execution Checklist
Before executing any copy trade, verify:
1. [ ] Token address is valid and not on a known scam list 2. [ ] Token has sufficient liquidity (> $25K in pool) 3. [ ] Position size does not exceed per-trade or per-token limits 4. [ ] Daily loss limit has not been reached 5. [ ] Maximum concurrent positions not exceeded 6. [ ] Jupiter quote obtained with acceptable slippage (< 5%) 7. [ ] Transaction simulation succeeds (no revert)
Handling Failed Trades
The Copied Wallet's Trade Reverts
If the source transaction reverts, do nothing. This is a non-event.
Your Copy Trade Reverts
Common causes and remedies:
- Insufficient SOL for fees: Maintain a fee buffer (0.01 SOL minimum)
- Slippage exceeded: Increase slippage tolerance or reduce position size
- Token is a honeypot: The source wallet may have bypassed restrictions you cannot
- Rate limited by RPC: Switch to a backup RPC endpoint
Partial Fills
Jupiter may partially fill your swap. Track the actual filled amount, not the requested amount, for P&L calculation.
Copy Trading Risk Framework
Portfolio-level controls, per-wallet limits, performance tracking, and decay detection for copy trading on Solana.
Portfolio-Level Limits
Total Copy-Trade Allocation
Limit total capital deployed via copy trades to a fraction of your portfolio.
| Risk Profile | Max Copy Allocation | Rationale |
|---|---|---|
| Conservative | 20% of portfolio | Copy trading as a supplement |
| Moderate | 40% of portfolio | Balanced between copy and independent trades |
| Aggressive | 60% of portfolio | Heavy reliance on copy signals |
Never allocate 100% to copy trades. Maintain capital for independent opportunities and to absorb losses.
Per-Wallet Allocation
No single copied wallet should dominate your copy-trade portfolio.
def max_wallet_allocation(
total_portfolio: float,
total_copy_allocation_pct: float,
num_wallets: int,
) -> float:
"""Maximum allocation to any single copied wallet."""
copy_budget = total_portfolio * total_copy_allocation_pct
# No wallet gets more than 30% of copy budget or 15% of total portfolio
per_wallet_max = min(copy_budget * 0.30, total_portfolio * 0.15)
# But also ensure it is at least evenly distributed
even_split = copy_budget / max(num_wallets, 1)
return min(per_wallet_max, even_split * 1.5)Concurrent Position Limits
| Limit Type | Recommended | Purpose |
|---|---|---|
| Max positions per wallet | 2-3 | Prevent overexposure to one signal source |
| Max total copy positions | 5-10 | Manageable monitoring load |
| Max positions per token | 1 | Prevent duplicate exposure from multiple wallets |
Loss Limits (Circuit Breakers)
Per-Trade Loss Limit
Stop loss on every copy trade. Recommended: -20% from entry price.
Per-Wallet Daily Loss Limit
If copy trades from a specific wallet lose more than 3% of your portfolio in one day, pause that wallet until the next day.
Portfolio Daily Loss Limit
If total copy-trade losses exceed 5% of your portfolio in one day, pause all copy trading until the next day.
def check_circuit_breakers(
daily_pnl_by_wallet: dict[str, float],
total_portfolio: float,
) -> dict[str, bool]:
"""Check which wallets should be paused due to loss limits."""
paused: dict[str, bool] = {}
total_daily_loss = 0.0
per_wallet_limit = total_portfolio * 0.03
portfolio_limit = total_portfolio * 0.05
for wallet, pnl in daily_pnl_by_wallet.items():
total_daily_loss += min(pnl, 0)
if pnl < -per_wallet_limit:
paused[wallet] = True
if total_daily_loss < -portfolio_limit:
# Pause all wallets
for wallet in daily_pnl_by_wallet:
paused[wallet] = True
return pausedWeekly Loss Limit
If total copy-trade losses exceed 10% of your portfolio in one week, pause all copy trading and review your wallet selection.
Correlation Risk
The Problem
Multiple copied wallets may trade the same tokens, creating unintended concentration. If you copy 5 wallets and 3 of them buy the same memecoin, you have 3x the intended exposure.
Detection
def detect_correlation(
active_positions: dict[str, list[str]],
) -> dict[str, list[str]]:
"""Find tokens held via multiple copied wallets."""
token_to_wallets: dict[str, list[str]] = {}
for wallet, tokens in active_positions.items():
for token in tokens:
token_to_wallets.setdefault(token, []).append(wallet)
return {t: ws for t, ws in token_to_wallets.items() if len(ws) > 1}Mitigation
- Track aggregate exposure per token across all copy sources
- If a token appears in 2+ copied wallets, reduce position size proportionally
- Set a hard per-token limit (e.g., 5% of portfolio) regardless of source
Decay Detection
Wallet performance degrades over time. Markets change, strategies stop working, or the wallet operator changes behavior. Detect decay early.
Rolling Performance Check
def detect_decay(
recent_trades: list[dict],
lookback_trades: int = 20,
min_win_rate: float = 0.45,
min_profit_factor: float = 1.1,
) -> tuple[bool, str]:
"""Check if a wallet's recent performance has decayed."""
recent = recent_trades[-lookback_trades:]
if len(recent) < lookback_trades:
return False, "Insufficient recent trades"
wins = sum(1 for t in recent if t["pnl"] > 0)
win_rate = wins / len(recent)
gross_profit = sum(t["pnl"] for t in recent if t["pnl"] > 0)
gross_loss = abs(sum(t["pnl"] for t in recent if t["pnl"] < 0))
pf = gross_profit / max(gross_loss, 0.001)
if win_rate < min_win_rate:
return True, f"Win rate decayed to {win_rate:.1%} (min: {min_win_rate:.1%})"
if pf < min_profit_factor:
return True, f"Profit factor decayed to {pf:.2f} (min: {min_profit_factor:.2f})"
return False, "Performance acceptable"When to Remove a Wallet
Remove a wallet from your copy list when:
1. Win rate drops below 45% over the last 20 trades 2. Profit factor drops below 1.1 over the last 20 trades 3. Inactivity: no trades in 14+ days 4. Style change: median hold time shifts by > 2x (e.g., swing trader becomes sniper) 5. Loss streak: 5+ consecutive losing trades 6. Sybil alert: wallet flagged by sybil-detection after initial approval
Diversification Requirements
By Wallet Style
Copy wallets across different trading styles to reduce correlation:
| Style | Target Allocation | Why |
|---|---|---|
| Scalper | 20-30% | High frequency, small gains |
| Day Trader | 30-40% | Moderate frequency, balanced risk |
| Swing Trader | 30-40% | Lower frequency, larger moves |
| Sniper | 0-10% | Very high risk, hard to replicate |
By Token Type
Avoid copying wallets that all trade the same token category:
- Mix PumpFun/memecoin traders with established token traders
- Include at least one wallet that trades higher-cap tokens (top 100)
Performance Tracking
Per-Wallet Attribution
Track P&L separately for each copied wallet:
from dataclasses import dataclass, field
@dataclass
class WalletCopyStats:
wallet: str
trades_copied: int = 0
wins: int = 0
losses: int = 0
total_pnl_sol: float = 0.0
total_invested_sol: float = 0.0
@property
def win_rate(self) -> float:
return self.wins / max(self.trades_copied, 1)
@property
def roi(self) -> float:
return self.total_pnl_sol / max(self.total_invested_sol, 0.001)Aggregate Metrics
Track overall copy-trade performance vs. your independent trades:
| Metric | Calculation | Target |
|---|---|---|
| Copy trade win rate | Wins / total copy trades | > 50% |
| Copy trade ROI | Total copy PnL / total copy capital deployed | > 0% |
| Copy vs. independent | Copy ROI - independent ROI | Positive (copy adds value) |
| Best wallet ROI | Highest per-wallet ROI | Identifies top signal sources |
| Worst wallet ROI | Lowest per-wallet ROI | Identifies wallets to remove |
Review Cadence
| Frequency | Action |
|---|---|
| Daily | Check circuit breakers, review active positions |
| Weekly | Review per-wallet performance, check for decay |
| Monthly | Full re-evaluation of all copied wallets, update scores |
| Quarterly | Review overall copy-trade strategy effectiveness |
Wallet Discovery for Copy Trading
Methods, sources, and scoring for finding wallets worth copying on Solana.
Discovery Sources
SolanaTracker — Top Traders per Token
The primary discovery source. Returns the highest-PnL wallets for any given token.
- Endpoint:
GET https://data.solanatracker.io/top-traders/{token_address} - Authentication:
x-api-keyheader (optional for basic access) - Rate Limit: 10 req/min (free), 60 req/min (paid)
- Response fields:
wallet,pnl,trades,bought,sold,volume
curl -H "x-api-key: $ST_API_KEY" \
"https://data.solanatracker.io/top-traders/So11111111111111111111111111111111111111112"Workflow: Identify a token that recently performed well, pull top traders, then evaluate each wallet independently.
SolanaTracker — Wallet PnL
Retrieve full trade-level PnL for a specific wallet.
- Endpoint:
GET https://data.solanatracker.io/pnl/{wallet_address} - Response fields:
summary.total_pnl,summary.total_trades,summary.win_rate,summary.profit_factor,tokens[](per-token breakdown)
curl -H "x-api-key: $ST_API_KEY" \
"https://data.solanatracker.io/pnl/WALLET_ADDRESS_HERE"Birdeye — Trader Rankings
Birdeye provides token-level and global trader leaderboards.
- Endpoint:
GET https://public-api.birdeye.so/defi/v3/token/trade/top-traders - Parameters:
address(token mint),time_frame(1h, 4h, 24h, 7d, 30d),sort_by(pnl, volume) - Authentication:
X-API-KEYheader - Rate Limit: 100 req/min (free tier)
curl -H "X-API-KEY: $BIRDEYE_API_KEY" \
"https://public-api.birdeye.so/defi/v3/token/trade/top-traders?address=TOKEN&time_frame=24h&sort_by=pnl"Helius — Transaction History
Not a discovery source per se, but essential for building a wallet's complete trade history after discovery.
- Endpoint:
POST https://api.helius.xyz/v0/addresses/{address}/transactions?api-key={key} - Returns: Parsed transaction history with swap details, token transfers, and program interactions
- Use case: After finding a wallet via SolanaTracker/Birdeye, fetch full history to build the profile
Community and Social Sources
- GMGN.ai: Wallet leaderboards by timeframe and chain
- Cielo Finance: Multi-chain wallet tracking with P&L
- Twitter/X: Alpha callers often share wallet addresses
- Telegram groups: Whale alert channels, alpha groups
- Arkham Intelligence: Institutional-grade wallet labeling
Caution: Social sources have strong survivorship bias. Always run independent evaluation before adding to a copy list.
Discovery Workflow
Step 1 — Seed Token Selection
Start with tokens that recently had significant price moves:
1. Pull trending tokens from DexScreener or Birdeye 2. Filter for tokens with > $100K 24h volume (sufficient liquidity) 3. Filter for tokens that gained > 50% in the last 24h (someone profited)
Step 2 — Extract Top Traders
For each seed token, pull top traders:
import httpx
async def get_top_traders(token: str, api_key: str) -> list[dict]:
"""Get top PnL wallets for a token from SolanaTracker."""
url = f"https://data.solanatracker.io/top-traders/{token}"
headers = {"x-api-key": api_key}
async with httpx.AsyncClient() as client:
resp = await client.get(url, headers=headers)
resp.raise_for_status()
return resp.json()Step 3 — Deduplicate Across Tokens
The same wallet may appear as a top trader on multiple tokens. Track unique wallets and how many seed tokens they appear in — appearing on multiple tokens is a positive signal of breadth.
Step 4 — Quick Filter
Before running full evaluation, apply quick filters to reduce the candidate set:
def quick_filter(wallet_data: dict) -> bool:
"""Fast pre-filter before expensive evaluation."""
pnl = wallet_data.get("pnl", 0)
trades = wallet_data.get("trades", 0)
if trades < 10:
return False # Not enough data from this token alone
if pnl <= 0:
return False # Not profitable on this token
return TrueStep 5 — Full Evaluation
Run comprehensive evaluation on each candidate (see scripts/evaluate_wallet.py).
Step 6 — Rank and Select
Sort evaluated wallets by composite copy score. Select the top N (typically 3-10) for your copy list.
Scoring Wallets for Copy Suitability
Composite Score Components
| Component | Weight | Source | Calculation |
|---|---|---|---|
| Trade count | 15% | PnL API | min(count / 200, 1.0) * 100 |
| Win rate | 20% | PnL API | max((wr - 0.40) / 0.30, 0) * 100 |
| Profit factor | 25% | PnL API | min((pf - 1.0) / 3.0, 1.0) * 100 |
| Consistency | 20% | Computed | (1.0 - rolling_wr_stddev) * 100 |
| Recency | 10% | PnL API | max(1.0 - days_inactive / 14, 0) * 100 |
| Human probability | 10% | Computed | (1.0 - bot_prob) * 100 |
Automated Thresholds
| Score Range | Label | Action |
|---|---|---|
| 80-100 | Excellent | Add to copy list, standard allocation |
| 60-79 | Good | Add with reduced allocation, review weekly |
| 40-59 | Marginal | Watchlist only, do not copy yet |
| 0-39 | Poor | Reject |
Red Flags
New Wallets
Wallets created in the last 7 days with high PnL are likely insider wallets or airdrop farmers. Require at least 30 days of history.
Single-Token Profits
If > 50% of a wallet's total PnL comes from one token, the track record is not diversified enough. One lucky trade does not indicate skill.
Insider Patterns
Wallets that consistently buy tokens within the first few transactions after pool creation, across many tokens, may have insider access to launch schedules. Their edge is not replicable.
Wash Trading
Wallets that buy and sell the same token repeatedly in small amounts to inflate trade count and win rate. Use the sybil-detection skill to check for cluster behavior.
Abnormal Timing
Trades at perfectly regular intervals (e.g., exactly every 60 seconds) indicate a bot. Bots have latency advantages that cannot be replicated by copy trading.
Concentration in Low-Liquidity Tokens
High PnL on tokens with < $10K daily volume may reflect price manipulation rather than genuine alpha. Verify that the tokens traded had real liquidity.
Batch Discovery Example
async def discover_candidates(
seed_tokens: list[str],
api_key: str,
min_trades: int = 10,
) -> dict[str, dict]:
"""Discover unique profitable wallets across seed tokens."""
candidates: dict[str, dict] = {}
for token in seed_tokens:
traders = await get_top_traders(token, api_key)
for t in traders:
wallet = t.get("wallet", "")
if not quick_filter(t):
continue
if wallet in candidates:
candidates[wallet]["appearances"] += 1
candidates[wallet]["tokens"].append(token)
else:
candidates[wallet] = {
"appearances": 1,
"tokens": [token],
"first_seen_pnl": t.get("pnl", 0),
}
return candidatesWallets appearing across 3 or more seed tokens are strong candidates for further evaluation.
#!/usr/bin/env python3
"""Evaluate a Solana wallet's suitability for copy trading.
Fetches trade history from SolanaTracker PnL API, computes a composite
copy-trade suitability score, and prints a comprehensive GO/NO-GO
recommendation. Includes a --demo mode with example data.
Usage:
python scripts/evaluate_wallet.py # uses WALLET_ADDRESS env var
python scripts/evaluate_wallet.py <wallet_address> # direct argument
python scripts/evaluate_wallet.py --demo # run with example data
Dependencies:
uv pip install httpx
Environment Variables:
WALLET_ADDRESS: Wallet to evaluate (optional if passed as argument)
ST_API_KEY: SolanaTracker API key (optional, improves rate limits)
"""
import json
import math
import os
import sys
import time
from dataclasses import dataclass
from typing import Optional
try:
import httpx
except ImportError:
print("Missing dependency. Install with: uv pip install httpx")
sys.exit(1)
# ── Configuration ───────────────────────────────────────────────────
ST_API_KEY = os.getenv("ST_API_KEY", "")
ST_BASE_URL = "https://data.solanatracker.io"
# Minimum thresholds for copy-trade suitability
MIN_TRADES = 50
MIN_WIN_RATE = 0.55
MIN_PROFIT_FACTOR = 1.5
MAX_DAYS_INACTIVE = 7
MIN_DISTINCT_TOKENS = 10
MAX_SINGLE_TRADE_PNL_PCT = 0.40
MAX_BOT_PROBABILITY = 0.30
# Score weights
WEIGHT_TRADE_COUNT = 0.15
WEIGHT_WIN_RATE = 0.20
WEIGHT_PROFIT_FACTOR = 0.25
WEIGHT_CONSISTENCY = 0.20
WEIGHT_RECENCY = 0.10
WEIGHT_HUMAN_PROB = 0.10
# ── Data Structures ────────────────────────────────────────────────
@dataclass
class TokenTrade:
"""Summarized trade on a single token."""
token_address: str
token_symbol: str
pnl_sol: float
pnl_usd: float
bought_sol: float
sold_sol: float
num_buys: int
num_sells: int
first_trade_ts: int
last_trade_ts: int
@property
def is_win(self) -> bool:
return self.pnl_sol > 0
@property
def hold_time_seconds(self) -> int:
return max(self.last_trade_ts - self.first_trade_ts, 0)
@dataclass
class WalletEvaluation:
"""Complete copy-trade evaluation for a wallet."""
wallet: str
total_trades: int
wins: int
losses: int
win_rate: float
profit_factor: float
total_pnl_sol: float
total_pnl_usd: float
distinct_tokens: int
days_since_last_trade: float
max_single_trade_pnl_pct: float
median_hold_time_hours: float
bot_probability: float
consistency_score: float
composite_score: float
rating: str
passed_all_minimums: bool
failures: list[str]
# ── API Functions ───────────────────────────────────────────────────
def fetch_wallet_pnl(wallet: str) -> dict:
"""Fetch wallet PnL data from SolanaTracker.
Args:
wallet: Solana wallet address.
Returns:
Parsed JSON response with PnL data.
Raises:
httpx.HTTPStatusError: On non-2xx response.
"""
url = f"{ST_BASE_URL}/pnl/{wallet}"
headers = {}
if ST_API_KEY:
headers["x-api-key"] = ST_API_KEY
with httpx.Client(timeout=30.0) as client:
resp = client.get(url, headers=headers)
resp.raise_for_status()
return resp.json()
# ── Scoring Functions ───────────────────────────────────────────────
def score_trade_count(count: int) -> float:
"""Score based on number of trades. Maxes out at 200."""
return min(count / 200.0, 1.0) * 100.0
def score_win_rate(win_rate: float) -> float:
"""Score win rate, scaled from 40% to 70%."""
return max((win_rate - 0.40) / 0.30, 0.0) * 100.0
def score_profit_factor(pf: float) -> float:
"""Score profit factor, scaled from 1.0 to 4.0."""
return min(max((pf - 1.0) / 3.0, 0.0), 1.0) * 100.0
def score_consistency(trades: list[TokenTrade], window: int = 10) -> float:
"""Score based on rolling win rate stability.
Lower standard deviation of rolling win rate = higher score.
"""
if len(trades) < window:
return 50.0 # Insufficient data, neutral score
sorted_trades = sorted(trades, key=lambda t: t.first_trade_ts)
rolling_win_rates: list[float] = []
for i in range(len(sorted_trades) - window + 1):
window_trades = sorted_trades[i : i + window]
wr = sum(1 for t in window_trades if t.is_win) / len(window_trades)
rolling_win_rates.append(wr)
if not rolling_win_rates:
return 50.0
mean_wr = sum(rolling_win_rates) / len(rolling_win_rates)
variance = sum((wr - mean_wr) ** 2 for wr in rolling_win_rates) / len(
rolling_win_rates
)
std_dev = math.sqrt(variance)
# Clamp std_dev to [0, 1] range, then invert
return max(0.0, (1.0 - min(std_dev, 1.0)) * 100.0)
def score_recency(days_since_last: float) -> float:
"""Score based on days since last trade. Decays over 14 days."""
return max(1.0 - days_since_last / 14.0, 0.0) * 100.0
def estimate_bot_probability(trades: list[TokenTrade]) -> float:
"""Estimate probability that the wallet is a bot.
Heuristics:
- Very short hold times (< 60s median) suggest bot
- Very high trade count (> 500 in 30 days) suggests bot
- Regular timing intervals suggest bot
"""
if not trades:
return 0.5
hold_times = [t.hold_time_seconds for t in trades if t.hold_time_seconds > 0]
if not hold_times:
return 0.3
median_hold = sorted(hold_times)[len(hold_times) // 2]
bot_score = 0.0
# Very short hold times
if median_hold < 30:
bot_score += 0.4
elif median_hold < 60:
bot_score += 0.25
elif median_hold < 120:
bot_score += 0.1
# High trade frequency
if len(trades) > 500:
bot_score += 0.3
elif len(trades) > 200:
bot_score += 0.15
# Check timing regularity (intervals between trades)
timestamps = sorted(t.first_trade_ts for t in trades)
if len(timestamps) >= 10:
intervals = [
timestamps[i + 1] - timestamps[i] for i in range(len(timestamps) - 1)
]
if intervals:
mean_interval = sum(intervals) / len(intervals)
if mean_interval > 0:
cv = math.sqrt(
sum((i - mean_interval) ** 2 for i in intervals) / len(intervals)
) / mean_interval
# Low coefficient of variation = regular = bot-like
if cv < 0.2:
bot_score += 0.3
elif cv < 0.5:
bot_score += 0.1
return min(bot_score, 1.0)
def score_human_probability(bot_prob: float) -> float:
"""Score based on probability of being human (not a bot)."""
return (1.0 - bot_prob) * 100.0
# ── Evaluation Pipeline ────────────────────────────────────────────
def parse_trades(pnl_data: dict) -> list[TokenTrade]:
"""Parse SolanaTracker PnL response into TokenTrade objects."""
trades: list[TokenTrade] = []
tokens = pnl_data.get("tokens", [])
for token_data in tokens:
token_info = token_data.get("token", {})
pnl = token_data.get("pnl", 0)
pnl_usd = token_data.get("pnl_usd", 0)
bought = token_data.get("total_bought_sol", 0) or token_data.get("bought", 0)
sold = token_data.get("total_sold_sol", 0) or token_data.get("sold", 0)
num_buys = token_data.get("num_buys", 1)
num_sells = token_data.get("num_sells", 0)
first_ts = token_data.get("first_trade_time", 0)
last_ts = token_data.get("last_trade_time", 0)
trades.append(
TokenTrade(
token_address=token_info.get("mint", token_data.get("token", "")),
token_symbol=token_info.get("symbol", "???"),
pnl_sol=float(pnl),
pnl_usd=float(pnl_usd) if pnl_usd else 0.0,
bought_sol=float(bought),
sold_sol=float(sold),
num_buys=int(num_buys),
num_sells=int(num_sells),
first_trade_ts=int(first_ts),
last_trade_ts=int(last_ts),
)
)
return trades
def evaluate_wallet(
wallet: str, trades: list[TokenTrade]
) -> WalletEvaluation:
"""Run full copy-trade suitability evaluation.
Args:
wallet: The wallet address being evaluated.
trades: List of parsed token trades.
Returns:
Complete WalletEvaluation with scores and recommendation.
"""
total_trades = len(trades)
wins = sum(1 for t in trades if t.is_win)
losses = total_trades - wins
win_rate = wins / max(total_trades, 1)
gross_profit = sum(t.pnl_sol for t in trades if t.pnl_sol > 0)
gross_loss = abs(sum(t.pnl_sol for t in trades if t.pnl_sol < 0))
profit_factor = gross_profit / max(gross_loss, 0.001)
total_pnl_sol = sum(t.pnl_sol for t in trades)
total_pnl_usd = sum(t.pnl_usd for t in trades)
distinct_tokens = len(set(t.token_address for t in trades))
# Days since last trade
last_trade_ts = max((t.last_trade_ts for t in trades), default=0)
if last_trade_ts > 0:
days_since_last = (time.time() - last_trade_ts) / 86400.0
else:
days_since_last = 999.0
# Max single-trade PnL as percentage of total
if total_pnl_sol > 0:
max_single_pnl = max(t.pnl_sol for t in trades)
max_single_pnl_pct = max_single_pnl / total_pnl_sol
else:
max_single_pnl_pct = 1.0
# Median hold time
hold_times = sorted(
t.hold_time_seconds / 3600.0 for t in trades if t.hold_time_seconds > 0
)
median_hold_hours = hold_times[len(hold_times) // 2] if hold_times else 0.0
# Bot probability
bot_prob = estimate_bot_probability(trades)
# Consistency
consistency = score_consistency(trades)
# Composite score
tc_score = score_trade_count(total_trades)
wr_score = score_win_rate(win_rate)
pf_score = score_profit_factor(profit_factor)
rec_score = score_recency(days_since_last)
hum_score = score_human_probability(bot_prob)
composite = (
tc_score * WEIGHT_TRADE_COUNT
+ wr_score * WEIGHT_WIN_RATE
+ pf_score * WEIGHT_PROFIT_FACTOR
+ consistency * WEIGHT_CONSISTENCY
+ rec_score * WEIGHT_RECENCY
+ hum_score * WEIGHT_HUMAN_PROB
)
# Rating
if composite >= 80:
rating = "EXCELLENT"
elif composite >= 60:
rating = "GOOD"
elif composite >= 40:
rating = "MARGINAL"
else:
rating = "POOR"
# Check minimum thresholds
failures: list[str] = []
if total_trades < MIN_TRADES:
failures.append(f"Trade count {total_trades} < {MIN_TRADES}")
if win_rate < MIN_WIN_RATE:
failures.append(f"Win rate {win_rate:.1%} < {MIN_WIN_RATE:.0%}")
if profit_factor < MIN_PROFIT_FACTOR:
failures.append(f"Profit factor {profit_factor:.2f} < {MIN_PROFIT_FACTOR}")
if days_since_last > MAX_DAYS_INACTIVE:
failures.append(
f"Last trade {days_since_last:.0f} days ago > {MAX_DAYS_INACTIVE} days"
)
if distinct_tokens < MIN_DISTINCT_TOKENS:
failures.append(f"Distinct tokens {distinct_tokens} < {MIN_DISTINCT_TOKENS}")
if max_single_pnl_pct > MAX_SINGLE_TRADE_PNL_PCT:
failures.append(
f"Top trade is {max_single_pnl_pct:.0%} of total PnL > {MAX_SINGLE_TRADE_PNL_PCT:.0%}"
)
if bot_prob > MAX_BOT_PROBABILITY:
failures.append(
f"Bot probability {bot_prob:.0%} > {MAX_BOT_PROBABILITY:.0%}"
)
return WalletEvaluation(
wallet=wallet,
total_trades=total_trades,
wins=wins,
losses=losses,
win_rate=win_rate,
profit_factor=profit_factor,
total_pnl_sol=total_pnl_sol,
total_pnl_usd=total_pnl_usd,
distinct_tokens=distinct_tokens,
days_since_last_trade=days_since_last,
max_single_trade_pnl_pct=max_single_pnl_pct,
median_hold_time_hours=median_hold_hours,
bot_probability=bot_prob,
consistency_score=consistency,
composite_score=composite,
rating=rating,
passed_all_minimums=len(failures) == 0,
failures=failures,
)
# ── Display ─────────────────────────────────────────────────────────
def print_evaluation(ev: WalletEvaluation) -> None:
"""Print formatted evaluation report."""
sep = "=" * 70
print(f"\n{sep}")
print(f" COPY-TRADE WALLET EVALUATION")
print(f"{sep}")
print(f" Wallet: {ev.wallet}")
print(f"{sep}\n")
print(" PERFORMANCE SUMMARY")
print(f" {'Total trades:':<30} {ev.total_trades}")
print(f" {'Wins / Losses:':<30} {ev.wins} / {ev.losses}")
print(f" {'Win rate:':<30} {ev.win_rate:.1%}")
print(f" {'Profit factor:':<30} {ev.profit_factor:.2f}")
print(f" {'Total PnL (SOL):':<30} {ev.total_pnl_sol:+.4f}")
print(f" {'Total PnL (USD):':<30} ${ev.total_pnl_usd:+,.2f}")
print(f" {'Distinct tokens:':<30} {ev.distinct_tokens}")
print(f" {'Days since last trade:':<30} {ev.days_since_last_trade:.1f}")
print(f" {'Median hold time:':<30} {ev.median_hold_time_hours:.1f} hours")
print(f" {'Max single-trade PnL %:':<30} {ev.max_single_trade_pnl_pct:.0%}")
print(f" {'Bot probability:':<30} {ev.bot_probability:.0%}")
print()
print(" SCORING BREAKDOWN")
tc = score_trade_count(ev.total_trades)
wr = score_win_rate(ev.win_rate)
pf = score_profit_factor(ev.profit_factor)
rec = score_recency(ev.days_since_last_trade)
hum = score_human_probability(ev.bot_probability)
print(f" {'Trade count:':<30} {tc:5.1f} / 100 (weight: {WEIGHT_TRADE_COUNT:.0%})")
print(f" {'Win rate:':<30} {wr:5.1f} / 100 (weight: {WEIGHT_WIN_RATE:.0%})")
print(
f" {'Profit factor:':<30} {pf:5.1f} / 100 (weight: {WEIGHT_PROFIT_FACTOR:.0%})"
)
print(
f" {'Consistency:':<30} {ev.consistency_score:5.1f} / 100 (weight: {WEIGHT_CONSISTENCY:.0%})"
)
print(f" {'Recency:':<30} {rec:5.1f} / 100 (weight: {WEIGHT_RECENCY:.0%})")
print(
f" {'Human probability:':<30} {hum:5.1f} / 100 (weight: {WEIGHT_HUMAN_PROB:.0%})"
)
print(f"\n {'COMPOSITE SCORE:':<30} {ev.composite_score:.1f} / 100")
print(f" {'RATING:':<30} {ev.rating}")
print()
print(" MINIMUM THRESHOLD CHECK")
if ev.passed_all_minimums:
print(" All minimum thresholds PASSED")
else:
print(" FAILED minimum thresholds:")
for f in ev.failures:
print(f" - {f}")
print()
print(f" RECOMMENDATION")
if ev.composite_score >= 80 and ev.passed_all_minimums:
print(" >>> GO — Strong copy-trade candidate")
print(" Recommended allocation: standard (full per-wallet budget)")
elif ev.composite_score >= 60 and len(ev.failures) <= 1:
print(" >>> CONDITIONAL GO — Suitable with monitoring")
print(" Recommended allocation: reduced (50-75% of per-wallet budget)")
elif ev.composite_score >= 40:
print(" >>> WATCHLIST — Not ready for copy trading")
print(" Add to watchlist and re-evaluate in 1-2 weeks")
else:
print(" >>> NO-GO — Do not copy this wallet")
print(" Insufficient evidence of replicable edge")
print(f"\n{sep}\n")
# ── Demo Mode ───────────────────────────────────────────────────────
def generate_demo_trades() -> list[TokenTrade]:
"""Generate realistic example trade data for demonstration."""
import random
random.seed(42)
now = int(time.time())
trades: list[TokenTrade] = []
symbols = [
"BONK", "WIF", "POPCAT", "MEW", "BOME", "MYRO", "WEN", "JUP",
"PYTH", "JTO", "TNSR", "KMNO", "DRIFT", "RENDER", "HNT",
"MOBILE", "HONEY", "MNDE", "STEP", "RAY", "ORCA", "SRM",
"FIDA", "ATLAS", "POLIS", "SAMO", "COPE", "MEDIA", "TULIP",
]
for i, symbol in enumerate(symbols):
# Simulate a mix of wins and losses (roughly 60% win rate)
is_win = random.random() < 0.60
bought = round(random.uniform(0.5, 5.0), 2)
if is_win:
pnl = round(random.uniform(0.1, 3.0), 4)
else:
pnl = round(-random.uniform(0.1, bought * 0.8), 4)
sold = bought + pnl
# Random timestamps within last 30 days
first_ts = now - random.randint(86400, 30 * 86400)
hold_seconds = random.randint(300, 3 * 86400) # 5 min to 3 days
last_ts = first_ts + hold_seconds
trades.append(
TokenTrade(
token_address=f"TokenMint{i:04d}{'x' * 36}"[:44],
token_symbol=symbol,
pnl_sol=pnl,
pnl_usd=pnl * 150.0, # Approximate SOL price
bought_sol=bought,
sold_sol=max(sold, 0),
num_buys=random.randint(1, 3),
num_sells=random.randint(1, 2),
first_trade_ts=first_ts,
last_trade_ts=last_ts,
)
)
# Add some extra trades for the same tokens to increase count
for _ in range(40):
base = random.choice(trades)
is_win = random.random() < 0.58
bought = round(random.uniform(0.3, 3.0), 2)
pnl = round(random.uniform(0.05, 1.5) if is_win else -random.uniform(0.05, bought * 0.7), 4)
first_ts = now - random.randint(86400, 25 * 86400)
hold_seconds = random.randint(600, 2 * 86400)
trades.append(
TokenTrade(
token_address=f"TokenMint{len(trades):04d}{'x' * 36}"[:44],
token_symbol=f"TKN{len(trades)}",
pnl_sol=pnl,
pnl_usd=pnl * 150.0,
bought_sol=bought,
sold_sol=max(bought + pnl, 0),
num_buys=1,
num_sells=1,
first_trade_ts=first_ts,
last_trade_ts=first_ts + hold_seconds,
)
)
return trades
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Main entry point."""
args = sys.argv[1:]
demo_mode = "--demo" in args
if demo_mode:
print("[DEMO MODE] Using generated example trade data\n")
wallet = "DemoWa11etXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
trades = generate_demo_trades()
evaluation = evaluate_wallet(wallet, trades)
print_evaluation(evaluation)
return
# Determine wallet address
wallet = None
for arg in args:
if not arg.startswith("-"):
wallet = arg
break
if not wallet:
wallet = os.getenv("WALLET_ADDRESS", "")
if not wallet:
print("Usage: python scripts/evaluate_wallet.py <wallet_address>")
print(" python scripts/evaluate_wallet.py --demo")
print("Or set WALLET_ADDRESS environment variable.")
sys.exit(1)
print(f"Evaluating wallet: {wallet}")
print("Fetching PnL data from SolanaTracker...\n")
try:
pnl_data = fetch_wallet_pnl(wallet)
except httpx.HTTPStatusError as e:
print(f"API error: {e.response.status_code} — {e.response.text[:200]}")
sys.exit(1)
except httpx.RequestError as e:
print(f"Request failed: {e}")
sys.exit(1)
trades = parse_trades(pnl_data)
if not trades:
print("No trade data found for this wallet.")
sys.exit(1)
print(f"Found {len(trades)} token trades. Evaluating...\n")
evaluation = evaluate_wallet(wallet, trades)
print_evaluation(evaluation)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Monitor a Solana wallet for new swap transactions in real time.
Polls for new transactions at a configurable interval, detects swaps,
and prints trade alerts with token details from DexScreener. Includes
a --demo mode that simulates incoming trades.
Usage:
python scripts/monitor_wallet.py <wallet_address>
python scripts/monitor_wallet.py --demo
Dependencies:
uv pip install httpx
Environment Variables:
WALLET_ADDRESS: Wallet to monitor (optional if passed as argument)
HELIUS_API_KEY: Helius API key for parsed transaction history
SOLANA_RPC_URL: Fallback RPC URL if no Helius key (default: public mainnet)
"""
import json
import os
import signal
import sys
import time
from dataclasses import dataclass
from typing import Optional
try:
import httpx
except ImportError:
print("Missing dependency. Install with: uv pip install httpx")
sys.exit(1)
# ── Configuration ───────────────────────────────────────────────────
HELIUS_API_KEY = os.getenv("HELIUS_API_KEY", "")
SOLANA_RPC_URL = os.getenv(
"SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com"
)
POLL_INTERVAL_SECONDS = 10
DEXSCREENER_BASE = "https://api.dexscreener.com/latest/dex"
# Known program IDs for DEX swaps
SWAP_PROGRAMS = {
"JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4": "Jupiter v6",
"JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB": "Jupiter v4",
"675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8": "Raydium AMM",
"whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc": "Orca Whirlpool",
"9W959DqEETiGZocYWCQPaJ6sBmUzgfxXfqGeTEdp3aQP": "Orca v1",
"6MLxLqiXaaSUpkgMnWDTuejNZEz3kE7k2woyHGVFw319": "Meteora",
}
# Graceful shutdown
_running = True
def _handle_signal(signum: int, frame: object) -> None:
global _running
_running = False
print("\nShutting down monitor...")
signal.signal(signal.SIGINT, _handle_signal)
signal.signal(signal.SIGTERM, _handle_signal)
# ── Data Structures ────────────────────────────────────────────────
@dataclass
class SwapAlert:
"""A detected swap transaction."""
signature: str
timestamp: int
dex: str
direction: str # "BUY" or "SELL"
token_address: str
token_symbol: str
token_name: str
amount_sol: float
token_amount: float
price_usd: Optional[float]
liquidity_usd: Optional[float]
market_cap_usd: Optional[float]
# ── API Functions ───────────────────────────────────────────────────
def fetch_recent_signatures(
wallet: str, limit: int = 5
) -> list[dict]:
"""Fetch recent transaction signatures for a wallet.
Args:
wallet: Solana wallet address to monitor.
limit: Number of recent signatures to fetch.
Returns:
List of signature objects with signature, slot, blockTime.
"""
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getSignaturesForAddress",
"params": [wallet, {"limit": limit}],
}
rpc_url = SOLANA_RPC_URL
if HELIUS_API_KEY:
rpc_url = f"https://mainnet.helius-rpc.com/?api-key={HELIUS_API_KEY}"
with httpx.Client(timeout=15.0) as client:
resp = client.post(rpc_url, json=payload)
resp.raise_for_status()
result = resp.json().get("result", [])
return result
def fetch_parsed_transaction(signature: str) -> Optional[dict]:
"""Fetch and parse a transaction using Helius.
Args:
signature: Transaction signature to parse.
Returns:
Parsed transaction data, or None if parsing fails.
"""
if not HELIUS_API_KEY:
return _fetch_transaction_rpc(signature)
url = f"https://api.helius.xyz/v0/transactions/?api-key={HELIUS_API_KEY}"
with httpx.Client(timeout=15.0) as client:
try:
resp = client.post(url, json={"transactions": [signature]})
resp.raise_for_status()
results = resp.json()
return results[0] if results else None
except (httpx.HTTPStatusError, IndexError, KeyError):
return None
def _fetch_transaction_rpc(signature: str) -> Optional[dict]:
"""Fallback: fetch transaction via standard RPC."""
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "getTransaction",
"params": [
signature,
{"encoding": "jsonParsed", "maxSupportedTransactionVersion": 0},
],
}
with httpx.Client(timeout=15.0) as client:
try:
resp = client.post(SOLANA_RPC_URL, json=payload)
resp.raise_for_status()
return resp.json().get("result")
except (httpx.HTTPStatusError, KeyError):
return None
def fetch_token_info(token_address: str) -> dict:
"""Fetch token info from DexScreener.
Args:
token_address: Token mint address.
Returns:
Dict with symbol, name, priceUsd, liquidity, marketCap.
"""
url = f"{DEXSCREENER_BASE}/tokens/{token_address}"
with httpx.Client(timeout=10.0) as client:
try:
resp = client.get(url)
resp.raise_for_status()
data = resp.json()
pairs = data.get("pairs", [])
if pairs:
pair = pairs[0]
base = pair.get("baseToken", {})
return {
"symbol": base.get("symbol", "???"),
"name": base.get("name", "Unknown"),
"priceUsd": pair.get("priceUsd"),
"liquidity": pair.get("liquidity", {}).get("usd"),
"marketCap": pair.get("marketCap"),
}
except (httpx.HTTPStatusError, httpx.RequestError):
pass
return {"symbol": "???", "name": "Unknown", "priceUsd": None, "liquidity": None, "marketCap": None}
# ── Transaction Parsing ────────────────────────────────────────────
def detect_swap(tx_data: dict) -> Optional[dict]:
"""Detect if a transaction is a DEX swap.
Args:
tx_data: Parsed transaction data (Helius or RPC format).
Returns:
Dict with swap details, or None if not a swap.
"""
if not tx_data:
return None
# Helius parsed format
if "type" in tx_data:
tx_type = tx_data.get("type", "")
if tx_type == "SWAP":
events = tx_data.get("events", {})
swap = events.get("swap", {})
if swap:
token_inputs = swap.get("tokenInputs", [])
token_outputs = swap.get("tokenOutputs", [])
native_input = swap.get("nativeInput", {})
native_output = swap.get("nativeOutput", {})
# Determine direction
if native_input and token_outputs:
# Spent SOL, got tokens = BUY
token = token_outputs[0] if token_outputs else {}
return {
"direction": "BUY",
"token_address": token.get("mint", ""),
"amount_sol": native_input.get("amount", 0) / 1e9,
"token_amount": token.get("amount", 0),
"dex": tx_data.get("source", "Unknown"),
}
elif token_inputs and native_output:
# Spent tokens, got SOL = SELL
token = token_inputs[0] if token_inputs else {}
return {
"direction": "SELL",
"token_address": token.get("mint", ""),
"amount_sol": native_output.get("amount", 0) / 1e9,
"token_amount": token.get("amount", 0),
"dex": tx_data.get("source", "Unknown"),
}
# RPC parsed format — check for known swap program IDs
if "transaction" in tx_data:
msg = tx_data.get("transaction", {}).get("message", {})
instructions = msg.get("instructions", [])
for ix in instructions:
program_id = ix.get("programId", "")
if program_id in SWAP_PROGRAMS:
return {
"direction": "UNKNOWN",
"token_address": "",
"amount_sol": 0,
"token_amount": 0,
"dex": SWAP_PROGRAMS[program_id],
}
return None
# ── Alert Display ───────────────────────────────────────────────────
def print_alert(alert: SwapAlert) -> None:
"""Print a formatted swap alert."""
sep = "-" * 60
direction_icon = ">>>" if alert.direction == "BUY" else "<<<"
print(f"\n{sep}")
print(f" {direction_icon} {alert.direction} DETECTED")
print(f"{sep}")
print(f" Token: {alert.token_symbol} ({alert.token_name})")
print(f" Mint: {alert.token_address[:20]}...")
print(f" DEX: {alert.dex}")
print(f" SOL amount: {alert.amount_sol:.4f} SOL")
if alert.price_usd:
print(f" Price: ${float(alert.price_usd):.8f}")
if alert.liquidity_usd:
print(f" Liquidity: ${alert.liquidity_usd:,.0f}")
if alert.market_cap_usd:
print(f" Market cap: ${alert.market_cap_usd:,.0f}")
print(f" Tx: {alert.signature[:30]}...")
print(f" Time: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(alert.timestamp))}")
print(sep)
# ── Monitor Loop ────────────────────────────────────────────────────
def monitor_wallet(wallet: str) -> None:
"""Poll wallet for new transactions and alert on swaps.
Args:
wallet: Solana wallet address to monitor.
"""
print(f"Monitoring wallet: {wallet}")
print(f"Poll interval: {POLL_INTERVAL_SECONDS}s")
print(f"Using: {'Helius API' if HELIUS_API_KEY else 'Public RPC (slower)'}")
print("Press Ctrl+C to stop.\n")
# Initialize with current latest signature
last_seen_sig: Optional[str] = None
try:
sigs = fetch_recent_signatures(wallet, limit=1)
if sigs:
last_seen_sig = sigs[0]["signature"]
print(f"Starting from signature: {last_seen_sig[:30]}...")
except Exception as e:
print(f"Warning: could not fetch initial signatures: {e}")
poll_count = 0
swaps_detected = 0
while _running:
poll_count += 1
try:
sigs = fetch_recent_signatures(wallet, limit=10)
except Exception as e:
print(f"[Poll {poll_count}] Error fetching signatures: {e}")
time.sleep(POLL_INTERVAL_SECONDS)
continue
# Find new signatures since last seen
new_sigs: list[dict] = []
for sig_obj in sigs:
if sig_obj["signature"] == last_seen_sig:
break
new_sigs.append(sig_obj)
if new_sigs:
# Update last seen
last_seen_sig = new_sigs[0]["signature"]
for sig_obj in reversed(new_sigs): # Process oldest first
sig = sig_obj["signature"]
block_time = sig_obj.get("blockTime", int(time.time()))
# Fetch and parse transaction
tx_data = fetch_parsed_transaction(sig)
swap_info = detect_swap(tx_data)
if swap_info:
token_addr = swap_info.get("token_address", "")
token_info = fetch_token_info(token_addr) if token_addr else {}
alert = SwapAlert(
signature=sig,
timestamp=block_time,
dex=swap_info.get("dex", "Unknown"),
direction=swap_info.get("direction", "UNKNOWN"),
token_address=token_addr,
token_symbol=token_info.get("symbol", "???"),
token_name=token_info.get("name", "Unknown"),
amount_sol=swap_info.get("amount_sol", 0),
token_amount=swap_info.get("token_amount", 0),
price_usd=token_info.get("priceUsd"),
liquidity_usd=token_info.get("liquidity"),
market_cap_usd=token_info.get("marketCap"),
)
print_alert(alert)
swaps_detected += 1
# Status update every 6 polls (1 minute)
if poll_count % 6 == 0:
print(
f"[{time.strftime('%H:%M:%S')}] "
f"Polls: {poll_count} | Swaps detected: {swaps_detected}"
)
time.sleep(POLL_INTERVAL_SECONDS)
print(f"\nMonitor stopped. Total polls: {poll_count}, Swaps detected: {swaps_detected}")
# ── Demo Mode ───────────────────────────────────────────────────────
def run_demo() -> None:
"""Simulate wallet monitoring with fake trade alerts."""
print("[DEMO MODE] Simulating wallet monitoring with example trades\n")
print("Monitoring wallet: DemoWa11etXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")
print("Press Ctrl+C to stop.\n")
demo_alerts = [
SwapAlert(
signature="5xYz" + "A" * 80,
timestamp=int(time.time()) - 120,
dex="Jupiter v6",
direction="BUY",
token_address="DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
token_symbol="BONK",
token_name="Bonk",
amount_sol=2.5,
token_amount=50_000_000,
price_usd="0.00002341",
liquidity_usd=4_500_000,
market_cap_usd=1_200_000_000,
),
SwapAlert(
signature="7kMn" + "B" * 80,
timestamp=int(time.time()) - 60,
dex="Raydium AMM",
direction="BUY",
token_address="EKpQGSJtjMFqKZ9KQanSqYXRcF8fBopzLHYxdM65zcjm",
token_symbol="WIF",
token_name="dogwifhat",
amount_sol=5.0,
token_amount=3_200,
price_usd="0.234",
liquidity_usd=12_000_000,
market_cap_usd=2_500_000_000,
),
SwapAlert(
signature="9pQr" + "C" * 80,
timestamp=int(time.time()),
dex="Jupiter v6",
direction="SELL",
token_address="DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263",
token_symbol="BONK",
token_name="Bonk",
amount_sol=3.1,
token_amount=50_000_000,
price_usd="0.00002412",
liquidity_usd=4_500_000,
market_cap_usd=1_200_000_000,
),
]
for i, alert in enumerate(demo_alerts):
if not _running:
break
print(f"\n[Simulated delay: waiting for next trade...]")
time.sleep(8)
if not _running:
break
alert.timestamp = int(time.time())
print_alert(alert)
print("\n[DEMO] All simulated trades shown.")
print("[DEMO] In live mode, monitoring continues until Ctrl+C.")
# ── Main ────────────────────────────────────────────────────────────
def main() -> None:
"""Main entry point."""
args = sys.argv[1:]
demo_mode = "--demo" in args
if demo_mode:
run_demo()
return
# Determine wallet address
wallet = None
for arg in args:
if not arg.startswith("-"):
wallet = arg
break
if not wallet:
wallet = os.getenv("WALLET_ADDRESS", "")
if not wallet:
print("Usage: python scripts/monitor_wallet.py <wallet_address>")
print(" python scripts/monitor_wallet.py --demo")
print("Or set WALLET_ADDRESS environment variable.")
sys.exit(1)
if not HELIUS_API_KEY and SOLANA_RPC_URL == "https://api.mainnet-beta.solana.com":
print("WARNING: Using public RPC endpoint. Set HELIUS_API_KEY for better")
print(" performance and parsed transaction data.\n")
monitor_wallet(wallet)
if __name__ == "__main__":
main()
Related skills
FAQ
What thresholds should a wallet pass before copying it?
The skill lists minimums such as >=50 trades in 30 days, win rate >=55%, profit factor >=1.5, active within 7 days, and bot probability <30%.
How is copy-trade sizing decided?
Positions are sized proportionally to your own portfolio, not the copied wallet's, with per-wallet allocation of 10-20% and independent stop losses.