launchtape 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
launchtape/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+ """Launch Tape: daily files of Solana launch data (https://launchtape.io) in Python.
2
+
3
+ import launchtape as lt
4
+ path = lt.sample("pregrad") # free 2-hour sample, SHA-256 checked
5
+ for row in lt.load(path, types="record", limit=3):
6
+ print(row["sym"], row["instant"])
7
+ """
8
+
9
+ from ._catalog import DATASETS, Dataset, datasets, schema
10
+ from ._download import ChecksumError, LaunchTapeError, cache_dir, fetch, sample
11
+ from ._read import frame, load
12
+ from ._version import __version__
13
+
14
+ __all__ = [
15
+ "DATASETS", "Dataset", "datasets", "schema",
16
+ "sample", "fetch", "cache_dir", "LaunchTapeError", "ChecksumError",
17
+ "load", "frame", "__version__",
18
+ ]
launchtape/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
launchtape/_catalog.py ADDED
@@ -0,0 +1,54 @@
1
+ """The datasets, their one-line descriptions and their bundled schema docs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.resources import files
6
+ from typing import List, NamedTuple
7
+
8
+
9
+ class Dataset(NamedTuple):
10
+ name: str
11
+ description: str
12
+
13
+
14
+ _DATASETS = (
15
+ Dataset(
16
+ "gradtrades",
17
+ "Every PumpSwap buy and sell in the first 30 minutes after each pump.fun graduation "
18
+ "(wallet, amounts, price, pool reserves, fees)",
19
+ ),
20
+ Dataset(
21
+ "pregrad",
22
+ "Who is behind each pump.fun launch: creator and its funding, curve and graduation-slot "
23
+ "buyers, the cap path, and a +10 min outcome",
24
+ ),
25
+ Dataset(
26
+ "padfeed",
27
+ "Graduations on Meteora DBC and Raydium LaunchLab: migration facts, pool balances every "
28
+ "few seconds, top holders, US$ quote prices",
29
+ ),
30
+ Dataset(
31
+ "meteora-pools",
32
+ "Per-minute price, TVL, volume and exact fees for ~200 Meteora DLMM pools",
33
+ ),
34
+ )
35
+
36
+ DATASETS = tuple(d.name for d in _DATASETS)
37
+
38
+
39
+ def check_dataset(dataset: str) -> str:
40
+ if dataset not in DATASETS:
41
+ raise ValueError(f"unknown dataset {dataset!r}; choose one of: {', '.join(DATASETS)}")
42
+ return dataset
43
+
44
+
45
+ def datasets() -> List[Dataset]:
46
+ """The datasets as (name, description) pairs."""
47
+ return list(_DATASETS)
48
+
49
+
50
+ def schema(dataset: str) -> str:
51
+ """The schema of a dataset as Markdown text (every row type and field)."""
52
+ check_dataset(dataset)
53
+ # one name per joinpath: Python 3.9's Traversable takes a single argument
54
+ return files("launchtape").joinpath("schemas").joinpath(f"{dataset}.md").read_text(encoding="utf-8")
@@ -0,0 +1,216 @@
1
+ """Downloads: the free samples and paid day files. A file is kept only after its SHA-256 matches the published one."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime as _dt
6
+ import hashlib
7
+ import json
8
+ import os
9
+ import re
10
+ import sys
11
+ import warnings
12
+ from pathlib import Path
13
+ from typing import Dict, Optional, Union
14
+ from urllib.parse import urlsplit
15
+
16
+ import requests
17
+
18
+ from ._catalog import check_dataset
19
+ from ._version import __version__
20
+
21
+ DEFAULT_URL = "https://launchtape.io"
22
+ CHUNK = 1 << 20
23
+ TIMEOUT = (15, 120) # connect, read (seconds)
24
+
25
+ PathLike = Union[str, "os.PathLike[str]"]
26
+
27
+ _HEX64 = re.compile(r"[0-9a-fA-F]{64}")
28
+ _TOKEN = re.compile(r"[0-9a-f]{32}")
29
+ _NAME = re.compile(r"[a-z][a-z0-9-]*")
30
+ _DAY = re.compile(r"\d{4}-\d{2}-\d{2}")
31
+
32
+
33
+ class LaunchTapeError(Exception):
34
+ """A download could not be completed or checked."""
35
+
36
+
37
+ class ChecksumError(LaunchTapeError):
38
+ """A downloaded file's SHA-256 does not match the published one (the file is not kept)."""
39
+
40
+
41
+ def base_url() -> str:
42
+ """The site to download from: $LAUNCHTAPE_URL, else https://launchtape.io."""
43
+ return os.environ.get("LAUNCHTAPE_URL", DEFAULT_URL).rstrip("/")
44
+
45
+
46
+ def cache_dir() -> Path:
47
+ """Where samples are kept: $LAUNCHTAPE_CACHE, else ~/.cache/launchtape."""
48
+ env = os.environ.get("LAUNCHTAPE_CACHE")
49
+ return Path(env).expanduser() if env else Path.home() / ".cache" / "launchtape"
50
+
51
+
52
+ def sha256_file(path: PathLike) -> str:
53
+ h = hashlib.sha256()
54
+ with open(path, "rb") as f:
55
+ for block in iter(lambda: f.read(CHUNK), b""):
56
+ h.update(block)
57
+ return h.hexdigest()
58
+
59
+
60
+ def _get(url: str, stream: bool = False) -> requests.Response:
61
+ try:
62
+ r = requests.get(url, stream=stream, timeout=TIMEOUT,
63
+ headers={"User-Agent": f"launchtape-python/{__version__}"})
64
+ except requests.RequestException as e:
65
+ raise LaunchTapeError(f"could not reach {url}: {e}") from e
66
+ if r.status_code != 200:
67
+ r.close()
68
+ hint = ""
69
+ if r.status_code == 404 and "/dl/" in url:
70
+ hint = " (download links last 7 days and list only the files you bought; write to us through launchtape.io)"
71
+ raise LaunchTapeError(f"{url}: HTTP {r.status_code}{hint}")
72
+ return r
73
+
74
+
75
+ def _progress(name: str, done: int, total: Optional[int]) -> None:
76
+ size = f"{done / 1e6:,.1f} / {total / 1e6:,.1f} MB" if total else f"{done / 1e6:,.1f} MB"
77
+ sys.stderr.write(f"\r{name}: {size}")
78
+ sys.stderr.flush()
79
+
80
+
81
+ def _download(url: str, target: Path, sha256: str, total: Optional[int] = None, progress: bool = False) -> Path:
82
+ """Stream url to target, hashing on the way; the file appears under its name only if the hash matches."""
83
+ target.parent.mkdir(parents=True, exist_ok=True)
84
+ part = target.with_name(f"{target.name}.{os.getpid()}.part")
85
+ h = hashlib.sha256()
86
+ done = 0
87
+ try:
88
+ with _get(url, stream=True) as r, open(part, "wb") as out:
89
+ total = total or int(r.headers.get("content-length") or 0) or None
90
+ for chunk in r.iter_content(CHUNK):
91
+ out.write(chunk)
92
+ h.update(chunk)
93
+ done += len(chunk)
94
+ if progress:
95
+ _progress(target.name, done, total)
96
+ if progress:
97
+ sys.stderr.write("\n")
98
+ got = h.hexdigest()
99
+ if got != sha256.lower():
100
+ raise ChecksumError(f"{target.name}: SHA-256 {got} does not match the published {sha256.lower()}; "
101
+ "the download was discarded, please try again")
102
+ os.replace(part, target)
103
+ except BaseException:
104
+ try:
105
+ part.unlink()
106
+ except FileNotFoundError:
107
+ pass
108
+ raise
109
+ return target
110
+
111
+
112
+ def _parse_sums(text: str) -> Dict[str, str]:
113
+ """`sha256sum` output -> {file name: hex digest}."""
114
+ sums = {}
115
+ for line in text.splitlines():
116
+ parts = line.strip().split(None, 1)
117
+ if len(parts) == 2 and _HEX64.fullmatch(parts[0]):
118
+ sums[parts[1].lstrip("*")] = parts[0].lower()
119
+ return sums
120
+
121
+
122
+ def sample(dataset: str, *, cache: Optional[PathLike] = None, progress: bool = False) -> Path:
123
+ """Download the free 2-hour sample of a dataset (checked against the site's SHA256SUMS) and return its path.
124
+
125
+ A sample already in the cache is reused when its hash still matches. If the site cannot be reached, a sample
126
+ already in the cache (checked when it was downloaded) is returned with a warning.
127
+ """
128
+ check_dataset(dataset)
129
+ folder = Path(cache).expanduser() if cache is not None else cache_dir()
130
+ pattern = re.compile(rf"{re.escape(dataset)}-\d{{4}}-\d{{2}}-\d{{2}}-\d{{4}}-\d{{4}}utc\.jsonl\.gz")
131
+ kept = sorted(p for p in folder.glob(f"{dataset}-*utc.jsonl.gz") if pattern.fullmatch(p.name))
132
+ base = base_url()
133
+ try:
134
+ with _get(f"{base}/sample/SHA256SUMS") as r:
135
+ sums = _parse_sums(r.text)
136
+ except LaunchTapeError as e:
137
+ if kept:
138
+ warnings.warn(f"{e}; using the {dataset} sample already in {folder}", stacklevel=2)
139
+ return kept[-1]
140
+ raise
141
+ names = sorted(n for n in sums if pattern.fullmatch(n))
142
+ if not names:
143
+ raise LaunchTapeError(f"no {dataset} sample is listed in {base}/sample/SHA256SUMS")
144
+ name = names[-1]
145
+ target = folder / name
146
+ if not (target.is_file() and sha256_file(target) == sums[name]):
147
+ _download(f"{base}/sample/{name}", target, sums[name], progress=progress)
148
+ for old in kept: # an older sample window of the same dataset
149
+ if old != target:
150
+ old.unlink()
151
+ return target
152
+
153
+
154
+ def _delivery_base(url_or_token: str) -> str:
155
+ s = str(url_or_token).strip()
156
+ if _TOKEN.fullmatch(s):
157
+ return f"{base_url()}/dl/{s}"
158
+ parts = urlsplit(s)
159
+ m = re.match(r"/dl/([0-9a-f]{32})(?:/|$)", parts.path)
160
+ if parts.scheme not in ("http", "https") or not parts.netloc or not m:
161
+ raise ValueError(f"not a Launch Tape delivery link or token: {url_or_token!r} "
162
+ "(expected https://launchtape.io/dl/<token> or the 32-character token)")
163
+ return f"{parts.scheme}://{parts.netloc}/dl/{m.group(1)}"
164
+
165
+
166
+ def _day(day: Union[str, _dt.date]) -> str:
167
+ if isinstance(day, _dt.datetime):
168
+ day = day.date()
169
+ if isinstance(day, _dt.date):
170
+ return day.isoformat()
171
+ s = str(day).strip()
172
+ if _DAY.fullmatch(s):
173
+ try:
174
+ _dt.date.fromisoformat(s)
175
+ return s
176
+ except ValueError:
177
+ pass
178
+ raise ValueError(f"day must be a UTC date like 2026-10-11, got {day!r}")
179
+
180
+
181
+ def fetch(url_or_token: str, dataset: str, day: Union[str, _dt.date], dest: Optional[PathLike] = None,
182
+ *, progress: bool = False) -> Path:
183
+ """Download one paid day file from a delivery link and return its path.
184
+
185
+ url_or_token: the link from your delivery mail (https://launchtape.io/dl/<token>) or just its token.
186
+ day: the UTC day, "YYYY-MM-DD" or a date. dest: a folder (default: the current folder) or a file path ending
187
+ in .gz. The file is checked against the SHA-256 in <dataset>-<day>.manifest.json, which is saved next to it.
188
+ A file already at the destination with the right hash is not downloaded again.
189
+ """
190
+ if not _NAME.fullmatch(str(dataset)):
191
+ raise ValueError(f"not a dataset name: {dataset!r}")
192
+ stem = f"{dataset}-{_day(day)}"
193
+ fname = f"{stem}.jsonl.gz"
194
+ base = _delivery_base(url_or_token)
195
+ with _get(f"{base}/{stem}.manifest.json") as r:
196
+ raw = r.content
197
+ try:
198
+ manifest = json.loads(raw)
199
+ except ValueError as e:
200
+ raise LaunchTapeError(f"{stem}.manifest.json is not valid JSON: {e}") from None
201
+ files = manifest.get("files") if isinstance(manifest, dict) else None
202
+ entry = next((f for f in files or [] if isinstance(f, dict) and f.get("file") == fname), None)
203
+ sha = entry.get("sha256") if entry else None
204
+ if not isinstance(sha, str) or not _HEX64.fullmatch(sha):
205
+ raise LaunchTapeError(f"{stem}.manifest.json has no SHA-256 for {fname}")
206
+ size = entry.get("bytes")
207
+ if dest is None:
208
+ target = Path.cwd() / fname
209
+ else:
210
+ target = Path(dest).expanduser()
211
+ if target.is_dir() or not target.name.endswith(".gz"):
212
+ target = target / fname
213
+ if not (target.is_file() and sha256_file(target) == sha.lower()):
214
+ _download(f"{base}/{fname}", target, sha, total=size if isinstance(size, int) else None, progress=progress)
215
+ (target.parent / f"{stem}.manifest.json").write_bytes(raw)
216
+ return target
launchtape/_read.py ADDED
@@ -0,0 +1,98 @@
1
+ """Reading the files: one JSON object per line, gzipped (plain .jsonl works too)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import gzip
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ from typing import IO, Any, Dict, FrozenSet, Iterable, Iterator, Optional, Union
10
+
11
+ from ._catalog import DATASETS
12
+ from ._download import sample
13
+
14
+ Source = Union[str, "os.PathLike[str]"]
15
+ Types = Union[str, Iterable[str], None]
16
+
17
+ # Every row is written as {"type":"<type>",...}: when filtering, a row of another type is skipped without parsing it.
18
+ _PREFIX = '{"type":"'
19
+
20
+
21
+ def _resolve(src: Source) -> Path:
22
+ if isinstance(src, os.PathLike):
23
+ return Path(src)
24
+ if not isinstance(src, str):
25
+ raise TypeError(f"expected a path or a dataset name, got {type(src).__name__}")
26
+ path = Path(src).expanduser()
27
+ if path.is_file():
28
+ return path
29
+ if src in DATASETS:
30
+ return sample(src)
31
+ raise FileNotFoundError(f"{src!r} is neither a file nor a dataset name ({', '.join(DATASETS)})")
32
+
33
+
34
+ def _types(types: Types) -> Optional[FrozenSet[str]]:
35
+ if types is None:
36
+ return None
37
+ wanted = frozenset([types] if isinstance(types, str) else types)
38
+ if not all(isinstance(t, str) for t in wanted):
39
+ raise TypeError("types must be a string or a list of strings")
40
+ return wanted
41
+
42
+
43
+ def _open(path: Path) -> IO[str]:
44
+ with open(path, "rb") as f:
45
+ gz = f.read(2) == b"\x1f\x8b"
46
+ return gzip.open(path, "rt", encoding="utf-8") if gz else open(path, "rt", encoding="utf-8")
47
+
48
+
49
+ def load(path_or_dataset: Source, types: Types = None, limit: Optional[int] = None) -> Iterator[Dict[str, Any]]:
50
+ """Stream the rows of a file as dicts.
51
+
52
+ path_or_dataset: a .jsonl.gz path, or a dataset name to read its free sample (downloaded on first use).
53
+ types: keep only rows whose `type` is this string or one of these strings, e.g. types=["record", "outcome"].
54
+ limit: stop after this many rows (counted after the type filter).
55
+ """
56
+ wanted = _types(types)
57
+ if limit is not None and (not isinstance(limit, int) or limit < 0):
58
+ raise ValueError(f"limit must be a whole number >= 0, got {limit!r}")
59
+ path = _resolve(path_or_dataset)
60
+ return _rows(path, wanted, limit)
61
+
62
+
63
+ def _rows(path: Path, wanted: Optional[FrozenSet[str]], limit: Optional[int]) -> Iterator[Dict[str, Any]]:
64
+ if limit == 0:
65
+ return
66
+ n = 0
67
+ with _open(path) as f:
68
+ for lineno, line in enumerate(f, 1):
69
+ if wanted is not None and line.startswith(_PREFIX):
70
+ end = line.find('"', len(_PREFIX))
71
+ ty = line[len(_PREFIX):end]
72
+ if end > 0 and "\\" not in ty and ty not in wanted:
73
+ continue
74
+ line = line.strip()
75
+ if not line:
76
+ continue
77
+ try:
78
+ row = json.loads(line)
79
+ except ValueError as e:
80
+ raise ValueError(f"{path}: line {lineno} is not valid JSON ({e})") from None
81
+ if wanted is not None and (not isinstance(row, dict) or row.get("type") not in wanted):
82
+ continue
83
+ yield row
84
+ n += 1
85
+ if limit is not None and n >= limit:
86
+ return
87
+
88
+
89
+ def frame(path_or_dataset: Source, types: Types = None, limit: Optional[int] = None):
90
+ """The rows as a pandas DataFrame (same arguments as load). Nested fields stay as dicts/lists in their column.
91
+
92
+ A full gradtrades day is millions of rows: pass types= and/or limit= to keep it to what you need.
93
+ """
94
+ try:
95
+ import pandas as pd
96
+ except ImportError as e:
97
+ raise ImportError("launchtape.frame() needs pandas: pip install 'launchtape[pandas]'") from e
98
+ return pd.DataFrame(list(load(path_or_dataset, types=types, limit=limit)))
launchtape/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
launchtape/cli.py ADDED
@@ -0,0 +1,80 @@
1
+ """Command line: launchtape datasets | sample | head | schema | fetch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import sys
9
+ from typing import List, Optional
10
+
11
+ from ._catalog import DATASETS, datasets, schema
12
+ from ._download import LaunchTapeError, fetch, sample
13
+ from ._read import load
14
+ from ._version import __version__
15
+
16
+
17
+ def _parser() -> argparse.ArgumentParser:
18
+ p = argparse.ArgumentParser(prog="launchtape", description="Launch Tape: daily files of Solana launch data "
19
+ "(https://launchtape.io).")
20
+ p.add_argument("--version", action="version", version=f"launchtape {__version__}")
21
+ sub = p.add_subparsers(dest="cmd", metavar="command", required=True)
22
+
23
+ sub.add_parser("datasets", help="list the datasets")
24
+
25
+ s = sub.add_parser("sample", help="download a free 2-hour sample (SHA-256 checked) and print its path")
26
+ s.add_argument("dataset", nargs="+", choices=DATASETS)
27
+
28
+ s = sub.add_parser("head", help="print the first rows of a dataset's sample, or of a file, as JSON lines")
29
+ s.add_argument("source", help="a dataset name (reads its free sample) or a .jsonl.gz path")
30
+ s.add_argument("-n", type=int, default=10, help="number of rows (default 10)")
31
+ s.add_argument("-t", "--type", dest="types", action="append", metavar="TYPE",
32
+ help="only rows of this type (repeat for several)")
33
+
34
+ s = sub.add_parser("schema", help="print a dataset's schema (Markdown)")
35
+ s.add_argument("dataset", choices=DATASETS)
36
+
37
+ s = sub.add_parser("fetch", help="download a paid day file from your delivery link and print its path")
38
+ s.add_argument("link", help="https://launchtape.io/dl/<token> from your delivery mail, or the token")
39
+ s.add_argument("dataset")
40
+ s.add_argument("day", help="UTC day, YYYY-MM-DD")
41
+ s.add_argument("-o", "--dest", help="folder or .gz file path (default: current folder)")
42
+ return p
43
+
44
+
45
+ def _run(a: argparse.Namespace) -> int:
46
+ progress = sys.stderr.isatty()
47
+ if a.cmd == "datasets":
48
+ width = max(len(d.name) for d in datasets())
49
+ for d in datasets():
50
+ print(f"{d.name:<{width}} {d.description}")
51
+ elif a.cmd == "sample":
52
+ for ds in a.dataset:
53
+ print(sample(ds, progress=progress))
54
+ elif a.cmd == "head":
55
+ if a.n < 0:
56
+ raise ValueError("-n must be 0 or more")
57
+ src = a.source
58
+ if src in DATASETS and not os.path.isfile(src):
59
+ src = sample(src, progress=progress) # the free sample, with download progress on a terminal
60
+ for row in load(src, types=a.types, limit=a.n):
61
+ print(json.dumps(row, separators=(",", ":")))
62
+ elif a.cmd == "schema":
63
+ sys.stdout.write(schema(a.dataset))
64
+ elif a.cmd == "fetch":
65
+ print(fetch(a.link, a.dataset, a.day, a.dest, progress=progress))
66
+ return 0
67
+
68
+
69
+ def main(argv: Optional[List[str]] = None) -> int:
70
+ args = _parser().parse_args(argv)
71
+ try:
72
+ return _run(args)
73
+ except BrokenPipeError: # e.g. `launchtape head gradtrades | head -1`
74
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
75
+ return 0
76
+ except KeyboardInterrupt:
77
+ return 130
78
+ except (LaunchTapeError, ValueError, OSError, EOFError) as e:
79
+ print(f"launchtape: error: {e}", file=sys.stderr)
80
+ return 1
launchtape/py.typed ADDED
File without changes
@@ -0,0 +1,40 @@
1
+ # gradtrades schema
2
+
3
+ One JSON object per line. Every row has `type` and `t` (ms since 1970 UTC, when our recorder received it). SOL amounts in SOL, token amounts in whole tokens (6 decimals). Source: Solana public RPC (PumpSwap program logs), first 30 minutes after each pump.fun graduation.
4
+
5
+ | type | field | type | meaning |
6
+ |---|---|---|---|
7
+ | grad | t | number | graduation time (ms; chain block time for late pick-ups) |
8
+ | grad | bt | number | block time (s) |
9
+ | grad | slot / sig | number / string | slot and signature of the graduation (migration) transaction |
10
+ | grad | mint | string | the coin's token mint |
11
+ | grad | pool | string | its PumpSwap pool |
12
+ | grad | coinCreator | string | wallet that receives the coin's creator fees |
13
+ | grad | poolBase / poolQuote | number | starting tokens / SOL in the pool |
14
+ | grad | virtualQuote | number | PumpSwap's extra virtual SOL in the price math (~17.6 SOL, Oct 2026) |
15
+ | grad | price | number | SOL per token at graduation |
16
+ | grad | capSol | number | price x 1e9 (market cap in SOL, ~496 for a normal graduate) |
17
+ | grad | curveSol | number | SOL raised by the bonding curve |
18
+ | grad | late | bool | picked up late; trades before `followFrom` are missing |
19
+ | grad | followFrom | number | ms; trades are complete from here on |
20
+ | grad | follow | bool | false = coin not followed (cap outside 300-600 SOL, or found too late) |
21
+ | trade | bt, slot, sig, i | number, number, string, number | block time (s), slot, signature, index of the PumpSwap trade inside the transaction. A `sig` of 64 `1`s means the RPC did not report the signature (rare: a few rows per million) |
22
+ | trade | mint | string | coin |
23
+ | trade | side | "buy" / "sell" | direction |
24
+ | trade | user | string | wallet that traded |
25
+ | trade | baseAmount | number | tokens bought or sold |
26
+ | trade | quoteAmount | number | SOL paid (buy, fees included) or received (sell, after fees) |
27
+ | trade | price | number | quoteAmount / baseAmount (effective price, fees included) |
28
+ | trade | poolBase / poolQuote | number | pool tokens / real SOL after the trade |
29
+ | trade | priceAfter | number | pool price after the trade: (poolQuote + virtualQuote) / poolBase |
30
+ | trade | virtualQuote | number | only when the pool's virtual SOL changes (rare) |
31
+ | trade | userTokensBefore | number | the wallet's balance of this coin just before the trade |
32
+ | trade | lpFee / protocolFee / creatorFee | number | fees in SOL |
33
+ | trade | creator | bool | true when the trader is the coin's creator |
34
+ | trade | ix | string | instruction variant, e.g. `buy_exact_quote_in`; absent for a normal buy |
35
+ | trade | recovered | bool | the log was truncated; trade read from the full transaction |
36
+ | end | mint, why, buys, sells | string, string, number, number | coin no longer followed: `window` (30 min done), `evicted` (cap of 30 coins), `budget` (data budget); counts recorded |
37
+ | down / up | t | number | a feed connection dropped / came back: trades in between may be missing |
38
+ | resume | mint | string | coin picked up again after a restart; the gap before `t` is missing |
39
+
40
+ Failed transactions are not included. Coverage gaps are marked, never filled in.
@@ -0,0 +1,17 @@
1
+ # meteora-pools schema
2
+
3
+ One row per pool per minute, Meteora DLMM pools with TVL >= US$50k, top 200 by 24h fee/TVL. Source: Meteora's public DLMM API (dlmm.datapi.meteora.ag). No RPC, no wallets.
4
+
5
+ | field | type | meaning |
6
+ |---|---|---|
7
+ | type | "pool" | row type |
8
+ | t | number | ms when fetched |
9
+ | pool | string | DLMM pool address |
10
+ | price | number | pool's current price (quote per base token, as Meteora reports it) |
11
+ | tvlUsd | number | total value locked, US$ |
12
+ | vol24hUsd | number | rolling 24 h volume, US$ |
13
+ | fees24hUsd | number | rolling 24 h fees, US$ |
14
+ | vol24hDeltaUsd | number/null | change in the rolling 24 h volume since the pool's previous row |
15
+ | feesMinUsd | number/null | exact fees earned since the previous row (diff of Meteora's all-time cumulative fees); null on a pool's first row or a reset |
16
+
17
+ Token mints and names are not in the row; look them up from the pool address (Meteora API or chain).
@@ -0,0 +1,19 @@
1
+ # padfeed schema
2
+
3
+ Graduations on two non-pump.fun Solana launchpads: `dbc` = Meteora Dynamic Bonding Curve (Bags, Jupiter Studio, ...) -> DAMM v2 pool, `ray` = Raydium LaunchLab (bonk.fun etc.) -> Raydium CPMM pool. Each coin followed 20 min (dbc) / 30 min (ray). Source: Solana RPC; US$ quote prices from Jupiter's free price API.
4
+
5
+ | type | field | type | meaning |
6
+ |---|---|---|---|
7
+ | all | t | number | ms when recorded |
8
+ | grad | pad | "dbc"/"ray" | launchpad |
9
+ | grad | platform | string | platform config / name |
10
+ | grad | mint, quote | string | coin mint; quote mint (SOL, or a token such as USD1/a tokenized stock) |
11
+ | grad | pool, curveAuth, baseVault, quoteVault | string/null | pool and vault accounts; `pool` is null on every dbc row (the DAMM v2 pool is not known when the migration is heard; the vaults identify it) and set on ray rows |
12
+ | grad | b, q | number | vault balances (tokens, quote units) at migration |
13
+ | grad | px | number | price in quote = q / b |
14
+ | grad | supply, decimals, tokenProgram | number, number, string | mint facts (Token or Token-2022) |
15
+ | grad | feeBps, freeze, mintAuth | | Token-2022 transfer fee, freeze / mint authorities (null = none) |
16
+ | grad | sig, blockTime, lagMs | string, number, number | migration signature, its block time (ms, not seconds like `bt` elsewhere) and how long after the block we heard of it (ms) |
17
+ | px | pad, mint, b, q | | vault balances when they changed (every ~6 s, ~2 s for fast-moving coins); price = q / b |
18
+ | holders | pad, mint, top10, top20[] | | +60 s and +5 min: 20 largest token accounts (`acct`, `owner`, `amt`); `top10` = share held by top 10 excluding pool vaults and curve |
19
+ | qpx | usd | object | every 30 s: US$ price of SOL and of every non-SOL quote mint seen in the last 2 h, keyed by mint |
@@ -0,0 +1,39 @@
1
+ # pregrad schema
2
+
3
+ Who is behind each pump.fun launch, as raw facts (addresses, amounts, times). No wallet labels, no risk flags, no images. Sources: PumpPortal free websocket (creations, migrations), Solana public RPC (bonding curves, transactions). Amounts in SOL; `pct` = % of the 1,000,000,000 supply; times in ms unless the name ends in `S` (seconds).
4
+
5
+ | type | field | type | meaning |
6
+ |---|---|---|---|
7
+ | create | t, mint, sig | number, string, string | coin creation |
8
+ | create | creator | string | creator wallet |
9
+ | create | name, sym | string | coin name / ticker |
10
+ | create | devTokens, devSol | number | creator's own buy at creation |
11
+ | create | capSol | number | curve market cap right after creation |
12
+ | create | curve | string | bonding-curve account |
13
+ | create | mayhem | bool | pump.fun "mayhem mode" launch |
14
+ | curvex | slot, curve, mint | number, string, string/null | bonding-curve cap crossing (mint null when not yet mapped) |
15
+ | curvex | cap, cross, sol, sinceFirstS | number | cap now (SOL), line crossed (100/200/300/400 SOL), SOL in curve, seconds since first seen |
16
+ | ctrade | bt, slot, sig, i, mint, side, user | | bonding-curve trade on a coin near graduation (read from cap 200 SOL up); `i` counts this coin's trades inside the transaction, so (`sig`, `mint`, `i`) is the unique key (one transaction can trade two coins) |
17
+ | ctrade | sol, tokens, capAfter, curveSol | number | SOL and tokens of the trade, cap and curve SOL after it. Note: pump.fun also runs curves priced in another token instead of SOL (the PUMP coin and a few others). In files up to 2026-10-12 17:47 UTC those coins' rows carry `sol` 0, `capAfter` null, `curveSol` 0 (the amount really is zero SOL; the trade was in the other token): treat `capAfter` null as "not a SOL curve". From 2026-10-12 17:47 UTC the recorder no longer follows non-SOL curves, so new files hold SOL curves only |
18
+ | migrate | mint, sig, pool | string | migration seen by PumpPortal (`pool` = destination AMM) |
19
+ | grad | mint, slot, capSol, creator | | graduation as seen on chain |
20
+ | record | mint, sym, name, pool, slot0, sig0, t0, bt0 | | one per graduate, written 10-40 s after graduation (t0 = graduation) |
21
+ | record | src | object | `first` = which feed saw it first, `gradRow` = a `grad` row exists, `migrate` = ms of the PumpPortal migration (or null), `curveCompleteS` = curve completed how long before t0 |
22
+ | record | creator, create | string, object | creator wallet; creation tx (`t`, `sig`, `src`, `slot`, `mayhem`, `capSol`), null when the creation was not seen |
23
+ | record | ageS | number | creation to graduation, seconds |
24
+ | record | instant | bool | graduated within 5 s of creation |
25
+ | record | cap0, curveSol | number | PumpSwap cap at graduation; SOL raised |
26
+ | record | devBuyTokens, devBuyPct, devBuySol | number | creator's buy at creation |
27
+ | record | creatorPctAtGrad | number/null | creator's holding at graduation |
28
+ | record | creatorInfo | object | `txs50` (tx count, capped 50), `capped`, `oldestS`, `nBefore10m`, `funder`, `fundSol`, `fundedBeforeS`, `fundMemo`, `priorCoins24h`, `priorCoins72h` |
29
+ | record | gradSlot | object/null | PumpSwap buys in the graduation slot: `pct`, `sol`, `nWallets`, `nTx`, `buys[]` (`w`, `tokens`, `pct`, `sol`, `sig`, `bundleSize`, `payer`, `signers`, `slotOffset`, `creatorInTx`), `whale` = the largest of those buys with its wallet facts (`txs25`, `oldestS`, `lastTxKind`, `funder`, `fundSol`, `fundedBeforeS`, `fundSig`, `fundMemo`, `gainers[]`) |
30
+ | record | curve | object | `coverage` (full/partial/none), `holders[]` (top 10: `w`, `pct`, `sol`, `creator`), `creationSlot` (`ok`, `failed`, `curveTxs`, `capped`: transactions in the creation slot), `bundleEst` (`buyers`, `avgPct`, `top5Pct`), `estimated`, `top1NcPct`, `top5NcPct`, `nOver3Pct`, `bundleWallets`, `bundlePct`, `creatorPct`, `path` (`firstCap`, `firstS`, `t100S`..`t400S`, `maxCap`, `updates`, `completeS`: the cap over time), `trades` (`fromCap`, `fromT`, `n`, `buys`, `sells`, `wallets`, `maxCap`, `lastCap`, `completeT`, `topBuyers[]` with `w`, `tokens`, `pct`, `sol`, `creator`: our own count of its curve trades) |
31
+ | outcome | mint, sym, t0, recT | | +10 min after graduation |
32
+ | outcome | capAt | object | PumpSwap cap (SOL) at +10/45/60/120/180/300/600 s |
33
+ | outcome | maxGain, minDrop | number | best / worst move in %, first 10 min |
34
+ | outcome | crashed | bool | fell by 80%+ |
35
+ | outcome | trades | number | PumpSwap trades counted |
36
+ | outcome | dump, firstBigSell, bigSells[] | object | the first sale breaking a fixed size rule and the large sales: `tS`, `w`, `frac` (share of pool sold), `sol`, `capAfter`, `rule`, `tokensFrom` (where the sold tokens came from: `kind`, `from`, `for`, `tokens`, `pct`, `sig`, `bt`, `account`, `accountTxs`, `beforeDumpS`, `afterGradS`) |
37
+ | outcome | src | string | where trades were read from |
38
+
39
+ Removed before sale: our internal "rogue" wallet list and every field derived from it, `flags`, `priorRugged`, the dumper `role`, picture fingerprints and metadata URIs, heartbeat/stats rows.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.5
2
+ Name: launchtape
3
+ Version: 0.1.0
4
+ Summary: Load Launch Tape's daily Solana launch data (pump.fun graduations and launches, other launchpads, Meteora pools) in Python
5
+ Project-URL: Homepage, https://launchtape.io
6
+ Project-URL: Datasets, https://launchtape.io/#datasets
7
+ Project-URL: Pricing, https://launchtape.io/#buy
8
+ Project-URL: Data licence, https://launchtape.io/#licence
9
+ Project-URL: Source, https://github.com/launchtape-io/launchtape
10
+ Author-email: Launch Tape <launchio48@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: crypto,dataset,market data,meteora,pump.fun,pumpswap,raydium,solana
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Financial and Insurance Industry
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Office/Business :: Financial :: Investment
21
+ Classifier: Topic :: Scientific/Engineering :: Information Analysis
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: requests>=2.20
25
+ Provides-Extra: pandas
26
+ Requires-Dist: pandas>=1.3; extra == 'pandas'
27
+ Provides-Extra: test
28
+ Requires-Dist: pytest>=7; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # launchtape
32
+
33
+ Python access to [Launch Tape](https://launchtape.io): daily files of Solana launch data, one gzipped JSON-lines
34
+ file per dataset per UTC day.
35
+
36
+ | Dataset | What it is |
37
+ |---|---|
38
+ | `gradtrades` | Every PumpSwap buy and sell in the first 30 minutes after each pump.fun graduation (wallet, amounts, price, pool reserves, fees) |
39
+ | `pregrad` | Who is behind each pump.fun launch: creator and its funding, curve and graduation-slot buyers, the cap path, and a +10 min outcome |
40
+ | `padfeed` | Graduations on Meteora DBC and Raydium LaunchLab: migration facts, pool balances every few seconds, top holders, US$ quote prices |
41
+ | `meteora-pools` | Per-minute price, TVL, volume and exact fees for ~200 Meteora DLMM pools |
42
+
43
+ A free 2-hour sample of every dataset is on the site; `launchtape` downloads it, checks its SHA-256 and reads it.
44
+
45
+ ## Install
46
+
47
+ ```sh
48
+ pip install launchtape # needs only requests
49
+ pip install "launchtape[pandas]" # adds pandas for launchtape.frame()
50
+ ```
51
+
52
+ Python 3.9 or newer.
53
+
54
+ ## Quickstart
55
+
56
+ Crash rate of pump.fun graduates, split by "instant" launches (graduated within 5 s of creation):
57
+
58
+ ```python
59
+ import launchtape as lt
60
+
61
+ path = lt.sample("pregrad") # free sample, SHA-256 checked, cached
62
+ rec = lt.frame(path, types="record")[["mint", "sym", "instant", "ageS"]]
63
+ out = lt.frame(path, types="outcome")[["mint", "crashed", "maxGain"]]
64
+ df = rec.merge(out, on="mint") # one row per graduate seen in both
65
+ print(df.groupby("instant")["crashed"].agg(["mean", "count"]))
66
+ print(lt.schema("pregrad")) # every row type and field
67
+ ```
68
+
69
+ The sample is two hours, so the counts are small: it shows the shape of the data, not a result.
70
+
71
+ ## API
72
+
73
+ | Call | Does |
74
+ |---|---|
75
+ | `lt.datasets()` | the datasets as `(name, description)` pairs |
76
+ | `lt.schema(dataset)` | the schema as Markdown text (bundled, works offline) |
77
+ | `lt.sample(dataset)` | downloads the free sample into `~/.cache/launchtape`, checks it against the site's `SHA256SUMS`, returns its path; a good cached copy is reused |
78
+ | `lt.load(path_or_dataset, types=None, limit=None)` | streams the rows as dicts; `types` keeps only rows whose `type` is one of them, `limit` stops after that many rows; a dataset name reads its sample |
79
+ | `lt.frame(path_or_dataset, types=None, limit=None)` | the same rows as a pandas DataFrame (nested fields stay dicts/lists) |
80
+ | `lt.fetch(link_or_token, dataset, day, dest=None)` | downloads a paid day file from your delivery link, checks it against its `.manifest.json`, returns its path |
81
+
82
+ ```python
83
+ for row in lt.load("gradtrades", types="grad", limit=5): # streams, so a 5-million-row day is fine
84
+ print(row["mint"], row["capSol"])
85
+
86
+ path = lt.fetch("https://launchtape.io/dl/<token>", "pregrad", "2026-10-11", dest="data/")
87
+ ```
88
+
89
+ Times: `t` is milliseconds since 1970 UTC (`pd.to_datetime(df["t"], unit="ms", utc=True)`). Coverage gaps are rows
90
+ in the files (`down`, `up`, `resume`, `end`), never filled in: see each schema.
91
+
92
+ A full `gradtrades` day is about 5 million rows; for a DataFrame, pass `types=` and/or `limit=`, or stream with
93
+ `load()`. A download that does not match its SHA-256 raises `launchtape.ChecksumError` and is not kept.
94
+ Environment: `LAUNCHTAPE_CACHE` changes the cache folder.
95
+
96
+ ## Command line
97
+
98
+ ```sh
99
+ launchtape datasets
100
+ launchtape sample gradtrades # prints the cached path
101
+ launchtape head gradtrades -n 5 # first rows of the sample, as JSON lines
102
+ launchtape head pregrad -t record -n 2 # only rows of one type
103
+ launchtape schema pregrad
104
+ launchtape fetch https://launchtape.io/dl/<token> pregrad 2026-10-11 -o data/
105
+ ```
106
+
107
+ `python -m launchtape ...` works too.
108
+
109
+ ## Full days: prices and how to buy
110
+
111
+ Day file (one dataset, one UTC day): 0.05 SOL or 6 USDC. Month of history (one dataset, 30 days): 0.5 SOL or
112
+ 60 USDC. Daily feed (every dataset, each new day as it closes): 1 SOL or 120 USDC a month. Send SOL or USDC on
113
+ Solana to `5vf1PzK8LrF9D5bemYJYPsFYp4nHULSTA1biBampgU58` with a memo giving your email address or Telegram handle
114
+ and the datasets and days you want; within 24 hours you get a private download link
115
+ (`https://launchtape.io/dl/<token>`, valid 7 days) that `launchtape fetch` reads. Or buy through
116
+ [launchtape.io](https://launchtape.io/#buy), which has a Solana Pay QR code and a contact form.
117
+
118
+ ## Licences
119
+
120
+ - **This package** (the code): MIT, see [LICENSE](LICENSE).
121
+ - **The data** it downloads, the free samples included, has its own licence: personal and research use inside
122
+ your own team. No resale, republishing or redistribution of the files, in whole or in part. Results you derive
123
+ from it (charts, papers, models, strategies) are yours. Full text: [launchtape.io](https://launchtape.io/#licence).
124
+
125
+ The data is market data recorded from public sources, provided as is. It is not financial advice.
@@ -0,0 +1,17 @@
1
+ launchtape/__init__.py,sha256=icTOueqqQdGT169cdX3E8H2KgQReAyYOFdGtHbBZVm8,674
2
+ launchtape/__main__.py,sha256=k1ocEWawweo1qCJWNFAAvyxz3tcY13dzvCenHszij30,48
3
+ launchtape/_catalog.py,sha256=A3ej6lgEafgVa1wGS9kTBIu2WtFzb0EFI-Zuu0NgshU,1649
4
+ launchtape/_download.py,sha256=pNR3ZK97483Bld84q4Fu5T_pAB8sSTkBHn8TU8q8LcY,8536
5
+ launchtape/_read.py,sha256=nBTIpwvHARrtesudJwCA4TZ3f436SSgo3RNNtlWva94,3763
6
+ launchtape/_version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
7
+ launchtape/cli.py,sha256=RRh7q2uO8KhqlWuJvowvjIM6nBJ8o7sjqvKc0j4qcxw,3347
8
+ launchtape/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ launchtape/schemas/gradtrades.md,sha256=wDOsIQwSaoVR21ImppsKe4MU3GJd4i4Z5HJ3j5i03HM,3144
10
+ launchtape/schemas/meteora-pools.md,sha256=670__xb3Nx23Lxm_q5qrIoVxmZnt_7lQ3tyjA7eqQLM,928
11
+ launchtape/schemas/padfeed.md,sha256=Pg0uWJYvvH-oipVYAb5rzb2-3O8ELeG9w2eiDTLStVg,1784
12
+ launchtape/schemas/pregrad.md,sha256=VqjNFqtZrlG3D_FXlCKF2SMeNoSLOHe6mTxovIwByH0,5038
13
+ launchtape-0.1.0.dist-info/METADATA,sha256=Uug1GFjoYD4_YRTzcOyGNCfwi1t24HuVic67fRLRCDk,6247
14
+ launchtape-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
15
+ launchtape-0.1.0.dist-info/entry_points.txt,sha256=qZr6NbuN6w0KGM0PrGFUYNLaZ4qssiqK7_4BxRwpTik,51
16
+ launchtape-0.1.0.dist-info/licenses/LICENSE,sha256=7-f_I6mVMjcLHcyA1uXpYcSmpuqTXJYyuQ5YDLUFO3E,1068
17
+ launchtape-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ launchtape = launchtape.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Launch Tape
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.