coinscan-sdk 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.
- coinscan_sdk-0.1.0/.gitignore +8 -0
- coinscan_sdk-0.1.0/LICENSE +21 -0
- coinscan_sdk-0.1.0/PKG-INFO +211 -0
- coinscan_sdk-0.1.0/README.md +184 -0
- coinscan_sdk-0.1.0/pyproject.toml +49 -0
- coinscan_sdk-0.1.0/scripts/gen_async.py +70 -0
- coinscan_sdk-0.1.0/src/coinscan/__init__.py +28 -0
- coinscan_sdk-0.1.0/src/coinscan/__main__.py +92 -0
- coinscan_sdk-0.1.0/src/coinscan/_async.py +517 -0
- coinscan_sdk-0.1.0/src/coinscan/_base.py +152 -0
- coinscan_sdk-0.1.0/src/coinscan/_errors.py +96 -0
- coinscan_sdk-0.1.0/src/coinscan/_sync.py +516 -0
- coinscan_sdk-0.1.0/src/coinscan/_types.py +364 -0
- coinscan_sdk-0.1.0/src/coinscan/_version.py +1 -0
- coinscan_sdk-0.1.0/src/coinscan/py.typed +0 -0
- coinscan_sdk-0.1.0/tests/test_client.py +331 -0
- coinscan_sdk-0.1.0/tests/test_live.py +63 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PIRB
|
|
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,211 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: coinscan-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the CoinScan API: token scans, holders, bubble maps, wallets, charts and market data for Solana, Ethereum, BSC and Base.
|
|
5
|
+
Project-URL: Homepage, https://coinscan.tech
|
|
6
|
+
Project-URL: Documentation, https://coinscan.tech/docs/sdks
|
|
7
|
+
Project-URL: API reference, https://coinscan.tech/docs/reference
|
|
8
|
+
Project-URL: Pricing, https://coinscan.tech/pricing
|
|
9
|
+
Author-email: PIRB <contact@pirb.tech>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,base,bsc,coinscan,crypto,ethereum,honeypot,pump.fun,rug check,sdk,solana,token scanner
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Requires-Dist: httpx<1,>=0.24
|
|
22
|
+
Requires-Dist: typing-extensions>=4.5; python_version < '3.11'
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# coinscan-sdk
|
|
29
|
+
|
|
30
|
+
Official Python client for the [CoinScan API](https://coinscan.tech): scan any token on Solana, Ethereum, BSC, Base and more for price, liquidity, holders, snipers, bundles, LP lock, honeypot and tax checks, dev history and an AI rug estimate; map holders and bubble clusters; read wallet PnL; render charts; pull market and launchpad data.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install coinscan-sdk
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Imported as `coinscan`. Python 3.9+. One dependency (`httpx`). Sync and async clients, typed responses, automatic retries on rate limits, and your remaining credits after every call.
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
Get a free key (25 scans a day) at [coinscan.tech/signup](https://coinscan.tech/signup), or from Python:
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from coinscan import CoinScan
|
|
44
|
+
|
|
45
|
+
print(CoinScan.signup(email="you@example.com")["api_key"]) # shown once
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from coinscan import CoinScan
|
|
50
|
+
|
|
51
|
+
cs = CoinScan("cs_live_...") # or set COINSCAN_API_KEY
|
|
52
|
+
scan = cs.scan("6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN")
|
|
53
|
+
|
|
54
|
+
print(scan["name"], scan["market"]["mcap"], scan["market"]["liquidity"])
|
|
55
|
+
print("bundles", scan["holders"]["bundle_pct"], "snipers", scan["holders"]["sniper_pct"])
|
|
56
|
+
print("rug probability", scan["ai_predictions"]["p_rug"])
|
|
57
|
+
print(cs.last_usage.remaining, "credits left today")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Async
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import asyncio
|
|
64
|
+
from coinscan import AsyncCoinScan
|
|
65
|
+
|
|
66
|
+
async def main():
|
|
67
|
+
async with AsyncCoinScan() as cs:
|
|
68
|
+
scans = await asyncio.gather(*(cs.scan(a) for a in ["6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN",
|
|
69
|
+
"0x6982508145454Ce325dDbE47a25d4ec3d2311933"]))
|
|
70
|
+
for s in scans:
|
|
71
|
+
print(s["symbol"], s["flags"])
|
|
72
|
+
|
|
73
|
+
asyncio.run(main())
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## What you can call
|
|
77
|
+
|
|
78
|
+
| method | what it returns | credits |
|
|
79
|
+
|-|-|-|
|
|
80
|
+
| `cs.scan(address, chain=None)` | full token scan (dict) | 10 |
|
|
81
|
+
| `cs.scan_lp_lock(address)` | finishes a scan made with `lp_defer=True` | 6 |
|
|
82
|
+
| `cs.detect(address)` | `{"address", "chain"}` from the address shape | 0 |
|
|
83
|
+
| `cs.resolve_ticker("PEPE")` | candidate contracts ranked by liquidity | 2 |
|
|
84
|
+
| `cs.resolve_pair(chain, pair)` | the token behind a pool | 0 |
|
|
85
|
+
| `cs.major("BTC")` | price, cap, supply, ATH of a major coin | 0 |
|
|
86
|
+
| `cs.mcap_batch([addr, ...])` | `{address: mcap}` for up to 500 tokens | 1 |
|
|
87
|
+
| `cs.max_mcap_batch(items)` | highest cap since a timestamp | 3 |
|
|
88
|
+
| `cs.lore(address)`, `cs.community(address)` | backstory; X community stats | 4 |
|
|
89
|
+
| `cs.search("cat")` | DexScreener pairs | 0 |
|
|
90
|
+
| `cs.chart(network, pool, plot_type="ta")` | PNG bytes | 3 |
|
|
91
|
+
| `cs.subscan.holders(token, chain=)` | top holders report | 1 |
|
|
92
|
+
| `cs.subscan.holders_map(token, chain=)` | holders by behaviour | 1 |
|
|
93
|
+
| `cs.subscan.whale_map(token, chain=)` | holders by size | 2 |
|
|
94
|
+
| `cs.subscan.bubble_map(token, chain=)` | `{"messages", "image": PNG bytes}` | 5 |
|
|
95
|
+
| `cs.subscan.early_buyers(token, chain=)` | first 50 buyers and what they did | 2 |
|
|
96
|
+
| `cs.subscan.wallet_map(token, chain=)` | holders labelled bot, insider, smart money | 1 |
|
|
97
|
+
| `cs.subscan.kol_holders(token)` | influencer wallets holding (Solana) | 1 |
|
|
98
|
+
| `cs.subscan.top_holder_bags(token)` | top holders' other pump.fun bags | 1 |
|
|
99
|
+
| `cs.subscan.overlap(token1, token2)` | wallets holding both (Solana) | 2 |
|
|
100
|
+
| `cs.subscan.dev(address, chain=)` | deployer launch history | 4 |
|
|
101
|
+
| `cs.subscan.top_traders(token, chain=)` | traders by realized PnL (list of dicts) | 1 |
|
|
102
|
+
| `cs.subscan.wallet_stats(wallet)` | 30-day stats across chains | 3 |
|
|
103
|
+
| `cs.subscan.pumpfun_meta()` | trending pump.fun words | 0 |
|
|
104
|
+
| `cs.wallet.pnl(wallet, chain=)` | PnL report | 5 |
|
|
105
|
+
| `cs.wallet.floor(token, chain=)` | holders' entry caps, supply in loss | 1 |
|
|
106
|
+
| `cs.wallet.token_balance(wallet, token, chain=)` | float | 1 |
|
|
107
|
+
| `cs.wallet.token_balances(token, wallets, chain=)` | `{wallet: balance}` | 1 |
|
|
108
|
+
| `cs.wallet.tx(hash, chain=)` | `{"from", "to", "value_wei", "block_number"}` | 1 |
|
|
109
|
+
| `cs.market.macro()`, `.news()`, `.calendar()`, `.gas()` | market data | 0 |
|
|
110
|
+
| `cs.market.dex_paid()`, `.dex_trending()`, `.dex_paid_status(token)` | DexScreener signals | 0 |
|
|
111
|
+
| `cs.market.pumpfun_graduates()`, `.pumpfun_search(q)` | pump.fun | 0 |
|
|
112
|
+
| `cs.market.treemap()`, `.top_coins_card()`, `.fear_greed_image()`, `.pnl_card(...)` | PNG bytes | 0 |
|
|
113
|
+
| `cs.launchpads.summary()`, `.stats()`, `.chart()`, `.heat()` | launchpad volume and heat | 0 |
|
|
114
|
+
| `cs.account.me()`, `.usage()`, `.rotate_key()` | your plan, usage, keys | 0 |
|
|
115
|
+
| `cs.billing.plans()`, `.create_invoice(...)`, `.wait_for_payment(id)` | plans and USDT/USDC invoices | 0 |
|
|
116
|
+
|
|
117
|
+
Field-by-field documentation: [coinscan.tech/docs](https://coinscan.tech/docs).
|
|
118
|
+
|
|
119
|
+
## Text reports
|
|
120
|
+
|
|
121
|
+
Holder, wallet and deployer routes return formatted reports (Telegram-style HTML), ready to post in a bot. `to_text` turns them into plain text:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from coinscan import CoinScan, to_text
|
|
125
|
+
|
|
126
|
+
cs = CoinScan()
|
|
127
|
+
print(to_text(cs.subscan.dev("6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN")))
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Images
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
from pathlib import Path
|
|
134
|
+
|
|
135
|
+
Path("chart.png").write_bytes(cs.chart("solana", "PAIR_ADDRESS", timeframe="hour", plot_type="ta"))
|
|
136
|
+
|
|
137
|
+
bubble = cs.subscan.bubble_map("6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN")
|
|
138
|
+
if bubble["image"]:
|
|
139
|
+
Path("bubble.png").write_bytes(bubble["image"])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Credits, limits and errors
|
|
143
|
+
|
|
144
|
+
`cs.last_usage` holds the usage headers of the last metered call: `cost`, `limit`, `remaining`, `reset` (next 00:00 UTC), `rate_limit`, `rate_remaining`.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from coinscan import CoinScan, QuotaExceededError, NotFoundError, UpstreamTimeoutError
|
|
148
|
+
|
|
149
|
+
cs = CoinScan()
|
|
150
|
+
try:
|
|
151
|
+
scan = cs.scan("So11111111111111111111111111111111111111112")
|
|
152
|
+
except NotFoundError:
|
|
153
|
+
print("no such token")
|
|
154
|
+
except QuotaExceededError as e:
|
|
155
|
+
print("out of credits until", e.reset_at, "upgrade:", e.upgrade_url)
|
|
156
|
+
except UpstreamTimeoutError:
|
|
157
|
+
print("a data provider was slow; try again")
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| exception | when |
|
|
161
|
+
|-|-|
|
|
162
|
+
| `AuthenticationError` | 401: missing or unknown key |
|
|
163
|
+
| `NotFoundError` | 404: token, pair or data not found |
|
|
164
|
+
| `ValidationError` | 422: bad input |
|
|
165
|
+
| `RateLimitError` | 429 per-minute limit (retried automatically, then raised; has `retry_after`) |
|
|
166
|
+
| `QuotaExceededError` | 429 daily credits spent (`reset_at`, `upgrade_url`) |
|
|
167
|
+
| `UpstreamTimeoutError` | a 45 s upstream deadline passed |
|
|
168
|
+
| `InternalServerError` | 5xx (502/503/504 retried; credits are refunded) |
|
|
169
|
+
| `APIConnectionError`, `APITimeoutError` | network problems |
|
|
170
|
+
|
|
171
|
+
All inherit from `CoinScanError`. Retries: `CoinScan(max_retries=2)` by default; `0` turns them off. The default timeout is 90 s because a scan can take up to 60.
|
|
172
|
+
|
|
173
|
+
## Paying from code
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
cs = CoinScan()
|
|
177
|
+
inv = cs.billing.create_invoice("starter", chain="solana", token="USDC")
|
|
178
|
+
print(f"send exactly {inv['amount']} {inv['token']} to {inv['pay_to']}")
|
|
179
|
+
print(inv["payment_uri"]) # Solana Pay / EIP-681 link
|
|
180
|
+
paid = cs.billing.wait_for_payment(inv["id"])
|
|
181
|
+
print(paid["status"], paid.get("plan_expires_at"))
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Command line
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
export COINSCAN_API_KEY=cs_live_...
|
|
188
|
+
coinscan scan 6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN
|
|
189
|
+
coinscan holders 6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN
|
|
190
|
+
coinscan chart solana PAIR_ADDRESS --plot-type ta > chart.png
|
|
191
|
+
coinscan me
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Configuration
|
|
195
|
+
|
|
196
|
+
| setting | argument | environment variable | default |
|
|
197
|
+
|-|-|-|-|
|
|
198
|
+
| API key | `CoinScan(api_key)` | `COINSCAN_API_KEY` | none |
|
|
199
|
+
| base URL | `base_url=` | `COINSCAN_BASE_URL` | `https://api.coinscan.tech` |
|
|
200
|
+
| timeout | `timeout=` | | 90 s |
|
|
201
|
+
| retries | `max_retries=` | | 2 |
|
|
202
|
+
| HTTP client | `http_client=httpx.Client(...)` | | one per client |
|
|
203
|
+
|
|
204
|
+
## Links
|
|
205
|
+
|
|
206
|
+
- Docs: https://coinscan.tech/docs
|
|
207
|
+
- Pricing: https://coinscan.tech/pricing (free 25 scans a day; from $50 a month, paid in USDT or USDC)
|
|
208
|
+
- MCP server for Claude and Cursor: https://coinscan.tech/docs/mcp
|
|
209
|
+
- Support: contact@pirb.tech
|
|
210
|
+
|
|
211
|
+
MIT licensed.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# coinscan-sdk
|
|
2
|
+
|
|
3
|
+
Official Python client for the [CoinScan API](https://coinscan.tech): scan any token on Solana, Ethereum, BSC, Base and more for price, liquidity, holders, snipers, bundles, LP lock, honeypot and tax checks, dev history and an AI rug estimate; map holders and bubble clusters; read wallet PnL; render charts; pull market and launchpad data.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install coinscan-sdk
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Imported as `coinscan`. Python 3.9+. One dependency (`httpx`). Sync and async clients, typed responses, automatic retries on rate limits, and your remaining credits after every call.
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
Get a free key (25 scans a day) at [coinscan.tech/signup](https://coinscan.tech/signup), or from Python:
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from coinscan import CoinScan
|
|
17
|
+
|
|
18
|
+
print(CoinScan.signup(email="you@example.com")["api_key"]) # shown once
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from coinscan import CoinScan
|
|
23
|
+
|
|
24
|
+
cs = CoinScan("cs_live_...") # or set COINSCAN_API_KEY
|
|
25
|
+
scan = cs.scan("6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN")
|
|
26
|
+
|
|
27
|
+
print(scan["name"], scan["market"]["mcap"], scan["market"]["liquidity"])
|
|
28
|
+
print("bundles", scan["holders"]["bundle_pct"], "snipers", scan["holders"]["sniper_pct"])
|
|
29
|
+
print("rug probability", scan["ai_predictions"]["p_rug"])
|
|
30
|
+
print(cs.last_usage.remaining, "credits left today")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Async
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
from coinscan import AsyncCoinScan
|
|
38
|
+
|
|
39
|
+
async def main():
|
|
40
|
+
async with AsyncCoinScan() as cs:
|
|
41
|
+
scans = await asyncio.gather(*(cs.scan(a) for a in ["6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN",
|
|
42
|
+
"0x6982508145454Ce325dDbE47a25d4ec3d2311933"]))
|
|
43
|
+
for s in scans:
|
|
44
|
+
print(s["symbol"], s["flags"])
|
|
45
|
+
|
|
46
|
+
asyncio.run(main())
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## What you can call
|
|
50
|
+
|
|
51
|
+
| method | what it returns | credits |
|
|
52
|
+
|-|-|-|
|
|
53
|
+
| `cs.scan(address, chain=None)` | full token scan (dict) | 10 |
|
|
54
|
+
| `cs.scan_lp_lock(address)` | finishes a scan made with `lp_defer=True` | 6 |
|
|
55
|
+
| `cs.detect(address)` | `{"address", "chain"}` from the address shape | 0 |
|
|
56
|
+
| `cs.resolve_ticker("PEPE")` | candidate contracts ranked by liquidity | 2 |
|
|
57
|
+
| `cs.resolve_pair(chain, pair)` | the token behind a pool | 0 |
|
|
58
|
+
| `cs.major("BTC")` | price, cap, supply, ATH of a major coin | 0 |
|
|
59
|
+
| `cs.mcap_batch([addr, ...])` | `{address: mcap}` for up to 500 tokens | 1 |
|
|
60
|
+
| `cs.max_mcap_batch(items)` | highest cap since a timestamp | 3 |
|
|
61
|
+
| `cs.lore(address)`, `cs.community(address)` | backstory; X community stats | 4 |
|
|
62
|
+
| `cs.search("cat")` | DexScreener pairs | 0 |
|
|
63
|
+
| `cs.chart(network, pool, plot_type="ta")` | PNG bytes | 3 |
|
|
64
|
+
| `cs.subscan.holders(token, chain=)` | top holders report | 1 |
|
|
65
|
+
| `cs.subscan.holders_map(token, chain=)` | holders by behaviour | 1 |
|
|
66
|
+
| `cs.subscan.whale_map(token, chain=)` | holders by size | 2 |
|
|
67
|
+
| `cs.subscan.bubble_map(token, chain=)` | `{"messages", "image": PNG bytes}` | 5 |
|
|
68
|
+
| `cs.subscan.early_buyers(token, chain=)` | first 50 buyers and what they did | 2 |
|
|
69
|
+
| `cs.subscan.wallet_map(token, chain=)` | holders labelled bot, insider, smart money | 1 |
|
|
70
|
+
| `cs.subscan.kol_holders(token)` | influencer wallets holding (Solana) | 1 |
|
|
71
|
+
| `cs.subscan.top_holder_bags(token)` | top holders' other pump.fun bags | 1 |
|
|
72
|
+
| `cs.subscan.overlap(token1, token2)` | wallets holding both (Solana) | 2 |
|
|
73
|
+
| `cs.subscan.dev(address, chain=)` | deployer launch history | 4 |
|
|
74
|
+
| `cs.subscan.top_traders(token, chain=)` | traders by realized PnL (list of dicts) | 1 |
|
|
75
|
+
| `cs.subscan.wallet_stats(wallet)` | 30-day stats across chains | 3 |
|
|
76
|
+
| `cs.subscan.pumpfun_meta()` | trending pump.fun words | 0 |
|
|
77
|
+
| `cs.wallet.pnl(wallet, chain=)` | PnL report | 5 |
|
|
78
|
+
| `cs.wallet.floor(token, chain=)` | holders' entry caps, supply in loss | 1 |
|
|
79
|
+
| `cs.wallet.token_balance(wallet, token, chain=)` | float | 1 |
|
|
80
|
+
| `cs.wallet.token_balances(token, wallets, chain=)` | `{wallet: balance}` | 1 |
|
|
81
|
+
| `cs.wallet.tx(hash, chain=)` | `{"from", "to", "value_wei", "block_number"}` | 1 |
|
|
82
|
+
| `cs.market.macro()`, `.news()`, `.calendar()`, `.gas()` | market data | 0 |
|
|
83
|
+
| `cs.market.dex_paid()`, `.dex_trending()`, `.dex_paid_status(token)` | DexScreener signals | 0 |
|
|
84
|
+
| `cs.market.pumpfun_graduates()`, `.pumpfun_search(q)` | pump.fun | 0 |
|
|
85
|
+
| `cs.market.treemap()`, `.top_coins_card()`, `.fear_greed_image()`, `.pnl_card(...)` | PNG bytes | 0 |
|
|
86
|
+
| `cs.launchpads.summary()`, `.stats()`, `.chart()`, `.heat()` | launchpad volume and heat | 0 |
|
|
87
|
+
| `cs.account.me()`, `.usage()`, `.rotate_key()` | your plan, usage, keys | 0 |
|
|
88
|
+
| `cs.billing.plans()`, `.create_invoice(...)`, `.wait_for_payment(id)` | plans and USDT/USDC invoices | 0 |
|
|
89
|
+
|
|
90
|
+
Field-by-field documentation: [coinscan.tech/docs](https://coinscan.tech/docs).
|
|
91
|
+
|
|
92
|
+
## Text reports
|
|
93
|
+
|
|
94
|
+
Holder, wallet and deployer routes return formatted reports (Telegram-style HTML), ready to post in a bot. `to_text` turns them into plain text:
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from coinscan import CoinScan, to_text
|
|
98
|
+
|
|
99
|
+
cs = CoinScan()
|
|
100
|
+
print(to_text(cs.subscan.dev("6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN")))
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Images
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from pathlib import Path
|
|
107
|
+
|
|
108
|
+
Path("chart.png").write_bytes(cs.chart("solana", "PAIR_ADDRESS", timeframe="hour", plot_type="ta"))
|
|
109
|
+
|
|
110
|
+
bubble = cs.subscan.bubble_map("6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN")
|
|
111
|
+
if bubble["image"]:
|
|
112
|
+
Path("bubble.png").write_bytes(bubble["image"])
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Credits, limits and errors
|
|
116
|
+
|
|
117
|
+
`cs.last_usage` holds the usage headers of the last metered call: `cost`, `limit`, `remaining`, `reset` (next 00:00 UTC), `rate_limit`, `rate_remaining`.
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from coinscan import CoinScan, QuotaExceededError, NotFoundError, UpstreamTimeoutError
|
|
121
|
+
|
|
122
|
+
cs = CoinScan()
|
|
123
|
+
try:
|
|
124
|
+
scan = cs.scan("So11111111111111111111111111111111111111112")
|
|
125
|
+
except NotFoundError:
|
|
126
|
+
print("no such token")
|
|
127
|
+
except QuotaExceededError as e:
|
|
128
|
+
print("out of credits until", e.reset_at, "upgrade:", e.upgrade_url)
|
|
129
|
+
except UpstreamTimeoutError:
|
|
130
|
+
print("a data provider was slow; try again")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
| exception | when |
|
|
134
|
+
|-|-|
|
|
135
|
+
| `AuthenticationError` | 401: missing or unknown key |
|
|
136
|
+
| `NotFoundError` | 404: token, pair or data not found |
|
|
137
|
+
| `ValidationError` | 422: bad input |
|
|
138
|
+
| `RateLimitError` | 429 per-minute limit (retried automatically, then raised; has `retry_after`) |
|
|
139
|
+
| `QuotaExceededError` | 429 daily credits spent (`reset_at`, `upgrade_url`) |
|
|
140
|
+
| `UpstreamTimeoutError` | a 45 s upstream deadline passed |
|
|
141
|
+
| `InternalServerError` | 5xx (502/503/504 retried; credits are refunded) |
|
|
142
|
+
| `APIConnectionError`, `APITimeoutError` | network problems |
|
|
143
|
+
|
|
144
|
+
All inherit from `CoinScanError`. Retries: `CoinScan(max_retries=2)` by default; `0` turns them off. The default timeout is 90 s because a scan can take up to 60.
|
|
145
|
+
|
|
146
|
+
## Paying from code
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
cs = CoinScan()
|
|
150
|
+
inv = cs.billing.create_invoice("starter", chain="solana", token="USDC")
|
|
151
|
+
print(f"send exactly {inv['amount']} {inv['token']} to {inv['pay_to']}")
|
|
152
|
+
print(inv["payment_uri"]) # Solana Pay / EIP-681 link
|
|
153
|
+
paid = cs.billing.wait_for_payment(inv["id"])
|
|
154
|
+
print(paid["status"], paid.get("plan_expires_at"))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Command line
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
export COINSCAN_API_KEY=cs_live_...
|
|
161
|
+
coinscan scan 6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN
|
|
162
|
+
coinscan holders 6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN
|
|
163
|
+
coinscan chart solana PAIR_ADDRESS --plot-type ta > chart.png
|
|
164
|
+
coinscan me
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Configuration
|
|
168
|
+
|
|
169
|
+
| setting | argument | environment variable | default |
|
|
170
|
+
|-|-|-|-|
|
|
171
|
+
| API key | `CoinScan(api_key)` | `COINSCAN_API_KEY` | none |
|
|
172
|
+
| base URL | `base_url=` | `COINSCAN_BASE_URL` | `https://api.coinscan.tech` |
|
|
173
|
+
| timeout | `timeout=` | | 90 s |
|
|
174
|
+
| retries | `max_retries=` | | 2 |
|
|
175
|
+
| HTTP client | `http_client=httpx.Client(...)` | | one per client |
|
|
176
|
+
|
|
177
|
+
## Links
|
|
178
|
+
|
|
179
|
+
- Docs: https://coinscan.tech/docs
|
|
180
|
+
- Pricing: https://coinscan.tech/pricing (free 25 scans a day; from $50 a month, paid in USDT or USDC)
|
|
181
|
+
- MCP server for Claude and Cursor: https://coinscan.tech/docs/mcp
|
|
182
|
+
- Support: contact@pirb.tech
|
|
183
|
+
|
|
184
|
+
MIT licensed.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "coinscan-sdk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Official Python client for the CoinScan API: token scans, holders, bubble maps, wallets, charts and market data for Solana, Ethereum, BSC and Base."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "PIRB", email = "contact@pirb.tech" }]
|
|
13
|
+
keywords = ["crypto", "token scanner", "solana", "ethereum", "bsc", "base", "pump.fun", "rug check", "honeypot", "api", "sdk", "coinscan"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"httpx>=0.24,<1",
|
|
25
|
+
"typing_extensions>=4.5; python_version < '3.11'",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://coinscan.tech"
|
|
30
|
+
Documentation = "https://coinscan.tech/docs/sdks"
|
|
31
|
+
"API reference" = "https://coinscan.tech/docs/reference"
|
|
32
|
+
Pricing = "https://coinscan.tech/pricing"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
coinscan = "coinscan.__main__:main"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.23"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.version]
|
|
41
|
+
path = "src/coinscan/_version.py"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/coinscan"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
asyncio_mode = "auto"
|
|
49
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate src/coinscan/_async.py from src/coinscan/_sync.py.
|
|
3
|
+
|
|
4
|
+
python scripts/gen_async.py # write
|
|
5
|
+
python scripts/gen_async.py --check # exit 1 if _async.py is stale (CI)
|
|
6
|
+
"""
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
PKG = Path(__file__).resolve().parent.parent / "src" / "coinscan"
|
|
12
|
+
SRC = PKG / "_sync.py"
|
|
13
|
+
DST = PKG / "_async.py"
|
|
14
|
+
|
|
15
|
+
CLASSES = ["CoinScan", "Subscan", "Wallet", "Market", "Launchpads", "AccountAPI", "Billing"]
|
|
16
|
+
NON_IO = {"__init__", "__repr__", "_headers", "__enter__", "__exit__"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def convert(src: str) -> str:
|
|
20
|
+
out = src
|
|
21
|
+
out = out.replace('"""Synchronous client.', '"""Asynchronous client. GENERATED from _sync.py by scripts/gen_async.py; do not edit.', 1)
|
|
22
|
+
out = out.replace("import time\n", "import asyncio\nimport time\n", 1)
|
|
23
|
+
out = out.replace("httpx.Client", "httpx.AsyncClient")
|
|
24
|
+
out = out.replace("Synchronous CoinScan API client.", "Asynchronous CoinScan API client.")
|
|
25
|
+
out = out.replace(" from coinscan import CoinScan\n cs = CoinScan()",
|
|
26
|
+
" from coinscan import AsyncCoinScan\n cs = AsyncCoinScan()")
|
|
27
|
+
out = out.replace("scan = cs.scan(", "scan = await cs.scan(")
|
|
28
|
+
|
|
29
|
+
for name in CLASSES:
|
|
30
|
+
out = re.sub(rf"\b{name}\b", f"Async{name}", out)
|
|
31
|
+
# Keep the product name in docs and messages.
|
|
32
|
+
out = out.replace("AsyncCoinScan API", "CoinScan API")
|
|
33
|
+
|
|
34
|
+
def _def(m):
|
|
35
|
+
indent, name = m.group(1), m.group(2)
|
|
36
|
+
if name in NON_IO:
|
|
37
|
+
return m.group(0)
|
|
38
|
+
return f"{indent}async def {name}("
|
|
39
|
+
out = re.sub(r"^(\s*)def (\w+)\(", _def, out, flags=re.M)
|
|
40
|
+
|
|
41
|
+
out = out.replace("def __enter__(self):", "async def __aenter__(self):")
|
|
42
|
+
out = out.replace("def __exit__(self, *exc):\n self.close()",
|
|
43
|
+
"async def __aexit__(self, *exc):\n await self.close()")
|
|
44
|
+
out = out.replace("self._http.close()", "await self._http.aclose()")
|
|
45
|
+
out = out.replace("self._http.request(", "await self._http.request(")
|
|
46
|
+
out = out.replace("time.sleep(", "await asyncio.sleep(")
|
|
47
|
+
out = re.sub(r"(?<!await )self\._c\._request\(", "await self._c._request(", out)
|
|
48
|
+
out = re.sub(r"(?<![\w.])(?<!await )self\._request\(", "await self._request(", out)
|
|
49
|
+
out = out.replace("invoice = self.get_invoice(", "invoice = await self.get_invoice(")
|
|
50
|
+
out = out.replace("with cls(api_key=\"\", base_url=base_url) as c:\n return c.account.signup(",
|
|
51
|
+
"async with cls(api_key=\"\", base_url=base_url) as c:\n return await c.account.signup(")
|
|
52
|
+
# A function that only defines the request (helpers like _evm_body, _gas) stays sync.
|
|
53
|
+
out = out.replace("async def _evm_body(", "def _evm_body(").replace("async def _gas(", "def _gas(")
|
|
54
|
+
return out
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> int:
|
|
58
|
+
generated = convert(SRC.read_text())
|
|
59
|
+
if "--check" in sys.argv:
|
|
60
|
+
if not DST.exists() or DST.read_text() != generated:
|
|
61
|
+
print("_async.py is stale: run python scripts/gen_async.py", file=sys.stderr)
|
|
62
|
+
return 1
|
|
63
|
+
return 0
|
|
64
|
+
DST.write_text(generated)
|
|
65
|
+
print(f"wrote {DST.relative_to(PKG.parent.parent)}")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
sys.exit(main())
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Official Python client for the CoinScan API (https://coinscan.tech).
|
|
2
|
+
|
|
3
|
+
from coinscan import CoinScan
|
|
4
|
+
|
|
5
|
+
cs = CoinScan() # reads COINSCAN_API_KEY
|
|
6
|
+
scan = cs.scan("6p6xgHyF7AeE6TZkSmFsko444wqoP15icUSqi2jfGiPN")
|
|
7
|
+
print(scan["holders"]["bundle_pct"], scan["ai_predictions"]["p_rug"])
|
|
8
|
+
"""
|
|
9
|
+
from coinscan._async import (
|
|
10
|
+
AsyncAccountAPI, AsyncBilling, AsyncCoinScan, AsyncLaunchpads, AsyncMarket, AsyncSubscan, AsyncWallet,
|
|
11
|
+
)
|
|
12
|
+
from coinscan._base import DEFAULT_BASE_URL, Usage, to_text
|
|
13
|
+
from coinscan._errors import (
|
|
14
|
+
APIConnectionError, APIStatusError, APITimeoutError, AuthenticationError, BadRequestError, CoinScanError,
|
|
15
|
+
ConflictError, InternalServerError, NotFoundError, PermissionDeniedError, QuotaExceededError, RateLimitError,
|
|
16
|
+
UpstreamTimeoutError, ValidationError,
|
|
17
|
+
)
|
|
18
|
+
from coinscan._sync import AccountAPI, Billing, CoinScan, Launchpads, Market, Subscan, Wallet
|
|
19
|
+
from coinscan._version import __version__
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"CoinScan", "AsyncCoinScan", "Usage", "to_text", "DEFAULT_BASE_URL", "__version__",
|
|
23
|
+
"Subscan", "Wallet", "Market", "Launchpads", "AccountAPI", "Billing",
|
|
24
|
+
"AsyncSubscan", "AsyncWallet", "AsyncMarket", "AsyncLaunchpads", "AsyncAccountAPI", "AsyncBilling",
|
|
25
|
+
"CoinScanError", "APIConnectionError", "APITimeoutError", "APIStatusError", "BadRequestError",
|
|
26
|
+
"AuthenticationError", "PermissionDeniedError", "NotFoundError", "ConflictError", "ValidationError",
|
|
27
|
+
"RateLimitError", "QuotaExceededError", "InternalServerError", "UpstreamTimeoutError",
|
|
28
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Command line: `coinscan scan <address>`, `coinscan me`, ... prints JSON.
|
|
2
|
+
|
|
3
|
+
The key comes from --api-key or COINSCAN_API_KEY.
|
|
4
|
+
"""
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from coinscan._base import to_text
|
|
10
|
+
from coinscan._errors import CoinScanError
|
|
11
|
+
from coinscan._sync import CoinScan
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _print(value, text: bool = False) -> None:
|
|
15
|
+
if isinstance(value, (bytes, bytearray)):
|
|
16
|
+
sys.stdout.buffer.write(value)
|
|
17
|
+
return
|
|
18
|
+
if text and isinstance(value, (str, list)):
|
|
19
|
+
print(to_text(value))
|
|
20
|
+
return
|
|
21
|
+
print(json.dumps(value, indent=2, ensure_ascii=False, default=str))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main(argv=None) -> int:
|
|
25
|
+
p = argparse.ArgumentParser(prog="coinscan", description="CoinScan API from the command line.")
|
|
26
|
+
p.add_argument("--api-key", help="defaults to $COINSCAN_API_KEY")
|
|
27
|
+
p.add_argument("--base-url", help="defaults to $COINSCAN_BASE_URL or https://api.coinscan.tech")
|
|
28
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
29
|
+
|
|
30
|
+
s = sub.add_parser("scan", help="full token scan (10 credits)")
|
|
31
|
+
s.add_argument("address")
|
|
32
|
+
s.add_argument("--chain")
|
|
33
|
+
d = sub.add_parser("detect", help="chain of an address (free)")
|
|
34
|
+
d.add_argument("address")
|
|
35
|
+
t = sub.add_parser("ticker", help="addresses behind a ticker (2 credits)")
|
|
36
|
+
t.add_argument("query")
|
|
37
|
+
h = sub.add_parser("holders", help="top holders (1 credit)")
|
|
38
|
+
h.add_argument("address")
|
|
39
|
+
h.add_argument("--chain", default="solana")
|
|
40
|
+
dv = sub.add_parser("dev", help="deployer history (4 credits)")
|
|
41
|
+
dv.add_argument("address")
|
|
42
|
+
dv.add_argument("--chain")
|
|
43
|
+
w = sub.add_parser("pnl", help="wallet PnL report (5 credits)")
|
|
44
|
+
w.add_argument("wallet")
|
|
45
|
+
w.add_argument("--chain", default="solana")
|
|
46
|
+
c = sub.add_parser("chart", help="PNG chart to stdout (3 credits): coinscan chart solana <pair> > chart.png")
|
|
47
|
+
c.add_argument("network")
|
|
48
|
+
c.add_argument("pool_address")
|
|
49
|
+
c.add_argument("--timeframe", default="hour")
|
|
50
|
+
c.add_argument("--plot-type", default="ohlcv")
|
|
51
|
+
sub.add_parser("macro", help="market snapshot (free)")
|
|
52
|
+
sub.add_parser("me", help="your plan and today's usage")
|
|
53
|
+
sub.add_parser("plans", help="plans and route costs")
|
|
54
|
+
su = sub.add_parser("signup", help="create a free account and print its key")
|
|
55
|
+
su.add_argument("--email")
|
|
56
|
+
|
|
57
|
+
a = p.parse_args(argv)
|
|
58
|
+
try:
|
|
59
|
+
if a.cmd == "signup":
|
|
60
|
+
_print(CoinScan.signup(email=a.email, base_url=a.base_url))
|
|
61
|
+
return 0
|
|
62
|
+
with CoinScan(a.api_key, base_url=a.base_url) as cs:
|
|
63
|
+
if a.cmd == "scan":
|
|
64
|
+
_print(cs.scan(a.address, chain=a.chain))
|
|
65
|
+
elif a.cmd == "detect":
|
|
66
|
+
_print(cs.detect(a.address))
|
|
67
|
+
elif a.cmd == "ticker":
|
|
68
|
+
_print(cs.resolve_ticker(a.query))
|
|
69
|
+
elif a.cmd == "holders":
|
|
70
|
+
_print(cs.subscan.holders(a.address, chain=a.chain), text=True)
|
|
71
|
+
elif a.cmd == "dev":
|
|
72
|
+
_print(cs.subscan.dev(a.address, chain=a.chain), text=True)
|
|
73
|
+
elif a.cmd == "pnl":
|
|
74
|
+
_print(cs.wallet.pnl(a.wallet, chain=a.chain), text=True)
|
|
75
|
+
elif a.cmd == "chart":
|
|
76
|
+
_print(cs.chart(a.network, a.pool_address, timeframe=a.timeframe, plot_type=a.plot_type))
|
|
77
|
+
elif a.cmd == "macro":
|
|
78
|
+
_print(cs.market.macro())
|
|
79
|
+
elif a.cmd == "me":
|
|
80
|
+
_print(cs.account.me())
|
|
81
|
+
elif a.cmd == "plans":
|
|
82
|
+
_print(cs.billing.plans())
|
|
83
|
+
if cs.last_usage and cs.last_usage.remaining is not None:
|
|
84
|
+
print(f"{cs.last_usage.remaining} credits left today", file=sys.stderr)
|
|
85
|
+
return 0
|
|
86
|
+
except CoinScanError as e:
|
|
87
|
+
print(f"error: {e.message}", file=sys.stderr)
|
|
88
|
+
return 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
sys.exit(main())
|