A production-oriented architecture for launch detection, market analysis, risk management, automated execution, position tracking, and reconciliation.
A simple trading bot can look like this:
Token Launch
↓
Signal
↓
BUY
That is enough for a prototype.
It is not enough for a production-oriented Pons memestock trading bot.
A real system needs to answer:
What token launched?
Is this the correct contract?
Where is it trading?
What is the current market state?
Is the token inside the launch-protection window?
Is there enough liquidity?
Does the strategy produce a signal?
How much should be traded?
What is the executable quote?
Was the transaction confirmed?
How much actually filled?
What is the real position now?
Does local state match the chain?
A better architecture is:
PONS EVENTS
↓
TOKEN INDEXER
↓
MARKET STATE
↓
STRATEGY
↓
RISK
↓
EXECUTION
↓
ORDER / FILL STATE
↓
POSITION
↓
RECONCILIATION
Pons currently runs on Robinhood Chain, chain ID 4663. Its current documentation describes launches that create the token and its WETH trading pool together, with trading occurring in that same pool after launch. The documentation also recommends indexing factory and pool events as the onchain source of truth.
The term “memestock” is used by projects in the broader Pons ecosystem, but it should not be presented as an official Pons protocol category. Pons describes the underlying protocol more generally as a place to launch and trade tokens on Robinhood Chain.
1. Scanner and trading bot are different systems
A scanner answers:
What just happened?
A trading bot answers:
What should I do about it?
For example, the scanner may produce:
NEW PONS TOKEN
Symbol:
MEMESTOCK
Contract:
0x41...
Pair:
WETH
Pool:
0x82...
Liquidity:
$125,000
That does not mean:
BUY
The trading engine still needs to validate:
Launch
↓
Contract
↓
Market
↓
Liquidity
↓
Trading State
↓
Strategy
↓
Risk
↓
Execution
This separation is one of the most important architectural decisions in the system.
2. Current Pons market model
A trading bot should be built against the actual protocol model rather than assumptions from older launchpad designs.
The current Pons documentation says each launch creates the token and its WETH trading pool together. Current launches do not start on a bonding curve and later migrate; trading occurs in that same pool from launch, and graduation confirms that the pool has reached its configured threshold.
The current flow is:
CREATE
↓
TOKEN + WETH POOL
↓
TRADING
↓
GRADUATION
The current integration docs identify the active factory and Uniswap V3 infrastructure and recommend registering the pool emitted by TokenLaunched, then indexing its Swap events.
A production bot should therefore make the market resolver explicit:
interface TokenMarket {
tokenAddress: string;
pairToken: string;
poolAddress: string;
marketType: "PONS_V3";
}
Do not hard-code assumptions throughout the strategy.
3. Build the blockchain client
A simple viem client:
import {
createPublicClient,
http,
} from "viem";
export const publicClient =
createPublicClient({
chain: {
id: 4663,
name: "Robinhood Chain",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
default: {
http: [
"https://rpc.mainnet.chain.robinhood.com",
],
},
},
},
transport: http(
process.env.RH_RPC_URL
),
});
In production, keep infrastructure configuration outside the repository:
const rpcUrl =
process.env.RH_RPC_URL;
if (!rpcUrl) {
throw new Error(
"RH_RPC_URL is required"
);
}
For live trading, use a dedicated RPC provider and proper monitoring rather than treating a public endpoint as unlimited infrastructure.
4. Detect new launches
The first event the scanner should process is:
TokenLaunched
Define the event:
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:
const logs =
await publicClient.getLogs({
address:
"0xA5aAb3F0c6EeadF30Ef1D3Eb997108E976351feB",
event:
tokenLaunchedEvent,
fromBlock: startBlock,
toBlock: endBlock,
});
The important step is not getting the log.
It is turning the log into persistent application state.
5. Normalize launch information
Create an internal model:
interface PonsLaunch {
tokenAddress: string;
deployer: string;
pairToken: string;
poolAddress: string;
launchBlock: bigint;
restrictionsEndBlock: bigint;
initialBuyAmount: bigint;
transactionHash: string;
createdAt: number;
}
Now the rest of the application works with normalized fields rather than raw ABI data.
6. Track the launch-protection window
Current Pons documentation describes temporary launch protection around the first blocks after launch. The launch event exposes restrictionsEndBlock, so the bot should model that state explicitly.
interface LaunchProtection {
startBlock: bigint;
endBlock: bigint;
active: boolean;
}
Then:
function isLaunchProtectionActive(
currentBlock: bigint,
endBlock: bigint
): boolean {
return currentBlock <= endBlock;
}
The strategy can therefore decide:
Launch detected
↓
Protection active?
│
├── YES → wait / strategy-specific handling
│
└── NO → normal execution rules
Do not assume that detecting a launch means an unrestricted buy is immediately executable.
7. Validate the token contract
A ticker is not a sufficient identity.
Pons's documentation warns that names and symbols can be copied and instructs users to verify the token address.
The bot should therefore store:
interface TokenIdentity {
address: string;
symbol: string;
name: string;
chainId: number;
}
Normalize addresses:
function normalizeAddress(
address: string
): string {
return address.toLowerCase();
}
The contract address becomes the primary identifier throughout the system.
8. Read token state directly
The current Pons launch token exposes useful state directly onchain, including metadata and its liquidity pool.
For example:
import { parseAbi } from "viem";
const tokenAbi = parseAbi([
"function name() view returns (string)",
"function symbol() view returns (string)",
"function decimals() view returns (uint8)",
"function totalSupply() view returns (uint256)",
"function logo() view returns (string)",
"function description() view returns (string)",
"function liquidityPool() view returns (address)",
]);
Then:
const [
name,
symbol,
decimals,
totalSupply,
logo,
description,
pool,
] = await Promise.all([
publicClient.readContract({
address: tokenAddress,
abi: tokenAbi,
functionName: "name",
}),
publicClient.readContract({
address: tokenAddress,
abi: tokenAbi,
functionName: "symbol",
}),
publicClient.readContract({
address: tokenAddress,
abi: tokenAbi,
functionName: "decimals",
}),
publicClient.readContract({
address: tokenAddress,
abi: tokenAbi,
functionName:
"totalSupply",
}),
publicClient.readContract({
address: tokenAddress,
abi: tokenAbi,
functionName: "logo",
}),
publicClient.readContract({
address: tokenAddress,
abi: tokenAbi,
functionName: "description",
}),
publicClient.readContract({
address: tokenAddress,
abi: tokenAbi,
functionName:
"liquidityPool",
}),
]);
This avoids depending on a centralized token database.
9. Store the token registry
Every discovered token should be persisted.
interface PonsToken {
address: string;
symbol: string;
name: string;
deployer: string;
pairToken: string;
poolAddress: string;
launchBlock: bigint;
restrictionsEndBlock: bigint;
firstSeenAt: number;
}
A PostgreSQL table might look like:
pons_tokens
----------------------------
address
symbol
name
deployer
pair_token
pool_address
launch_block
restrictions_end_block
first_seen_at
updated_at
Use the token address as a unique identifier.
10. Index pool trades
After detecting TokenLaunched, register the emitted pool and index its Swap events.
Conceptually:
TokenLaunched
↓
Pool Address
↓
Swap Events
↓
Normalized Trades
↓
Market Metrics
A normalized trade model:
interface NormalizedTrade {
tokenAddress: string;
trader: string;
side: "BUY" | "SELL";
tokenAmount: bigint;
quoteAmount: bigint;
blockNumber: bigint;
transactionHash: string;
logIndex: number;
timestamp: number;
}
Now the strategy does not need to know the original ABI structure of the swap.
11. Derive trade direction correctly
For a Uniswap-style pool, the token ordering matters.
A normalized approach:
function getTradeSide(
tokenIsToken0: boolean,
amount0: bigint,
amount1: bigint
): "BUY" | "SELL" {
const signedPairAmount =
tokenIsToken0
? amount1
: amount0;
return signedPairAmount > 0n
? "BUY"
: "SELL";
}
This belongs in the market-data layer.
Do not duplicate it inside every strategy.
12. Build a market snapshot
The strategy needs a clean market representation:
interface MarketSnapshot {
tokenAddress: string;
price: number;
liquidity: number;
volume24h: number;
buyVolume: number;
sellVolume: number;
tradeCount: number;
uniqueTraders: number;
updatedAt: number;
}
These number fields are analytics/display values. They are not the exact onchain execution quantities.
The scanner can then expose:
MEMESTOCK
Price
0.00042 WETH
Liquidity
$125K
24h Volume
$890K
Buys
1,241
Sells
934
The exact USD conversions should come from a separate price service rather than hard-coded assumptions.
13. Keep the strategy separate from market data
The strategy receives:
interface StrategyContext {
token: PonsToken;
market: MarketSnapshot;
currentBlock: bigint;
now: number;
}
and returns:
interface TradingSignal {
action:
| "IGNORE"
| "WATCH"
| "BUY"
| "SELL";
reason: string;
generatedAt: number;
}
The strategy might check:
Is market active?
Is launch protection over?
Is liquidity sufficient?
Is activity sufficient?
Is the token already in the portfolio?
Is there already an open order?
The signal still does not execute anything.
14. Example signal engine
A simple example:
function generateSignal(
context: StrategyContext
): TradingSignal {
if (
context.currentBlock <=
context.token.restrictionsEndBlock
) {
return {
action: "WATCH",
reason:
"Launch protection is active",
generatedAt: Date.now(),
};
}
if (
context.market.liquidity < 50_000
) {
return {
action: "IGNORE",
reason:
"Liquidity below configured threshold",
generatedAt: Date.now(),
};
}
if (
context.market.volume24h < 100_000
) {
return {
action: "WATCH",
reason:
"Trading activity below configured threshold",
generatedAt: Date.now(),
};
}
return {
action: "BUY",
reason:
"Configured market filters passed",
generatedAt: Date.now(),
};
}
The values here are examples.
They are not recommendations for trading thresholds.
The architecture is the important part.
15. Fix the amount model before adding risk
This is where TypeScript needs to be strict.
There are at least three different kinds of numeric values in this system:
Token amount
USD amount
Basis points
They should not all be plain number.
Use explicit types:
type TokenAmount = bigint;
/**
* USD expressed in whole cents.
*
* Example:
* $12.34 → 1234n
*/
type UsdCents = bigint;
/**
* Basis points.
*
* 100 bps = 1%
* 200 bps = 2%
*/
type BasisPoints = bigint;
This gives the compiler a clear convention.
For example:
const tradeAmount: TokenAmount =
1_500_000_000_000_000_000n;
const maxTradeUsd: UsdCents =
100_000n;
const maxSlippage: BasisPoints =
100n;
The important rule is:
Never compare a token amount directly with a USD amount.
16. Correct position-sizing types
Instead of mixing USD numbers with token quantities, keep the two steps separate.
First calculate notional:
function calculateTradeNotionalUsd(
availableCapitalUsdCents: UsdCents,
allocationBps: BasisPoints
): UsdCents {
return (
availableCapitalUsdCents *
allocationBps /
10_000n
);
}
Example:
const notional =
calculateTradeNotionalUsd(
100_000n,
500n
);
// $1,000.00
Then use an executable quote to convert that notional into an exact token/WETH amount.
Do not try to derive:
USD → token amount
with floating-point multiplication inside the risk layer.
The quote/execution layer should produce the exact atomic quantity.
17. Correct risk types
The old model mixed units:
interface RiskRequest {
tokenAddress: string;
side: "BUY" | "SELL";
requestedAmount: bigint;
estimatedPriceImpactBps: number;
}
interface PositionLimits {
maxTradeUsd: number;
}
That is unsafe because:
requestedAmount
is a token amount, while:
maxTradeUsd
is USD.
Use:
interface RiskRequest {
tokenAddress: string;
side: "BUY" | "SELL";
/**
* Exact token quantity / atomic quantity
* that would be executed.
*/
requestedAmount: TokenAmount;
/**
* Estimated USD notional represented
* by requestedAmount.
*/
notionalUsdCents: UsdCents;
estimatedPriceImpactBps: BasisPoints;
}
And:
interface PositionLimits {
maxTradeUsdCents: UsdCents;
maxPositionUsdCents: UsdCents;
maxPortfolioWeightBps: BasisPoints;
maxPriceImpactBps: BasisPoints;
}
Now every comparison uses the same unit.
18. Correct risk-engine implementation
A type-safe version:
function validateRisk(
request: RiskRequest,
limits: PositionLimits
): RiskDecision {
if (
request.notionalUsdCents >
limits.maxTradeUsdCents
) {
return {
approved: false,
reason:
"Maximum USD trade size exceeded",
};
}
if (
request.estimatedPriceImpactBps >
limits.maxPriceImpactBps
) {
return {
approved: false,
reason:
"Price impact above configured limit",
};
}
return {
approved: true,
approvedAmount:
request.requestedAmount,
reason:
"Risk checks passed",
};
}
No:
Number(bigint)
is required.
No token-vs-USD comparison is possible.
19. Add an explicit risk decision type
interface RiskDecision {
approved: boolean;
approvedAmount?: TokenAmount;
reason: string;
}
The approvedAmount is a token quantity, so it stays bigint.
A rejected trade should not return a fake zero value.
Use undefined:
{
approved: false,
reason: "Maximum USD trade size exceeded"
}
That makes downstream handling explicit.
20. Quote the actual trade
Never assume:
displayed price = execution price
For a meaningful trade size, request an executable quote.
interface ExecutionQuote {
tokenAddress: string;
amountIn: TokenAmount;
expectedAmountOut: TokenAmount;
averagePrice: number;
priceImpactBps: BasisPoints;
minimumAmountOut: TokenAmount;
expiresAt: number;
}
Notice the distinction:
amountIn
expectedAmountOut
minimumAmountOut
are exact chain quantities.
averagePrice can remain a number if it is used only for display/analytics.
For safety-critical calculations, prefer integer units or fixed-point representations.
21. Working end-to-end execution example
The current Pons deployment documents WETH, SwapRouter02, QuoterV2, Robinhood Chain chain ID 4663, and the 1% pool fee for current launches.
The execution path is:
ETH
↓
WETH
↓
Quote
↓
Slippage Bound
↓
WETH Approval
↓
SwapRouter02
↓
Transaction Receipt
↓
Token Balance
Install:
npm install viem dotenv
.env:
RH_RPC_URL=https://rpc.mainnet.chain.robinhood.com
PRIVATE_KEY=0xYOUR_DEDICATED_TRADING_WALLET_PRIVATE_KEY
TOKEN_ADDRESS=0xTHE_PONS_TOKEN_ADDRESS
WETH_AMOUNT=0.01
Never commit .env.
Complete execution example:
import "dotenv/config";
import {
createPublicClient,
createWalletClient,
formatEther,
http,
parseAbi,
parseEther,
type Address,
} from "viem";
import {
privateKeyToAccount,
} from "viem/accounts";
const CHAIN_ID = 4663;
const RPC_URL =
process.env.RH_RPC_URL ??
"https://rpc.mainnet.chain.robinhood.com";
const PRIVATE_KEY =
process.env.PRIVATE_KEY as
| `0x${string}`
| undefined;
const TOKEN_ADDRESS =
process.env.TOKEN_ADDRESS as
| Address
| undefined;
if (!PRIVATE_KEY) {
throw new Error(
"PRIVATE_KEY is required"
);
}
if (!TOKEN_ADDRESS) {
throw new Error(
"TOKEN_ADDRESS is required"
);
}
const WETH =
"0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73"
as Address;
const SWAP_ROUTER =
"0xCaf681a66D020601342297493863E78C959E5cb2"
as Address;
const QUOTER_V2 =
"0x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7"
as Address;
const POOL_FEE = 10_000;
const account =
privateKeyToAccount(
PRIVATE_KEY
);
const chain = {
id: CHAIN_ID,
name: "Robinhood Chain",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
default: {
http: [RPC_URL],
},
},
} as const;
const publicClient =
createPublicClient({
chain,
transport: http(RPC_URL),
});
const walletClient =
createWalletClient({
account,
chain,
transport: http(RPC_URL),
});
const erc20Abi = parseAbi([
"function balanceOf(address owner) view returns (uint256)",
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
]);
const wethAbi = parseAbi([
"function deposit() payable",
]);
const quoterAbi = [
{
name: "quoteExactInputSingle",
type: "function",
stateMutability: "nonpayable",
inputs: [
{
name: "params",
type: "tuple",
components: [
{
name: "tokenIn",
type: "address",
},
{
name: "tokenOut",
type: "address",
},
{
name: "amountIn",
type: "uint256",
},
{
name: "fee",
type: "uint24",
},
{
name: "sqrtPriceLimitX96",
type: "uint160",
},
],
},
],
outputs: [
{
name: "amountOut",
type: "uint256",
},
{
name: "sqrtPriceX96After",
type: "uint160",
},
{
name: "initializedTicksCrossed",
type: "uint32",
},
{
name: "gasEstimate",
type: "uint256",
},
],
},
] as const;
const routerAbi = [
{
name: "exactInputSingle",
type: "function",
stateMutability: "payable",
inputs: [
{
name: "params",
type: "tuple",
components: [
{
name: "tokenIn",
type: "address",
},
{
name: "tokenOut",
type: "address",
},
{
name: "fee",
type: "uint24",
},
{
name: "recipient",
type: "address",
},
{
name: "amountIn",
type: "uint256",
},
{
name: "amountOutMinimum",
type: "uint256",
},
{
name: "sqrtPriceLimitX96",
type: "uint160",
},
],
},
],
outputs: [
{
name: "amountOut",
type: "uint256",
},
],
},
] as const;
async function main() {
const wallet =
account.address;
const wethAmount =
parseEther(
process.env.WETH_AMOUNT ?? "0.01"
);
console.log(
`Trading wallet: ${wallet}`
);
console.log(
`Token: ${TOKEN_ADDRESS}`
);
console.log(
`Input: ${formatEther(
wethAmount
)} WETH`
);
/*
* 1. Check WETH balance.
*/
let wethBalance =
await publicClient.readContract({
address: WETH,
abi: erc20Abi,
functionName: "balanceOf",
args: [wallet],
});
/*
* 2. Wrap native ETH → WETH if required.
*/
if (wethBalance < wethAmount) {
const required =
wethAmount - wethBalance;
const ethBalance =
await publicClient.getBalance({
address: wallet,
});
if (ethBalance < required) {
throw new Error(
`Not enough ETH. Need at least ${formatEther(
required
)} ETH`
);
}
console.log(
`Wrapping ${formatEther(
required
)} ETH`
);
const wrapHash =
await walletClient.writeContract({
address: WETH,
abi: wethAbi,
functionName: "deposit",
value: required,
});
await publicClient.waitForTransactionReceipt({
hash: wrapHash,
});
wethBalance =
await publicClient.readContract({
address: WETH,
abi: erc20Abi,
functionName: "balanceOf",
args: [wallet],
});
}
if (wethBalance < wethAmount) {
throw new Error(
"WETH funding step failed"
);
}
/*
* 3. Get an executable quote.
*/
const { result } =
await publicClient.simulateContract({
address: QUOTER_V2,
abi: quoterAbi,
functionName:
"quoteExactInputSingle",
args: [
{
tokenIn: WETH,
tokenOut: TOKEN_ADDRESS,
amountIn: wethAmount,
fee: POOL_FEE,
sqrtPriceLimitX96: 0n,
},
],
});
const quotedAmountOut =
result[0];
/*
* 4. Apply a slippage bound.
*
* 200 basis points = 2%.
* This is only an example configuration.
*/
const slippageBps: BasisPoints =
200n;
const amountOutMinimum =
quotedAmountOut *
(10_000n - slippageBps) /
10_000n;
console.log(
`Quoted output: ${
quotedAmountOut.toString()
}`
);
console.log(
`Minimum output: ${
amountOutMinimum.toString()
}`
);
/*
* 5. Approve WETH.
*/
const allowance =
await publicClient.readContract({
address: WETH,
abi: erc20Abi,
functionName: "allowance",
args: [
wallet,
SWAP_ROUTER,
],
});
if (allowance < wethAmount) {
console.log(
"Approving WETH..."
);
const approvalHash =
await walletClient.writeContract({
address: WETH,
abi: erc20Abi,
functionName: "approve",
args: [
SWAP_ROUTER,
wethAmount,
],
});
await publicClient.waitForTransactionReceipt({
hash: approvalHash,
});
}
/*
* 6. Execute the swap.
*/
console.log(
"Submitting swap..."
);
const swapHash =
await walletClient.writeContract({
address: SWAP_ROUTER,
abi: routerAbi,
functionName:
"exactInputSingle",
args: [
{
tokenIn: WETH,
tokenOut: TOKEN_ADDRESS,
fee: POOL_FEE,
recipient: wallet,
amountIn: wethAmount,
amountOutMinimum,
sqrtPriceLimitX96: 0n,
},
],
});
console.log(
`Transaction: ${swapHash}`
);
/*
* 7. Wait for confirmation.
*/
const receipt =
await publicClient.waitForTransactionReceipt({
hash: swapHash,
});
if (receipt.status !== "success") {
throw new Error(
"Swap transaction failed"
);
}
console.log(
"Swap confirmed."
);
/*
* 8. Verify final token balance.
*/
const tokenBalance =
await publicClient.readContract({
address: TOKEN_ADDRESS,
abi: erc20Abi,
functionName: "balanceOf",
args: [wallet],
});
console.log(
`Final token balance: ${
tokenBalance.toString()
}`
);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
The exact chain amounts remain bigint from start to finish:
wethAmount
↓
amountIn
↓
quotedAmountOut
↓
amountOutMinimum
↓
tokenBalance
No floating-point conversion is required for the transaction values.
33. Keep USD risk accounting separate from chain quantities
This is the key type boundary:
Portfolio / Risk Layer
USD:
bigint cents
↓
Quote / Execution Layer
Token quantities:
bigint atomic units
↓
Blockchain
uint256
For example:
const maxTradeUsdCents: UsdCents =
100_000n; // $1,000
const requestedTokenAmount:
TokenAmount =
quotedAmountOut;
The risk layer receives both:
const riskRequest: RiskRequest = {
tokenAddress,
side: "BUY",
requestedAmount:
quotedAmountOut,
notionalUsdCents:
100_000n,
estimatedPriceImpactBps:
25n,
};
Now the risk engine can correctly evaluate:
request.notionalUsdCents >
limits.maxTradeUsdCents
instead of incorrectly comparing a token quantity against USD.
34. Make unit names part of the API
Prefer:
maxTradeUsdCents
over:
maxTradeUsd
Prefer:
estimatedPriceImpactBps
over:
estimatedPriceImpact
Prefer:
amountOutMinimum
over:
minimumTokens
The name itself communicates the unit.
That makes code review much safer.
35. Never use Number() on large chain amounts for risk checks
Avoid:
Number(request.requestedAmount)
for token quantities.
Large uint256 values can exceed JavaScript's safe integer range.
Bad:
if (
Number(
request.requestedAmount
) > limit
) {
...
}
Better:
if (
request.notionalUsdCents >
limits.maxTradeUsdCents
) {
...
}
And if token amounts themselves need comparison:
if (
request.requestedAmount >
maxTokenAmount
) {
...
}
Both sides remain bigint.
36. Complete amount/risk type definitions
For a reusable codebase, I would put these in one file:
// src/domain/units.ts
export type TokenAmount = bigint;
export type UsdCents = bigint;
export type BasisPoints = bigint;
Then:
// src/risk/types.ts
import type {
BasisPoints,
TokenAmount,
UsdCents,
} from "../domain/units";
export interface RiskRequest {
tokenAddress: string;
side: "BUY" | "SELL";
requestedAmount: TokenAmount;
notionalUsdCents: UsdCents;
estimatedPriceImpactBps:
BasisPoints;
}
export interface PositionLimits {
maxTradeUsdCents: UsdCents;
maxPositionUsdCents:
UsdCents;
maxPortfolioWeightBps:
BasisPoints;
maxPriceImpactBps:
BasisPoints;
}
export interface RiskDecision {
approved: boolean;
approvedAmount?:
TokenAmount;
reason: string;
}
Now every service shares the same unit model.
37. Order types
Use the same convention for orders:
import type {
BasisPoints,
TokenAmount,
UsdCents,
} from "../domain/units";
export interface ExecutionRequest {
tokenAddress: string;
side: "BUY" | "SELL";
amountIn: TokenAmount;
maxSlippageBps:
BasisPoints;
notionalUsdCents:
UsdCents;
amountOutMinimum:
TokenAmount;
}
This creates a clean contract between risk and execution.
38. Final trading flow with correct units
The whole system becomes:
TOKEN DISCOVERY
↓
MARKET DATA
↓
SIGNAL
↓
POSITION SIZING
USD cents
↓
RISK
USD cents
+
basis points
+
token amount
↓
QUOTE
token atomic units
↓
SLIPPAGE
basis points
↓
EXECUTION
uint256 / bigint
↓
FILL
uint256 / bigint
↓
POSITION
uint256 / bigint
↓
RECONCILIATION
The unit boundaries are now explicit.
39. Reconciliation
Do not trust only the application's own state.
Imagine:
Database:
6,500 tokens
Chain:
6,500 tokens
Everything matches.
But after a connection problem:
Database:
6,500
Chain:
7,200
The system needs to notice.
Architecture:
Local State
+
Onchain State
↓
Reconciliation
↓
Correct Position
A basic model:
interface PositionSnapshot {
tokenAddress: string;
amount: TokenAmount;
updatedAt: number;
}
Then:
function positionsMatch(
local: PositionSnapshot,
chain: PositionSnapshot
): boolean {
return (
local.tokenAddress ===
chain.tokenAddress &&
local.amount ===
chain.amount
);
}
40. Prevent duplicate execution
Consider:
Signal
↓
BUY
↓
RPC timeout
↓
Retry
The original transaction may actually have succeeded.
A blind retry can create a duplicate position.
Use an idempotency key:
const idempotencyKey =
[
strategyId,
tokenAddress,
signalId,
].join(":");
Persist it before execution.
Then:
Same signal
↓
Same idempotency key
↓
Already processed
↓
Do not submit another order
41. Alerts
The bot should notify important state changes:
NEW_LAUNCH
SIGNAL_CREATED
RISK_APPROVED
ORDER_SUBMITTED
ORDER_PARTIALLY_FILLED
ORDER_FILLED
ORDER_FAILED
POSITION_OPEN
POSITION_CLOSED
RECONCILIATION_REQUIRED
For example:
PONS BOT
MEMESTOCK
ENTRY PARTIALLY FILLED
Requested:
10,000
Filled:
6,500
Remaining:
3,500
State:
PARTIALLY_OPEN
A normalized alert:
interface TradingAlert {
type:
| "SIGNAL"
| "ORDER"
| "FILL"
| "POSITION"
| "RECONCILIATION";
tokenAddress: string;
message: string;
createdAt: number;
}
42. Restart recovery
A trading process can crash.
The architecture should survive that.
Startup:
START
↓
Load Open Orders
↓
Load Open Positions
↓
Query Chain
↓
Reconcile
↓
Resume
For example:
async function recover() {
const openOrders =
await orderStore.getOpenOrders();
const openPositions =
await positionStore.getOpenPositions();
for (const order of openOrders) {
await reconciler.reconcileOrder(
order
);
}
for (const position of openPositions) {
await reconciler.reconcilePosition(
position
);
}
}
The bot does not assume the previous process ended cleanly.
43. API architecture
The bot can expose its state to a dashboard:
GET /api/tokens
GET /api/tokens/:address
GET /api/signals
GET /api/orders
GET /api/orders/:id
GET /api/positions
GET /api/positions/:address
GET /api/alerts
Then:
Trading Terminal
↓
Trading API
↓
Trading Engine
↓
Robinhood Chain
The same backend can power web and mobile interfaces.
44. Scanner to bot architecture
The complete product becomes:
PONS / ROBINHOOD CHAIN
│
▼
EVENT INDEXER
│
▼
TOKEN SCANNER
│
▼
MARKET STATE
│
▼
STRATEGY ENGINE
│
▼
RISK ENGINE
│
▼
EXECUTION ENGINE
│
┌──────┴──────┐
▼ ▼
FILLS FAILED
│
▼
POSITION
│
▼
RECONCILIATION
That is the architecture I would implement rather than putting everything into one bot.ts file.
45. Suggested TypeScript project structure
pons-memestock-trading-bot/
│
├── src/
│ ├── domain/
│ │ └── units.ts
│ │
│ ├── chain/
│ │ ├── client.ts
│ │ ├── contracts.ts
│ │ └── events.ts
│ │
│ ├── indexer/
│ │ ├── worker.ts
│ │ ├── cursor.ts
│ │ └── processor.ts
│ │
│ ├── tokens/
│ │ ├── registry.ts
│ │ └── metadata.ts
│ │
│ ├── markets/
│ │ ├── resolver.ts
│ │ ├── pricing.ts
│ │ └── trades.ts
│ │
│ ├── strategy/
│ │ ├── filters.ts
│ │ ├── signal.ts
│ │ └── positionSizing.ts
│ │
│ ├── risk/
│ │ ├── riskEngine.ts
│ │ └── types.ts
│ │
│ ├── execution/
│ │ ├── executor.ts
│ │ ├── orders.ts
│ │ └── transactionMonitor.ts
│ │
│ ├── positions/
│ │ ├── manager.ts
│ │ └── stateMachine.ts
│ │
│ ├── reconciliation/
│ │ └── reconciler.ts
│ │
│ ├── alerts/
│ │ └── router.ts
│ │
│ └── api/
│ └── server.ts
│
├── database/
├── tests/
├── .env.example
├── package.json
└── README.md
46. Paper trading
Before enabling live execution, use the same strategy and risk pipeline with a simulation adapter.
interface ExecutionAdapter {
execute(
request: ExecutionRequest
): Promise<string>;
}
Paper:
class PaperExecutor
implements ExecutionAdapter {
async execute(
request: ExecutionRequest
): Promise<string> {
console.log(
"[PAPER]",
request
);
return "paper-trade";
}
}
Live:
class LiveExecutor
implements ExecutionAdapter {
async execute(
request: ExecutionRequest
): Promise<string> {
// Build transaction
// Submit transaction
// Return tx hash
return "0x...";
}
}
That lets you test:
Scanner
↓
Signal
↓
Risk
↓
Execution
↓
Position
without immediately connecting the final step to live capital.
47. Optional Pons v2 adapter
If you want the repository to support multiple Pons generations, do not mix their mechanics inside the same execution code.
Use adapters:
interface PonsMarketAdapter {
detectLaunch(
token: string
): Promise<PonsLaunch>;
getMarketState(
token: string
): Promise<MarketSnapshot>;
getQuote(
token: string,
amount: TokenAmount
): Promise<ExecutionQuote>;
}
Then:
PonsMarketAdapter
│
├── CurrentPoolAdapter
│
└── V2CurveAdapter
This keeps version-specific behavior isolated.
48. Complete architecture
PONS
│
▼
EVENT INDEXER
│
▼
TOKEN REGISTRY
│
▼
MARKET RESOLVER
│
▼
MARKET SNAPSHOT
│
▼
STRATEGY ENGINE
│
▼
RISK ENGINE
│
┌──────┴──────┐
│ │
REJECT APPROVE
│ │
│ ▼
│ EXECUTION
│ │
│ ▼
│ ORDER STATE
│ │
│ ┌────┴────┐
│ ▼ ▼
│ FILLED FAILED
│ │
│ ▼
│ POSITION
│ │
│ ▼
│ RECONCILIATION
│ │
└────────┴──────► MONITORING
The architecture separates:
DISCOVERY
from:
STRATEGY
from:
RISK
from:
EXECUTION
from:
STATE
That separation is the core of the system.
Conclusion
A Pons memestock trading bot on Robinhood Chain should not be designed as:
NEW TOKEN → BUY
A more reliable architecture is:
Pons Events
↓
Token Indexing
↓
Market State
↓
Strategy
↓
Risk
↓
Execution
↓
Orders / Fills
↓
Position
↓
Reconciliation
The most important TypeScript rule is just as simple:
Never mix units.
Use:
type TokenAmount = bigint;
type UsdCents = bigint;
type BasisPoints = bigint;
Then make the boundaries explicit:
USD cents
↓
Risk
↓
Token atomic units
↓
Blockchain uint256
This avoids unsafe Number(bigint) conversions and prevents bugs such as comparing a token quantity directly with a USD risk limit.
The executable path then becomes:
WETH
↓
Quote
↓
Slippage Bound
↓
Approval
↓
SwapRouter02
↓
Receipt
↓
Balance Verification
The signal says what the strategy wants.
The risk engine decides whether it is permitted.
The execution engine submits the transaction.
The fill engine records what actually happened.
The position manager calculates the resulting state.
Reconciliation verifies that state against the chain.
That is what turns a Pons trading script into a real trading system.
Building a custom Pons trading bot?
I build custom Pons and Robinhood Chain trading infrastructure, including:
- Pons memestock trading bots
- Pons sniper bots
- Pons token scanners
- Pons launch monitors
- Pons copy-trading systems
- Pons trading terminals
- market-data APIs
- automated execution engines
- position-management systems
- reconciliation infrastructure
The system can be built as a standalone bot, a web trading terminal, or a backend/API that powers an existing trading application.
Technical note: Pons terminology and deployed contracts can evolve through new factory versions. The integration should therefore resolve protocol version and contract addresses from the current official documentation rather than hard-coding assumptions from an older implementation.








