A token scanner is often treated as a simple script:
listen for new token
→ print address
That is enough for a demo.
It is not enough for a useful trading product.
A production Pons Token Scanner needs to turn raw blockchain activity into structured state that other systems can consume:
Robinhood Chain
↓
Pons Events
↓
Event Decoder
↓
Token Registry
↓
Market State
↓
Filters
↓
Opportunity State
↓
API / Alerts / Dashboard
↓
Sniper / Trading Bot
The important architectural distinction is:
The scanner discovers and understands what happened. The trading bot decides what to do.
That makes the scanner useful on its own and reusable as the data layer underneath other trading products.
Why build the scanner onchain?
The current Pons V2 documentation explicitly describes factory and curve events as the integration source of truth: indexing the factory gives you launches, while indexing each curve gives you the launch's trading history. It states that this is enough to reconstruct protocol state without depending on a Pons service.
That gives us:
Pons Contracts
↓
Events
↓
Own Indexer
↓
Own Database
Instead of:
Pons UI
↓
Third-Party API
↓
Scanner
Building the indexer yourself gives control over:
- data model
- filtering
- historical backfill
- latency
- alerts
- APIs
- downstream automation
Robinhood Chain connection
Robinhood Chain is an EVM-compatible Arbitrum Layer-2 on Ethereum. Mainnet currently uses chain ID 4663 and ETH as the native gas token. The official deployment documentation supports standard Solidity tooling and EVM libraries.
For a TypeScript scanner, a familiar stack works:
TypeScript
↓
viem
↓
RPC
↓
Robinhood Chain
↓
Pons Contracts
A basic client can look like:
import {
createPublicClient,
http,
} from "viem";
const robinhoodChain = {
id: 4663,
name: "Robinhood Chain",
nativeCurrency: {
name: "Ether",
symbol: "ETH",
decimals: 18,
},
rpcUrls: {
default: {
http: ["https://rpc.mainnet.chain.robinhood.com"],
},
},
};
const client = createPublicClient({
chain: robinhoodChain,
transport: http(),
});
For production infrastructure, Robinhood currently recommends using a dedicated infrastructure provider such as Alchemy rather than relying on the public RPC endpoint for everything.
Step 1: Separate Pons V1 and V2
A scanner should be version-aware from the beginning.
Pons V2 has a bonding-curve lifecycle:
Create
↓
Trade Curve
↓
Graduate
↓
Uniswap V4 Pool
The current V2 documentation describes the curve as the initial trading venue, with graduation occurring when the curve is bought out and the resulting liquidity moving into a permanently locked Uniswap V4 pool.
That is different from the earlier Pons launch architecture.
So I would structure the scanner like:
Pons Scanner
↓
Protocol Adapter
├── V1
└── V2
The rest of the application should consume a normalized internal representation.
Step 2: Detect Pons V2 launches
The current V2 documentation provides the event signature:
TokenLaunched(
token,
curve,
deployer,
pairToken,
launchConfigId,
graduationThreshold
)
and explicitly shows using getLogs() to index every launch.
With viem:
import { parseAbiItem } from "viem";
const tokenLaunchedEvent = parseAbiItem(
"event TokenLaunched(address indexed token, address indexed curve, address indexed deployer, address pairToken, uint256 launchConfigId, uint256 graduationThreshold)"
);
const logs = await client.getLogs({
address: PONS_V2_FACTORY,
event: tokenLaunchedEvent,
fromBlock: deploymentBlock,
toBlock: "latest",
});
The scanner has now answered:
A new Pons V2 launch exists.
But we still know very little about it.
Step 3: Normalize the launch
I would immediately convert the raw event into an application object.
type PonsVersion = "V1" | "V2";
interface PonsLaunch {
token: `0x${string}`;
deployer: `0x${string}`;
version: PonsVersion;
pairToken: `0x${string}`;
curve?: `0x${string}`;
pool?: `0x${string}`;
launchConfigId: bigint;
graduationThreshold?: bigint;
launchBlock: bigint;
launchTxHash: `0x${string}`;
}
Then:
Blockchain Event
↓
V2 Adapter
↓
PonsLaunch
This is important because application code should not have to understand the raw ABI of every protocol version.
Step 4: Create a canonical token ID
A ticker is not a unique blockchain identity.
The scanner should identify a token using:
chain ID
+
contract address
For example:
function tokenId(
chainId: number,
address: string
): string {
return `${chainId}:${address.toLowerCase()}`;
}
Then:
4663:0x1234...
becomes the canonical application identifier.
This also prevents duplicate records caused by address casing differences.
For Pons V2 specifically, the documentation warns that token names and symbols can be copied, so the address should be treated as the actual identity.
Step 5: Enrich token metadata
After the launch event, the scanner should load the token's metadata.
For example:
interface TokenMetadata {
address: `0x${string}`;
name: string;
symbol: string;
decimals: number;
totalSupply: bigint;
deployer: `0x${string}`;
description?: string;
image?: string;
website?: string;
twitter?: string;
telegram?: string;
}
The pipeline becomes:
TokenLaunched
↓
Token Address
↓
ERC-20 Metadata
↓
Pons-Specific Metadata
↓
Token Registry
Now the scanner can answer more useful questions:
What is this token?
Who launched it?
Which Pons version?
Which pair?
Which curve?
When was it launched?
Step 6: Track the market state
For V2, a new token begins on a bonding curve.
The current documentation describes the curve as the initial market, with trading continuing there until graduation and then continuing in a Uniswap V4 pool.
I would represent market state explicitly:
type MarketPhase =
| "CURVE"
| "GRADUATING"
| "POOL"
| "RESCUED"
| "UNKNOWN";
The scanner can then maintain:
interface MarketState {
phase: MarketPhase;
quoteAsset: `0x${string}`;
curve?: `0x${string}`;
pool?: `0x${string}`;
lastBlock: bigint;
updatedAt: number;
}
This is much more useful than keeping only:
token = 0x...
Step 7: Index the curve
The current Pons V2 docs explicitly recommend indexing the curve for trade history.
The documented events include:
CurveBuy
CurveSell
CurveBuyRefunded
CurveCompleted
and the factory also emits LaunchSwept.
For example:
const curveBuyEvent = parseAbiItem(
"event CurveBuy(address indexed buyer, address indexed recipient, uint256 quoteIn, uint256 tokensOut, uint256 fee, uint256 tax)"
);
const curveSellEvent = parseAbiItem(
"event CurveSell(address indexed seller, address indexed recipient, uint256 tokensIn, uint256 quoteOut, uint256 fee, uint256 tax)"
);
const tradeLogs = await client.getLogs({
address: curveAddress,
events: [
curveBuyEvent,
curveSellEvent,
],
fromBlock: launchBlock,
toBlock: "latest",
});
Now the scanner is no longer just a launch detector.
It has become a market activity indexer.
Step 8: Store actual execution values
This is an important implementation detail.
The current Pons V2 documentation notes that a buy which finishes a curve can be partially filled. It specifically recommends reading tokensOut and quoteIn from the event rather than assuming the requested amount is what was executed.
So the scanner should store:
interface CurveTrade {
txHash: `0x${string}`;
logIndex: number;
side: "BUY" | "SELL";
trader: `0x${string}`;
recipient: `0x${string}`;
quoteAmount: bigint;
tokenAmount: bigint;
fee: bigint;
tax: bigint;
blockNumber: bigint;
}
Do not reconstruct the history from UI assumptions.
Use the onchain event.
Step 9: Build a token registry
Once launch and trade events are normalized, the database becomes the core of the scanner.
A simple schema:
tokens
-------------------
id
address
version
name
symbol
deployer
created_at
pons_launches
-------------------
token_id
curve_address
pool_address
pair_token
launch_config_id
launch_block
launch_tx_hash
market_states
-------------------
token_id
phase
quote_asset
last_block
updated_at
curve_trades
-------------------
token_id
tx_hash
log_index
side
trader
recipient
quote_amount
token_amount
fee
tax
block_number
The advantage is that downstream applications can query a normalized database instead of decoding raw blockchain logs themselves.
Step 10: Add event idempotency
A scanner will reconnect.
It will backfill.
It may process overlapping block ranges.
That means the same event can appear more than once.
A useful event identity is:
function eventId(
chainId: number,
transactionHash: string,
logIndex: number
): string {
return `${chainId}:${transactionHash}:${logIndex}`;
}
Then:
Event
↓
Already processed?
├── yes → ignore
└── no → store
The database should enforce uniqueness:
UNIQUE(chain_id, transaction_hash, log_index)
This is a small feature that prevents large state problems later.
Step 11: Real-time listener and historical backfill should be separate
There are really two jobs:
Historical Indexer
↓
Backfill missing blocks
and:
Live Listener
↓
New events
I would not combine them into one giant process.
Instead:
Pons Contracts
│
┌────────┴────────┐
↓ ↓
Backfill Worker Live Worker
↓ ↓
└────────┬────────┘
↓
Database
This makes restarts and catch-up much easier.
Robinhood's documentation currently recommends dedicated infrastructure providers for production use, while its network documentation also exposes the public RPC endpoints.
Step 12: Bounded block processing
A common indexing mistake is requesting an enormous block range in one call.
I prefer bounded chunks:
const CHUNK_SIZE = 2_000n;
async function backfill(
startBlock: bigint,
endBlock: bigint
) {
for (
let from = startBlock;
from <= endBlock;
from += CHUNK_SIZE
) {
const to =
from + CHUNK_SIZE - 1n > endBlock
? endBlock
: from + CHUNK_SIZE - 1n;
await indexRange(from, to);
}
}
That gives the indexer:
smaller requests
+
retryable work
+
clear progress
instead of one enormous RPC request that can fail and force the entire operation to restart.
Step 13: Filters
Once the token registry exists, we can build a filter engine.
For example:
interface TokenFilter {
name: string;
check(
token: TokenRecord
): Promise<FilterResult>;
}
interface FilterResult {
passed: boolean;
reason: string;
}
Then:
Token
↓
Contract Filter
↓
Metadata Filter
↓
Deployer Filter
↓
Market Filter
↓
Liquidity Filter
↓
Custom Rules
This is much cleaner than putting everything into:
shouldTrade(token)
with hundreds of conditions.
Step 14: Opportunity state
A scanner should distinguish detection from eligibility.
I would use something like:
type OpportunityState =
| "DETECTED"
| "WATCHING"
| "ELIGIBLE"
| "REJECTED"
| "EXPIRED";
For example:
TokenLaunched
↓
DETECTED
↓
Filters
↓
WATCHING
↓
ELIGIBLE
Now the scanner can feed different systems.
Scanner → Sniper Bot
The Pons Sniper Bot can consume:
ELIGIBLE TOKEN
and then perform its own:
quote
↓
risk
↓
execution
So:
Pons Scanner
↓
Eligible Opportunity
↓
Pons Sniper
↓
Execution
This is cleaner than implementing launch discovery separately inside the sniper.
Scanner → Trading Terminal
A Pons Trading Terminal can consume the scanner's API:
New Launches
Market Phase
Recent Trades
Liquidity
Token Metadata
The frontend can then show:
NEW
WATCHING
ELIGIBLE
GRADUATED
without knowing anything about raw contract logs.
Scanner → Alerts
A simpler product might only need alerts.
For example:
Pons Token Scanner
↓
Filter
↓
Telegram / Discord
The alert can contain:
New Pons Launch
Token: XYZ
Version: V2
Pair: WETH
Curve: 0x...
Deployer: 0x...
Launch Block: 123456
Status: Watching
This can already be a useful standalone application.
Scanner → Copy Trading
The same data layer can support wallet intelligence.
For example:
New Token
↓
Trade Events
↓
Identify Active Wallets
↓
Track Wallet
↓
Generate Signal
That turns the scanner into the first stage of a copy-trading system.
Step 15: Market state and graduation
The scanner must understand when a Pons V2 launch leaves the bonding curve.
The current Pons documentation describes:
CurveBuy
↓
CurveCompleted
↓
Graduation
↓
PoolCreated
↓
Uniswap V4
and documents the relevant factory/curve events for reconstruction.
So the scanner should update:
market.phase = "POOL";
when the protocol state confirms graduation.
Do not infer this only from token balances.
Use protocol state and events.
Step 16: Reconciliation
Even a good indexer can fall behind or miss an event.
So I would build reconciliation.
For example:
Database
↓
Stored Market State
↓
Read Contract State
↓
Compare
↓
Repair
The scanner can periodically check:
launch exists
curve exists
phase
pool state
latest processed block
and detect inconsistencies.
This gives the system a recovery mechanism instead of assuming the indexer is permanently correct.
Step 17: Keep scanner state separate from trading state
This is important.
The scanner should record:
what happened
The trading engine should decide:
what to do
So:
Scanner
↓
Market State
↓
Strategy
↓
Risk
↓
Execution
The scanner itself should not secretly submit trades.
That separation makes the system much easier to reuse.
A production TypeScript structure
I would organize the project roughly like:
src/
├── config/
│ └── pons.ts
│
├── clients/
│ └── chain.ts
│
├── protocols/
│ ├── v1/
│ │ └── adapter.ts
│ └── v2/
│ └── adapter.ts
│
├── indexer/
│ ├── backfill.ts
│ ├── live.ts
│ └── decoder.ts
│
├── tokens/
│ ├── registry.ts
│ └── metadata.ts
│
├── markets/
│ └── marketState.ts
│
├── filters/
│ ├── creator.ts
│ ├── liquidity.ts
│ └── engine.ts
│
├── opportunities/
│ └── state.ts
│
├── alerts/
│ ├── telegram.ts
│ └── discord.ts
│
├── reconciliation/
│ └── reconcile.ts
│
├── api/
│ └── server.ts
│
└── index.ts
This gives every subsystem one responsibility.
Database-backed scanning
A production scanner should persist its progress.
For example:
indexer_state
-----------------
network
contract
last_block
updated_at
Then after a restart:
Database
↓
Last processed block
↓
Backfill
↓
Live listener
No need to start from zero.
Observability
The scanner should expose operational metrics.
I would monitor:
latest indexed block
RPC latency
RPC errors
events processed
duplicate events
tokens discovered
filters rejected
eligible tokens
backfill progress
reconciliation mismatches
For example:
Latest block: 2,145,892
Launches indexed: 8,421
Trades indexed: 91,420
Duplicate events: 117
Eligible opportunities: 38
RPC errors: 4
Reconciliation mismatches: 0
That gives a much better view of system health than simply checking whether the process is running.
A scanner is not necessarily an automated trader
This distinction matters from a product perspective.
A scanner can be:
Token Discovery Product
without ever executing a trade.
It can provide:
alerts
API
dashboard
historical data
analytics
Or it can become the first stage of:
Scanner
↓
Sniper
or:
Scanner
↓
Trading Terminal
or:
Scanner
↓
Copy Trading
This makes the scanner a reusable infrastructure component.
What I would build for a client
Starter
Launch Detection
+
Metadata
+
Telegram Alerts
MVP
Launch Detection
+
Metadata
+
Filters
+
Database
+
API
+
Dashboard
Advanced
V1 + V2
+
Historical Backfill
+
Market State
+
Liquidity
+
Wallet Intelligence
+
Alerts
+
API
+
Sniper Integration
Full Trading Platform
Scanner
+
Strategy
+
Risk
+
Execution
+
Portfolio
+
Reconciliation
+
Terminal
This lets the client start with exactly the part they need.
Connecting the scanner to Solidity and EVM engineering
This is also where the scanner connects with the Solidity/EVM work I have been publishing.
The scanner is mostly an offchain data system:
RPC
+
viem
+
TypeScript
+
Database
But the data originates from:
Solidity Contracts
↓
EVM Events
↓
Indexer
So understanding the contract layer matters.
You need to know:
event signatures
indexed parameters
state-changing functions
contract addresses
protocol versions
That is one reason I see Solidity + EVM + TypeScript as one connected engineering skillset rather than three unrelated topics.
Public implementation work
The same development approach is already being applied to other Robinhood Chain products.
Pons Sniper Bot
Launch detection, V2 curve execution, risk controls, transaction monitoring, and reconciliation.
https://github.com/0xhamssog/pons-sniper-bot
Pons Bundler
Pons V2 launch-and-buy and multi-wallet execution.
https://github.com/0xhamssog/pons-bundler
Stock Token Arbitrage Bot
Reference/onchain pricing and trading infrastructure.
https://github.com/0xhamssog/robinhood-stock-token-arbitrage-bot
The scanner fits underneath these products as the discovery and market-data layer.
The bigger architecture
Eventually, several products can share the same scanner:
ROBINHOOD CHAIN
↓
PONS CONTRACTS
↓
EVENT INDEXER
↓
NORMALIZED STATE
↓
PONS SCANNER
↓
┌─────────────────────┼─────────────────────┐
↓ ↓ ↓
Alerts API Analytics
↓ ↓ ↓
Telegram Terminal Dashboard
↓
Trading Engine
↓
┌──────┼──────┐
↓ ↓ ↓
Sniper Copy Manual
↓ ↓ ↓
└──────┼──────┘
↓
Risk
↓
Execution
↓
Reconciliation
Now the scanner is not a standalone script.
It is the data foundation underneath a trading ecosystem.
Conclusion
A Pons Token Scanner may begin with one event:
TokenLaunched
But a useful production implementation becomes:
Onchain Events
↓
Protocol Version
↓
Token Registry
↓
Metadata
↓
Market State
↓
Trade History
↓
Filtering
↓
Opportunity State
↓
Alerts / API / Dashboard
↓
Trading Automation
That is the difference between an event listener and a real discovery system.
The scanner can power a standalone application.
It can feed a Pons Sniper Bot.
It can become the market-data layer of a Trading Terminal.
It can support wallet intelligence and copy trading.
And because it is built directly from EVM events, it becomes reusable infrastructure rather than a one-off script.
That is the type of Robinhood Chain engineering I want to demonstrate: Solidity/EVM understanding underneath, TypeScript infrastructure in the middle, and real trading products on top.
Need a custom Pons token scanner?
I build custom Pons token scanners, launch monitors, event indexers, APIs, dashboards, sniper integrations, and trading infrastructure on Robinhood Chain.
Top comments (0)