Real-time Pons token discovery and market-state indexing on Robinhood Chain.
This repository is a read-only discovery/indexing system. It does not custody funds or execute trades.
The scanner watches the Pons launch factories on Robinhood Chain (chain id 4663). It backfills historical launches, follows new ones over WebSocket or HTTP polling, normalizes V1 and V2 into one token model, stores curve trades from the events that actually settled, and exposes that state over a REST API.
When a token matches the configured filters it is marked ELIGIBLE. That state is a record and, optionally, a webhook. The process never signs or sends a transaction.
Robinhood Chain
↓
Pons Events
↓
Indexer
↓
Normalizer
↓
Token Registry
↓
Market State
↓
Filters
↓
Opportunities
↓
API / Alerts
ROBINHOOD CHAIN
↓
PONS CONTRACTS
↓
EVENT INGESTION
↓
┌───────────────────────┐
│ Protocol Adapters │
│ │
│ Pons V1 │
│ Pons V2 │
└───────────┬───────────┘
↓
EVENT DECODER
↓
TOKEN REGISTRY
↓
METADATA ENRICHMENT
↓
MARKET STATE ENGINE
↓
FILTER ENGINE
↓
OPPORTUNITY STATE
↓
┌──────────────┼──────────────┐
↓ ↓ ↓
Alerts API Dashboard-ready data
V1 and V2 stay behind adapters. The rest of the application uses normalized launches, phases, and trades.
Pons V2 is the current launch protocol: a token is created, traded on its own bonding curve, swept, and graduated into a Uniswap v4 pool. The factory used here is 0x7eD598BcEf8bd9Edd8C97A195C6d13f40801EC7e. Event and read ABIs follow the current V2 builder docs at docs.ponsfamily.com/v2. Curve trade amounts are taken from CurveBuy and CurveSell, not from the quantity a caller requested.
V2 market phase is read from getLaunchedToken on the factory:
| Contract phase | Scanner phase | Meaning |
|---|---|---|
| 0 NotGraduated | CURVE |
Still trading on the bonding curve |
| 1 Swept | SWEPT |
Curve closed, pool not created yet |
| 2 PoolCreated | POOL |
Trading on the Uniswap v4 pool |
| 3 Rescued | RESCUED |
Recovery path |
A V2 pool does not have its own contract address. The scanner stores a derived Uniswap v4 pool id when the launch record supplies fee, tick spacing, and the configured hook. quoteReserve, realQuoteReserve, tokenReserve, and sellableTokens are raw curve reads. The spot ratio is labeled display-only. It is not an executable quote and it is not a USD price.
Pons V1 is a separate factory (0xA5aAb3F0c6EeadF30Ef1D3Eb997108E976351feB). Its TokenLaunched event creates a Uniswap V3 pool immediately and does not emit V2 curve events. The V1 adapter decodes that event and reads getLaunchedToken. It does not invent curve reserves.
LaunchGraduationRescued is recognized by its verified topic so reconciliation can refresh contract state. Its argument names are not in the official V2 docs, so those fields are not decoded into domain data.
- Resumable historical backfill in configurable block chunks
- WebSocket live listener, with HTTP polling when
WS_URLis empty - Reconnect overlap and idempotent replay
- V1 and V2 protocol adapters
- Token identity as
chainId + address - Event identity as
chainId + transactionHash + logIndex - ERC-20 and Pons metadata, best-effort
- V2 curve buys, sells, refunds, completion, and factory sweep/graduation events
- Filter engine and opportunity states
DETECTED,WATCHING,ELIGIBLE,REJECTED,EXPIRED - Optional webhook alerts
- REST API, health, and indexer status
- Lightweight reorg repair and recent-window reconciliation
- Structured Pino logs and in-process counters
- Foundry fixtures for the V2 events the TypeScript decoder understands
Node.js, TypeScript, viem, PostgreSQL, Drizzle ORM, Fastify, Zod, Pino, Vitest, Foundry.
pnpm install
cp .env.example .env
docker compose up -d postgres
pnpm db:migrate
pnpm demo
pnpm scannerpnpm demo only needs an RPC. It prints the chain id, checks the V2 factory code, and decodes recent TokenLaunched logs. It does not write to the database and it does not send transactions.
The public RPC in .env.example is for local development. Production should set RPC_URL and WS_URL to a dedicated provider.
See .env.example.
| Variable | Purpose |
|---|---|
RPC_URL |
HTTP RPC. Required. |
WS_URL |
WebSocket RPC. Empty disables live push and keeps polling. |
DATABASE_URL |
PostgreSQL connection string. |
PONS_V2_FACTORY |
V2 factory. Invalid addresses fail startup. |
PONS_V1_FACTORY / ENABLE_V1 |
Optional V1 factory indexing. |
START_BLOCK |
First block for a fresh backfill. Empty skips history and starts at the head. |
BACKFILL_CHUNK_SIZE |
Inclusive log-query window. |
CONFIRMATION_BLOCKS |
Blocks left unindexed at the tip. |
RECONNECT_BACKFILL_BLOCKS |
Overlap replayed after a websocket reconnect. |
MODE |
BACKFILL, LIVE, or FULL (default). |
ALERT_WEBHOOK_URL |
Optional. The scanner runs without it. |
ALLOWED_PAIR_TOKENS, BLOCKED_DEPLOYERS, ALLOWED_DEPLOYERS, MIN_TOKEN_AGE_SECONDS |
Filter rules. Empty lists do not restrict. |
RUN_MIGRATIONS |
Apply SQL migrations on startup when true. |
Native ETH as a quote asset is 0x0000000000000000000000000000000000000000.
PRIVATE_KEY and mnemonics are ignored. Startup logs a warning if they are present.
docker compose up -d postgres
pnpm db:migratepnpm db:generate writes Drizzle Kit review artifacts to drizzle/. The migration the app applies is src/db/migrations/0001_init.sql.
pnpm backfillThe worker loads its cursor, requests BACKFILL_CHUNK_SIZE blocks at a time, decodes logs, commits token state, then advances the cursor. Restarting resumes from the last committed block. A second pass over the same range does not insert duplicate events.
Backfill:
from=2100000
to=2101999
events=17
durationMs=843
Set START_BLOCK before the first run if you want history. Without it, a fresh database starts at the confirmed head.
pnpm dev
pnpm scannerMODE=FULL backfills through the confirmed head, then polls. If WS_URL is set, a websocket subscription wakes the poller. On disconnect the listener replays RECONNECT_BACKFILL_BLOCKS and deduplicates. If WS_URL is empty, it logs that realtime monitoring is disabled and keeps polling.
MODE=BACKFILL indexes history and serves the API. MODE=LIVE does not walk back to START_BLOCK; it follows the head and still replays the overlap window.
Shutdown on SIGINT or SIGTERM stops listeners, finishes the current transaction boundary, persists the cursor, and closes the database.
GET /health
GET /tokens?version=V2&phase=CURVE
GET /tokens/:address
GET /tokens/:address/trades?limit=100
GET /launches/recent
GET /opportunities?state=ELIGIBLE
GET /markets/:address
GET /indexer/status
Responses are normalized records. uint256 values are decimal strings, not JavaScript numbers.
GET /health returns 503 when the database or RPC check fails.
GET /markets/:address includes curve reserves and, when both reserves are present, an informational spot ratio labeled as display-only.
Robinhood Chain
Chain ID: 4663
Pons Scanner
Version: V2
Factory: 0x7eD5...
Recent launches:
----------------------------------------
XYZ
Token: 0x...
Curve: 0x...
Pair: 0x...
Phase: CURVE
State: WATCHING
----------------------------------------
A launch with contract phase, metadata, and curve reserves can move to ELIGIBLE. Nothing is traded when that happens.
pnpm test
pnpm typecheck
pnpm lint
pnpm buildVitest covers identity, ABI topic hashes, launch and trade decoding, phase mapping, filters, opportunity transitions, cursor and reconnect math, duplicate suppression, backfill resume, metadata and curve read failures, transaction rollback, and the HTTP API. Integration tests apply the SQL migration to embedded PostgreSQL (PGlite).
contracts/mocks/PonsEventFixtures.sol is a test fixture. It is not the Pons protocol and must not be deployed.
forge install foundry-rs/forge-std
pnpm test:foundryThe Foundry test records TokenLaunched, CurveBuy, and CurveSell and checks their topic0 values against the same signatures the TypeScript decoder uses.
- Point
RPC_URLandWS_URLat a provider with a high enougheth_getLogsrange forBACKFILL_CHUNK_SIZE. - Leave the public RPC for local development.
- Run one scanner writer per database. Event identity is unique, but two writers will contend.
CONFIRMATION_BLOCKSshould stay above zero on mainnet.- Reconciliation rereads a configurable window of recent launches. It does not rescan the full history.
- Metrics counters live in the process and are copied onto the indexer cursor. A Prometheus exporter is not bundled.
- The scanner container runs as a non-root user.
- no private keys required
- read-only runtime
- no trade execution
- no custody
- environment variables for credentials
- explicit RPC configuration
- validated contract addresses
- database uniqueness constraints
- idempotent event processing
There is no walletClient, writeContract, or sendTransaction on the scanner path.
- Solidity / EVM
- Foundry testing
- TypeScript
- viem
- PostgreSQL
- Event indexing
- Protocol adapters
- Historical backfill
- WebSocket monitoring
- Idempotent event processing
- Reconciliation
- REST APIs
- Structured logging
Related Pons Projects
Pons Sniper Bot https://github.com/0xhamssog/pons-sniper-bot
Pons Bundler https://github.com/0xhamssog/pons-bundler
Stock Token Arbitrage Bot https://github.com/0xhamssog/robinhood-stock-token-arbitrage-bot
These are related trading systems. This scanner does not import them and does not execute their strategies. It is the read-only indexing layer underneath that kind of product: launch monitoring, market phase, and opportunity state, without a wallet.
- Prometheus scrape endpoint for the existing counters
- V1 Uniswap V3 pool state, only from a documented position read
- Decode
LaunchGraduationRescuedfields once parameter names are published in the official docs - Archive
eth_getLogspagination hints for providers that cap responses below the configured chunk size
pnpm dev
pnpm build
pnpm test
pnpm test:watch
pnpm typecheck
pnpm lint
pnpm db:migrate
pnpm db:generate
pnpm backfill
pnpm scanner
pnpm demo