alphatrack 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 AlphaTrack
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.
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: alphatrack
3
+ Version: 0.1.0
4
+ Summary: AlphaTrack open-source local toolkit — NFT drop eligibility checkers (keys stay on your machine)
5
+ Author: AlphaTrack
6
+ License: MIT
7
+ Project-URL: Homepage, https://alphatrack.xyz/
8
+ Project-URL: Documentation, https://github.com/alphatrack/alphatrack
9
+ Keywords: alphatrack,opensea,eligibility,nft,presale
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: requests>=2.28.0
20
+ Requires-Dist: eth-account>=0.10.0
21
+ Requires-Dist: python-dotenv>=1.0.0
22
+ Requires-Dist: tqdm>=4.66.0
23
+ Requires-Dist: PyYAML>=6.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: build; extra == "dev"
26
+ Requires-Dist: twine; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # AlphaTrack
30
+
31
+ Open-source **local** toolkit for NFT drop eligibility checks.
32
+
33
+ Private keys stay on your machine. This repo is separate from the hosted AlphaTrack platform.
34
+
35
+ ## Install
36
+
37
+ ```bash
38
+ # from PyPI (once published)
39
+ pip install alphatrack
40
+
41
+ # or from this folder
42
+ pip install -e .
43
+ ```
44
+
45
+ ## Quick start
46
+
47
+ ```bash
48
+ cp wallets.example.txt wallets.txt
49
+ # add burner private keys to wallets.txt
50
+
51
+ cp config.example.yaml config.yaml
52
+ # edit `checks:` — one block per platform, many collections each
53
+
54
+ cp .env.example .env
55
+ # optional: set ALCHEMY_API_KEY for balance checks
56
+
57
+ alphatrack platforms
58
+
59
+ # run every platform/collection block in config.yaml
60
+ alphatrack check
61
+
62
+ # or pass collections on the CLI (single platform per invocation)
63
+ alphatrack check -p opensea -c your-collection-slug -c another-slug
64
+ alphatrack check -c https://opensea.io/collection/your-collection/overview --balances
65
+ ```
66
+
67
+ Example `config.yaml` with multiple platforms:
68
+
69
+ ```yaml
70
+ wallets: wallets.txt
71
+ balances: false
72
+ checks:
73
+ - platform: opensea
74
+ collections:
75
+ - deadmood-42226579
76
+ - the-gh0sts
77
+ - platform: launchmynft # when adapter is available
78
+ collections:
79
+ - some-lmnft-slug
80
+ ```
81
+
82
+ Or without installing the script entrypoint:
83
+
84
+ ```bash
85
+ python -m alphatrack check
86
+ ```
87
+
88
+ ## What this does
89
+
90
+ 1. Loads wallets from a local file
91
+ 2. Signs in to the marketplace locally (OpenSea SIWE today)
92
+ 3. Reports which wallets are eligible for which mint stages
93
+ 4. Optionally checks whether eligible wallets can afford the mint (Alchemy)
94
+
95
+ It does **not** mint, submit transactions, or upload private keys to AlphaTrack servers.
96
+
97
+ ## Supported platforms
98
+
99
+ | Platform | Status |
100
+ |----------|--------|
101
+ | OpenSea | ✅ v0.1 |
102
+ | LaunchMyNFT | planned |
103
+ | Transient | planned |
104
+
105
+ ## Security
106
+
107
+ - Use **burner wallets** when possible
108
+ - Never commit `wallets.txt` or `.env`
109
+ - Network calls go to the marketplace (and optionally Alchemy) — not to AlphaTrack
110
+ - v1 only signs SIWE login messages, not mint transactions
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,86 @@
1
+ # AlphaTrack
2
+
3
+ Open-source **local** toolkit for NFT drop eligibility checks.
4
+
5
+ Private keys stay on your machine. This repo is separate from the hosted AlphaTrack platform.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ # from PyPI (once published)
11
+ pip install alphatrack
12
+
13
+ # or from this folder
14
+ pip install -e .
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ ```bash
20
+ cp wallets.example.txt wallets.txt
21
+ # add burner private keys to wallets.txt
22
+
23
+ cp config.example.yaml config.yaml
24
+ # edit `checks:` — one block per platform, many collections each
25
+
26
+ cp .env.example .env
27
+ # optional: set ALCHEMY_API_KEY for balance checks
28
+
29
+ alphatrack platforms
30
+
31
+ # run every platform/collection block in config.yaml
32
+ alphatrack check
33
+
34
+ # or pass collections on the CLI (single platform per invocation)
35
+ alphatrack check -p opensea -c your-collection-slug -c another-slug
36
+ alphatrack check -c https://opensea.io/collection/your-collection/overview --balances
37
+ ```
38
+
39
+ Example `config.yaml` with multiple platforms:
40
+
41
+ ```yaml
42
+ wallets: wallets.txt
43
+ balances: false
44
+ checks:
45
+ - platform: opensea
46
+ collections:
47
+ - deadmood-42226579
48
+ - the-gh0sts
49
+ - platform: launchmynft # when adapter is available
50
+ collections:
51
+ - some-lmnft-slug
52
+ ```
53
+
54
+ Or without installing the script entrypoint:
55
+
56
+ ```bash
57
+ python -m alphatrack check
58
+ ```
59
+
60
+ ## What this does
61
+
62
+ 1. Loads wallets from a local file
63
+ 2. Signs in to the marketplace locally (OpenSea SIWE today)
64
+ 3. Reports which wallets are eligible for which mint stages
65
+ 4. Optionally checks whether eligible wallets can afford the mint (Alchemy)
66
+
67
+ It does **not** mint, submit transactions, or upload private keys to AlphaTrack servers.
68
+
69
+ ## Supported platforms
70
+
71
+ | Platform | Status |
72
+ |----------|--------|
73
+ | OpenSea | ✅ v0.1 |
74
+ | LaunchMyNFT | planned |
75
+ | Transient | planned |
76
+
77
+ ## Security
78
+
79
+ - Use **burner wallets** when possible
80
+ - Never commit `wallets.txt` or `.env`
81
+ - Network calls go to the marketplace (and optionally Alchemy) — not to AlphaTrack
82
+ - v1 only signs SIWE login messages, not mint transactions
83
+
84
+ ## License
85
+
86
+ MIT
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "alphatrack"
7
+ version = "0.1.0"
8
+ description = "AlphaTrack open-source local toolkit — NFT drop eligibility checkers (keys stay on your machine)"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "AlphaTrack" }]
13
+ keywords = ["alphatrack", "opensea", "eligibility", "nft", "presale"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Software Development :: Libraries",
21
+ ]
22
+ dependencies = [
23
+ "requests>=2.28.0",
24
+ "eth-account>=0.10.0",
25
+ "python-dotenv>=1.0.0",
26
+ "tqdm>=4.66.0",
27
+ "PyYAML>=6.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ dev = ["build", "twine"]
32
+
33
+ [project.scripts]
34
+ alphatrack = "alphatrack.cli:main"
35
+
36
+ [project.urls]
37
+ Homepage = "https://alphatrack.xyz/"
38
+ Documentation = "https://github.com/alphatrack/alphatrack"
39
+
40
+ [tool.setuptools.packages.find]
41
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """AlphaTrack — open-source local NFT eligibility toolkit."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from alphatrack.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,3 @@
1
+ from alphatrack.adapters.base import EligibilityAdapter, get_adapter, list_platforms
2
+
3
+ __all__ = ["EligibilityAdapter", "get_adapter", "list_platforms"]
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, List
5
+
6
+ from alphatrack.core.models import EligibilityResult, Wallet
7
+
8
+
9
+ class EligibilityAdapter(ABC):
10
+ """Platform-specific eligibility checker."""
11
+
12
+ name: str
13
+
14
+ @abstractmethod
15
+ def normalize_collection(self, collection_ref: str) -> str:
16
+ """Turn a URL or slug into the canonical collection id for this platform."""
17
+
18
+ @abstractmethod
19
+ def check_wallet(self, wallet: Wallet, collection: str) -> EligibilityResult:
20
+ """Check a single wallet against one collection."""
21
+
22
+ def check_wallets(
23
+ self,
24
+ wallets: List[Wallet],
25
+ collection: str,
26
+ ) -> Dict[str, EligibilityResult]:
27
+ """Default sequential check — adapters may override for concurrency."""
28
+ results: Dict[str, EligibilityResult] = {}
29
+ slug = self.normalize_collection(collection)
30
+ for wallet in wallets:
31
+ results[wallet.address] = self.check_wallet(wallet, slug)
32
+ return results
33
+
34
+
35
+ _REGISTRY: Dict[str, EligibilityAdapter] = {}
36
+
37
+
38
+ def register_adapter(adapter: EligibilityAdapter) -> None:
39
+ _REGISTRY[adapter.name] = adapter
40
+
41
+
42
+ def get_adapter(platform: str) -> EligibilityAdapter:
43
+ key = platform.lower().strip()
44
+ if key not in _REGISTRY:
45
+ # Lazy import built-in adapters
46
+ _ensure_builtins()
47
+ if key not in _REGISTRY:
48
+ available = ", ".join(sorted(_REGISTRY)) or "(none)"
49
+ raise ValueError(f"Unknown platform '{platform}'. Available: {available}")
50
+ return _REGISTRY[key]
51
+
52
+
53
+ def list_platforms() -> List[str]:
54
+ _ensure_builtins()
55
+ return sorted(_REGISTRY)
56
+
57
+
58
+ def _ensure_builtins() -> None:
59
+ if "opensea" not in _REGISTRY:
60
+ from alphatrack.adapters.opensea import OpenSeaAdapter
61
+
62
+ register_adapter(OpenSeaAdapter())
@@ -0,0 +1,3 @@
1
+ from alphatrack.adapters.opensea.checker import OpenSeaAdapter
2
+
3
+ __all__ = ["OpenSeaAdapter"]
@@ -0,0 +1,128 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import threading
5
+ import time
6
+
7
+ import requests
8
+ from eth_account import Account
9
+ from eth_account.messages import encode_defunct
10
+
11
+ # Limit concurrent SIWE logins to avoid 429s
12
+ _auth_semaphore = threading.Semaphore(2)
13
+
14
+ NONCE_URL = "https://opensea.io/__api/auth/siwe/nonce"
15
+ VERIFY_URL = "https://opensea.io/__api/auth/siwe/verify"
16
+
17
+ STATEMENT = (
18
+ "Click to sign in and accept the OpenSea Terms of Service "
19
+ "(https://opensea.io/tos) and Privacy Policy "
20
+ "(https://opensea.io/privacy)."
21
+ )
22
+
23
+ BASE_HEADERS = {
24
+ "User-Agent": (
25
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
26
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
27
+ "Chrome/148.0.0.0 Safari/537.36"
28
+ ),
29
+ "Origin": "https://opensea.io",
30
+ "Referer": "https://opensea.io/",
31
+ }
32
+
33
+
34
+ def _post_with_retry(session: requests.Session, url: str, **kwargs):
35
+ for attempt in range(5):
36
+ r = session.post(url, **kwargs)
37
+ if r.status_code == 429:
38
+ time.sleep(2**attempt)
39
+ continue
40
+ r.raise_for_status()
41
+ return r
42
+ r.raise_for_status()
43
+ return r
44
+
45
+
46
+ def _get_nonce(session: requests.Session) -> str:
47
+ r = _post_with_retry(
48
+ session,
49
+ NONCE_URL,
50
+ headers={**BASE_HEADERS, "Content-Type": "application/json"},
51
+ )
52
+ return r.json()["nonce"]
53
+
54
+
55
+ def _build_message(address: str, nonce: str, uri: str = "https://opensea.io") -> dict:
56
+ issued_at = datetime.datetime.utcnow().isoformat(timespec="milliseconds") + "Z"
57
+ return {
58
+ "domain": "opensea.io",
59
+ "address": address,
60
+ "statement": STATEMENT,
61
+ "uri": uri,
62
+ "version": "1",
63
+ "chainId": "1",
64
+ "nonce": nonce,
65
+ "issuedAt": issued_at,
66
+ "accountType": "Ethereum",
67
+ }
68
+
69
+
70
+ def _format_siwe_text(msg: dict) -> str:
71
+ return (
72
+ f"{msg['domain']} wants you to sign in with your Ethereum account:\n"
73
+ f"{msg['address']}\n\n"
74
+ f"{msg['statement']}\n\n"
75
+ f"URI: {msg['uri']}\n"
76
+ f"Version: {msg['version']}\n"
77
+ f"Chain ID: {msg['chainId']}\n"
78
+ f"Nonce: {msg['nonce']}\n"
79
+ f"Issued At: {msg['issuedAt']}"
80
+ )
81
+
82
+
83
+ def _siwe_login(session: requests.Session, address: str, private_key: str) -> str:
84
+ nonce = _get_nonce(session)
85
+ message = _build_message(address, nonce)
86
+ text = _format_siwe_text(message)
87
+ signed = Account.sign_message(encode_defunct(text=text), private_key=private_key)
88
+
89
+ payload = {
90
+ "message": message,
91
+ "signature": signed.signature.hex(),
92
+ "chainArch": "EVM",
93
+ "connectorId": "io.metamask",
94
+ }
95
+ r = _post_with_retry(
96
+ session,
97
+ VERIFY_URL,
98
+ json=payload,
99
+ headers={**BASE_HEADERS, "Content-Type": "application/json"},
100
+ )
101
+
102
+ access_token = r.cookies.get("access_token")
103
+ if not access_token:
104
+ raise ValueError("access_token not returned by OpenSea verify endpoint")
105
+ return access_token
106
+
107
+
108
+ def login(session: requests.Session, address: str, private_key: str) -> str:
109
+ """
110
+ SIWE login against OpenSea. Sets session cookies used by GraphQL.
111
+
112
+ Private keys and signatures stay on this machine — only OpenSea auth
113
+ endpoints are contacted.
114
+ """
115
+ with _auth_semaphore:
116
+ access_token = _siwe_login(session, address, private_key)
117
+
118
+ for name in ("access_token", "auth_hint", "auth_access_hint"):
119
+ val = session.cookies.get(name)
120
+ if val:
121
+ session.cookies.set(name, val, domain="gql.opensea.io", path="/")
122
+
123
+ # Value used by OpenSea web client
124
+ os2 = "Wdr6yywSUvHzIWYeBybn17MOdxKRbbhM"
125
+ session.cookies.set("os2AccessEx", os2, domain="opensea.io", path="/")
126
+ session.cookies.set("os2AccessEx", os2, domain="gql.opensea.io", path="/")
127
+
128
+ return access_token
@@ -0,0 +1,158 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import time
5
+ from concurrent.futures import ThreadPoolExecutor, as_completed
6
+ from typing import Dict, List, Optional
7
+ from urllib.parse import urlparse
8
+
9
+ import requests
10
+ from tqdm import tqdm
11
+
12
+ from alphatrack.adapters.base import EligibilityAdapter
13
+ from alphatrack.adapters.opensea import auth, client
14
+ from alphatrack.core.config import SUBMIT_STAGGER_SEC, WORKERS
15
+ from alphatrack.core.models import EligibilityResult, StageResult, Wallet
16
+
17
+ log = logging.getLogger(__name__)
18
+
19
+
20
+ def parse_collection_slug(collection_ref: str) -> str:
21
+ """Accept a slug or full OpenSea collection URL."""
22
+ ref = collection_ref.strip().rstrip("/")
23
+ if "opensea.io" in ref or ref.startswith("http"):
24
+ path = urlparse(ref).path
25
+ parts = [p for p in path.split("/") if p]
26
+ if "collection" in parts:
27
+ idx = parts.index("collection")
28
+ if idx + 1 < len(parts):
29
+ return parts[idx + 1]
30
+ raise ValueError(f"Could not parse collection slug from URL: {collection_ref}")
31
+ return ref
32
+
33
+
34
+ def _price_from_stage(stage: dict) -> tuple[Optional[float], str]:
35
+ price_info = stage.get("eligiblePrice") or {}
36
+ token = price_info.get("token") or {}
37
+ unit = token.get("unit")
38
+ symbol = token.get("symbol") or "ETH"
39
+ if unit is None:
40
+ return None, symbol
41
+ return float(unit), symbol
42
+
43
+
44
+ def _to_stage_results(
45
+ stages: List[dict],
46
+ schedule_by_idx: Dict[int, dict],
47
+ ) -> List[StageResult]:
48
+ label_map = {
49
+ "SIGNED_PRESALE": "Presale",
50
+ "PUBLIC_SALE": "Public",
51
+ }
52
+ results: List[StageResult] = []
53
+ for stage in stages:
54
+ if not isinstance(stage, dict):
55
+ continue
56
+ idx = stage.get("stageIndex", 0)
57
+ sched = schedule_by_idx.get(idx, {})
58
+ price_eth, symbol = _price_from_stage(stage)
59
+ stage_type = stage.get("stageType", "STAGE")
60
+ api_label = (sched.get("label") or "").strip()
61
+ short = api_label or label_map.get(stage_type, stage_type)
62
+ if stage_type == "SIGNED_PRESALE" and not api_label:
63
+ short = f"{short} {idx}".strip()
64
+
65
+ results.append(
66
+ StageResult(
67
+ stage_index=idx,
68
+ stage_type=stage_type,
69
+ label=short,
70
+ is_eligible=bool(stage.get("isEligible")),
71
+ max_mint=stage.get("eligibleMaxTotalMintableByWallet"),
72
+ price_eth=price_eth,
73
+ price_symbol=symbol,
74
+ start_time=sched.get("startTime"),
75
+ end_time=sched.get("endTime"),
76
+ raw=stage,
77
+ )
78
+ )
79
+ return results
80
+
81
+
82
+ class OpenSeaAdapter(EligibilityAdapter):
83
+ name = "opensea"
84
+
85
+ def normalize_collection(self, collection_ref: str) -> str:
86
+ return parse_collection_slug(collection_ref)
87
+
88
+ def check_wallet(self, wallet: Wallet, collection: str) -> EligibilityResult:
89
+ slug = self.normalize_collection(collection)
90
+ try:
91
+ session = requests.Session()
92
+ auth.login(session, wallet.address, wallet.private_key)
93
+ schedule = client.get_drop_schedule(slug)
94
+ schedule_by_idx = {s.get("stageIndex"): s for s in schedule}
95
+ stages = client.get_eligibility(session, wallet.address, slug)
96
+ return EligibilityResult(
97
+ address=wallet.address,
98
+ platform=self.name,
99
+ collection=slug,
100
+ stages=_to_stage_results(stages, schedule_by_idx),
101
+ )
102
+ except Exception as e:
103
+ return EligibilityResult(
104
+ address=wallet.address,
105
+ platform=self.name,
106
+ collection=slug,
107
+ error=str(e),
108
+ )
109
+
110
+ def check_wallets(
111
+ self,
112
+ wallets: List[Wallet],
113
+ collection: str,
114
+ ) -> Dict[str, EligibilityResult]:
115
+ slug = self.normalize_collection(collection)
116
+ # Prefetch schedule once (shared across wallets)
117
+ try:
118
+ schedule = client.get_drop_schedule(slug)
119
+ except Exception as e:
120
+ log.warning("Failed to fetch drop schedule: %s", e)
121
+ schedule = []
122
+ schedule_by_idx = {s.get("stageIndex"): s for s in schedule}
123
+
124
+ results: Dict[str, EligibilityResult] = {}
125
+ futures = {}
126
+
127
+ def _one(wallet: Wallet) -> EligibilityResult:
128
+ try:
129
+ session = requests.Session()
130
+ auth.login(session, wallet.address, wallet.private_key)
131
+ stages = client.get_eligibility(session, wallet.address, slug)
132
+ return EligibilityResult(
133
+ address=wallet.address,
134
+ platform=self.name,
135
+ collection=slug,
136
+ stages=_to_stage_results(stages, schedule_by_idx),
137
+ )
138
+ except Exception as e:
139
+ return EligibilityResult(
140
+ address=wallet.address,
141
+ platform=self.name,
142
+ collection=slug,
143
+ error=str(e),
144
+ )
145
+
146
+ with ThreadPoolExecutor(max_workers=WORKERS) as executor:
147
+ for wallet in wallets:
148
+ future = executor.submit(_one, wallet)
149
+ futures[future] = wallet.address
150
+ time.sleep(SUBMIT_STAGGER_SEC)
151
+
152
+ with tqdm(total=len(wallets), desc=slug, unit="wallet") as pbar:
153
+ for future in as_completed(futures):
154
+ addr = futures[future]
155
+ results[addr] = future.result()
156
+ pbar.update(1)
157
+
158
+ return results