Historical Replay
Starting from April 18, 2026, we save everything our Data Stream emits, every hour. Perfect for backtesting strategies, replaying market conditions, research, or anything else you can think of.
How it works
Every hour we flush all incoming events to a compressed archive named after the UTC hour it covers:
https://replay.pumpapi.io/YEAR/MONTH/DAY/HOUR.jsonl.zst
For example, the events between 2026-04-18 00:00:00 UTC and 2026-04-18 01:00:00 UTC live at:
https://replay.pumpapi.io/2026/04/18/01.jsonl.zst
We use zstandard (zst) compression so downloads are as fast as possible:
- ~400 MB per hour compressed
- ~2 GB in memory after decompression
- ~1 second to decompress one hour
Each line in the decompressed file is a single JSON event — the exact same format you get from the live Data Stream.
Browsing available archives
You can browse what's saved directly in your browser:
https://replay.pumpapi.io/2026/— list all months in 2026https://replay.pumpapi.io/2026/04/— list all days in April 2026https://replay.pumpapi.io/2026/04/18/— list all hourly files for April 18
Example: Replay the last N hours
The snippets below take a HOURS variable and stream every event from that window. For example, if it's currently 12:45 UTC and HOURS = 2, you'll replay every event between 10:00 and 12:00 UTC.
- Python
- JavaScript
- Rust
- Go
import asyncio
from datetime import datetime, timezone, timedelta
import orjson as json # or use the standard json module (orjson is faster)
import aiohttp
import zstandard as zstd
HOURS = 2 # last 2 hours
ALLOW_GAPS = False
async def fetch(session, hour_dt):
url = f"https://replay.pumpapi.io/{hour_dt:%Y/%m/%d/%H}.jsonl.zst"
print(f"[fetch] {url}")
async with session.get(url) as r:
if r.status == 404:
if not ALLOW_GAPS:
raise RuntimeError(f"missing: {url}")
return None
r.raise_for_status()
return await r.read()
async def main():
now = datetime.now(timezone.utc).replace(minute=0, second=0, microsecond=0)
hours = [now - timedelta(hours=i) for i in range(HOURS, 0, -1)]
dctx = zstd.ZstdDecompressor()
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout()) as session:
for hour_dt in hours:
compressed = await fetch(session, hour_dt)
if compressed is None:
continue
print('downloaded')
decompressed = dctx.decompress(compressed)
print('decompressed')
for line in decompressed.splitlines():
event = json.loads(line.decode())
print(event)
if __name__ == "__main__":
asyncio.run(main())
import { ZstdInit } from '@oneidentity/zstd-js';
const HOURS = 2; // last 2 hours
const ALLOW_GAPS = false;
async function fetchHour(hourDt) {
const y = hourDt.getUTCFullYear();
const m = String(hourDt.getUTCMonth() + 1).padStart(2, '0');
const d = String(hourDt.getUTCDate()).padStart(2, '0');
const h = String(hourDt.getUTCHours()).padStart(2, '0');
const url = `https://replay.pumpapi.io/${y}/${m}/${d}/${h}.jsonl.zst`;
console.log(`[fetch] ${url}`);
const res = await fetch(url);
if (res.status === 404) {
if (!ALLOW_GAPS) throw new Error(`missing: ${url}`);
return null;
}
if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
return new Uint8Array(await res.arrayBuffer());
}
async function main() {
const { ZstdSimple } = await ZstdInit();
const now = new Date();
now.setUTCMinutes(0, 0, 0);
const hours = [];
for (let i = HOURS; i > 0; i--) {
hours.push(new Date(now.getTime() - i * 3600 * 1000));
}
const decoder = new TextDecoder();
for (const hourDt of hours) {
const compressed = await fetchHour(hourDt);
if (compressed === null) continue;
console.log('downloaded');
const decompressed = ZstdSimple.decompress(compressed);
console.log('decompressed');
const text = decoder.decode(decompressed);
for (const line of text.split('\n')) {
if (!line) continue;
const event = JSON.parse(line);
console.log(event);
}
}
}
main().catch(console.error);
use chrono::{Duration, Timelike, Utc};
use reqwest::Client;
use std::io::{BufRead, BufReader};
use zstd::stream::read::Decoder;
const HOURS: i64 = 2; // last 2 hours
const ALLOW_GAPS: bool = false;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let now = Utc::now()
.with_minute(0).unwrap()
.with_second(0).unwrap()
.with_nanosecond(0).unwrap();
let hours: Vec<_> = (1..=HOURS)
.rev()
.map(|i| now - Duration::hours(i))
.collect();
for hour_dt in hours {
let url = format!(
"https://replay.pumpapi.io/{}.jsonl.zst",
hour_dt.format("%Y/%m/%d/%H")
);
println!("[fetch] {}", url);
let res = client.get(&url).send().await?;
if res.status().as_u16() == 404 {
if !ALLOW_GAPS {
return Err(format!("missing: {}", url).into());
}
continue;
}
let compressed = res.error_for_status()?.bytes().await?;
println!("downloaded");
let decoder = Decoder::new(&compressed[..])?;
let reader = BufReader::new(decoder);
println!("decompressed");
for line in reader.lines() {
let line = line?;
if line.is_empty() {
continue;
}
let event: serde_json::Value = serde_json::from_str(&line)?;
println!("{}", event);
}
}
Ok(())
}
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/klauspost/compress/zstd"
)
const (
HOURS = 2 // last 2 hours
ALLOW_GAPS = false
)
func fetchHour(hourDt time.Time) ([]byte, error) {
url := fmt.Sprintf(
"https://replay.pumpapi.io/%s.jsonl.zst",
hourDt.Format("2006/01/02/15"),
)
fmt.Printf("[fetch] %s\n", url)
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
if !ALLOW_GAPS {
return nil, fmt.Errorf("missing: %s", url)
}
return nil, nil
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, url)
}
return io.ReadAll(resp.Body)
}
func main() {
now := time.Now().UTC().Truncate(time.Hour)
var hours []time.Time
for i := HOURS; i > 0; i-- {
hours = append(hours, now.Add(-time.Duration(i)*time.Hour))
}
for _, hourDt := range hours {
compressed, err := fetchHour(hourDt)
if err != nil {
panic(err)
}
if compressed == nil {
continue
}
fmt.Println("downloaded")
decoder, err := zstd.NewReader(bytes.NewReader(compressed))
if err != nil {
panic(err)
}
fmt.Println("decompressed")
scanner := bufio.NewScanner(decoder)
scanner.Buffer(make([]byte, 1024*1024), 64*1024*1024)
for scanner.Scan() {
var event map[string]interface{}
if err := json.Unmarshal(scanner.Bytes(), &event); err != nil {
panic(err)
}
fmt.Println(event)
}
decoder.Close()
}
}
When you backtest on replay data, remember that a huge portion of transactions on AMMs like pump.fun are sent through Jito Bundles. A bundle must be treated as one big atomic transaction — you cannot insert your own transaction in the middle of one.
Bundles are not explicitly labeled in the event stream, but you can detect them heuristically:
- Every event has a millisecond-precision timestamp. Transactions inside the same bundle are executed simultaneously, so their timestamps are almost identical — typically within 1–3 ms of each other.
- Bundled transactions usually interact with the same token.
A solid rule of thumb: treat any group of transactions that hit the same token within ~3 ms as a single bundled event. For certainty, you can cross-check any suspicious cluster on https://explorer.jito.wtf/.
Also, add a slightly larger-than-usual latency buffer to your simulation. Real execution will always be a bit slower than replay, and being conservative here prevents your backtest from looking more profitable than reality.
localTimestampIn the replay archives you'll encounter an extra field that does not exist in the live Data Stream: localTimestamp. It's the time at which our replay server in Frankfurt am Main (Germany) received the transaction — which may be slightly later than what you'd observe on your end in real time. You generally don't need it for backtesting purposes, but it can be useful for checking whether your own server had good latency at a given moment.
Need help? Join our Telegram group.