Historical data API

Hourly Parquet files for pump_fun (the bonding curve) and pump_amm (PumpSwap). Every file is one venue, one UTC hour, every event.

Not public yet: the site is still being tested. The routes and columns below are v1.

1. Get an API key

Sign in on the account page with your Solana wallet and create a key. It is shown once, so store it safely. Send it on every request as X-API-Key. GET /v1/account shows your balance and how far back your credits reach.

2. Check what's available

Free. The first and last day on file, how many hour-files exist, and how fresh the newest one is (example answer):

GET https://api.dataonchain.io/v1/range?exchange=pump_fun
X-API-Key: doc_live_...

{ "exchange": "pump_fun", "start": "2026-07-29", "end": "2026-10-31",
  "files": 2280, "bytes": 57000000000, "first_hour": "2026-07-29T00:00Z",
  "newest_hour": "2026-10-31T23:00Z", "lag_seconds": 690,
  "statuses": { "complete": 2250, "healed": 28, "partial": 2 } }

GET /v1/files?exchange=pump_fun&date=2026-10-28 lists that day's 24 hours: status, rows, bytes, SHA-256, whether you already own each hour, what it would cost you (0 or 1 credit) and whether your credits reach it.

3. Download an hour

GET /v1/download?exchange=<pump_fun|pump_amm>&date=YYYY-MM-DD&hour=HH answers with a redirect to the file: a link that works for 5 minutes. The first download of an hour costs 1 credit; downloading an hour you already own is free, forever. Refused requests are never charged.

curl -H "X-API-Key: doc_live_..." -L \
  "https://api.dataonchain.io/v1/download?exchange=pump_amm&date=2026-10-28&hour=12" \
  -o pump_amm_2026-10-28_12.parquet

Add redirect=false to get the link as JSON instead: url, expires_at, sha256, bytes, rows, status, charged and your balance. When an HTTP client follows a redirect it usually sends your X-API-Key header on to the storage host as well; with the JSON form your key never leaves our API. The redirect also carries X-Credits-Charged, X-Credits-Balance and X-File-Sha256 headers.

The same calls also answer without the /v1 prefix (/range, /download), so scripts written for that common shape work after changing the host and key.

4. Download many hours

POST /v1/bulk takes a date range and answers with one link per hour. It counts as a single request against your rate limit, however many hours you ask for.

POST https://api.dataonchain.io/v1/bulk
X-API-Key: doc_live_...
Content-Type: application/json

{ "exchange": "pump_fun", "from": "2026-11-01", "to": "2026-11-07" }
  • from and to are UTC and both included. A date (2026-11-01) means the whole day; add an hour (2026-11-01T07) for one hour. At most 744 hours (31 days) per request.
  • 1 credit per hour you do not own yet. Hours you already own come back free, with a fresh link (as long as the whole request can be paid for — see below).
  • dry_run: true gives you the whole plan and its cost and charges nothing.
{
  "exchange": "pump_fun", "from": "2026-11-01T00", "to": "2026-11-07T23",
  "hours_requested": 168, "hours_delivered": 166, "owned_already": 2,
  "cost": 164, "charged": 164, "balance": 836,
  "expires_at": "2026-11-11T12:15:00Z", "expires_in": 900,
  "files": [
    { "date": "2026-11-01", "hour": 0, "status": "complete",
      "rows": 184203, "bytes": 24117248,
      "sha256": "9f1c...", "notes": [],
      "owned_before": false, "charged": true, "cost": 1,
      "url": "https://...", "expires_at": "2026-11-11T12:15:00Z" }
  ],
  "skipped": [
    { "date": "2026-11-03", "hour": 4, "status": "missing",
      "reason": "there is no file for that hour", "notes": [] }
  ]
}

Every hour you asked for is in exactly one of the two lists, so you can always see what you got and what you paid. files holds the hours you can download (with charged and cost per hour); skipped holds the rest, each with a plain reason: there is no file for that hour, the hour has not ended yet, it is not for sale, it is being updated right now, or it is older than your credits reach.

