Index pump.fun events at scale
When to use
- Building a dashboard, analytics platform, or trading bot that needs more than one-shot event handling.
- Existing public-RPC
logsSubscribe is dropping events under load.
- The user wants historical replay (e.g., "all trades for mint X in the last 30 days").
- The user wants a denormalised table they can SQL against.
Ingestion choice (decision table)
| Volume |
Recommendation |
Why |
| 1 coin, hobby |
Public RPC logsSubscribe (existing LaunchMonitor) |
Free; reliability is fine for one watcher |
| 5–10 coins |
Paid RPC |
More connections, better buffering |
| All launches, real-time |
Geyser (Triton Yellowstone, Helius) |
Lowest latency; raw account writes |
| Analytics + history |
Helius enhanced webhooks → Postgres |
Easiest ops; supports replay |
| Self-hosted production |
Geyser → Kafka → Postgres + S3 archive |
Full sovereignty |
See tutorials/54-indexing-v2-events.md for client code per approach.
Schema starter
CREATE TABLE pump_trades (
signature TEXT NOT NULL,
ix_index INT NOT NULL,
slot BIGINT NOT NULL,
block_time TIMESTAMPTZ NOT NULL,
mint TEXT NOT NULL,
quote_mint TEXT NOT NULL,
side TEXT CHECK (side IN ('buy', 'sell')),
user_pubkey TEXT NOT NULL,
quote_amount NUMERIC(38, 0) NOT NULL,
token_amount NUMERIC(38, 0) NOT NULL,
fee_quote NUMERIC(38, 0) DEFAULT 0,
is_v2 BOOLEAN NOT NULL,
PRIMARY KEY (signature, ix_index)
);
CREATE INDEX idx_pump_trades_mint_time ON pump_trades (mint, block_time DESC);
CREATE INDEX idx_pump_trades_user ON pump_trades (user_pubkey, block_time DESC);
CREATE INDEX idx_pump_trades_quote ON pump_trades (quote_mint, block_time DESC);
CREATE TABLE pump_launches (
signature TEXT PRIMARY KEY,
slot BIGINT NOT NULL,
block_time TIMESTAMPTZ NOT NULL,
mint TEXT NOT NULL UNIQUE,
creator TEXT NOT NULL,
quote_mint TEXT NOT NULL,
name TEXT NOT NULL,
symbol TEXT NOT NULL,
uri TEXT NOT NULL,
is_v2 BOOLEAN NOT NULL,
creator_fee_shares JSONB
);
CREATE INDEX idx_pump_launches_creator ON pump_launches (creator, block_time DESC);
CREATE INDEX idx_pump_launches_quote ON pump_launches (quote_mint, block_time DESC);
Deduplication
Dedup on (signature, ix_index). Postgres INSERT ... ON CONFLICT DO NOTHING is the cheapest way:
INSERT INTO pump_trades (signature, ix_index, slot, ...)
VALUES ($1, $2, $3, ...)
ON CONFLICT (signature, ix_index) DO NOTHING;
In-memory LRU dedup for hot path before hitting Postgres:
import { LRUCache } from 'lru-cache';
const seen = new LRUCache<string, boolean>({ max: 100_000 });
Backfill
import { Connection, PublicKey } from '@solana/web3.js';
async function backfill(connection: Connection, programId: PublicKey, beforeSig?: string) {
let cursor = beforeSig;
while (true) {
const batch = await connection.getSignaturesForAddress(programId, { before: cursor, limit: 1000 });
if (batch.length === 0) break;
for (const { signature } of batch) {
const tx = await connection.getTransaction(signature, { maxSupportedTransactionVersion: 0 });
if (!tx) continue;
// decode + insert with ON CONFLICT DO NOTHING
}
cursor = batch[batch.length - 1].signature;
}
}
Notes:
getTransaction is rate-limited on most paid RPCs. Batch + sleep.
- Skip the backfill entirely if you can — use the live stream from t=now and accept that you don't have history.
- For partial backfills, persist a
last_backfilled_slot watermark and resume from there on next run.
Schema migrations as the protocol evolves
Pump.fun's event shapes change. Plan for it:
- Don't model every event field as a column. Capture the raw payload in a
JSONB column too, so future migrations can re-decode from raw.
- Do add an
event_schema_version INT column. Bump when the SDK adds a new field you care about.
- Don't silently drop unknown fields. Log them, alert if unknown-field rate > 1%.
Cost guardrails
Indexers love eating money. Limits to add at design time:
- Rate limit
getTransaction calls per minute. Most providers cap somewhere.
- Cap Kafka topic retention to N days (cheaper than infinite).
- TTL old raw-payload rows to S3 archive after N days; keep aggregates indefinitely.
- Add a
cost_per_event_micro_usd metric so you notice if a provider price change blows up your bill.
Common mistakes
- No primary key. You'll have duplicates within a week.
- JSON-only payload, no structured columns. Querying becomes unbearable.
- Single-row inserts. Use
COPY or batched inserts; per-row inserts cap at ~1k/s.
- No backfill watermark. You restart the indexer and re-process the last 30 days.
- Subscribing to the whole program for one coin. If you only care about 5 coins, subscribe to those mints, not the program.
- Ignoring V1 vs V2. Add
is_v2 from day one. Adding it later requires backfill.
Avoid
- Sharing your Geyser auth token in commits, logs, or Telegram.
- Letting the indexer write directly to a production analytics DB without a staging table — protocol-shape changes can poison your aggregates.
- Treating the indexer as the source of truth for trading decisions. The chain is the source of truth; the indexer is a derived view.
See also
1---2name: index-pump-events3description: Use this skill when designing or running a pump.fun event indexer — choosing between RPC websockets, paid RPC, Geyser, or webhook indexers; designing the schema; deduplication; backfill; cost/latency tradeoffs. Triggers on "index pump events", "indexer schema", "Geyser stream", "backfill pump trades", "Helius webhooks pump".4---56# Index pump.fun events at scale78## When to use910- Building a dashboard, analytics platform, or trading bot that needs more than one-shot event handling.11- Existing public-RPC `logsSubscribe` is dropping events under load.12- The user wants historical replay (e.g., "all trades for mint X in the last 30 days").13- The user wants a denormalised table they can SQL against.1415## Ingestion choice (decision table)1617| Volume | Recommendation | Why |18|---|---|---|19| 1 coin, hobby | Public RPC `logsSubscribe` (existing `LaunchMonitor`) | Free; reliability is fine for one watcher |20| 5–10 coins | Paid RPC | More connections, better buffering |21| All launches, real-time | Geyser (Triton Yellowstone, Helius) | Lowest latency; raw account writes |22| Analytics + history | Helius enhanced webhooks → Postgres | Easiest ops; supports replay |23| Self-hosted production | Geyser → Kafka → Postgres + S3 archive | Full sovereignty |2425See [tutorials/54-indexing-v2-events.md](../../../tutorials/54-indexing-v2-events.md) for client code per approach.2627## Schema starter2829```sql30CREATE TABLE pump_trades (31 signature TEXT NOT NULL,32 ix_index INT NOT NULL,33 slot BIGINT NOT NULL,34 block_time TIMESTAMPTZ NOT NULL,35 mint TEXT NOT NULL,36 quote_mint TEXT NOT NULL,37 side TEXT CHECK (side IN ('buy', 'sell')),38 user_pubkey TEXT NOT NULL,39 quote_amount NUMERIC(38, 0) NOT NULL,40 token_amount NUMERIC(38, 0) NOT NULL,41 fee_quote NUMERIC(38, 0) DEFAULT 0,42 is_v2 BOOLEAN NOT NULL,43 PRIMARY KEY (signature, ix_index)44);4546CREATE INDEX idx_pump_trades_mint_time ON pump_trades (mint, block_time DESC);47CREATE INDEX idx_pump_trades_user ON pump_trades (user_pubkey, block_time DESC);48CREATE INDEX idx_pump_trades_quote ON pump_trades (quote_mint, block_time DESC);4950CREATE TABLE pump_launches (51 signature TEXT PRIMARY KEY,52 slot BIGINT NOT NULL,53 block_time TIMESTAMPTZ NOT NULL,54 mint TEXT NOT NULL UNIQUE,55 creator TEXT NOT NULL,56 quote_mint TEXT NOT NULL,57 name TEXT NOT NULL,58 symbol TEXT NOT NULL,59 uri TEXT NOT NULL,60 is_v2 BOOLEAN NOT NULL,61 creator_fee_shares JSONB62);6364CREATE INDEX idx_pump_launches_creator ON pump_launches (creator, block_time DESC);65CREATE INDEX idx_pump_launches_quote ON pump_launches (quote_mint, block_time DESC);66```6768## Deduplication6970Dedup on `(signature, ix_index)`. Postgres `INSERT ... ON CONFLICT DO NOTHING` is the cheapest way:7172```sql73INSERT INTO pump_trades (signature, ix_index, slot, ...)74VALUES ($1, $2, $3, ...)75ON CONFLICT (signature, ix_index) DO NOTHING;76```7778In-memory LRU dedup for hot path before hitting Postgres:7980```typescript81import { LRUCache } from 'lru-cache';82const seen = new LRUCache<string, boolean>({ max: 100_000 });83```8485## Backfill8687```typescript88import { Connection, PublicKey } from '@solana/web3.js';8990async function backfill(connection: Connection, programId: PublicKey, beforeSig?: string) {91 let cursor = beforeSig;92 while (true) {93 const batch = await connection.getSignaturesForAddress(programId, { before: cursor, limit: 1000 });94 if (batch.length === 0) break;95 for (const { signature } of batch) {96 const tx = await connection.getTransaction(signature, { maxSupportedTransactionVersion: 0 });97 if (!tx) continue;98 // decode + insert with ON CONFLICT DO NOTHING99 }100 cursor = batch[batch.length - 1].signature;101 }102}103```104105**Notes:**106- `getTransaction` is rate-limited on most paid RPCs. Batch + sleep.107- Skip the backfill entirely if you can — use the live stream from t=now and accept that you don't have history.108- For partial backfills, persist a `last_backfilled_slot` watermark and resume from there on next run.109110## Schema migrations as the protocol evolves111112Pump.fun's event shapes change. Plan for it:113114- **Don't** model every event field as a column. Capture the raw payload in a `JSONB` column too, so future migrations can re-decode from raw.115- **Do** add an `event_schema_version INT` column. Bump when the SDK adds a new field you care about.116- **Don't** silently drop unknown fields. Log them, alert if unknown-field rate > 1%.117118## Cost guardrails119120Indexers love eating money. Limits to add at design time:121122- Rate limit `getTransaction` calls per minute. Most providers cap somewhere.123- Cap Kafka topic retention to N days (cheaper than infinite).124- TTL old raw-payload rows to S3 archive after N days; keep aggregates indefinitely.125- Add a `cost_per_event_micro_usd` metric so you notice if a provider price change blows up your bill.126127## Common mistakes128129- **No primary key.** You'll have duplicates within a week.130- **JSON-only payload, no structured columns.** Querying becomes unbearable.131- **Single-row inserts.** Use `COPY` or batched inserts; per-row inserts cap at ~1k/s.132- **No backfill watermark.** You restart the indexer and re-process the last 30 days.133- **Subscribing to the whole program for one coin.** If you only care about 5 coins, subscribe to those mints, not the program.134- **Ignoring V1 vs V2.** Add `is_v2` from day one. Adding it later requires backfill.135136## Avoid137138- Sharing your Geyser auth token in commits, logs, or Telegram.139- Letting the indexer write directly to a production analytics DB without a staging table — protocol-shape changes can poison your aggregates.140- Treating the indexer as the source of truth for trading decisions. The chain is the source of truth; the indexer is a derived view.141142## See also143144- [tutorials/54-indexing-v2-events.md](../../../tutorials/54-indexing-v2-events.md)145- [.claude/skills/monitor-usdc-pairs/SKILL.md](../monitor-usdc-pairs/SKILL.md)146- [docs/events-reference.md](../../../docs/events-reference.md)147- [packages/core/src/monitor/](../../../packages/core/src/monitor/)