A practical implementation architecture for monitoring wallets, decoding trades, applying copy rules, sizing positions, executing trades, and reconciling on-chain state.
A Pons copy trading bot watches a source wallet, detects supported trading activity, decides whether the trade should be copied, and executes a separate position from the follower wallet.
The simple version looks like:
Source Wallet
↓
Detect Trade
↓
Copy Trade
A useful implementation looks more like:
Source Wallet
↓
Block / Event Monitor
↓
Trade Decoder
↓
Copy Strategy
↓
Risk Engine
↓
Position Sizing
↓
Fresh Quote
↓
Pons Execution
↓
Transaction Monitor
↓
Portfolio
↓
Reconciliation
Pons is a launch-and-trade application on Robinhood Chain, and its current documentation describes live token trading through each token's own pool.
The goal of this article is to show how I would structure a Pons copy trading bot as a real automation system rather than a script that blindly mirrors wallet transactions.
What Is a Pons Copy Trading Bot?
A Pons copy trading bot monitors one or more wallets and converts their supported trades into signals.
For example:
Trader Wallet
↓
BUY Token A
↓
Copy Engine
↓
Risk Check
↓
Follower Wallet
↓
BUY Token A
But the follower does not have to copy the transaction exactly.
It can apply its own:
- copy ratio
- maximum position
- slippage
- token filters
- exposure limits
- signal-age limits
- gas reserve
- emergency stop
That is the difference between transaction replication and copy-trading infrastructure.
Why the Architecture Matters
The source wallet controls only the signal.
Your system should control the money.
SOURCE
↓
OBSERVATION
↓
STRATEGY
↓
RISK
↓
EXECUTION
That means the source wallet should never directly determine:
how much you buy
how much you risk
which tokens you accept
how much slippage you tolerate
how many positions you can hold
Those are follower-side decisions.
Project Architecture
I would separate the implementation into:
bot/
├── monitor/
│ └── wallet_monitor.py
├── decoder/
│ └── trade_decoder.py
├── strategy/
│ └── copy_strategy.py
├── risk/
│ └── risk_engine.py
├── sizing/
│ └── position_sizer.py
├── execution/
│ └── pons_executor.py
├── portfolio/
│ └── position_manager.py
├── reconciliation/
│ └── reconciler.py
└── engine.py
config/
├── wallets.json
└── default.yaml
tests/
The exact structure can differ, but the separation should remain.
A monitor should monitor.
An executor should execute.
A portfolio manager should track positions.
1. Connect to Robinhood Chain
Robinhood Chain mainnet currently uses chain ID 4663 and ETH as its native gas asset.
A minimal Python connection can start with:
from web3 import Web3
import os
rpc_url = os.environ["RH_RPC_URL"]
web3 = Web3(
Web3.HTTPProvider(rpc_url)
)
if not web3.is_connected():
raise RuntimeError("Failed to connect to Robinhood Chain")
print("Connected:", web3.is_connected())
print("Chain ID:", web3.eth.chain_id)
The important production rule is:
Do not assume the RPC is healthy.
Add:
timeouts
retries
backoff
health checks
And distinguish:
RPC error
from:
transaction reverted
They are not the same failure.
2. Configure Source Wallets
The first useful configuration is a list of source wallets.
For example:
{
"wallets": [
{
"address": "0xSourceWallet...",
"enabled": true,
"label": "alpha-trader",
"copy_ratio": 0.10,
"max_position_eth": 0.02
}
]
}
This configuration gives the system:
wallet identity
copy ratio
position limit
enable/disable state
A production implementation should also support:
per-wallet risk
priority
score
token filters
daily limits
3. Monitor New Blocks
One practical approach is block polling.
last_block = web3.eth.block_number
while True:
current_block = web3.eth.block_number
if current_block > last_block:
for block_number in range(
last_block + 1,
current_block + 1
):
process_block(block_number)
last_block = current_block
The monitor's job is to identify transactions involving the source wallet.
It should not immediately execute trades.
Instead:
Block
↓
Source wallet transaction
↓
Candidate transaction
Then hand that transaction to the decoder.
4. Detect Source Wallet Activity
A source wallet may produce many types of transactions:
ETH transfer
Token transfer
Approval
Launch
Buy
Sell
Other contract call
The monitor should only forward transactions that could represent a supported trade.
def is_candidate(tx, source_wallet):
return (
tx["from"].lower()
== source_wallet.lower()
)
That is only the first filter.
You still need to inspect the receipt and contract interaction.
5. Decode the Actual Trade
This is one of the most important parts of the system.
A transaction alone may not tell you the complete economic action.
The decoder should inspect:
transaction input
receipt
logs
Transfer events
router interaction
token addresses
amounts
The normalized output might look like:
from dataclasses import dataclass
from typing import Literal, Optional
@dataclass
class SourceTrade:
trade_id: str
wallet: str
token: str
side: Literal["BUY", "SELL"]
amount_in: int
amount_out: int
tx_hash: str
block_number: int
timestamp: int
Now the strategy layer doesn't need to know anything about raw blockchain logs.
6. Deduplicate Trades
The same transaction may be observed more than once.
This can happen after:
RPC reconnect
process restart
polling overlap
event + polling
So generate a deterministic ID.
For example:
trade_id = f"{chain_id}:{tx_hash}"
Then:
if registry.exists(trade_id):
return
registry.add(trade_id)
The workflow becomes:
Trade
↓
Registry
↓
Seen?
├── YES → ignore
└── NO → evaluate
Duplicate prevention is one of the most important pieces of copy-trading infrastructure.
7. Copy Strategy
Now we have a real trade.
The next question:
Should the follower copy it?
Create a strategy function:
def should_copy(trade: SourceTrade) -> bool:
if trade.side not in {"BUY", "SELL"}:
return False
if trade.token in BLOCKED_TOKENS:
return False
return True
The production version can add:
token allowlist
token denylist
minimum trade size
maximum trade size
source-wallet score
trade age
liquidity
existing position
The important thing is to keep the rules configurable.
8. Copy Ratio
A simple copy strategy might use a percentage of the source trade.
Suppose:
Source:
0.50 ETH
Copy ratio:
10%
Then:
Follower:
0.05 ETH
Using integer arithmetic:
BPS = 10_000
def copy_amount(
source_amount: int,
copy_ratio_bps: int,
) -> int:
return (
source_amount
* copy_ratio_bps
// BPS
)
This avoids floating-point calculations.
9. Add a Maximum Position
Even if the copy calculation says:
0.08 ETH
the strategy might only allow:
0.02 ETH
So:
amount = min(
proportional_amount,
max_position
)
The final process becomes:
Source Trade
↓
Copy Ratio
↓
Maximum Position
↓
Risk Limits
↓
Final Position Size
This is much safer than blindly copying source trade size.
10. Signal Freshness
Copy trading has a latency problem.
The source transaction may already be on-chain before the follower detects it.
So every signal should have an age:
trade_age = now - trade.timestamp
Then:
if trade_age > MAX_SIGNAL_AGE_SECONDS:
return False
The pipeline becomes:
Source Trade
↓
Detection
↓
Age Check
↓
Fresh?
├── NO → SKIP
└── YES
A stale signal can produce a completely different execution outcome from the original trade.
11. Risk Engine
The copy strategy should not have direct permission to spend funds.
Add a risk layer:
class RiskEngine:
def validate(
self,
token: str,
amount: int,
wallet_balance: int,
) -> tuple[bool, str]:
if amount <= 0:
return False, "invalid amount"
if amount > MAX_POSITION:
return False, "position too large"
if wallet_balance < amount + GAS_RESERVE:
return False, "insufficient balance"
return True, "approved"
The architecture is:
Source Trade
↓
Copy Strategy
↓
Risk Engine
↓
Execution
That separation is important.
12. Gas Reserve
Never use:
wallet balance = trading capital
Instead:
wallet balance
↓
gas reserve
↓
spendable balance
For example:
spendable = (
wallet_balance
- gas_reserve
)
if spendable <= 0:
raise RuntimeError(
"No spendable balance"
)
The risk engine should reject a trade if the gas reserve would be violated.
13. Fresh Pons Quote
A follower should calculate its own current quote.
Do not simply assume:
source execution price
=
follower execution price
Pons currently documents token trading through each token's live pool, so the follower should evaluate current execution conditions rather than blindly copying the source's historical price.
Conceptually:
Source Trade
↓
Current Pons State
↓
Fresh Quote
↓
Expected Output
↓
Slippage
↓
Follower Transaction
If you already have a Pons-specific quote implementation, reuse it.
Don't create a second pricing engine with an approximate formula.
14. Minimum Output
A buy should define an acceptable execution boundary.
For example:
expected_out = quote.tokens_out
min_out = (
expected_out
* (10_000 - slippage_bps)
// 10_000
)
Then:
Expected Output
↓
Slippage
↓
Minimum Output
↓
Execution
The exact formula should match the protocol's token decimals and contract behavior.
15. Pons Execution Layer
Keep protocol-specific execution in a dedicated class:
class PonsExecutor:
def buy(
self,
token: str,
amount_in: int,
min_tokens_out: int,
):
...
The strategy layer should never need to understand:
nonce
gas
raw transaction
private-key signing
RPC broadcasting
It should only submit an execution request.
16. Transaction State Machine
A copied trade needs explicit state.
SIGNAL_RECEIVED
↓
DECODED
↓
STRATEGY_APPROVED
↓
RISK_APPROVED
↓
QUOTED
↓
PREPARED
↓
SIGNED
↓
SUBMITTED
↓
PENDING
├── CONFIRMED
├── REVERTED
└── UNKNOWN
This state should be persisted.
For example:
@dataclass
class Execution:
trade_id: str
wallet: str
tx_hash: str | None
status: str
amount_in: int
created_at: int
Now the system can recover after a restart.
17. Never Blindly Retry Timeouts
A dangerous pattern is:
RPC timeout
↓
retry transaction
The original transaction might already exist.
Instead:
RPC timeout
↓
Check transaction state
↓
Reconcile
↓
Decide
Possible state:
UNKNOWN
means:
I don't know yet.
It should not mean:
It failed.
18. Nonce Management
For each follower wallet, use a single nonce-management strategy.
For example:
Wallet
↓
Nonce Manager
↓
Transaction A
Transaction B
Transaction C
Do not let:
copy strategy
retry worker
sell worker
transfer worker
all manage the same wallet nonce independently.
That creates race conditions.
19. Portfolio Tracking
Once a copy trade confirms, create or update a follower position.
@dataclass
class Position:
token: str
wallet: str
quantity: int
entry_amount: int
entry_tx: str
status: str
Then:
Trade
↓
Receipt
↓
Actual Token Balance
↓
Position
Do not simply record:
expected token output
as the actual position.
20. Copying Sells
Buy copying is only half the problem.
Suppose the source does:
BUY Token A
and later:
SELL 50% Token A
The follower needs its own sell policy.
Possible models:
Sell 50% of follower position
or:
Sell the same token quantity
or:
Reduce follower exposure to a target
A good architecture keeps this in the copy strategy.
21. Reconciliation
Reconciliation compares local state with the chain.
LOCAL POSITION
↕
RECONCILIATION
↕
CHAIN STATE
Verify:
transaction receipt
token balance
native balance
position state
Run reconciliation:
after execution
after restart
after timeout
after reconnect
periodically
This is what turns a long-running bot from an unreliable process into recoverable infrastructure.
22. Restart Recovery
Imagine:
Source trade detected
↓
Risk approved
↓
Transaction submitted
↓
Process crashes
After restart:
Load state
↓
Find pending execution
↓
Query blockchain
↓
Reconcile
↓
Continue
The bot should not simply start from an empty in-memory state.
23. Paper Trading
Before enabling live execution, build paper trading.
For example:
if PAPER_TRADING:
return PaperExecutionResult(...)
The system should still perform:
monitoring
decoding
strategy
risk
position sizing
quote
but not broadcast a transaction.
That allows you to validate the strategy before risking capital.
24. Emergency Stop
A production bot should always have a kill switch.
For example:
if STOP_TRADING:
raise RuntimeError(
"Trading disabled by emergency stop"
)
Possible triggers:
manual stop
daily loss limit
RPC instability
unexpected execution behavior
too many failed transactions
The emergency stop should block new entries while allowing reconciliation and reporting to continue.
25. Observability
Track the complete funnel:
Source Trades
↓
Decoded Trades
↓
Eligible Trades
↓
Risk Approved
↓
Execution Submitted
↓
Confirmed
Example metrics:
Trades detected: 250
Trades decoded: 231
Signals accepted: 42
Risk rejected: 17
Submitted: 25
Confirmed: 22
Reverted: 2
Pending: 1
This tells you where the system is actually losing opportunities.
26. Latency Measurements
For copy trading, measure:
source transaction
↓
detection
↓
decoding
↓
strategy
↓
risk
↓
quote
↓
submission
For example:
Detection: 80 ms
Decode: 15 ms
Strategy: 4 ms
Risk: 3 ms
Quote: 20 ms
Preparation: 18 ms
Submission: 30 ms
Now you know which component should be optimized.
Without this instrumentation, latency discussions are mostly guesswork.
27. Example End-to-End Flow
A complete copy event might look like:
1. Source wallet buys Token A
↓
2. Block monitor detects transaction
↓
3. Decoder identifies Pons trade
↓
4. Opportunity registry checks duplication
↓
5. Copy strategy approves
↓
6. Signal freshness check passes
↓
7. Position size calculated
↓
8. Risk engine approves
↓
9. Fresh quote calculated
↓
10. minTokensOut calculated
↓
11. Transaction prepared
↓
12. Transaction signed
↓
13. Transaction submitted
↓
14. Receipt monitored
↓
15. Position created
↓
16. Reconciliation verifies state
That is the complete product.
28. CLI
A simple interface could expose:
python main.py connect
python main.py monitor
python main.py run
python main.py reconcile
python main.py portfolio
And live trading should have explicit safety gates.
A useful default is:
PAPER / DRY RUN
with live execution requiring explicit configuration.
29. Production Project Structure
The final project can evolve into:
bot/
├── monitor/
│ ├── block_monitor.py
│ └── wallet_tracker.py
│
├── decoder/
│ ├── trade_decoder.py
│ └── swap_classifier.py
│
├── strategy/
│ ├── copy_strategy.py
│ ├── filters.py
│ └── sizing.py
│
├── risk/
│ └── risk_engine.py
│
├── execution/
│ ├── pons_executor.py
│ ├── nonce_manager.py
│ └── transaction_manager.py
│
├── portfolio/
│ ├── positions.py
│ └── pnl.py
│
├── reconciliation/
│ └── reconciler.py
│
└── engine.py
This structure also makes future strategy development much easier.
30. From Copy Trading to Multiple Strategies
Once the execution engine exists, copy trading becomes one strategy.
For example:
Pons Trading Engine
│
┌──────────────┼──────────────┐
↓ ↓ ↓
Sniper Copy Momentum
Strategy Strategy Strategy
└──────────────┼──────────────┘
↓
Risk Engine
↓
Execution Engine
↓
Robinhood Chain
This is the architecture I would build toward.
The execution engine should be reusable.
Final Thoughts
A Pons copy trading bot is not:
watch wallet
↓
copy transaction
It is:
MONITOR
↓
DECODE
↓
FILTER
↓
SIZE
↓
MANAGE RISK
↓
QUOTE
↓
EXECUTE
↓
MONITOR
↓
RECONCILE
That difference matters.
The source wallet provides a signal.
The follower system controls the decision.
The blockchain determines the actual result.
And reconciliation keeps the application's state aligned with reality.
The Bigger Product Opportunity
Once this execution layer works, it can support more than copy trading.
Pons Bundler
↓
Pons Sniper Bot
↓
Pons Copy Trading Bot
↓
Pons Launch Monitor
↓
Pons Trading Terminal
↓
Stock Token Arbitrage Bot
↓
Stock Token Trading Bot
The common foundation is:
Data
↓
Strategy
↓
Risk
↓
Execution
↓
Monitoring
↓
Reconciliation
That is the part I care about most.
Not just making a bot that can send a transaction.
Building reliable trading infrastructure around a specific protocol.
Need a Pons Copy Trading Bot?
I build custom trading automation for Robinhood Chain, including:
Pons Copy Trading
Pons Sniper Bots
Pons Bundlers
Trading Automation
Risk Engines
Execution Systems
Transaction Monitoring
Portfolio / Reconciliation
The implementation should be designed around the client's strategy, wallet configuration, execution requirements, and risk controls rather than a generic one-size-fits-all bot.
Reference
Pons documentation: https://docs.ponsfamily.com/
Pons V2 documentation: https://docs.ponsfamily.com/v2
Pons bundler implementation: https://github.com/wooyang/pons-bundler
Top comments (0)