Nothing is charged unless the whole request can be paid for. If your balance is smaller than the number of new hours, the answer is 402 with needed and have and not one credit moves — so a big range is never half bought. Otherwise all the new hours are charged together, in one go. Hours that are not in files are never charged.

That 402 refuses the whole request, including the hours of it you have already paid for, because one answer carries one set of links. It tells you how many those are (owned_already): ask for them on their own and they come back free, whatever your balance.

{ "exchange": "pump_fun", "from": "2026-11-01", "to": "2026-11-07", "dry_run": true }

# The same answer, with "dry_run": true, "charged": 0, no links, and
# "cost" = what the real request would cost. Nothing is charged.

Bulk links live 15 minutes (one file's link lives 5). A big range takes longer than that, so ask again for anything whose link ran out: hours you already own are free, as often as you like.

Python example

import hashlib
import os

import requests

API_KEY = os.environ["DATAONCHAIN_API_KEY"]
BASE = "https://api.dataonchain.io/v1"
HEADERS = {"X-API-Key": API_KEY}

# What's available (free)
print(requests.get(f"{BASE}/range", params={"exchange": "pump_fun"}, headers=HEADERS).json())


def download(exchange, date, hour):
    """1 credit the first time, free after that. Returns the file name, or None if there is no file."""
    r = requests.get(
        f"{BASE}/download",
        params={"exchange": exchange, "date": date, "hour": hour, "redirect": "false"},
        headers=HEADERS,
    )
    if r.status_code == 404:
        return None  # no file for that hour; nothing was charged
    r.raise_for_status()
    link = r.json()
    data = requests.get(link["url"]).content  # no key here: the link itself is the permission
    if hashlib.sha256(data).hexdigest() != link["sha256"]:
        raise ValueError("checksum mismatch: download it again (free, you own it now)")
    name = f"{exchange}_{date}_{hour:02d}.parquet"
    with open(name, "wb") as f:
        f.write(data)
    return name


# A whole day
for hour in range(24):
    download("pump_fun", "2026-10-28", hour)

Python package and command line

There is a small package that does all of the above for you: it downloads a date range in bulk requests, checks every file against the SHA-256 we published before keeping it, picks up where it left off after a crash, and never asks for — or pays for — an hour that is already on your disk (no request even spans one, because a range is charged for every hour inside it). It has no dependencies (Python 3.9 or newer), so it installs anywhere your work already lives.

Not on PyPI yet: install it from the client/ folder of the website's repository. The package, the import and the command are named after the brand, so they will change with it.
# Until it is on PyPI, install it from the website's repo:
pip install ./client

export DATAONCHAIN_API_KEY=doc_live_...          # Windows: $env:DATAONCHAIN_API_KEY = "..."
export DATAONCHAIN_BASE_URL=https://api.dataonchain.io

dataonchain account                             # credits, and how far back they reach
dataonchain range pump_fun                      # first and newest hour on file
dataonchain files pump_fun 2026-11-09            # one day, hour by hour

# A date range. --dry-run shows the cost and downloads nothing.
dataonchain download pump_fun 2026-11-01 2026-11-07 --out ./data --dry-run
# Without --dry-run it shows the cost, asks before spending, then downloads:
dataonchain download pump_fun 2026-11-01 2026-11-07 --out ./data --workers 4
# A script or scheduled job has nobody to ask: say yes in advance.
dataonchain download pump_fun 2026-11-01 2026-11-07 --out ./data --yes

There is deliberately no --api-key option: a key typed on a command line ends up in your shell history and in the list of running processes. Use the environment variable, or --api-key-file.

download shows what the range costs and asks before it spends a credit. With no terminal to ask on (a pipe, a scheduled job) it stops before any charge unless you add --yes, and a plan that costs more than your balance stops before any charge too. The download then asks for exactly the hours the plan priced, so it never costs more than the price you agreed to. A run that costs nothing does not ask.

Two switches worth knowing: --verify hashes the files already on your disk against the SHA-256 we give and fetches again only the ones that differ (hours you already own are free), which catches a file some other tool changed while leaving its size and timestamp alone; and --quiet prints the result only. Both work before or after the subcommand.

Files land in one folder per exchange and day, with a small record beside each one:

data/pump_fun/2026-11-09/07.parquet
data/pump_fun/2026-11-09/07.parquet.json   <- its SHA-256, so a later run can skip it

From Python:

from dataonchain import Client

c = Client()   # key from DATAONCHAIN_API_KEY, host from DATAONCHAIN_BASE_URL

# What a range would cost, charging nothing:
plan = c.download_range("pump_fun", "2026-11-01", "2026-11-07", "./data", dry_run=True)
print(plan.cost, "credits")

# Then do it, four files at a time:
got = c.download_range("pump_fun", "2026-11-01", "2026-11-07", "./data", workers=4)
print(len(got.downloaded), "files,", got.charged, "credits, balance", got.balance)
for missing in got.skipped:
    print("not for sale:", missing.date, missing.hour, missing.reason)

What it does for your money:

  • Your key never reaches the storage host: links are always asked for as JSON, and redirects are never followed.
  • A file is streamed to .part, hashed as it is written, and renamed into place only when its length and its SHA-256 both match. A mismatch leaves nothing behind.
  • Run the same range again and it downloads nothing: finished hours are recognised from the record beside them, with no request at all, so they cannot be charged for twice.
  • A crash costs at most the file it was in the middle of: the next run asks the storage host to carry on from that byte.
  • 429 and 5xx are waited out, honouring Retry-After. 402 and 403 stop at once — asking again would only cost you.

Query the folder with DuckDB

import duckdb

# The whole folder in one query - no loading step.
duckdb.sql("""
    SELECT token_mint, count(*) AS trades, sum(lamports_amount) / 1e9 AS sol
    FROM 'data/pump_fun/2026-11-0*/*.parquet'
    WHERE event_type = 'swap'
      AND lamports_mint = 'So11111111111111111111111111111111111111112'  -- priced in SOL
      AND NOT implausible
    GROUP BY 1 ORDER BY sol DESC LIMIT 10
""").show()

Or with Polars

import polars as pl

SOL = "So11111111111111111111111111111111111111112"

# Lazily: only the columns and rows you ask for are read off disk.
# SOL-priced swaps only, none flagged implausible:
week = (
    pl.scan_parquet("data/pump_fun/**/*.parquet")
      .filter((pl.col("event_type") == "swap") & (pl.col("lamports_mint") == SOL)
              & ~pl.col("implausible"))
      .group_by("action")
      .agg(trades=pl.len(), sol=pl.col("lamports_amount").sum() / 1e9)
      .collect()
)
print(week)

Coverage and schema

No key needed.

  • GET /v1/coverage?exchange=pump_fun&from=2026-10-01&to=2026-10-28: every hour's status, day by day (up to 400 days per request; default the last 31 days).
  • GET /v1/schema?exchange=pump_fun: the column list below, as JSON.
  • GET /v1/samples: the free sample files and a download link for each.

Limits and errors

120 requests per minute per key — and one /v1/bulk request counts as one, whether it asks for one hour or 744. The free routes above are counted by address instead: 60 a minute from one address, and a larger shared allowance for requests that reach us through a proxy, which cannot be told apart. Over the limit you get a 429 with a Retry-After header. Errors return JSON with a detail message. Nothing is charged on any error.

400A bad parameter: exchange must be pump_fun or pump_amm, date YYYY-MM-DD (UTC), hour 0 to 23
401Missing, wrong or revoked API key
402Not enough credits
403The hour is older than your credits reach (Recent = last 30 days). Hours you already own stay free
404No file for that hour
429Too many requests: wait the seconds in the Retry-After header
503The files can't be read right now, or that hour is being updated: retry after Retry-After

File format

  • Path: v1/{venue}/{YYYY-MM-DD}/{HH}.parquet, UTC hours, zstd-compressed.
  • Sorted by slot_number, tx_index, outer_ix_index, inner_ix_index: the order the chain executed them.
  • Unique row key: signature + outer_ix_index + inner_ix_index.
  • Amounts are raw integers. Divide by 10^decimals (SOL 9; Pump.fun tokens 6).
  • Empty means unknown: a value we can't know is NULL, never a guessed 0.
  • Inside v1 columns are only ever added. Anything breaking ships as v2.

Every hour has a status:

completeEvery row of the hour, all captured live
healedSome rows were repaired after a capture gap; those rows have no ts_local, everything else is identical
partialA busy hour with minutes that have no rows: some rows are missing
withheldNot for sale: the file failed our checks
missingNo file for that hour
not_yetThe hour hasn't ended yet

Only complete, healed and partial hours can be downloaded.

Schema: pump_fun (bonding curve)

45 columns, in file order.

FieldTypeMeaningEmpty when
event_typestringswap | create | bonding_complete (the curve filled; the coin graduates).never
signaturestringTransaction signature (base58).never
slot_numberint64Solana slot of the block that holds the transaction.never
timestampdoubleBlock time, Unix seconds (whole seconds; the chain's own clock).never
token_mintstringThe coin's mint address.never
token_creatorstringThe coin's creator wallet as recorded in the event.not in the event (bonding_complete rows, implausible rows)
can_be_frozenboolTRUE if the coin's mint still has a freeze authority (the issuer can freeze holders' tokens). NOT FILLED YET: NULL on every row until the mint-safety lookup ships (build step 2).always NULL in v1 so far (not looked up yet)
is_cashback_coinboolTRUE if the coin pays trader cashback (its cashback fee rate is above 0).never
actionstringbuy | sell on swaps.create and bonding_complete rows
user_walletstringThe trader: the event's user field (not necessarily the transaction's fee payer).never
token_amountuint64Coins moved, raw units (coins launched on pump.fun have 6 decimals).never
lamports_amountuint64The trade's quote amount in lamports_mint raw units (SOL: lamports). Buys: what went into the curve (the buyer paid fee_lamports on top). Sells: what the seller received, after fees. On curves quoted in another asset it is that asset's raw units (pumpfundata.com's documented meaning: 'not always SOL, check lamports_mint').unknown: swaps on non-SOL curves before 2026-08-31 ~19:00 UTC (our capture did not record their quote amounts before then); also non-swap rows
fee_lamportsuint64Protocol + creator + buyback fees, lamports_mint raw units. Cashback is not included (see cashback).non-swap rows
lamports_mintstringThe asset the lamports_* columns are counted in. SOL is always written as WSOL (So11111111111111111111111111111111111111112) on both venues; any other value is the quote asset's mint (USDC, tokenized stocks, other coins).never
lamports_decimalint64Decimals of lamports_mint (SOL = 9). Divide a lamports_* value by 10^lamports_decimal for whole units.the quote asset is not SOL: filled by the mint lookup (build step 2)
namestringCoin name (create rows only).never
symbolstringCoin ticker (create rows only).never
uristringCoin metadata URI (create rows only).never
token_total_supplyuint64Total supply at launch, raw units (create rows only).never
is_mayhem_modeboolpump.fun mayhem-mode coin flag (create rows only).never
real_token_reserveuint64The curve's real coin reserve AFTER the trade, raw units.bonding_complete rows; implausible rows
real_lamports_reserveuint64The curve's real quote reserve AFTER the trade, lamports_mint raw units.unknown (same cases as lamports_amount); create and bonding_complete rows
virtual_token_reserveuint64The curve's virtual coin reserve AFTER the trade (the curve prices on virtual reserves).bonding_complete rows; implausible rows
virtual_lamports_reserveuint64The curve's virtual quote reserve AFTER the trade, lamports_mint raw units. On a create row: the curve's starting quote reserve.unknown (same cases as lamports_amount); bonding_complete rows
buyback_feeuint64Buyback fee, lamports_mint raw units.non-swap rows
buyback_fee_basis_pointsint64Buyback fee rate, basis points.non-swap rows
tx_indexint64Position of the transaction inside its block (0 = first). With slot_number it orders trades exactly, even within one block.never
outer_ix_indexint64Top-level instruction index of the event inside its transaction.never
inner_ix_indexint64Inner instruction index of the event. signature + outer_ix_index + inner_ix_index identify a row uniquely.never
ts_localdoubleWhen our collector received the transaction, Unix seconds with sub-second precision. Nobody can recreate this after the fact.a backfilled row (see ts_local_source)
ts_local_sourcestring'live' = received in real time by our collector (ts_local is set); 'backfill' = repaired after a capture gap or rebuilt from Solana's public archive (ts_local is NULL).never
compute_unitsuint64Compute units the whole transaction consumed.never
tx_feeuint64The transaction's total fee in lamports (base fee + priority fee).never
fee_basis_pointsint64Protocol fee rate, basis points.non-swap rows
creator_feeuint64Creator fee, lamports_mint raw units.non-swap rows
creator_fee_basis_pointsint64Creator fee rate, basis points.non-swap rows
cashbackuint64Cashback returned to the trader, lamports_mint raw units (not part of fee_lamports).non-swap rows
ix_namestringThe pump.fun instruction behind the trade (buy, sell, buy_exact_sol_in, buy_exact_quote_in, multi_hop_swap, ...).non-swap rows
quote_amountuint64Gross quote amount of the trade, lamports_mint raw units: buys = lamports_amount; sells = lamports_amount + fee_lamports.before 2026-08-31 ~19:00 UTC (not captured yet); non-swap rows
virtual_quote_reservesuint64The curve's virtual quote reserve after the trade (equals virtual_lamports_reserve in this file).before 2026-08-31 ~19:00 UTC; bonding_complete rows
real_quote_reservesuint64The curve's real quote reserve after the trade (equals real_lamports_reserve in this file).before 2026-08-31 ~19:00 UTC; non-swap rows
implausibleboolTRUE = the row failed a sanity check (an amount at or above 2^63, more SOL than exists, or a curve swap without reserves). These are almost always fake events emitted by copycat programs: exclude them from analysis. Kept and flagged, never deleted.never
bonding_curvestringThe coin's bonding-curve account (create rows only).never
track_volumeboolpump.fun's volume-tracking flag on the trade.non-swap rows
venuestringpump_fun or pump_amm (the file's venue; handy after concatenating files).never

Schema: pump_amm (PumpSwap)

46 columns, in file order.

FieldTypeMeaningEmpty when
event_typestringswap | create (a pool was created) | liquidity (a deposit or withdrawal).never
signaturestringTransaction signature (base58).never
slot_numberint64Solana slot of the block that holds the transaction.never
timestampdoubleBlock time, Unix seconds (whole seconds; the chain's own clock).never
token_mintstringThe coin's mint: the pool side that is not lamports_mint.never
token_creatorstringThe coin's creator as recorded in the event (11111111111111111111111111111111 = none: the coin was not launched on pump.fun).not in the event (some create/liquidity rows)
can_be_frozenboolTRUE if the coin's mint still has a freeze authority (the issuer can freeze holders' tokens). NOT FILLED YET: NULL on every row until the mint-safety lookup ships (build step 2).always NULL in v1 so far (not looked up yet)
is_cashback_coinboolTRUE if the coin pays trader cashback (its cashback fee rate is above 0).never
actionstringbuy | sell on swaps, from the trader's side (buy = received the coin); deposit | withdraw on liquidity rows.create rows; swaps whose pool orientation was unknown at capture (see event_side)
user_walletstringThe trader: the event's user field (not necessarily the transaction's fee payer).never
token_amountuint64Coins moved, raw units (coins launched on pump.fun have 6 decimals).never
lamports_amountuint64The lamports_mint side of the trade, raw units, exactly as PumpSwap's event records it (identical to pumpfundata.com's definition). Fees are taken on the POOL's quote side: on normal pools a sell's lamports_amount is after fees; on inverted pools (pool_inverted = TRUE) the fees are taken in the coin instead.create rows
fee_lamportsuint64Buys: lp + protocol + creator + buyback fees; sells: protocol + creator + buyback. Counted in the POOL's quote units: SOL on normal pools, the COIN on inverted pools (pool_inverted = TRUE). Cashback is not included.non-swap rows
lamports_mintstringThe asset the lamports_* columns are counted in. SOL is always written as WSOL (So11111111111111111111111111111111111111112) on both venues; any other value is the quote asset's mint (USDC, tokenized stocks, other coins).never
lamports_decimalint64Decimals of lamports_mint (SOL = 9). Divide a lamports_* value by 10^lamports_decimal for whole units.the quote asset is not SOL: filled by the mint lookup (build step 2)
is_mayhem_modeboolpump.fun mayhem-mode flag (pool create rows only).never
pool_creatorstringThe pool's creator (pool create rows only). Unlike pumpfundata.com's files, which repeat it on every row, it is set only where the event carries it: join on pool_address.never
lp_token_amountuint64LP tokens minted or burned (liquidity and create rows only).never
lp_mint_supplyuint64LP token supply (liquidity and create rows only).never
real_token_reserveuint64The pool's real coin reserve BEFORE the trade, raw units.create rows
real_lamports_reserveuint64The pool's real lamports_mint reserve BEFORE the trade, raw units.create rows
virtual_token_reserveuint64Effective coin reserve PumpSwap prices on, BEFORE the trade: the real coin reserve, plus virtual_quote_adjustment when the coin sits on the pool's quote side (inverted pools).non-swap rows; implausible rows; pool orientation unknown; adjustment unknown (our capture before 2026-08-14 ~02:00 UTC did not record it)
virtual_lamports_reserveuint64Effective lamports_mint reserve PumpSwap prices on, BEFORE the trade: the real reserve, plus virtual_quote_adjustment on normal pools. A pool sell pays floor(in x this / (other + in)) before fees (verified exact on every pool sell).same cases as virtual_token_reserve
buyback_feeuint64Buyback fee, pool quote units. On PumpSwap it is a share of protocol_fee, not an extra charge.non-swap rows
buyback_fee_basis_pointsint64Buyback fee rate, basis points.non-swap rows
pool_addressstringThe PumpSwap pool.never
tx_indexint64Position of the transaction inside its block (0 = first). With slot_number it orders trades exactly, even within one block.never
outer_ix_indexint64Top-level instruction index of the event inside its transaction.never
inner_ix_indexint64Inner instruction index of the event. signature + outer_ix_index + inner_ix_index identify a row uniquely.never
ts_localdoubleWhen our collector received the transaction, Unix seconds with sub-second precision. Nobody can recreate this after the fact.a backfilled row (see ts_local_source)
ts_local_sourcestring'live' = received in real time by our collector (ts_local is set); 'backfill' = repaired after a capture gap or rebuilt from Solana's public archive (ts_local is NULL).never
compute_unitsuint64Compute units the whole transaction consumed.never
tx_feeuint64The transaction's total fee in lamports (base fee + priority fee).never
lp_feeuint64Fee left in the pool for liquidity providers, pool quote units.non-swap rows
protocol_feeuint64Protocol fee, pool quote units.non-swap rows
coin_creator_feeuint64Coin creator's fee, pool quote units.non-swap rows
cashbackuint64Cashback returned to the trader, pool quote units. When set it replaces the creator fee.non-swap rows
ix_namestringThe PumpSwap instruction behind a pool buy event (buy, buy_exact_quote_in, multi_hop_swap, ...).pool sell events (the event does not name it); non-swap rows
base_mintstringThe pool's base mint, as the pool stores it.liquidity rows
pool_quote_mintstringThe pool's quote mint, as the pool stores it.liquidity rows
pool_invertedboolTRUE when SOL (WSOL) is the pool's BASE side, so the coin sits on the quote side. token_* / lamports_* columns are already oriented to (coin, lamports_mint); the pool-frame columns (base_mint, pool_quote_mint, virtual_quote_adjustment, the fee legs) are not.orientation unknown at capture (rare)
user_quote_amount_inuint64Pool buy events only: the event's user_quote_amount_in, pool quote units. On buy_exact_quote_in it is the net amount into the pool; on plain buys the trader's gross debit.pool sell events and non-swap rows; before 2026-08-02 ~02:00 UTC
virtual_quote_adjustmentint64The pool's virtual quote reserve (signed, pool frame: on inverted pools it sits on the coin side). Effective quote reserve = pool quote reserve + this. 0 = no adjustment.non-swap rows; unknown (our capture before 2026-08-14 ~02:00 UTC did not record it)
event_sidestringBuyEvent | SellEvent in pool terms. Set only on swaps whose pool orientation was unknown at capture (where action is NULL).every other row
implausibleboolTRUE = the row failed a sanity check (an amount at or above 2^63, more SOL than exists, or a curve swap without reserves). These are almost always fake events emitted by copycat programs: exclude them from analysis. Kept and flagged, never deleted.never
venuestringpump_fun or pump_amm (the file's venue; handy after concatenating files).never

Planned tables

Not available yet:

  • tokens: One row per mint: creator, name, symbol, launch time, dev's first buy, flags, mint safety, graduation time and pool.
  • pools: PumpSwap pools: base and quote mints, orientation, creator, creation time.
  • graduations: Curve complete to pool created, with timings.
  • candles_1m: One-minute candles per mint: open, high, low, close, volume, trades, buyers, sellers.

Free samples

One real hour of each exchange, free: no account, no key, no credits. Each parquet file below is the same file you would buy for that hour, byte for byte. Next to it is a CSV of the first rows, for a quick look without parquet tools.

Loading the sample files…

Open it with pandas

# pip install pandas pyarrow
import pandas as pd

df = pd.read_parquet("pump_fun_2026-11-09_12.parquet")
print(len(df), "rows,", len(df.columns), "columns")
print(df.head())

# Block time is Unix seconds; turn it into real timestamps when you need them:
df["block_time"] = pd.to_datetime(df["timestamp"], unit="s", utc=True)

# Buys and sells of one coin, in the order the chain executed them:
one = df[df["token_mint"] == df["token_mint"].iloc[0]]
print(one[["block_time", "action", "user_wallet", "token_amount", "lamports_amount"]].head(20))

Or query it straight away with DuckDB

-- pip install duckdb   (or use the DuckDB CLI)
-- No loading step: DuckDB reads the file where it lies.
-- Amounts are in the units of the coin each row is priced in (lamports_mint):
-- to add up SOL, keep the SOL-priced rows and leave out rows flagged implausible.
SELECT action, count(*) AS trades, sum(lamports_amount) / 1e9 AS sol
FROM 'pump_fun_2026-11-09_12.parquet'
WHERE event_type = 'swap'
  AND lamports_mint = 'So11111111111111111111111111111111111111112'
  AND NOT implausible
GROUP BY action
ORDER BY sol DESC;

-- The busiest coins of the hour, by SOL traded:
SELECT token_mint, count(*) AS trades, sum(lamports_amount) / 1e9 AS sol
FROM 'pump_fun_2026-11-09_12.parquet'
WHERE event_type = 'swap'
  AND lamports_mint = 'So11111111111111111111111111111111111111112'
  AND NOT implausible
GROUP BY token_mint
ORDER BY sol DESC
LIMIT 10;

The samples list is also an endpoint, if you would rather script it: GET https://api.dataonchain.io/v1/samples — no key, and it answers with every file's size, SHA-256 and a download link that works for a few minutes.