Building a Pons bundler on Robinhood Chain is less about sending multiple transactions and more about coordinating an entire launch workflow reliably.
A useful open-source implementation is wooyang/pons-bundler, a TypeScript CLI for Pons v2 on Robinhood Chain.
The project calls launchAndBuy, registers buyer wallets for the launch flow, and submits additional curve buys in parallel. It also includes wallet generation, funding, dry-run execution, buying, selling, and sweeping.
This article walks through the architecture and the engineering decisions behind a production-oriented Pons launch-automation system.
What Is a Pons Bundler?
First, an important distinction.
A Pons bundler is not an ERC-4337 bundler.
The referenced implementation describes Robinhood Chain as FCFS and notes that there is no atomic multi-signer transaction. The launch and initial buy happen in one transaction, while additional buyer wallets submit separate transactions targeting the same launch window.
The execution model is therefore closer to:
Pons Launch
│
▼
launchAndBuy()
│
┌─────────┴─────────┐
│ │
Master Wallet Token + Curve
│
┌─────────────┼─────────────┐
▼ ▼ ▼
Wallet A Wallet B Wallet C
│ │ │
└─────────────┼─────────────┘
▼
Parallel Buy Txs
The goal is to coordinate execution, not to create a fake notion of atomicity.
Project Structure
A clean Pons bundler can separate the application into several layers:
CLI
│
├── status
├── wallets
├── launch
├── buy
├── sell
└── sweep
│
▼
Execution Layer
│
├── launch orchestration
├── wallet coordination
├── quote calculation
└── transaction handling
│
▼
Pons Protocol Layer
│
├── Factory
├── LaunchAndBuy
├── Curve
└── Token
│
▼
Robinhood Chain
The repository is organized as a TypeScript CLI with its main library entry point under src/index.ts.
The separation matters because CLI code should not contain all of your blockchain logic.
Connecting to Robinhood Chain
The first requirement is an RPC connection.
A basic configuration can look like:
import { JsonRpcProvider } from "ethers";
const provider = new JsonRpcProvider(
process.env.RH_RPC_URL
);
The repository recommends using a paid RPC when possible because the public RPC is rate-limited.
For a production service, I would add:
RPC
├── timeout
├── retry policy
├── health checks
└── fallback provider
It is important to distinguish an RPC failure from a blockchain transaction failure.
For example:
RPC timeout
does not mean:
transaction reverted
Your execution engine needs to understand the difference.
Contract Configuration
The repository interacts with a Pons factory and launch-and-buy contract on Robinhood Chain. It documents the relevant contract addresses in its README.
Instead of scattering addresses throughout the codebase, I prefer a protocol configuration object:
interface PonsConfig {
factory: string;
launchAndBuy: string;
rpcUrl: string;
}
Then initialize it once:
const config: PonsConfig = {
factory: process.env.PONS_FACTORY!,
launchAndBuy: process.env.PONS_LAUNCH_AND_BUY!,
rpcUrl: process.env.RH_RPC_URL!,
};
This makes upgrades easier and reduces configuration mistakes.
Check canLaunch Before Spending Gas
One of the best ideas in the reference implementation is the status command.
It reads the protocol's canLaunch state before attempting a launch.
The workflow should be:
status
│
├── canLaunch = false
│ └── stop
│
└── canLaunch = true
└── continue
Conceptually:
const canLaunch = await pons.canLaunch();
if (!canLaunch) {
throw new Error("Pons launch is currently unavailable");
}
This is a general rule for blockchain automation:
Read protocol state before executing state-changing transactions.
Do not hard-code the assumption that a launch operation is always available.
Wallet Management
A multi-wallet application needs a dedicated wallet-management layer.
The reference project includes commands for generating, funding, listing, and sweeping buyer wallets.
For example:
npm run pons -- wallets generate \
--count 8 \
--out wallets.json
The resulting architecture is:
Master Wallet
│
├── fund
│
▼
Buyer Wallet A
Buyer Wallet B
Buyer Wallet C
...
Each wallet should have explicit state.
interface BuyerWallet {
address: string;
encryptedKey: string;
status: "READY" | "FUNDED" | "USED" | "FAILED";
}
The exact storage model can vary, but the key is to keep wallet management independent from execution strategy.
Protect Private Keys
A wallet file containing private keys is effectively a financial credential store.
The reference repository keeps wallets.json gitignored and explicitly warns not to commit it.
For production infrastructure, I would prefer:
Encrypted storage
↓
Wallet service
↓
Signing operation
↓
Transaction
rather than passing raw private keys around the application.
A useful rule is:
Private key
↓
Signer
↓
Signed transaction
↓
Broadcast
The private key should disappear from the rest of the application boundary.
The Core Pons Launch Flow
The key operation in the reference implementation is launchAndBuy.
Conceptually:
const tx = await pons.launchAndBuy(
tokenConfig,
launchConfig,
initialBuy
);
await tx.wait();
The launch flow can be represented as:
Validate
↓
Quote
↓
Build transaction
↓
Sign
↓
Broadcast
↓
Wait for receipt
↓
Read resulting state
The important part is not the single contract call.
It is everything around it.
Dry Run First
A useful feature in the repository is:
--dry-run
The README documents dry-run execution as a way to simulate the launch flow without broadcasting the transaction.
This should become a standard development mode:
Execution
│
┌─────────┴─────────┐
▼ ▼
Dry Run Live
│ │
simulate broadcast
Before a live transaction, validate:
✓ launch allowed
✓ wallets available
✓ wallet balances sufficient
✓ parameters valid
✓ quote available
✓ gas reserve available
✓ slippage configured
✓ contracts configured
Only then broadcast.
Curve Quotes
Pons v2 uses a curve-based pricing model.
That means you cannot treat the price as a static number.
A buy changes the curve.
The repository calculates minTokensOut using curve reserve information and fees, while noting that parallel buys can move the curve.
A simplified model looks like:
current reserves
│
▼
curve quote
│
▼
expected tokens
│
▼
minimum tokens out
For example:
const quote = await getBuyQuote({
token,
amountIn,
});
const minTokensOut =
applySlippage(quote.tokensOut, slippageBps);
The important value is not only:
tokensOut
but:
minTokensOut
That gives the transaction a defined execution boundary.
Slippage Protection
Automated execution should never blindly accept whatever output the transaction receives.
A configuration might be:
interface BuyConfig {
amountIn: bigint;
slippageBps: number;
}
Then:
const minTokensOut =
quote.tokensOut *
BigInt(10_000 - slippageBps) /
10_000n;
The actual implementation should use the protocol's precise integer arithmetic and decimals.
The principle is:
Expected output
↓
Slippage boundary
↓
Transaction
This converts slippage from a UI preference into an explicit risk parameter.
Multiple Buyer Wallets
The reference implementation supports registering buyer wallets for the launch workflow and then submitting their curve buys separately. It documents a maximum of 32 buyer wallets for this flow.
The important architecture is:
Master launch
│
├── Wallet A → buy
├── Wallet B → buy
├── Wallet C → buy
└── Wallet D → buy
These are separate transactions.
Therefore, the application should track each one independently.
interface Execution {
wallet: string;
txHash?: string;
status:
| "READY"
| "SUBMITTED"
| "CONFIRMED"
| "REVERTED";
}
This is much more robust than having one global:
launchStatus = SUCCESS
Parallel Transaction Submission
Sequential execution looks like:
Wallet A
↓
wait
↓
Wallet B
↓
wait
↓
Wallet C
Parallel submission can instead prepare transactions independently:
Launch Window
│
┌──────────┼──────────┐
▼ ▼ ▼
Buy A Buy B Buy C
The repository explicitly submits the additional curve buys in parallel.
But parallel submission does not create deterministic ordering.
The network still decides transaction ordering and inclusion.
Therefore, every transaction needs its own state machine.
Transaction State Machine
A useful model is:
CREATED
↓
SIGNED
↓
SUBMITTED
↓
PENDING
├── CONFIRMED
├── REVERTED
└── UNKNOWN
For example:
type TxStatus =
| "CREATED"
| "SIGNED"
| "SUBMITTED"
| "PENDING"
| "CONFIRMED"
| "REVERTED"
| "UNKNOWN";
Then the monitoring layer can periodically query the chain.
const receipt = await provider.getTransactionReceipt(
txHash
);
if (!receipt) {
status = "PENDING";
} else if (receipt.status === 1) {
status = "CONFIRMED";
} else {
status = "REVERTED";
}
That small distinction becomes extremely important when a process crashes or an RPC request times out.
Gas Management
A buyer wallet needs more than the exact amount intended for the purchase.
It also needs a gas reserve.
The reference README recommends funding each buyer with the intended buy amount plus an additional ETH buffer for gas.
The conceptual calculation is:
Wallet balance
│
├── trading capital
│
└── gas reserve
Do not assume:
buyAmount === walletBalance
Instead:
const spendable =
balance - gasReserve;
if (spendable <= 0n) {
throw new Error("Insufficient balance");
}
The gas reserve should be configurable rather than buried inside business logic.
Nonces
Nonce management is another important part of transaction infrastructure.
For a single wallet:
nonce 10 → transaction A
nonce 11 → transaction B
nonce 12 → transaction C
The application must avoid accidental reuse.
A transaction manager should therefore own nonce allocation rather than having several unrelated services query and assign nonces independently.
For independent buyer wallets, each wallet naturally has its own nonce sequence.
That makes the system easier to coordinate.
Reconciliation
One of the most important features to add after the first working version is reconciliation.
Suppose the local database says:
Wallet B
Status: CONFIRMED
Tokens: 10,000
Do not automatically trust the local state.
Read the chain:
wallet balance
token balance
transaction receipt
contract state
Then compare:
Local State
↕
Chain State
A reconciliation process can run periodically:
async function reconcile(wallet: WalletState) {
const nativeBalance =
await provider.getBalance(wallet.address);
const tokenBalance =
await token.balanceOf(wallet.address);
return {
nativeBalance,
tokenBalance,
};
}
This protects against process crashes, missed RPC responses, reconnects, and inconsistent local state.
Recovery
Consider this execution:
Master → CONFIRMED
Wallet A → CONFIRMED
Wallet B → CONFIRMED
Wallet C → REVERTED
Wallet D → UNKNOWN
A robust system should not restart the entire launch.
Instead:
Master → keep
A → keep
B → keep
C → handle failure
D → reconcile
This requires persistence.
For example:
interface LaunchExecution {
launchId: string;
masterTx?: string;
wallets: {
address: string;
txHash?: string;
status: TxStatus;
}[];
}
Now the process can restart without losing its understanding of the operation.
Idempotency
One particularly dangerous situation is:
transaction submitted
↓
application crashes
↓
restart
↓
transaction appears unknown
If the application simply submits again, it can unintentionally duplicate an operation.
Instead, use an execution identifier:
Launch ID
│
├── Master transaction
├── Wallet A transaction
├── Wallet B transaction
└── Wallet C transaction
On restart:
Does this wallet already have
an execution associated with this launch?
If yes:
reconcile
rather than immediately submitting another transaction.
Buy, Sell, and Sweep
A Pons automation system should also treat post-launch operations as first-class workflows.
The reference CLI exposes:
npm run pons -- buy \
--token 0x... \
--file wallets.json \
--each-buy 0.02
and:
npm run pons -- sell \
--token 0x... \
--file wallets.json \
--percent 100
as well as a wallet sweep operation.
That naturally leads to:
Launch
↓
Entry
↓
Position Tracking
↓
Exit
↓
Fund Management
Once these operations share the same execution engine, the system becomes much easier to extend.
Monitoring
A production application should expose more than transaction hashes.
Useful metrics include:
Launch status
Transaction status
Wallet balances
Token balances
Quote
Minimum output
Gas usage
Execution latency
Failed transactions
RPC failures
A simple terminal view could be:
Pons Launch
────────────────────────────
Master
CONFIRMED
0x123...
Wallet A
CONFIRMED
0x456...
Wallet B
PENDING
0x789...
Wallet C
REVERTED
0xabc...
Wallet D
CONFIRMED
0xdef...
The same state can later power a web dashboard.
From CLI to Pons Trading Infrastructure
This is where the project gets more interesting.
A CLI is only the execution layer.
The same engine can become:
Pons Automation Platform
│
┌──────────────────┼──────────────────┐
│ │ │
Launch Wallets Positions
│ │ │
▼ ▼ ▼
Automation Management Tracking
│ │ │
└──────────────────┼──────────────────┘
▼
Execution Engine
│
▼
Robinhood Chain
Then additional services can be added:
Launch Monitor
Copy Trading
Trading Terminal
Analytics
Alerts
Risk Management
Portfolio Tracking
The important architecture decision is to keep these strategies above the execution engine.
For example:
Strategy
↓
Risk
↓
Execution
↓
Blockchain
↓
Reconciliation
A strategy should decide what to trade.
The execution engine should decide how to execute it safely and track what actually happened.
Security and Responsible Automation
Multi-wallet launch automation should be used for authorized and legitimate activity.
The goal of the engineering should be reliable transaction coordination, controlled execution, and accurate state tracking—not artificial volume, wash trading, or market manipulation.
Private-key management should also be treated as a production security boundary.
The protocol itself should remain the source of truth for what transactions are permitted, and the application should continuously validate live protocol state.
What I Would Improve for Production
The reference project is a good example of a focused TypeScript CLI. For a production system, I would build additional layers around it:
┌─────────────────────┐
│ Web Dashboard │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Strategy Layer │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Risk Engine │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Execution Engine │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Pons Protocol │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Robinhood Chain │
└─────────────────────┘
The CLI then becomes one interface to the same backend.
Final Thoughts
A Pons bundler may look like a small blockchain script from the outside.
The real engineering problem is much larger:
Protocol State
↓
Launch Validation
↓
Wallet Coordination
↓
Curve Quoting
↓
Slippage Protection
↓
Transaction Signing
↓
Parallel Execution
↓
Receipt Tracking
↓
Reconciliation
↓
Recovery
That is why I see Pons bundler as more than a single bot.
It can be the execution foundation for a broader Robinhood Chain trading stack:
Pons Bundler
↓
Pons Sniper
↓
Pons Copy Trading
↓
Pons Trading Terminal
↓
Robinhood Chain Trading Infrastructure
The interesting part is not simply interacting with a smart contract.
It is building an execution system that remains understandable and recoverable when transactions behave differently from what your local application expected.
That is the difference between a blockchain script and trading infrastructure.
Reference Implementation
The implementation discussed in this article:
GitHub: https://github.com/wooyang/pons-bundler
The repository documents a TypeScript Pons v2 CLI, launch-and-buy execution, buyer-wallet coordination, curve quoting, dry runs, buying, selling, and wallet management.