DEV Community

Cover image for Building a Pons Token Scanner on Robinhood Chain with TypeScript
hamssog
hamssog

Posted on Originally published at hamssog.substack.com

Building a Pons Token Scanner on Robinhood Chain with TypeScript

A token scanner is often treated as a simple script:

listen for new token
→ print address
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Instead of:

   Pons UI
      ↓
Third-Party API
      ↓ 
   Scanner
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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(),
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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",
});
Enter fullscreen mode Exit fullscreen mode

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}`;
}
Enter fullscreen mode Exit fullscreen mode

Then:

Blockchain Event
      ↓
V2 Adapter
      ↓
PonsLaunch
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For example:

function tokenId(
  chainId: number,
  address: string
): string {
  return `${chainId}:${address.toLowerCase()}`;
}
Enter fullscreen mode Exit fullscreen mode

Then:

4663:0x1234...
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

The pipeline becomes:

TokenLaunched
      ↓
Token Address
      ↓
ERC-20 Metadata
      ↓
Pons-Specific Metadata
      ↓
Token Registry
Enter fullscreen mode Exit fullscreen mode

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?
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

The scanner can then maintain:

interface MarketState {
  phase: MarketPhase;

  quoteAsset: `0x${string}`;

  curve?: `0x${string}`;
  pool?: `0x${string}`;

  lastBlock: bigint;
  updatedAt: number;
}
Enter fullscreen mode Exit fullscreen mode

This is much more useful than keeping only:

token = 0x...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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",
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}`;
}
Enter fullscreen mode Exit fullscreen mode

Then:

Event
 ↓
Already processed?
 ├── yes → ignore
 └── no  → store
Enter fullscreen mode Exit fullscreen mode

The database should enforce uniqueness:

UNIQUE(chain_id, transaction_hash, log_index)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and:

Live Listener
       ↓
New events
Enter fullscreen mode Exit fullscreen mode

I would not combine them into one giant process.

Instead:

                 Pons Contracts
                       │
              ┌────────┴────────┐
              ↓                 ↓
         Backfill Worker    Live Worker
              ↓                 ↓
              └────────┬────────┘
                       ↓
                    Database
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

That gives the indexer:

smaller requests
+
retryable work
+
clear progress
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Then:

Token
  ↓
Contract Filter
  ↓
Metadata Filter
  ↓
Deployer Filter
  ↓
Market Filter
  ↓
Liquidity Filter
  ↓
Custom Rules
Enter fullscreen mode Exit fullscreen mode

This is much cleaner than putting everything into:

shouldTrade(token)
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

For example:

TokenLaunched
      ↓
DETECTED
      ↓
Filters
      ↓
WATCHING
      ↓
ELIGIBLE
Enter fullscreen mode Exit fullscreen mode

Now the scanner can feed different systems.


Scanner → Sniper Bot

The Pons Sniper Bot can consume:

ELIGIBLE TOKEN
Enter fullscreen mode Exit fullscreen mode

and then perform its own:

quote
↓
risk
↓
execution
Enter fullscreen mode Exit fullscreen mode

So:

Pons Scanner
      ↓
Eligible Opportunity
      ↓
Pons Sniper
      ↓
Execution
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The frontend can then show:

NEW
WATCHING
ELIGIBLE
GRADUATED
Enter fullscreen mode Exit fullscreen mode

without knowing anything about raw contract logs.


Scanner → Alerts

A simpler product might only need alerts.

For example:

Pons Token Scanner
        ↓
Filter
        ↓
Telegram / Discord
Enter fullscreen mode Exit fullscreen mode

The alert can contain:

New Pons Launch

Token: XYZ
Version: V2
Pair: WETH
Curve: 0x...
Deployer: 0x...
Launch Block: 123456
Status: Watching
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

and documents the relevant factory/curve events for reconstruction.

So the scanner should update:

market.phase = "POOL";
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The scanner can periodically check:

launch exists
curve exists
phase
pool state
latest processed block
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The trading engine should decide:

what to do
Enter fullscreen mode Exit fullscreen mode

So:

Scanner
   ↓
Market State
   ↓
Strategy
   ↓
Risk
   ↓
Execution
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then after a restart:

Database
   ↓
Last processed block
   ↓
Backfill
   ↓
Live listener
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

without ever executing a trade.

It can provide:

alerts
API
dashboard
historical data
analytics
Enter fullscreen mode Exit fullscreen mode

Or it can become the first stage of:

Scanner
   ↓
Sniper
Enter fullscreen mode Exit fullscreen mode

or:

Scanner
   ↓
Trading Terminal
Enter fullscreen mode Exit fullscreen mode

or:

Scanner
   ↓
Copy Trading
Enter fullscreen mode Exit fullscreen mode

This makes the scanner a reusable infrastructure component.


What I would build for a client

Starter

Launch Detection
+
Metadata
+
Telegram Alerts
Enter fullscreen mode Exit fullscreen mode

MVP

Launch Detection
+
Metadata
+
Filters
+
Database
+
API
+
Dashboard
Enter fullscreen mode Exit fullscreen mode

Advanced

V1 + V2
+
Historical Backfill
+
Market State
+
Liquidity
+
Wallet Intelligence
+
Alerts
+
API
+
Sniper Integration
Enter fullscreen mode Exit fullscreen mode

Full Trading Platform

Scanner
+
Strategy
+
Risk
+
Execution
+
Portfolio
+
Reconciliation
+
Terminal
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

But the data originates from:

Solidity Contracts
      ↓
EVM Events
      ↓
Indexer
Enter fullscreen mode Exit fullscreen mode

So understanding the contract layer matters.

You need to know:

event signatures
indexed parameters
state-changing functions
contract addresses
protocol versions
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)