A Pons copy trading bot sounds simple:
Trader Wallet
↓
Detect Buy
↓
Copy Buy
That architecture is not enough for a serious trading system.
A usable implementation needs to answer several additional questions:
Was the event really a trade?
What token was traded?
Was it a buy or sell?
How much did the trader actually trade?
How much should the follower copy?
Is the copy trade allowed?
What is the current price?
What slippage is acceptable?
Was the transaction actually confirmed?
What position does the wallet really have now?
A production-oriented architecture therefore looks more like:
Pons / Robinhood Chain
↓
Event Indexer
↓
Trade Decoder
↓
Signal Generator
↓
Copy Strategy
↓
Risk Engine
↓
Execution Engine
↓
Transaction State
↓
Position Engine
↓
Reconciliation
This article walks through that implementation from the TypeScript side.
Current Pons Integration Surface
The current Pons documentation identifies Robinhood Chain as chain ID 4663.
For the current protocol, a launch creates its trading pool at launch, and current tokens trade against WETH in their own pools. The official integration surface is based on reading the factory's TokenLaunched event, registering the emitted pool, and indexing that pool's Swap events. Pons describes onchain events as the authoritative source of truth.
That makes an event-driven architecture a natural starting point:
TokenLaunched
↓
Discover Pool
↓
Index Swap Events
↓
Identify Tracked Wallet
↓
Generate Trade Signal
One important implementation detail: current Pons documentation also says wide public-RPC eth_getLogs ranges can time out, so historical indexing should use bounded block ranges.
1. Create the TypeScript Project
A simple project can start with:
npm install viem
A useful structure is:
src/
├── chain/
│ ├── client.ts
│ ├── contracts.ts
│ └── events.ts
│
├── indexer/
│ ├── runner.ts
│ ├── backfill.ts
│ ├── cursor.ts
│ └── dedupe.ts
│
├── wallets/
│ └── registry.ts
│
├── signals/
│ ├── decoder.ts
│ └── generator.ts
│
├── strategy/
│ └── copy.ts
│
├── risk/
│ ├── sizing.ts
│ └── checks.ts
│
├── execution/
│ ├── quote.ts
│ ├── swap.ts
│ └── state.ts
│
├── positions/
│ ├── engine.ts
│ └── reconciliation.ts
│
└── app.ts
The goal is to avoid putting everything inside one blockchain event listener.
2. Connect to Robinhood Chain
Using viem:
import { createPublicClient, http } from "viem";
export const robinhoodChain = {
id: 4663,
name: "Robinhood Chain",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
default: {
http: [
"https://rpc.mainnet.chain.robinhood.com",
],
},
},
} as const;
export const client = createPublicClient({
chain: robinhoodChain,
transport: http(),
});
The current Pons docs publish the active factory, router, quoter, WETH, and other deployed contract addresses. Those should be configuration values rather than duplicated throughout the codebase.
For example:
export const PONS_FACTORY =
"0xA5aAb3F0c6EeadF30Ef1D3Eb997108E976351feB";
export const PONS_ROUTER =
"0xCaf681a66D020601342297493863E78C959E5cb2";
export const PONS_QUOTER =
"0x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7";
export const WETH =
"0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73";
Keep those in one configuration module.
3. Define the Pons Launch Event
The current integration documentation provides the TokenLaunched event.
A viem definition can look like:
import { parseAbiItem } from "viem";
export const tokenLaunchedEvent = parseAbiItem(
"event TokenLaunched(" +
"address indexed token," +
"address indexed deployer," +
"address indexed dexFactory," +
"address pairToken," +
"address pool," +
"uint256 dexId," +
"uint256 launchConfigId," +
"uint256 positionId," +
"uint256 restrictionsEndBlock," +
"uint256 initialBuyAmount" +
")"
);
Then the indexer can discover current pools:
const logs = await client.getLogs({
address: PONS_FACTORY,
event: tokenLaunchedEvent,
fromBlock: START_BLOCK,
toBlock: "latest",
});
Pons documents this event-driven factory → pool → swap indexing pattern directly.
4. Build a Wallet Registry
The bot should not hard-code tracked wallets into the event handler.
Create a registry:
export type TrackedWallet = {
id: string;
address: `0x${string}`;
label?: string;
enabled: boolean;
copyRatioBps: bigint;
maxTradeUsdCents: bigint;
maxPositionUsdCents: bigint;
};
An example configuration:
const trader: TrackedWallet = {
id: "trader-a",
address: "0x1234...abcd",
label: "Trader A",
enabled: true,
copyRatioBps: 2_500n,
maxTradeUsdCents: 100_000n,
maxPositionUsdCents: 500_000n,
};
That represents:
Copy ratio: 25%
Maximum trade: $1,000
Maximum position: $5,000
Different tracked wallets can have different configurations.
5. Normalize Blockchain Activity
The rest of the application should not consume raw event arguments.
Create a domain model:
export type TradeSide = "BUY" | "SELL";
export type WalletTrade = {
wallet: `0x${string}`;
token: `0x${string}`;
pool: `0x${string}`;
side: TradeSide;
tokenAmount: bigint;
quoteAmount: bigint;
txHash: `0x${string}`;
blockNumber: bigint;
logIndex: bigint;
timestamp: number;
};
Now the strategy engine receives:
BUY
TOKEN
$4,000
Wallet A
rather than an opaque blockchain event.
This is one of the most useful boundaries in the architecture.
6. Decode Buy vs Sell
For the current Pons pool integration, token ordering determines which signed amount should be examined to derive the side.
Pons documents the logic using isToken0 / token ordering and the signed paired-asset amount.
A simplified implementation:
function classifyTrade(
token: `0x${string}`,
pairToken: `0x${string}`,
amount0: bigint,
amount1: bigint,
): TradeSide {
const tokenIsToken0 =
token.toLowerCase() < pairToken.toLowerCase();
const pairSigned = tokenIsToken0
? amount1
: amount0;
return pairSigned > 0n
? "BUY"
: "SELL";
}
The decoder is then responsible for turning:
Raw Swap
into:
WalletTrade
The copy-trading strategy does not need to understand the pool's internal amount ordering.
7. Use Strong Numeric Types
Trading code should avoid JavaScript floating-point arithmetic for blockchain quantities.
I use explicit types:
type TokenAmount = bigint;
type UsdCents = bigint;
type BasisPoints = bigint;
Then:
type RiskRequest = {
requestedAmount: TokenAmount;
notionalUsdCents: UsdCents;
estimatedPriceImpactBps: BasisPoints;
};
And:
type PositionLimits = {
maxTradeUsdCents: UsdCents;
maxPositionUsdCents: UsdCents;
maxPortfolioWeightBps: BasisPoints;
maxPriceImpactBps: BasisPoints;
};
This avoids mistakes such as comparing:
token atomic units
against:
USD cents
The unit conversion should happen at explicit boundaries.
8. Generate the Copy Signal
Suppose a tracked wallet buys:
MEMESTOCK
$4,000
The indexer produces:
const signal = {
wallet: trader.address,
token: "0xabcd...1234",
side: "BUY" as const,
notionalUsdCents: 400_000n,
};
This is still only a signal.
No transaction should be sent yet.
The signal flows into the copy strategy.
9. Calculate the Copy Size
With a 25% copy ratio:
function calculateCopyNotional(
originalUsdCents: bigint,
copyRatioBps: bigint,
): bigint {
return (
originalUsdCents *
copyRatioBps
) / 10_000n;
}
Example:
const copySize = calculateCopyNotional(
400_000n,
2_500n,
);
Result:
100,000 cents
=
$1,000
The strategy now wants to copy $1,000.
That still does not mean the bot is allowed to trade $1,000.
10. Apply the Maximum Trade Limit
Suppose:
Requested copy: $1,000
Maximum trade: $750
Use a cap:
function capTrade(
requestedUsdCents: bigint,
maxTradeUsdCents: bigint,
): bigint {
return requestedUsdCents >
maxTradeUsdCents
? maxTradeUsdCents
: requestedUsdCents;
}
The result:
Requested
$1,000
Risk maximum
$750
Approved candidate
$750
The distinction is important:
The strategy calculates intent. The risk engine decides permission.
11. Check Existing Position Exposure
Now suppose the follower already has:
Current MEMESTOCK position = $4,600
and the maximum position is:
$5,000
The actual capacity is:
$5,000 - $4,600
= $400
Therefore:
Requested: $1,000
Trade cap: $750
Position capacity: $400
Final maximum:
$400
A simple check can be:
function getPositionCapacity(
currentPositionUsdCents: bigint,
maxPositionUsdCents: bigint,
): bigint {
if (currentPositionUsdCents >= maxPositionUsdCents) {
return 0n;
}
return (
maxPositionUsdCents -
currentPositionUsdCents
);
}
This prevents a copy ratio from accidentally overriding portfolio limits.
12. Add Portfolio-Level Rules
Trade-level limits are only one layer.
A complete risk engine can check:
Maximum trade size
Maximum position size
Maximum portfolio weight
Maximum price impact
Maximum daily allocation
Available balance
Allowed tokens
Allowed wallets
For example:
type RiskDecision =
| {
approved: true;
finalNotionalUsdCents: bigint;
}
| {
approved: false;
reason: string;
};
A rejected signal should become a first-class event:
SIGNAL_CREATED
↓
RISK_REJECTED
↓
reason = MAX_POSITION
That makes the system observable.
13. Separate Price Impact From Slippage
The bot should not treat these as the same thing.
Pons documentation describes price impact as the movement caused by the size of the trade and slippage as the execution movement the transaction is willing to accept.
So configuration can have separate limits:
const MAX_PRICE_IMPACT_BPS = 300n;
const MAX_SLIPPAGE_BPS = 100n;
Meaning:
Maximum price impact = 3%
Maximum slippage = 1%
The exact values are application-specific.
The important architectural point is that they are separate controls.
14. Build an Execution Request
Once risk approves the trade:
type ExecutionRequest = {
signalId: string;
wallet: `0x${string}`;
token: `0x${string}`;
side: "BUY" | "SELL";
notionalUsdCents: bigint;
slippageBps: bigint;
maxPriceImpactBps: bigint;
};
Example:
Signal:
BUY MEMESTOCK
Requested:
$1,000
Risk-approved:
$400
Max slippage:
1%
Max price impact:
3%
The execution layer now receives a clean request.
It does not need to know why the trade was selected.
15. Quote Immediately Before Execution
Copy trading has an inherent delay:
Original wallet trades
↓
Event appears
↓
Indexer processes it
↓
Strategy runs
↓
Risk runs
↓
Follower requests quote
↓
Follower executes
The market can change during that sequence.
Therefore a quote should be requested close to transaction submission.
Conceptually:
Approved Request
↓
Fresh Quote
↓
Minimum Output
↓
Transaction
Do not reuse the original trader's execution price as if the follower could reproduce it exactly.
16. Track Execution State Explicitly
A transaction submission is not the same as a completed trade.
Use a state machine:
SIGNAL_CREATED
↓
RISK_APPROVED
↓
EXECUTION_REQUESTED
↓
QUOTE_RECEIVED
↓
TRANSACTION_SUBMITTED
↓
TRANSACTION_CONFIRMED
↓
POSITION_UPDATED
↓
RECONCILED
Failure states should also be explicit:
RISK_REJECTED
QUOTE_FAILED
INSUFFICIENT_BALANCE
TRANSACTION_FAILED
SLIPPAGE_EXCEEDED
RECONCILIATION_REQUIRED
This is much more robust than treating execution as one function call.
17. Store the Transaction
For example:
type ExecutionRecord = {
signalId: string;
status:
| "SUBMITTED"
| "CONFIRMED"
| "FAILED";
txHash?: `0x${string}`;
requestedAmount: bigint;
executedAmount?: bigint;
submittedAt: number;
confirmedAt?: number;
};
Now the system can trace:
Wallet signal
↓
Strategy decision
↓
Risk decision
↓
Execution request
↓
Transaction hash
↓
Receipt
That trace is extremely useful when debugging a live trading system.
18. Do Not Assume Requested Amount = Executed Amount
Suppose the bot requested:
100,000 TOKEN
The final execution could differ.
The internal database should therefore not simply write:
position += requestedAmount;
Instead:
Requested Amount
↓
Transaction
↓
Receipt / Events
↓
Actual Result
↓
Position Engine
The resulting position should be based on the actual onchain outcome.
19. Position Reconciliation
A dedicated reconciliation process can compare internal state against the wallet's onchain state.
Internal Position
vs
Onchain Balance
↓
Compare
/ \
Match Difference
↓ ↓
Done Reconcile
This matters because the wallet may change outside the copy bot.
For example:
Copy bot trade
Manual trade
Token transfer
Another application
All of these can modify the actual wallet.
The chain remains the final state.
20. Track Transfers Too
A wallet tracker that only watches swaps can miss position changes.
Suppose:
Wallet A
↓
TOKEN TRANSFER
↓
Wallet B
Wallet B now owns more tokens even though it did not buy them through the Pons pool.
The broader activity model should therefore distinguish:
BUY
SELL
TRANSFER_IN
TRANSFER_OUT
This becomes particularly important for reconciliation.
21. Make the Indexer Restartable
A production indexer needs a persistent cursor:
type IndexerCursor = {
contract: `0x${string}`;
lastProcessedBlock: bigint;
};
Processing:
Saved Block
↓
Read Next Range
↓
Decode Events
↓
Persist Events
↓
Update Position State
↓
Save Cursor
If the process dies, it resumes from the saved position.
The current Pons documentation specifically warns that broad public-RPC log queries can time out and recommends bounded block chunks for backfills.
For example:
const CHUNK_SIZE = 10_000n;
for (
let from = startBlock;
from <= latestBlock;
from += CHUNK_SIZE
) {
const to =
from + CHUNK_SIZE - 1n > latestBlock
? latestBlock
: from + CHUNK_SIZE - 1n;
const logs = await client.getLogs({
address: PONS_FACTORY,
event: tokenLaunchedEvent,
fromBlock: from,
toBlock: to,
});
await processLogs(logs);
}
The actual chunk size should be configurable.
22. Add Idempotency
Indexers should be safe to retry.
A practical event identity is:
transaction hash + log index
Database:
UNIQUE (tx_hash, log_index)
Then:
Process blocks
↓
Worker crashes
↓
Retry same range
↓
Duplicate event
↓
Database ignores duplicate
Without this, a retry can create duplicate trades and corrupt position calculations.
23. End-to-End Copy Trade Example
Now combine the pieces.
Original wallet
Wallet A
BUY MEMESTOCK
$4,000
Event detection
Pons Swap
↓
Wallet A identified
↓
BUY
↓
$4,000
Strategy
Copy ratio = 25%
$4,000 × 25%
= $1,000
Risk
Maximum trade = $750
Maximum position = $5,000
Current position = $4,600
Available position capacity = $400
Therefore:
Requested: $1,000
Trade limit: $750
Position limit: $400
Final approved: $400
Execution
Fresh quote
↓
Slippage check
↓
Transaction
↓
Receipt
Reconciliation
Actual token result
↓
Position engine
↓
Onchain balance
↓
Reconciled position
The complete pipeline becomes:
TRACKED WALLET
↓
SWAP DETECTED
↓
TRADE DECODER
↓
NORMALIZED SIGNAL
↓
COPY RATIO
↓
RISK LIMITS
↓
POSITION LIMIT
↓
FRESH QUOTE
↓
EXECUTION
↓
RECEIPT
↓
POSITION
↓
RECONCILIATION
That is the important part of the implementation.
24. Suggested Database Tables
A simple PostgreSQL schema could include:
tracked_wallets
trades
signals
risk_decisions
execution_orders
transactions
positions
position_events
portfolio_snapshots
indexer_cursors
For example:
execution_orders
---------------------------
id
signal_id
wallet_id
token
side
requested_amount
approved_amount
status
tx_hash
created_at
updated_at
Separating signals from execution orders is useful because:
Signal exists
does not necessarily mean:
Trade exists
A signal can be rejected.
25. Recommended Repository
A practical implementation:
src/
├── chain/
│ ├── client.ts
│ ├── contracts.ts
│ └── events.ts
│
├── indexer/
│ ├── runner.ts
│ ├── backfill.ts
│ ├── cursor.ts
│ └── dedupe.ts
│
├── wallets/
│ ├── registry.ts
│ └── activity.ts
│
├── signals/
│ ├── decoder.ts
│ ├── classifier.ts
│ └── generator.ts
│
├── strategy/
│ ├── copyRatio.ts
│ └── filters.ts
│
├── risk/
│ ├── sizing.ts
│ ├── limits.ts
│ └── checks.ts
│
├── execution/
│ ├── quote.ts
│ ├── swap.ts
│ ├── state.ts
│ └── receipt.ts
│
├── positions/
│ ├── engine.ts
│ └── reconciliation.ts
│
└── api/
├── wallets.ts
├── signals.ts
├── executions.ts
└── positions.ts
The benefit is that a new strategy can reuse the same:
Indexer
Risk Engine
Execution Engine
Position Engine
Reconciliation
without rebuilding everything.
26. Test the Failure Paths
Before allowing automated transaction signing, the important tests are not only:
BUY succeeds
SELL succeeds
Test:
Wallet event duplicated
RPC timeout
Worker restart
Unknown token
Missing price
Trade exceeds maximum
Position limit reached
Insufficient balance
Quote fails
Price impact too high
Slippage exceeded
Transaction reverted
Transaction confirmed but database update fails
Manual wallet trade
Manual token transfer
Reconciliation mismatch
Trading infrastructure should be designed around failure recovery.
The happy path is the easy part.
27. Why This Architecture Is Different From a Simple Copier
A simple script:
WATCH
↓
BUY
A more complete system:
WATCH
↓
INDEX
↓
DECODE
↓
NORMALIZE
↓
STRATEGY
↓
RISK
↓
QUOTE
↓
EXECUTE
↓
CONFIRM
↓
UPDATE
↓
RECONCILE
That extra structure is what makes the implementation reusable.
The same execution infrastructure can later support:
Copy Trading
Pons Sniper Strategies
Manual Trading
Portfolio Rebalancing
Trading Terminal Execution
Automated Strategies
Only the signal-generation layer needs to change.
Pons Wallet Tracker → Pons Copy Trading Bot
This also explains why a wallet tracker is a useful first component.
The progression is:
Pons Wallet Tracker
↓
Trade Detection
↓
Normalized Signals
↓
Copy Strategy
↓
Risk Engine
↓
Execution Engine
↓
Position Reconciliation
The wallet tracker observes.
The strategy decides.
The risk engine controls.
The execution engine acts.
The reconciliation system verifies.
That separation is the foundation of a reusable trading platform.
Conclusion
A Pons copy trading bot should not be implemented as:
Detect wallet transaction
↓
Send same transaction
A better design is:
Pons Events
↓
Wallet Indexer
↓
Trade Decoder
↓
Signal Generator
↓
Copy Strategy
↓
Risk Engine
↓
Execution Engine
↓
Transaction State
↓
Position Engine
↓
Reconciliation
The most important engineering decisions are the boundaries between these components.
A detected trade is not automatically a trading instruction.
A trading instruction is not automatically an approved trade.
An approved trade is not automatically a confirmed transaction.
And a confirmed transaction is not automatically the same as the internal position state.
Those distinctions are what make the system testable, recoverable, and extensible.
I build custom Pons and Robinhood Chain trading infrastructure, including Pons copy trading bots, wallet trackers, sniper systems, trading terminals, token scanners, risk engines, and automated execution systems.
The architecture can start from an existing bot, prototype, codebase, or technical specification and be extended around the required strategy, risk model, and execution workflow.
Top comments (0)