In the previous article, we connected FMZ to Robinhood Chain and built a Uniswap V4 new-pool radar. Once a new pool appeared, the program could quickly capture information such as the trading pair, fee tier, and liquidity. That part went smoothly. With the pools now discoverable, the next question was obvious: could we provide liquidity when trading activity is high and earn some fees?
So I continued modifying the code, adding simulated entries, position valuation, and exit logic. The first results looked quite good—the simulated equity curve climbed almost all the way up. That was tempting enough that I went on to add approvals, token purchases, liquidity provision, liquidity removal, and token selling, completing the live-trading workflow.
After launching the bot, I was excited to watch the returns keep rising. Instead, I quickly learned that there are bad actors out there.
1. Three Tokens, Two Could Not Be Sold
Of the first three tokens I tried, two became impossible to sell after the purchase. I had barely used ETH to buy the target token when the liquidity on the other side was removed. It almost felt as if someone had been waiting specifically for my order. I had entered the pool hoping to earn trading fees, but before earning anything, I was already left holding tokens in the wallet.
My first reaction was that I had run into a honeypot token. But after checking the on-chain transactions, the causes still needed to be separated clearly. A token contract that restricts selling and a pool whose liquidity has been drained can both make a token effectively unsellable, but they are not the same problem.
The perp case was especially obvious. Roughly two seconds after the purchase, the original LP removed its liquidity. Our own LP position had not even been established yet. I later tried reducing the sell amount, but the quote still reported insufficient liquidity. Increasing slippage and repeating approvals did not help—the counterparty liquidity required for the swap was simply no longer there.
Looking back at the simulation curve at that point made it clear that I had celebrated too early. A simulation can value assets using the pool price, but whether that price is actually executable and whether sufficient liquidity will still exist at exit must be validated separately. Appreciation in the target token also cannot simply be counted as LP fee income. There is a full execution process between seeing a profit on paper and actually getting the capital back.
So I first changed the accounting model. Estimated income, position valuation, realized LP fees, and final cash flows are now recorded separately. If the token has not yet been sold or the transaction has not been confirmed, the round cannot be considered successfully completed and profitable.
For incomplete transactions, the program also preserves execution progress. After a restart, it first checks previously submitted transactions so that a temporarily missing receipt is not misclassified as a failure and the same purchase is not sent again.
2. Stop Racing into Brand-New Pools and Look for Pools That Can Keep Trading
After taking that loss, I changed direction. A newly created pool has very little history, and its liquidity can disappear at any moment. No matter how carefully we check before buying, there is no guarantee that the state will not change one second later.
Since the goal is to earn trading fees continuously, it makes more sense to start with pools that have already been operating for some time and still have ongoing trading activity. At least then, the main challenge is no longer fighting for the first few seconds after a pool is created.
The first version of the filter, however, was too strict: the pool had to be at least seven days old, TVL had to be at least $500,000, the fee tier could not exceed 0.3%, and additional volume and price conditions were layered on top. Each rule made sense individually, but after combining them, very few V4 pools survived the filter. The bot was certainly cautious—it just rarely opened a position.
After breaking the problem down, I found that some pools were excluded because the thresholds were too strict, some were never discovered by the program, and some belonged to protocols that were not yet supported.
So while loosening the pool-age, liquidity, and short-term volume thresholds, I also added V3 support. In practice, V3 contained more pools worth further review. Focusing only on V4 unnecessarily narrowed the candidate universe.
Adding V3 did not simply mean inserting pool addresses into a list. The V3 workflow also required identity verification, quoting, approval, entry, position creation, and exit logic.
Here, a “stable pool” means a pool whose trading activity and liquidity are relatively stable. It does not mean that we only trade stablecoin pairs. What matters is whether continuing volume can plausibly support fee income and whether capital can enter and exit under reasonable conditions.
The current thresholds—such as a minimum pool age of 12 hours and TVL of $20,000—are only research settings for this stage and still need to be calibrated against live operating data.
3. The Pool Is Clearly Visible on the Website, but the Program Cannot Find It
After expanding the search scope, another frustrating problem appeared: some pools were clearly visible when opened manually in the browser, yet the program never placed them into the candidate list. MOO was one example.
Early on, when we queried related pools through the ETH, WETH, and USDG token addresses, each API call returned only a limited number of results, and MOO was not among them. When we queried using the MOO token address itself, however, the pool appeared immediately.
The problem was simple: it had never entered our pool directory in the first place. No matter how sophisticated the downstream filtering logic was, the pool could never reach that stage.
We later added a hot-pool list and also tried reconstructing the historical directory from V3 and V4 pool-creation events. Technically, this works. But the node plan we were using allowed each eth_getLogs request to cover only ten blocks, making a historical backfill expensive in both request count and waiting time.
For a new-pool radar, following the latest events forward is a good fit. But once the goal becomes finding already-active pools, rebuilding the full directory from historical blocks turns into a separate infrastructure project.
After going around in circles, we eventually adopted the KyberSwap Earn pool-directory API. Without specifying MOO by name or address, the pool appeared in ordinary pagination.
From that point on, we separated pool discovery, market statistics, and on-chain verification:
| Data Source | Responsibility |
|---|---|
| KyberSwap Earn API | Paginate through the V3/V4 pool directory and refresh it periodically |
| DEX Screener | Provide trading volume, buy/sell transaction counts, and price-change statistics |
| On-chain RPC | Verify pool identity, current price, active liquidity, and transaction state |
Each directory page retrieves 100 pools. After completing one full pass, the program waits 30 minutes and then paginates again, merging and refreshing the results.
Market statistics come directly from the available API fields, while on-chain requests are concentrated on candidate verification and position management. This way, the bot does not need to spend its first hour accumulating its own market history after startup, nor does it need to repeatedly scan historical blocks just to filter existing pools.
Pagination itself also caused problems. The API sorts pools by real-time TVL, so the order can change while the program is moving through pages. As a result, the number of unique pools after deduplication may not exactly match the total reported by the API.
If the program rigidly requires those numbers to match, it may keep restarting from page one forever. We therefore retained discrepancy logging, retry handling, and periodic refreshes, while separating the idea of “this pagination pass has finished” from “the entire chain has been covered completely.”
The API provides a much broader candidate universe, but indexing delays and omissions can still occur, so continued rechecking is necessary.
4. Testing with Only 3U — So How Did Gas Reach 2.33U?
Once the pools could be discovered reliably, the next step was to measure costs seriously.
During simulation, gas was mostly treated as an estimate. In live execution, however, one LP cycle can involve wrapping ETH, approvals, swaps, adding liquidity, removing liquidity and collecting fees, selling back, and unwrapping. V3 and V4 also use different approval paths, so it is not accurate to estimate both with a rough fixed number of transactions.
We therefore wrote a standalone fee-measurement script. Using a principal budget of roughly 3U (~$3), it performed short-duration entry and exit cycles in pools pairing USDG with ETH/WETH.
The tests were designed to answer two questions first: can the entire workflow complete successfully, and how much does each individual step actually cost?
| Test Step | Main Check |
|---|---|
| Capital preparation | Whether ETH is wrapped into WETH when needed and whether the target token is purchased successfully |
| Approval and entry | Whether the approval target and amount are correct and whether the LP position is actually created |
| Liquidity removal and collection | Whether liquidity is removed successfully and both assets return to the wallet |
| Sell-back and unwrap | Whether the target token is sold back and the WETH belonging to the position is unwrapped |
| Fee settlement | Calculate gas transaction by transaction and reconcile the net capital change for the whole cycle |
| Recovery after interruption | Continue checking unconfirmed transactions and avoid resending steps that were already submitted |
Gas is calculated from the actual usage and gas price recorded in the transaction receipt rather than from the configured estimate:
javascript
// Actual gas expenditure, in wei
var gasWei =
BigInt(receipt.gasUsed) *
BigInt(receipt.effectiveGasPrice);
totalGasWei += gasWei;
In the tests, the first complete V3 cycle consumed about 0.326U in gas, while a later V3 cycle reached approximately 2.33U. That result was surprising: with just a little over three dollars in principal, how had gas suddenly climbed above two dollars?
Comparing the transactions showed that the later round required more approval steps and also ran during a period of significantly higher gas prices.
In both rounds, wrapping ETH used exactly 57,647 gas, yet the actual cost was approximately 0.0139U in one round and 0.1545U in the other.
Even the same operation can cost very different amounts. We cannot simply reuse the previous result as the next transaction's expected cost.
A subsequent standalone V4 test consumed about 0.226U for the full cycle. Again, that number is only a cost sample from that particular execution.
With these measurements available, the production code was changed to reserve gas separately for each operation—wrapping, approval, swapping, LP creation, and liquidity removal—while also considering the current gas price and a safety buffer.
For token operations that have not yet been measured in live conditions, the program still keeps a relatively conservative fallback budget. What we are calibrating here is the budgeting model. Actual expenditure is still calculated from receipts, rather than hard-coding one historical test result into the strategy.
The accounting scope also needs to be clear. In the standalone fee-measurement script, the roughly 3U principal was tracked separately from gas. In the production strategy, the current 3U budget per position already includes entry gas and execution reserves.
The net change over a full cycle also mixes swap loss, price movement, and LP income. The amount remaining after subtracting gas cannot simply be labeled “swap fees.”
These rounds successfully validated the V3 and V4 capital-in/capital-out workflow and fee accounting, but they were not long-term profitability tests.
With such a small position, fixed execution costs represent a very large percentage of capital. Increasing the principal may dilute those fixed costs, but it will also change price impact, the strategy's share of liquidity, and the amount of capital at risk. Those effects still need to be evaluated separately.
5. Estimate What This Position Can Earn Before Deciding Whether to Open It
A pool can have a lot of trading activity without necessarily generating much fee income for our position.
For a concentrated-liquidity position, revenue also depends on the selected price range, current active liquidity, and the positions of other LPs. The current model therefore estimates our share of active liquidity before estimating fee income instead of directly using the APR displayed on a webpage.
For volume, the model uses the smaller of the one-hour trading volume and the six-hour average hourly trading volume. This avoids making an overly optimistic decision based only on a recent one-hour spike.
After combining the fee tier, the share of fees distributable to LPs, and the position's expected liquidity share, the estimated fee income is compared with the full round-trip cost. The core logic can be simplified as follows:
javascript
// Revenue and costs are normalized to ETH
var hourlyVolumeUSD = Math.min(volume1hUSD, volume6hUSD / 6);
var share = myLiquidity / (poolLiquidity + myLiquidity);
var expectedFeesETH = hourlyVolumeUSD / ethUSD
* feeRate * lpFeeShare * share * 0.5;
var totalCostETH =
quoteLossETH + slippageReserveETH + gasBudgetETH;
var requiredFeesETH =
totalCostETH * 1.5 + principalETH * 0.01;
var incomeConditionMet = expectedFeesETH >= requiredFeesETH;
The revenue side applies a 50% discount factor. On the cost side, expected fees must cover 1.5× the total cost, with an additional 1% of principal reserved for inventory risk.
Even after that condition is met, the position must still pass capital and execution checks.
These coefficients are still model assumptions and are not supported by a sufficiently large sample of profitable trades. The discount factor and buffers are used to leave room between historical trading activity and future realized income—not to guarantee that a position will cover its costs after entry.
After a position is opened, we also need to check whether the original assumptions are actually being realized. Price deviation, net loss, and declining liquidity can all trigger an exit. Once a position has been held for ten minutes, the strategy begins checking whether actual fee accumulation is materially lagging behind expectations.
At entry, we rely on estimates. Once the position is live, actual data is available, so that data should be used to continue validating the thesis.
The earlier design of forcing an exit after a fixed 30 minutes was removed. If pool conditions have not changed, repeatedly entering and exiting simply because a timer expired adds unnecessary costs. If conditions have already deteriorated, the strategy should not wait for the timer either.
One hour is still used as the revenue-evaluation window, but it is no longer treated as a mandatory holding period.
6. What to Optimize Next After Completing This Version
At this point, the complete strategy workflow is in place: the API supplies candidates, V3 and V4 use separate execution paths, expected revenue and costs are evaluated before entry, positions are continuously checked while open, and exits are settled using actual cash flows.
Unconfirmed transactions and unresolved assets remain in the ledger. After a restart, the strategy first resumes existing workflows so that an interruption does not leave behind an untracked position.
In addition to small-scale full-cycle live tests, code checks now cover directory-pagination anomalies, pool-identity mismatches, quote failures, insufficient budgets, delayed transaction receipts, and restart recovery.
The priority is to make sure the program stops opening new positions when conditions are not satisfied and, when transaction state is unclear, verifies already-submitted transactions before sending anything again.
Only after the execution process is sufficiently robust does it make sense to evaluate the real effectiveness of pool selection and the revenue model.
Future adjustments will therefore revolve around actual operating data. In particular, we need to avoid a common trap: lowering thresholds simply because the strategy is not opening positions, then immediately tightening them after a loss, until the system is just oscillating between parameter sets.
Every change should correspond to a specific problem and have a measurable comparison metric:
| Optimization Area | Next Step | Evaluation Metric |
|---|---|---|
| Revenue forecasting | Compare entry-time forecasts with actual fee accumulation position by position and recalibrate assumptions about volume persistence | Forecast error, actual cost-coverage ratio |
| Gas budgeting | Reduce unnecessary reserves based on current approval state and collect more operation samples across different tokens | Difference between budgeted and actual gas expenditure |
| Capital and range | Compare results across different position sizes and price-range widths | Net return, price impact, effective time in range |
| Pool quality | Improve detection of major LP withdrawals, abnormal trading, and token permission risks | Risk-blocking effectiveness, false-filter rate |
| Position exit | Examine the full process from exit trigger to actual sell-back and analyze delay and execution loss | Exit latency, realized loss, residual assets |
Looking back, the main value of this experiment was that it forced us to fill in the parts that simulations easily gloss over: where the pool comes from, how capital actually enters, how costs are calculated, and whether the exit is truly completed.
With those pieces now represented explicitly in the strategy, later optimization can build on a stable foundation instead of redesigning the entire system every time a new problem appears.
Strategy source: Robinhood V3/V4 Active Pool LP Parameter Version
- 1





