scrapling-data 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.
- scrapling_data/__init__.py +43 -0
- scrapling_data/__main__.py +3 -0
- scrapling_data/_keccak.py +73 -0
- scrapling_data/catalog.py +163 -0
- scrapling_data/cli.py +178 -0
- scrapling_data/client.py +253 -0
- scrapling_data/local.py +284 -0
- scrapling_data/x402.py +132 -0
- scrapling_data-0.1.0.dist-info/METADATA +155 -0
- scrapling_data-0.1.0.dist-info/RECORD +13 -0
- scrapling_data-0.1.0.dist-info/WHEEL +4 -0
- scrapling_data-0.1.0.dist-info/entry_points.txt +2 -0
- scrapling_data-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""scrapling-data — lead-magnet SDK for Scrapling Data (https://scraplingdata.xyz).
|
|
2
|
+
|
|
3
|
+
Free local address tools + thin x402 client. No scraping logic, no secrets.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from scrapling_data.catalog import BASE_URL, DOCS_URL, PAID, catalog_summary, price_of
|
|
7
|
+
from scrapling_data.client import ScraplingData
|
|
8
|
+
from scrapling_data.local import (
|
|
9
|
+
AddressInfo,
|
|
10
|
+
chain_hint,
|
|
11
|
+
inspect_address,
|
|
12
|
+
is_ens_name,
|
|
13
|
+
is_evm_address,
|
|
14
|
+
is_solana_address,
|
|
15
|
+
lookalike_distance,
|
|
16
|
+
normalize_evm,
|
|
17
|
+
paid_teasers_for,
|
|
18
|
+
to_checksum_address,
|
|
19
|
+
)
|
|
20
|
+
from scrapling_data.x402 import PaymentRequired, PaymentRequiredError
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
__all__ = [
|
|
24
|
+
"BASE_URL",
|
|
25
|
+
"DOCS_URL",
|
|
26
|
+
"PAID",
|
|
27
|
+
"AddressInfo",
|
|
28
|
+
"PaymentRequired",
|
|
29
|
+
"PaymentRequiredError",
|
|
30
|
+
"ScraplingData",
|
|
31
|
+
"catalog_summary",
|
|
32
|
+
"chain_hint",
|
|
33
|
+
"inspect_address",
|
|
34
|
+
"is_ens_name",
|
|
35
|
+
"is_evm_address",
|
|
36
|
+
"is_solana_address",
|
|
37
|
+
"lookalike_distance",
|
|
38
|
+
"normalize_evm",
|
|
39
|
+
"paid_teasers_for",
|
|
40
|
+
"price_of",
|
|
41
|
+
"to_checksum_address",
|
|
42
|
+
"__version__",
|
|
43
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Minimal Keccak-256 (Ethereum) for EIP-55 checksums. No third-party crypto deps."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
_RC = (
|
|
6
|
+
0x0000000000000001, 0x0000000000008082, 0x800000000000808A, 0x8000000080008000,
|
|
7
|
+
0x000000000000808B, 0x0000000080000001, 0x8000000080008081, 0x8000000000008009,
|
|
8
|
+
0x000000000000008A, 0x0000000000000088, 0x0000000080008009, 0x000000008000000A,
|
|
9
|
+
0x000000008000808B, 0x800000000000008B, 0x8000000000008089, 0x8000000000008003,
|
|
10
|
+
0x8000000000008002, 0x8000000000000080, 0x000000000000800A, 0x800000008000000A,
|
|
11
|
+
0x8000000080008081, 0x8000000000008080, 0x0000000080000001, 0x8000000080008008,
|
|
12
|
+
)
|
|
13
|
+
_ROT = (
|
|
14
|
+
(0, 36, 3, 41, 18),
|
|
15
|
+
(1, 44, 10, 45, 2),
|
|
16
|
+
(62, 6, 43, 15, 61),
|
|
17
|
+
(28, 55, 25, 21, 56),
|
|
18
|
+
(27, 20, 39, 8, 14),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _rol64(x: int, n: int) -> int:
|
|
23
|
+
return ((x << n) | (x >> (64 - n))) & 0xFFFFFFFFFFFFFFFF
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _keccak_f1600(st: list[int]) -> None:
|
|
27
|
+
for rnd in range(24):
|
|
28
|
+
c = [st[x] ^ st[x + 5] ^ st[x + 10] ^ st[x + 15] ^ st[x + 20] for x in range(5)]
|
|
29
|
+
d = [c[(x - 1) % 5] ^ _rol64(c[(x + 1) % 5], 1) for x in range(5)]
|
|
30
|
+
for i in range(25):
|
|
31
|
+
st[i] ^= d[i % 5]
|
|
32
|
+
b = [0] * 25
|
|
33
|
+
for x in range(5):
|
|
34
|
+
for y in range(5):
|
|
35
|
+
b[y + ((2 * x + 3 * y) % 5) * 5] = _rol64(st[x + 5 * y], _ROT[x][y])
|
|
36
|
+
for x in range(5):
|
|
37
|
+
for y in range(5):
|
|
38
|
+
i = x + 5 * y
|
|
39
|
+
st[i] = b[i] ^ ((~b[((x + 1) % 5) + 5 * y]) & b[((x + 2) % 5) + 5 * y])
|
|
40
|
+
st[0] ^= _RC[rnd]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _absorb_block(st: list[int], block: bytes) -> None:
|
|
44
|
+
for i in range(0, len(block), 8):
|
|
45
|
+
st[i // 8] ^= int.from_bytes(block[i : i + 8], "little")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def keccak256(data: bytes) -> bytes:
|
|
49
|
+
"""Return the 32-byte Keccak-256 digest of *data*."""
|
|
50
|
+
rate = 136
|
|
51
|
+
st = [0] * 25
|
|
52
|
+
offset = 0
|
|
53
|
+
n = len(data)
|
|
54
|
+
while offset + rate <= n:
|
|
55
|
+
_absorb_block(st, data[offset : offset + rate])
|
|
56
|
+
_keccak_f1600(st)
|
|
57
|
+
offset += rate
|
|
58
|
+
padded = bytearray(rate)
|
|
59
|
+
rem = data[offset:]
|
|
60
|
+
padded[: len(rem)] = rem
|
|
61
|
+
padded[len(rem)] = 0x01
|
|
62
|
+
padded[-1] |= 0x80
|
|
63
|
+
_absorb_block(st, bytes(padded))
|
|
64
|
+
_keccak_f1600(st)
|
|
65
|
+
out = bytearray()
|
|
66
|
+
while len(out) < 32:
|
|
67
|
+
for i in range(rate // 8):
|
|
68
|
+
out.extend(st[i].to_bytes(8, "little"))
|
|
69
|
+
if len(out) >= 32:
|
|
70
|
+
break
|
|
71
|
+
if len(out) < 32:
|
|
72
|
+
_keccak_f1600(st)
|
|
73
|
+
return bytes(out[:32])
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Endpoint catalog with live prices (sourced Nov 5–6 2026 from scraplingdata.xyz).
|
|
2
|
+
|
|
3
|
+
Prices match BRIEF.md + /llms.txt + OpenAPI x-payment-info. Always prefer the
|
|
4
|
+
HTTP 402 PAYMENT-REQUIRED amount at call time over these hints.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
BASE_URL = "https://scraplingdata.xyz"
|
|
12
|
+
DOCS_URL = f"{BASE_URL}/docs"
|
|
13
|
+
TEST_BUY_URL = f"{BASE_URL}/test-buy"
|
|
14
|
+
X402_MANIFEST = f"{BASE_URL}/.well-known/x402"
|
|
15
|
+
MCP_URL = f"{BASE_URL}/mcp"
|
|
16
|
+
PAY_TO_EVM = "0x72C80C01fD89Bac74f47cf604b61ACd119c9BCdb"
|
|
17
|
+
PAY_TO_SOLANA = "4ZUtqtQSrL522bcXTag1eR59cwfQhQGW16nYuzyYgRWX"
|
|
18
|
+
|
|
19
|
+
# path -> price USD (hints; 402 response is authoritative)
|
|
20
|
+
PAID: Dict[str, float] = {
|
|
21
|
+
"/v1/lookup/address-poisoning": 0.0009,
|
|
22
|
+
"/v1/lookup/coin": 0.001,
|
|
23
|
+
"/v1/lookup/protocol": 0.001,
|
|
24
|
+
"/v1/lookup/pool": 0.001,
|
|
25
|
+
"/v1/bundle/market-pulse": 0.001,
|
|
26
|
+
"/v1/bundle/defi-intel": 0.001,
|
|
27
|
+
"/v1/token/holders": 0.001,
|
|
28
|
+
"/v1/ping": 0.001,
|
|
29
|
+
"/v1/stocks/price": 0.001,
|
|
30
|
+
"/v1/lookup/ens": 0.001,
|
|
31
|
+
"/v1/lookup/evm-address": 0.001,
|
|
32
|
+
"/v1/lookup/fx": 0.001,
|
|
33
|
+
"/v1/tombstones/check": 0.001,
|
|
34
|
+
"/v1/lookup/yields": 0.002,
|
|
35
|
+
"/v1/ads/cost-benchmarks": 0.002,
|
|
36
|
+
"/v1/alerts/price-moves": 0.002,
|
|
37
|
+
"/v1/wallet-label": 0.002,
|
|
38
|
+
"/v1/lookup/evm-balance": 0.002,
|
|
39
|
+
"/v1/lookup/news": 0.002,
|
|
40
|
+
"/v1/lookup/market-overview": 0.002,
|
|
41
|
+
"/v1/stocks/filings": 0.003,
|
|
42
|
+
"/v1/traders/wallet": 0.003,
|
|
43
|
+
"/v1/lookup/prices": 0.003,
|
|
44
|
+
"/v1/company": 0.003,
|
|
45
|
+
"/v1/lookup/web-search": 0.003,
|
|
46
|
+
"/v1/scrape/text": 0.003,
|
|
47
|
+
"/v1/check/counterparty": 0.004,
|
|
48
|
+
"/v1/intel/risk-check": 0.0045,
|
|
49
|
+
"/v1/lookup/polymarket": 0.005,
|
|
50
|
+
"/v1/lookup/token-risk": 0.005,
|
|
51
|
+
"/v1/whales/moves": 0.005,
|
|
52
|
+
"/v1/whales/token": 0.005,
|
|
53
|
+
"/v1/token/verdict": 0.005,
|
|
54
|
+
"/v1/wallet/profile": 0.005,
|
|
55
|
+
"/v1/yields/staking": 0.005,
|
|
56
|
+
"/v1/meme/trending": 0.005,
|
|
57
|
+
"/v1/meme/pumpfun": 0.005,
|
|
58
|
+
"/v1/meme/new-pools": 0.005,
|
|
59
|
+
"/v1/stocks/fundamentals": 0.005,
|
|
60
|
+
"/v1/stocks/insiders": 0.005,
|
|
61
|
+
"/v1/traders/top": 0.005,
|
|
62
|
+
"/v1/lookup/tokenized-stocks": 0.005,
|
|
63
|
+
"/v1/lookup/crypto-treasuries": 0.005,
|
|
64
|
+
"/v1/lookup/tokenized-real-estate": 0.005,
|
|
65
|
+
"/v1/lookup/candles": 0.005,
|
|
66
|
+
"/v1/lookup/pdf": 0.005,
|
|
67
|
+
"/v1/lookup/youtube-transcript": 0.005,
|
|
68
|
+
"/v1/lookup/domain": 0.005,
|
|
69
|
+
"/v1/ecom/product": 0.01,
|
|
70
|
+
"/v1/yields/pendle": 0.01,
|
|
71
|
+
"/v1/perps/funding": 0.01,
|
|
72
|
+
"/v1/meme/history": 0.01,
|
|
73
|
+
"/v1/signals/whale-netflow": 0.02,
|
|
74
|
+
"/v1/regime/now": 0.02,
|
|
75
|
+
"/v1/ecom/changes": 0.02,
|
|
76
|
+
"/v1/lookup/binance-flows": 0.02,
|
|
77
|
+
"/v1/lookup/hack-tracker": 0.03,
|
|
78
|
+
"/v1/trust/agent": 0.04,
|
|
79
|
+
"/v1/regime/full": 0.05,
|
|
80
|
+
"/v1/ecom/snapshot": 0.05,
|
|
81
|
+
"/v1/wallet-label/bulk": 0.05,
|
|
82
|
+
"/v1/intel/risk-check/batch": 0.15,
|
|
83
|
+
"/v1/dd/quick": 0.25,
|
|
84
|
+
"/v1/ecom/catalog": 0.25,
|
|
85
|
+
"/v1/lookup/binance-flows/report": 0.25,
|
|
86
|
+
"/v1/dd/report": 1.0,
|
|
87
|
+
"/v1/pass/day": 2.0,
|
|
88
|
+
"/v1/alerts/subscribe/week": 5.0,
|
|
89
|
+
"/v1/bundles/defi-yields-pools-snapshot/download": 5.0,
|
|
90
|
+
"/v1/bundles/hack-tracker-export/download": 10.0,
|
|
91
|
+
"/v1/bundles/tokenized-assets-snapshot/download": 10.0,
|
|
92
|
+
"/v1/bundles/stolen-funds/download": 12.0,
|
|
93
|
+
"/v1/alerts/subscribe/month": 15.0,
|
|
94
|
+
"/v1/bundles/wallet-labels-full/download": 15.0,
|
|
95
|
+
"/v1/pass/month": 25.0,
|
|
96
|
+
"/v1/bundles/all-in-crypto-data/download": 25.0,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
FREE_PATHS = [
|
|
100
|
+
"/v1/free/ping",
|
|
101
|
+
"/v1/free/feeds",
|
|
102
|
+
"/v1/free/crypto/top10",
|
|
103
|
+
"/v1/free/defi/top-yields",
|
|
104
|
+
"/v1/free/regime",
|
|
105
|
+
"/v1/free/staking-yields",
|
|
106
|
+
"/v1/free/funding",
|
|
107
|
+
"/v1/free/meme-trending",
|
|
108
|
+
"/v1/free/stocks",
|
|
109
|
+
"/v1/free/traders",
|
|
110
|
+
"/v1/free/tokenized-stocks",
|
|
111
|
+
"/v1/free/crypto-treasuries",
|
|
112
|
+
"/v1/free/tokenized-real-estate",
|
|
113
|
+
"/v1/free/hack-tracker",
|
|
114
|
+
"/v1/free/hack-tracker/alerts",
|
|
115
|
+
"/v1/free/prices",
|
|
116
|
+
"/v1/free/address-poisoning",
|
|
117
|
+
"/v1/free/wallet-label",
|
|
118
|
+
"/v1/free/company",
|
|
119
|
+
"/v1/free/intel/risk-check",
|
|
120
|
+
"/v1/free/binance-flows",
|
|
121
|
+
"/v1/free/candles",
|
|
122
|
+
"/v1/free/ens",
|
|
123
|
+
"/v1/free/fx",
|
|
124
|
+
"/v1/free/news",
|
|
125
|
+
"/v1/free/market-overview",
|
|
126
|
+
"/v1/free/ecom",
|
|
127
|
+
"/v1/free/ecom/stores",
|
|
128
|
+
"/v1/free/alerts",
|
|
129
|
+
"/v1/free/labels/changelog",
|
|
130
|
+
"/v1/quality",
|
|
131
|
+
"/v1/bundles",
|
|
132
|
+
"/v1/datasets",
|
|
133
|
+
]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def price_of(path: str) -> Optional[float]:
|
|
137
|
+
"""Return catalog hint price for a path, or None if unknown / free."""
|
|
138
|
+
path = path.split("?")[0]
|
|
139
|
+
if not path.startswith("/"):
|
|
140
|
+
path = "/" + path
|
|
141
|
+
return PAID.get(path)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def catalog_summary() -> Dict[str, Any]:
|
|
145
|
+
return {
|
|
146
|
+
"base_url": BASE_URL,
|
|
147
|
+
"docs": DOCS_URL,
|
|
148
|
+
"mcp": MCP_URL,
|
|
149
|
+
"x402_manifest": X402_MANIFEST,
|
|
150
|
+
"pay_to_evm": PAY_TO_EVM,
|
|
151
|
+
"pay_to_solana": PAY_TO_SOLANA,
|
|
152
|
+
"paid_routes": len(PAID),
|
|
153
|
+
"free_routes": len(FREE_PATHS),
|
|
154
|
+
"note": "402 PAYMENT-REQUIRED amount is authoritative; these are catalog hints.",
|
|
155
|
+
"source": "https://scraplingdata.xyz/llms.txt + /openapi.json + company BRIEF (2026-11-05/06)",
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def list_paid(max_price: Optional[float] = None) -> List[Dict[str, Any]]:
|
|
160
|
+
items = [{"path": p, "price_usd": v} for p, v in sorted(PAID.items(), key=lambda kv: (kv[1], kv[0]))]
|
|
161
|
+
if max_price is not None:
|
|
162
|
+
items = [i for i in items if i["price_usd"] <= max_price]
|
|
163
|
+
return items
|
scrapling_data/cli.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""CLI entrypoint: scrapling-data check <address>"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from typing import Any, Dict, List
|
|
9
|
+
|
|
10
|
+
from scrapling_data import catalog
|
|
11
|
+
from scrapling_data.client import ScraplingData
|
|
12
|
+
from scrapling_data.local import inspect_address, lookalike_distance, paid_teasers_for
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _print_json(data: Any) -> None:
|
|
16
|
+
print(json.dumps(data, indent=2, default=str))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def cmd_check(args: argparse.Namespace) -> int:
|
|
20
|
+
info = inspect_address(args.address)
|
|
21
|
+
report: Dict[str, Any] = {
|
|
22
|
+
"local": info.to_dict(),
|
|
23
|
+
"store": catalog.BASE_URL,
|
|
24
|
+
"free": {},
|
|
25
|
+
"paid_teasers": paid_teasers_for(info.kind),
|
|
26
|
+
"how_to_pay": {
|
|
27
|
+
"protocol": "x402 USDC (Base preferred)",
|
|
28
|
+
"docs": catalog.DOCS_URL,
|
|
29
|
+
"test_buy": catalog.TEST_BUY_URL,
|
|
30
|
+
"sdk": "ScraplingData(payment_handler=...)",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if args.compare:
|
|
35
|
+
try:
|
|
36
|
+
report["lookalike"] = lookalike_distance(args.address, args.compare)
|
|
37
|
+
except ValueError as e:
|
|
38
|
+
report["lookalike_error"] = str(e)
|
|
39
|
+
|
|
40
|
+
if not args.offline and info.valid and info.kind in {"evm", "solana"}:
|
|
41
|
+
addr = info.checksum or info.normalized or args.address
|
|
42
|
+
with ScraplingData(timeout=args.timeout) as client:
|
|
43
|
+
try:
|
|
44
|
+
report["free"]["wallet_label"] = client.free_wallet_label(addr)
|
|
45
|
+
except Exception as e: # noqa: BLE001 — CLI should stay useful offline-ish
|
|
46
|
+
report["free"]["wallet_label_error"] = str(e)
|
|
47
|
+
try:
|
|
48
|
+
risk = client.free_intel_risk_check(addr)
|
|
49
|
+
report["free"]["intel_risk_check"] = risk
|
|
50
|
+
except Exception as e: # noqa: BLE001
|
|
51
|
+
report["free"]["intel_risk_check_error"] = str(e)
|
|
52
|
+
if info.kind == "evm":
|
|
53
|
+
try:
|
|
54
|
+
report["free"]["address_poisoning_sample"] = client.free_address_poisoning(addr)
|
|
55
|
+
except Exception as e: # noqa: BLE001
|
|
56
|
+
report["free"]["address_poisoning_error"] = str(e)
|
|
57
|
+
|
|
58
|
+
if args.json:
|
|
59
|
+
_print_json(report)
|
|
60
|
+
return 0
|
|
61
|
+
|
|
62
|
+
# Human-friendly teaser output
|
|
63
|
+
print(f"Scrapling Data check — {catalog.BASE_URL}")
|
|
64
|
+
print("=" * 56)
|
|
65
|
+
print(f"input: {info.input}")
|
|
66
|
+
print(f"kind: {info.kind}")
|
|
67
|
+
print(f"valid: {info.valid}")
|
|
68
|
+
if info.normalized:
|
|
69
|
+
print(f"normalized: {info.normalized}")
|
|
70
|
+
if info.checksum:
|
|
71
|
+
print(f"checksum: {info.checksum}")
|
|
72
|
+
if info.checksum_ok is not None:
|
|
73
|
+
print(f"eip55_ok: {info.checksum_ok}")
|
|
74
|
+
for n in info.notes:
|
|
75
|
+
print(f"note: {n}")
|
|
76
|
+
|
|
77
|
+
if "lookalike" in report:
|
|
78
|
+
la = report["lookalike"]
|
|
79
|
+
print()
|
|
80
|
+
print(f"lookalike vs compare: {la['verdict']} (score {la['poison_lookalike_score_0_100']})")
|
|
81
|
+
print(f" prefix={la['matching_prefix_chars']} suffix={la['matching_suffix_chars']} hamming={la['hamming_hex_chars']}")
|
|
82
|
+
|
|
83
|
+
free = report.get("free") or {}
|
|
84
|
+
if free.get("wallet_label"):
|
|
85
|
+
wl = free["wallet_label"]
|
|
86
|
+
print()
|
|
87
|
+
print("FREE wallet-label teaser:")
|
|
88
|
+
print(f" entity={wl.get('entity')} category={wl.get('category')} match={wl.get('match')}")
|
|
89
|
+
print(" (paid /v1/wallet-label $0.002 adds confidence + sources)")
|
|
90
|
+
if free.get("intel_risk_check") and isinstance(free["intel_risk_check"], dict):
|
|
91
|
+
rk = free["intel_risk_check"]
|
|
92
|
+
if rk.get("error") == "payment_required":
|
|
93
|
+
print()
|
|
94
|
+
print(f"FREE intel risk-check: daily free tries used → {rk.get('message')}")
|
|
95
|
+
else:
|
|
96
|
+
print()
|
|
97
|
+
print("FREE intel risk-check teaser:")
|
|
98
|
+
data = rk.get("data") if isinstance(rk.get("data"), dict) else rk
|
|
99
|
+
verdict = data.get("verdict") or data.get("risk") or rk.get("verdict")
|
|
100
|
+
score = data.get("risk_score")
|
|
101
|
+
entity = data.get("entity") or data.get("primary_label")
|
|
102
|
+
remaining = rk.get("free_tries_remaining_today_utc")
|
|
103
|
+
bits = []
|
|
104
|
+
if verdict is not None:
|
|
105
|
+
bits.append(f"verdict={verdict}")
|
|
106
|
+
if score is not None:
|
|
107
|
+
bits.append(f"score={score}")
|
|
108
|
+
if entity is not None:
|
|
109
|
+
bits.append(f"entity={entity}")
|
|
110
|
+
if remaining is not None:
|
|
111
|
+
bits.append(f"free_left_today={remaining}")
|
|
112
|
+
print(" " + (" ".join(bits) if bits else json.dumps(rk, default=str)[:240]))
|
|
113
|
+
print(" (paid /v1/intel/risk-check $0.0045 after 3 free/IP/day)")
|
|
114
|
+
|
|
115
|
+
print()
|
|
116
|
+
print("Paid unlocks (x402 USDC, no API key):")
|
|
117
|
+
for t in report["paid_teasers"][:6]:
|
|
118
|
+
print(f" ${t['price_usd']:<7} {t['path']:<36} {t['blurb']}")
|
|
119
|
+
print()
|
|
120
|
+
print(f"Docs: {catalog.DOCS_URL}")
|
|
121
|
+
print(f"Pay flow test: {catalog.TEST_BUY_URL}")
|
|
122
|
+
print("Python: from scrapling_data import ScraplingData")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def cmd_catalog(args: argparse.Namespace) -> int:
|
|
127
|
+
data = catalog.catalog_summary()
|
|
128
|
+
if args.max_price is not None:
|
|
129
|
+
data["routes"] = catalog.list_paid(max_price=args.max_price)
|
|
130
|
+
else:
|
|
131
|
+
data["sample_routes"] = catalog.list_paid()[:15]
|
|
132
|
+
data["hint"] = "pass --max-price 0.01 to list cheap routes"
|
|
133
|
+
_print_json(data)
|
|
134
|
+
return 0
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def cmd_ping(args: argparse.Namespace) -> int:
|
|
138
|
+
with ScraplingData(timeout=args.timeout) as client:
|
|
139
|
+
_print_json(client.free_ping())
|
|
140
|
+
return 0
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
144
|
+
p = argparse.ArgumentParser(
|
|
145
|
+
prog="scrapling-data",
|
|
146
|
+
description="Free address tools + teaser for Scrapling Data x402 crypto intel (scraplingdata.xyz).",
|
|
147
|
+
)
|
|
148
|
+
p.add_argument("--timeout", type=float, default=30.0)
|
|
149
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
150
|
+
|
|
151
|
+
c = sub.add_parser("check", help="Local validate + free teasers + paid price menu for an address")
|
|
152
|
+
c.add_argument("address", help="EVM address, Solana address, or ENS name")
|
|
153
|
+
c.add_argument("--compare", help="Second EVM address for free lookalike / poisoning heuristic")
|
|
154
|
+
c.add_argument("--offline", action="store_true", help="Skip free network teasers")
|
|
155
|
+
c.add_argument("--json", action="store_true", help="Machine-readable JSON")
|
|
156
|
+
c.set_defaults(func=cmd_check)
|
|
157
|
+
|
|
158
|
+
cat = sub.add_parser("catalog", help="Show store catalog summary / cheap routes")
|
|
159
|
+
cat.add_argument("--max-price", type=float, default=None)
|
|
160
|
+
cat.set_defaults(func=cmd_catalog)
|
|
161
|
+
|
|
162
|
+
ping = sub.add_parser("ping", help="Hit free /v1/free/ping")
|
|
163
|
+
ping.set_defaults(func=cmd_ping)
|
|
164
|
+
|
|
165
|
+
return p
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def main(argv: List[str] | None = None) -> int:
|
|
169
|
+
parser = build_parser()
|
|
170
|
+
args = parser.parse_args(argv)
|
|
171
|
+
try:
|
|
172
|
+
return int(args.func(args))
|
|
173
|
+
except KeyboardInterrupt:
|
|
174
|
+
return 130
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
sys.exit(main())
|
scrapling_data/client.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""HTTP client for Scrapling Data free + paid (x402) routes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Mapping, Optional, Union
|
|
6
|
+
from urllib.parse import urlencode
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from scrapling_data import catalog
|
|
11
|
+
from scrapling_data.x402 import PaymentHandler, PaymentRequiredError, parse_402
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ScraplingData:
|
|
15
|
+
"""
|
|
16
|
+
Thin public SDK for https://scraplingdata.xyz.
|
|
17
|
+
|
|
18
|
+
Free routes need no wallet. Paid routes return HTTP 402 unless you pass
|
|
19
|
+
``payment_handler`` (returns a PAYMENT-SIGNATURE value) or ``pass_token`` /
|
|
20
|
+
``api_key``. This library never holds private keys.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
base_url: str = catalog.BASE_URL,
|
|
26
|
+
*,
|
|
27
|
+
timeout: float = 60.0,
|
|
28
|
+
payment_handler: Optional[PaymentHandler] = None,
|
|
29
|
+
pass_token: Optional[str] = None,
|
|
30
|
+
api_key: Optional[str] = None,
|
|
31
|
+
trial_code: Optional[str] = None,
|
|
32
|
+
client: Optional[httpx.Client] = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.base_url = base_url.rstrip("/")
|
|
35
|
+
self.payment_handler = payment_handler
|
|
36
|
+
self.pass_token = pass_token
|
|
37
|
+
self.api_key = api_key
|
|
38
|
+
self.trial_code = trial_code
|
|
39
|
+
self._owns_client = client is None
|
|
40
|
+
self._client = client or httpx.Client(timeout=timeout, headers={"User-Agent": "scrapling-data-sdk/0.1.0"})
|
|
41
|
+
|
|
42
|
+
def close(self) -> None:
|
|
43
|
+
if self._owns_client:
|
|
44
|
+
self._client.close()
|
|
45
|
+
|
|
46
|
+
def __enter__(self) -> "ScraplingData":
|
|
47
|
+
return self
|
|
48
|
+
|
|
49
|
+
def __exit__(self, *exc: object) -> None:
|
|
50
|
+
self.close()
|
|
51
|
+
|
|
52
|
+
# ---- core transport -------------------------------------------------
|
|
53
|
+
def _auth_headers(self) -> Dict[str, str]:
|
|
54
|
+
h: Dict[str, str] = {}
|
|
55
|
+
if self.pass_token:
|
|
56
|
+
h["X-Pass-Token"] = self.pass_token
|
|
57
|
+
if self.api_key:
|
|
58
|
+
h["Authorization"] = f"Bearer {self.api_key}"
|
|
59
|
+
if self.trial_code:
|
|
60
|
+
h["X-Trial-Code"] = self.trial_code
|
|
61
|
+
return h
|
|
62
|
+
|
|
63
|
+
def request(
|
|
64
|
+
self,
|
|
65
|
+
method: str,
|
|
66
|
+
path: str,
|
|
67
|
+
*,
|
|
68
|
+
params: Optional[Mapping[str, Any]] = None,
|
|
69
|
+
json_body: Any = None,
|
|
70
|
+
headers: Optional[Mapping[str, str]] = None,
|
|
71
|
+
raise_on_402: bool = True,
|
|
72
|
+
) -> Any:
|
|
73
|
+
"""Low-level request. On 402: try payment_handler once, else raise or return challenge."""
|
|
74
|
+
if not path.startswith("/"):
|
|
75
|
+
path = "/" + path
|
|
76
|
+
url = self.base_url + path
|
|
77
|
+
clean_params = None
|
|
78
|
+
if params:
|
|
79
|
+
clean_params = {k: v for k, v in params.items() if v is not None}
|
|
80
|
+
|
|
81
|
+
hdrs = self._auth_headers()
|
|
82
|
+
if headers:
|
|
83
|
+
hdrs.update(dict(headers))
|
|
84
|
+
|
|
85
|
+
resp = self._client.request(method.upper(), url, params=clean_params, json=json_body, headers=hdrs)
|
|
86
|
+
if resp.status_code != 402:
|
|
87
|
+
resp.raise_for_status()
|
|
88
|
+
if not resp.content:
|
|
89
|
+
return None
|
|
90
|
+
ctype = resp.headers.get("content-type", "")
|
|
91
|
+
return resp.json() if "json" in ctype else resp.text
|
|
92
|
+
|
|
93
|
+
body: Any
|
|
94
|
+
try:
|
|
95
|
+
body = resp.json()
|
|
96
|
+
except Exception:
|
|
97
|
+
body = resp.text
|
|
98
|
+
payment = parse_402(path, resp.status_code, dict(resp.headers), body)
|
|
99
|
+
|
|
100
|
+
if self.payment_handler is not None:
|
|
101
|
+
signature = self.payment_handler(payment)
|
|
102
|
+
hdrs2 = dict(hdrs)
|
|
103
|
+
hdrs2["PAYMENT-SIGNATURE"] = signature
|
|
104
|
+
# Some clients still send X-PAYMENT
|
|
105
|
+
hdrs2.setdefault("X-PAYMENT", signature)
|
|
106
|
+
resp2 = self._client.request(method.upper(), url, params=clean_params, json=json_body, headers=hdrs2)
|
|
107
|
+
if resp2.status_code == 402:
|
|
108
|
+
body2: Any
|
|
109
|
+
try:
|
|
110
|
+
body2 = resp2.json()
|
|
111
|
+
except Exception:
|
|
112
|
+
body2 = resp2.text
|
|
113
|
+
payment2 = parse_402(path, resp2.status_code, dict(resp2.headers), body2)
|
|
114
|
+
if raise_on_402:
|
|
115
|
+
raise PaymentRequiredError(payment2)
|
|
116
|
+
return payment2.to_dict()
|
|
117
|
+
resp2.raise_for_status()
|
|
118
|
+
return resp2.json() if resp2.content else None
|
|
119
|
+
|
|
120
|
+
if raise_on_402:
|
|
121
|
+
raise PaymentRequiredError(payment)
|
|
122
|
+
return payment.to_dict()
|
|
123
|
+
|
|
124
|
+
def get(self, path: str, **params: Any) -> Any:
|
|
125
|
+
return self.request("GET", path, params=params)
|
|
126
|
+
|
|
127
|
+
def get_soft(self, path: str, **params: Any) -> Any:
|
|
128
|
+
"""Like get(), but return a payment-required dict instead of raising."""
|
|
129
|
+
return self.request("GET", path, params=params, raise_on_402=False)
|
|
130
|
+
|
|
131
|
+
# ---- free helpers ---------------------------------------------------
|
|
132
|
+
def free_ping(self) -> Any:
|
|
133
|
+
return self.get("/v1/free/ping")
|
|
134
|
+
|
|
135
|
+
def free_feeds(self) -> Any:
|
|
136
|
+
return self.get("/v1/free/feeds")
|
|
137
|
+
|
|
138
|
+
def free_crypto_top10(self) -> Any:
|
|
139
|
+
return self.get("/v1/free/crypto/top10")
|
|
140
|
+
|
|
141
|
+
def free_defi_top_yields(self) -> Any:
|
|
142
|
+
return self.get("/v1/free/defi/top-yields")
|
|
143
|
+
|
|
144
|
+
def free_wallet_label(self, address: str) -> Any:
|
|
145
|
+
return self.get("/v1/free/wallet-label", address=address)
|
|
146
|
+
|
|
147
|
+
def free_intel_risk_check(self, address: str) -> Any:
|
|
148
|
+
"""3 free/IP/UTC-day, then 402 at $0.0045."""
|
|
149
|
+
return self.get_soft("/v1/free/intel/risk-check", address=address)
|
|
150
|
+
|
|
151
|
+
def free_hack_tracker(self, incident: str = "binance-2026") -> Any:
|
|
152
|
+
return self.get("/v1/free/hack-tracker", incident=incident)
|
|
153
|
+
|
|
154
|
+
def free_market_overview(self) -> Any:
|
|
155
|
+
return self.get("/v1/free/market-overview")
|
|
156
|
+
|
|
157
|
+
def free_address_poisoning(self, address: str) -> Any:
|
|
158
|
+
return self.get("/v1/free/address-poisoning", address=address)
|
|
159
|
+
|
|
160
|
+
def free_ens(self) -> Any:
|
|
161
|
+
return self.get("/v1/free/ens")
|
|
162
|
+
|
|
163
|
+
def free_prices(self, tokens: str = "btc,eth,sol") -> Any:
|
|
164
|
+
return self.get("/v1/free/prices", tokens=tokens)
|
|
165
|
+
|
|
166
|
+
# ---- paid methods (x402) --------------------------------------------
|
|
167
|
+
def counterparty(self, address: str, chain: str = "ethereum") -> Any:
|
|
168
|
+
return self.get("/v1/check/counterparty", address=address, chain=chain)
|
|
169
|
+
|
|
170
|
+
def wallet_label(self, address: str) -> Any:
|
|
171
|
+
return self.get("/v1/wallet-label", address=address)
|
|
172
|
+
|
|
173
|
+
def wallet_profile(self, address: str, chain: str = "ethereum") -> Any:
|
|
174
|
+
return self.get("/v1/wallet/profile", address=address, chain=chain)
|
|
175
|
+
|
|
176
|
+
def intel_risk_check(self, address: str) -> Any:
|
|
177
|
+
return self.get("/v1/intel/risk-check", address=address)
|
|
178
|
+
|
|
179
|
+
def intel_risk_batch(self, addresses: str) -> Any:
|
|
180
|
+
return self.get("/v1/intel/risk-check/batch", addresses=addresses)
|
|
181
|
+
|
|
182
|
+
def hack_tracker(self, incident: str = "binance-2026", limit: int = 100) -> Any:
|
|
183
|
+
return self.get("/v1/lookup/hack-tracker", incident=incident, limit=limit)
|
|
184
|
+
|
|
185
|
+
def market_pulse(self) -> Any:
|
|
186
|
+
return self.get("/v1/bundle/market-pulse")
|
|
187
|
+
|
|
188
|
+
def defi_intel(self) -> Any:
|
|
189
|
+
return self.get("/v1/bundle/defi-intel")
|
|
190
|
+
|
|
191
|
+
def yields(self, chain: Optional[str] = None, token: Optional[str] = None) -> Any:
|
|
192
|
+
return self.get("/v1/lookup/yields", chain=chain, token=token)
|
|
193
|
+
|
|
194
|
+
def tokenized_stocks(self, ticker: Optional[str] = None, **kw: Any) -> Any:
|
|
195
|
+
return self.get("/v1/lookup/tokenized-stocks", ticker=ticker, **kw)
|
|
196
|
+
|
|
197
|
+
def crypto_treasuries(self, asset: Optional[str] = None, **kw: Any) -> Any:
|
|
198
|
+
return self.get("/v1/lookup/crypto-treasuries", asset=asset, **kw)
|
|
199
|
+
|
|
200
|
+
def tokenized_real_estate(self, city: Optional[str] = None, **kw: Any) -> Any:
|
|
201
|
+
return self.get("/v1/lookup/tokenized-real-estate", city=city, **kw)
|
|
202
|
+
|
|
203
|
+
def token_verdict(self, address: str, chain: str = "base") -> Any:
|
|
204
|
+
return self.get("/v1/token/verdict", address=address, chain=chain)
|
|
205
|
+
|
|
206
|
+
def token_risk(self, address: str) -> Any:
|
|
207
|
+
return self.get("/v1/lookup/token-risk", address=address)
|
|
208
|
+
|
|
209
|
+
def trust_agent(self, address: str, chain: str = "base") -> Any:
|
|
210
|
+
return self.get("/v1/trust/agent", address=address, chain=chain)
|
|
211
|
+
|
|
212
|
+
def coin(self, q: str) -> Any:
|
|
213
|
+
return self.get("/v1/lookup/coin", q=q)
|
|
214
|
+
|
|
215
|
+
def protocol(self, q: str) -> Any:
|
|
216
|
+
return self.get("/v1/lookup/protocol", q=q)
|
|
217
|
+
|
|
218
|
+
def ens(self, name: Optional[str] = None, address: Optional[str] = None) -> Any:
|
|
219
|
+
return self.get("/v1/lookup/ens", name=name, address=address)
|
|
220
|
+
|
|
221
|
+
def address_poisoning(self, address: str, hours: int = 6) -> Any:
|
|
222
|
+
return self.get("/v1/lookup/address-poisoning", address=address, hours=hours)
|
|
223
|
+
|
|
224
|
+
def whale_moves(self, min_usd: float = 100000, window: str = "1h", **kw: Any) -> Any:
|
|
225
|
+
return self.get("/v1/whales/moves", min_usd=min_usd, window=window, **kw)
|
|
226
|
+
|
|
227
|
+
def whale_netflow(self, chain: str = "all", window: str = "24h", **kw: Any) -> Any:
|
|
228
|
+
return self.get("/v1/signals/whale-netflow", chain=chain, window=window, **kw)
|
|
229
|
+
|
|
230
|
+
def regime_now(self) -> Any:
|
|
231
|
+
return self.get("/v1/regime/now")
|
|
232
|
+
|
|
233
|
+
def dd_quick(self, address: str, chain: str = "base") -> Any:
|
|
234
|
+
return self.get("/v1/dd/quick", address=address, chain=chain)
|
|
235
|
+
|
|
236
|
+
def dd_report(self, address: str, chain: str = "base") -> Any:
|
|
237
|
+
return self.get("/v1/dd/report", address=address, chain=chain)
|
|
238
|
+
|
|
239
|
+
def ping(self) -> Any:
|
|
240
|
+
"""Cheapest paid call ($0.001) — payment smoke test."""
|
|
241
|
+
return self.get("/v1/ping")
|
|
242
|
+
|
|
243
|
+
def pass_day(self) -> Any:
|
|
244
|
+
return self.get("/v1/pass/day")
|
|
245
|
+
|
|
246
|
+
def pass_month(self) -> Any:
|
|
247
|
+
return self.get("/v1/pass/month")
|
|
248
|
+
|
|
249
|
+
def url(self, path: str, **params: Any) -> str:
|
|
250
|
+
if not path.startswith("/"):
|
|
251
|
+
path = "/" + path
|
|
252
|
+
q = urlencode({k: v for k, v in params.items() if v is not None})
|
|
253
|
+
return self.base_url + path + (("?" + q) if q else "")
|
scrapling_data/local.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Free local helpers — no network, no payment. Sticky first taste of Scrapling Data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from scrapling_data._keccak import keccak256
|
|
10
|
+
|
|
11
|
+
_HEX40 = re.compile(r"^0x[0-9a-fA-F]{40}$")
|
|
12
|
+
_HEX_BARE = re.compile(r"^[0-9a-fA-F]{40}$")
|
|
13
|
+
# Solana base58 addresses are typically 32–44 chars; exclude pure hex/EVM.
|
|
14
|
+
_B58 = re.compile(r"^[1-9A-HJ-NP-Za-km-z]{32,44}$")
|
|
15
|
+
_ENS = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$", re.I)
|
|
16
|
+
|
|
17
|
+
# Well-known chain ids (hints only — not a network call).
|
|
18
|
+
CHAIN_IDS = {
|
|
19
|
+
"ethereum": 1,
|
|
20
|
+
"base": 8453,
|
|
21
|
+
"polygon": 137,
|
|
22
|
+
"arbitrum": 42161,
|
|
23
|
+
"optimism": 10,
|
|
24
|
+
"bsc": 56,
|
|
25
|
+
"avalanche": 43114,
|
|
26
|
+
"gnosis": 100,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
ZERO_ADDRESS = "0x0000000000000000000000000000000000000000"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class AddressInfo:
|
|
34
|
+
"""Result of free local address inspection."""
|
|
35
|
+
|
|
36
|
+
input: str
|
|
37
|
+
kind: str # evm | solana | ens | unknown
|
|
38
|
+
valid: bool
|
|
39
|
+
normalized: Optional[str] = None
|
|
40
|
+
checksum: Optional[str] = None
|
|
41
|
+
checksum_ok: Optional[bool] = None
|
|
42
|
+
is_zero: bool = False
|
|
43
|
+
notes: tuple = ()
|
|
44
|
+
|
|
45
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
46
|
+
d = asdict(self)
|
|
47
|
+
d["notes"] = list(self.notes)
|
|
48
|
+
return d
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_evm_address(value: str) -> bool:
|
|
52
|
+
"""True if *value* looks like a 20-byte EVM address (with or without 0x)."""
|
|
53
|
+
if not isinstance(value, str):
|
|
54
|
+
return False
|
|
55
|
+
v = value.strip()
|
|
56
|
+
return bool(_HEX40.match(v) or _HEX_BARE.match(v))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def is_solana_address(value: str) -> bool:
|
|
60
|
+
"""Heuristic: base58 length typical of Solana pubkeys, not an EVM hex address."""
|
|
61
|
+
if not isinstance(value, str):
|
|
62
|
+
return False
|
|
63
|
+
v = value.strip()
|
|
64
|
+
if is_evm_address(v) or "." in v:
|
|
65
|
+
return False
|
|
66
|
+
return bool(_B58.match(v))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_ens_name(value: str) -> bool:
|
|
70
|
+
if not isinstance(value, str):
|
|
71
|
+
return False
|
|
72
|
+
v = value.strip().lower()
|
|
73
|
+
return bool(_ENS.match(v) and "." in v and " " not in v)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def to_checksum_address(address: str) -> str:
|
|
77
|
+
"""EIP-55 checksum encoding. Raises ValueError on bad input."""
|
|
78
|
+
if not is_evm_address(address):
|
|
79
|
+
raise ValueError(f"not an EVM address: {address!r}")
|
|
80
|
+
hex_part = address.strip().lower().replace("0x", "")
|
|
81
|
+
digest = keccak256(hex_part.encode("ascii")).hex()
|
|
82
|
+
out = ["0x"]
|
|
83
|
+
for i, ch in enumerate(hex_part):
|
|
84
|
+
if ch in "0123456789":
|
|
85
|
+
out.append(ch)
|
|
86
|
+
else:
|
|
87
|
+
out.append(ch.upper() if int(digest[i], 16) >= 8 else ch)
|
|
88
|
+
return "".join(out)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def normalize_evm(address: str) -> str:
|
|
92
|
+
"""Lowercase 0x-prefixed EVM address."""
|
|
93
|
+
if not is_evm_address(address):
|
|
94
|
+
raise ValueError(f"not an EVM address: {address!r}")
|
|
95
|
+
v = address.strip()
|
|
96
|
+
if not v.startswith("0x") and not v.startswith("0X"):
|
|
97
|
+
v = "0x" + v
|
|
98
|
+
return "0x" + v[2:].lower()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def lookalike_distance(a: str, b: str) -> Dict[str, Any]:
|
|
102
|
+
"""
|
|
103
|
+
Free address-poisoning heuristic: how similar are two EVM addresses?
|
|
104
|
+
|
|
105
|
+
Compares first/last nibbles (what wallets show truncated) and Hamming distance
|
|
106
|
+
on the full hex. High head+tail overlap with a different middle is a classic
|
|
107
|
+
poisoning pattern. Does NOT call any API.
|
|
108
|
+
"""
|
|
109
|
+
if not (is_evm_address(a) and is_evm_address(b)):
|
|
110
|
+
raise ValueError("both inputs must be EVM addresses")
|
|
111
|
+
aa = normalize_evm(a)[2:]
|
|
112
|
+
bb = normalize_evm(b)[2:]
|
|
113
|
+
hamming = sum(x != y for x, y in zip(aa, bb))
|
|
114
|
+
head = 0
|
|
115
|
+
for x, y in zip(aa, bb):
|
|
116
|
+
if x != y:
|
|
117
|
+
break
|
|
118
|
+
head += 1
|
|
119
|
+
tail = 0
|
|
120
|
+
for x, y in zip(aa[::-1], bb[::-1]):
|
|
121
|
+
if x != y:
|
|
122
|
+
break
|
|
123
|
+
tail += 1
|
|
124
|
+
# Score 0–100: identical head/tail with different middle = suspicious.
|
|
125
|
+
same = a.lower() == b.lower()
|
|
126
|
+
poison_score = 0
|
|
127
|
+
if not same:
|
|
128
|
+
poison_score = min(100, head * 4 + tail * 4 - hamming)
|
|
129
|
+
poison_score = max(0, poison_score)
|
|
130
|
+
return {
|
|
131
|
+
"a": to_checksum_address(a),
|
|
132
|
+
"b": to_checksum_address(b),
|
|
133
|
+
"identical": same,
|
|
134
|
+
"hamming_hex_chars": hamming,
|
|
135
|
+
"matching_prefix_chars": head,
|
|
136
|
+
"matching_suffix_chars": tail,
|
|
137
|
+
"poison_lookalike_score_0_100": poison_score,
|
|
138
|
+
"verdict": (
|
|
139
|
+
"identical"
|
|
140
|
+
if same
|
|
141
|
+
else "high_lookalike_risk"
|
|
142
|
+
if poison_score >= 60
|
|
143
|
+
else "moderate_lookalike"
|
|
144
|
+
if poison_score >= 30
|
|
145
|
+
else "distinct"
|
|
146
|
+
),
|
|
147
|
+
"upgrade": {
|
|
148
|
+
"endpoint": "/v1/lookup/address-poisoning",
|
|
149
|
+
"price_usd": 0.0009,
|
|
150
|
+
"why": "Live Base USDC transfer lookalike flags with tx hashes (paid via x402).",
|
|
151
|
+
"docs": "https://scraplingdata.xyz/docs",
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def detect_kind(value: str) -> str:
|
|
157
|
+
if is_evm_address(value):
|
|
158
|
+
return "evm"
|
|
159
|
+
if is_ens_name(value):
|
|
160
|
+
return "ens"
|
|
161
|
+
if is_solana_address(value):
|
|
162
|
+
return "solana"
|
|
163
|
+
return "unknown"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def inspect_address(value: str) -> AddressInfo:
|
|
167
|
+
"""Free local inspection of an address / ENS name."""
|
|
168
|
+
raw = (value or "").strip()
|
|
169
|
+
kind = detect_kind(raw)
|
|
170
|
+
notes: List[str] = []
|
|
171
|
+
if kind == "evm":
|
|
172
|
+
norm = normalize_evm(raw)
|
|
173
|
+
checksum = to_checksum_address(norm)
|
|
174
|
+
body = raw[2:] if raw.lower().startswith("0x") else raw
|
|
175
|
+
mixed = any(c.isupper() for c in body) and any(c.islower() for c in body)
|
|
176
|
+
checksum_ok = (body == checksum[2:]) if mixed else None
|
|
177
|
+
is_zero = norm == ZERO_ADDRESS
|
|
178
|
+
if is_zero:
|
|
179
|
+
notes.append("This is the zero address — usually invalid as a counterparty.")
|
|
180
|
+
if mixed and checksum_ok is False:
|
|
181
|
+
notes.append("Mixed-case address fails EIP-55 checksum — treat with caution.")
|
|
182
|
+
notes.append(
|
|
183
|
+
"Free local check only. Paid counterparty / risk-check / wallet-label add real intel."
|
|
184
|
+
)
|
|
185
|
+
return AddressInfo(
|
|
186
|
+
input=raw,
|
|
187
|
+
kind="evm",
|
|
188
|
+
valid=True,
|
|
189
|
+
normalized=norm,
|
|
190
|
+
checksum=checksum,
|
|
191
|
+
checksum_ok=checksum_ok,
|
|
192
|
+
is_zero=is_zero,
|
|
193
|
+
notes=tuple(notes),
|
|
194
|
+
)
|
|
195
|
+
if kind == "ens":
|
|
196
|
+
notes.append("ENS name detected — resolve with free /v1/free/ens sample or paid /v1/lookup/ens ($0.001).")
|
|
197
|
+
return AddressInfo(input=raw, kind="ens", valid=True, normalized=raw.lower(), notes=tuple(notes))
|
|
198
|
+
if kind == "solana":
|
|
199
|
+
notes.append("Solana-looking address — use chain=solana on counterparty / wallet profile / holders.")
|
|
200
|
+
return AddressInfo(input=raw, kind="solana", valid=True, normalized=raw, notes=tuple(notes))
|
|
201
|
+
notes.append("Could not classify. Pass an EVM 0x address, Solana base58, or ENS name.")
|
|
202
|
+
return AddressInfo(input=raw, kind="unknown", valid=False, notes=tuple(notes))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def chain_hint(name_or_id: str | int) -> Dict[str, Any]:
|
|
206
|
+
"""Map a chain name or id to a hint dict (local only)."""
|
|
207
|
+
if isinstance(name_or_id, int):
|
|
208
|
+
for name, cid in CHAIN_IDS.items():
|
|
209
|
+
if cid == name_or_id:
|
|
210
|
+
return {"name": name, "chain_id": cid, "source": "local"}
|
|
211
|
+
return {"name": None, "chain_id": name_or_id, "source": "local", "known": False}
|
|
212
|
+
key = str(name_or_id).strip().lower()
|
|
213
|
+
aliases = {"eth": "ethereum", "arb": "arbitrum", "matic": "polygon", "avax": "avalanche", "bnb": "bsc"}
|
|
214
|
+
key = aliases.get(key, key)
|
|
215
|
+
if key in CHAIN_IDS:
|
|
216
|
+
return {"name": key, "chain_id": CHAIN_IDS[key], "source": "local", "known": True}
|
|
217
|
+
return {"name": key, "chain_id": None, "source": "local", "known": False}
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def paid_teasers_for(kind: str) -> List[Dict[str, Any]]:
|
|
221
|
+
"""What paid Scrapling Data calls unlock next — used by CLI and agents."""
|
|
222
|
+
common = [
|
|
223
|
+
{
|
|
224
|
+
"name": "counterparty_check",
|
|
225
|
+
"path": "/v1/check/counterparty",
|
|
226
|
+
"price_usd": 0.004,
|
|
227
|
+
"blurb": "safe / caution / block + score before you pay or trade",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
"name": "intel_risk_check",
|
|
231
|
+
"path": "/v1/intel/risk-check",
|
|
232
|
+
"price_usd": 0.0045,
|
|
233
|
+
"blurb": "MALICIOUS / HIGH_RISK / WATCH / LOW_RISK (3 free/IP/day)",
|
|
234
|
+
"free_teaser": "/v1/free/intel/risk-check",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
"name": "wallet_label",
|
|
238
|
+
"path": "/v1/wallet-label",
|
|
239
|
+
"price_usd": 0.002,
|
|
240
|
+
"blurb": "entity, category, confidence, source links",
|
|
241
|
+
"free_teaser": "/v1/free/wallet-label",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"name": "wallet_profile",
|
|
245
|
+
"path": "/v1/wallet/profile",
|
|
246
|
+
"price_usd": 0.005,
|
|
247
|
+
"blurb": "exchange/whale/trader/fresh/dormant + activity score",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"name": "hack_tracker",
|
|
251
|
+
"path": "/v1/lookup/hack-tracker",
|
|
252
|
+
"price_usd": 0.03,
|
|
253
|
+
"blurb": "stolen-funds tracker (attacker wallets, swaps, CEX alerts)",
|
|
254
|
+
"free_teaser": "/v1/free/hack-tracker",
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"name": "trust_agent",
|
|
258
|
+
"path": "/v1/trust/agent",
|
|
259
|
+
"price_usd": 0.04,
|
|
260
|
+
"blurb": "allow / caution / block trust bundle for agents",
|
|
261
|
+
},
|
|
262
|
+
]
|
|
263
|
+
if kind == "solana":
|
|
264
|
+
common.insert(
|
|
265
|
+
1,
|
|
266
|
+
{
|
|
267
|
+
"name": "token_holders",
|
|
268
|
+
"path": "/v1/token/holders",
|
|
269
|
+
"price_usd": 0.001,
|
|
270
|
+
"blurb": "Solana holder concentration + exchange-held share",
|
|
271
|
+
},
|
|
272
|
+
)
|
|
273
|
+
if kind == "ens":
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
"name": "ens_lookup",
|
|
277
|
+
"path": "/v1/lookup/ens",
|
|
278
|
+
"price_usd": 0.001,
|
|
279
|
+
"blurb": "resolve any ENS name or reverse-resolve an address",
|
|
280
|
+
"free_teaser": "/v1/free/ens",
|
|
281
|
+
},
|
|
282
|
+
*common[:3],
|
|
283
|
+
]
|
|
284
|
+
return common
|
scrapling_data/x402.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""x402 payment-required parsing and helpers (no private keys, no auto-signing)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
9
|
+
|
|
10
|
+
from scrapling_data import catalog
|
|
11
|
+
|
|
12
|
+
PaymentHandler = Callable[["PaymentRequired"], str]
|
|
13
|
+
# Handler returns the value for the PAYMENT-SIGNATURE header (or X-PAYMENT).
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PaymentRequired:
|
|
18
|
+
"""Parsed HTTP 402 challenge from Scrapling Data."""
|
|
19
|
+
|
|
20
|
+
status_code: int
|
|
21
|
+
path: str
|
|
22
|
+
body: Dict[str, Any]
|
|
23
|
+
payment_required_header: Optional[str] = None
|
|
24
|
+
accepts: List[Dict[str, Any]] = field(default_factory=list)
|
|
25
|
+
price_usd_hint: Optional[float] = None
|
|
26
|
+
message: str = ""
|
|
27
|
+
|
|
28
|
+
def preferred_accept(self) -> Optional[Dict[str, Any]]:
|
|
29
|
+
"""Prefer Base (eip155:8453) USDC exact payment when listed."""
|
|
30
|
+
if not self.accepts:
|
|
31
|
+
return None
|
|
32
|
+
for a in self.accepts:
|
|
33
|
+
if a.get("network") == "eip155:8453" and a.get("scheme") == "exact":
|
|
34
|
+
return a
|
|
35
|
+
return self.accepts[0]
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
38
|
+
pref = self.preferred_accept()
|
|
39
|
+
return {
|
|
40
|
+
"error": "payment_required",
|
|
41
|
+
"status_code": self.status_code,
|
|
42
|
+
"path": self.path,
|
|
43
|
+
"price_usd_hint": self.price_usd_hint,
|
|
44
|
+
"message": self.message,
|
|
45
|
+
"accepts": self.accepts,
|
|
46
|
+
"preferred": pref,
|
|
47
|
+
"how_to_pay": {
|
|
48
|
+
"steps": [
|
|
49
|
+
"Pick one entry from accepts (Base eip155:8453 USDC preferred).",
|
|
50
|
+
"Sign an x402 exact payment with your wallet/signer (never paste a private key into this library).",
|
|
51
|
+
"Retry the same request with header PAYMENT-SIGNATURE: <signed payload>.",
|
|
52
|
+
"Or use day/month pass: /v1/pass/day ($2) / /v1/pass/month ($25).",
|
|
53
|
+
],
|
|
54
|
+
"docs": catalog.DOCS_URL,
|
|
55
|
+
"test_buy": catalog.TEST_BUY_URL,
|
|
56
|
+
"manifest": catalog.X402_MANIFEST,
|
|
57
|
+
"python_hint": "client = ScraplingData(payment_handler=your_signer_fn)",
|
|
58
|
+
},
|
|
59
|
+
"body": self.body,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class PaymentRequiredError(Exception):
|
|
64
|
+
"""Raised when a paid route returns 402 and no payment_handler settled it."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, payment: PaymentRequired):
|
|
67
|
+
self.payment = payment
|
|
68
|
+
super().__init__(payment.message or "Payment required (x402)")
|
|
69
|
+
|
|
70
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
71
|
+
return self.payment.to_dict()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _b64json(header_val: str) -> Optional[Dict[str, Any]]:
|
|
75
|
+
try:
|
|
76
|
+
raw = base64.b64decode(header_val + "===") # tolerate missing pad
|
|
77
|
+
return json.loads(raw.decode("utf-8"))
|
|
78
|
+
except Exception:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_402(
|
|
83
|
+
path: str,
|
|
84
|
+
status_code: int,
|
|
85
|
+
headers: Dict[str, str],
|
|
86
|
+
body: Union[Dict[str, Any], str, None],
|
|
87
|
+
) -> PaymentRequired:
|
|
88
|
+
hdrs = {k.lower(): v for k, v in headers.items()}
|
|
89
|
+
pr_header = hdrs.get("payment-required")
|
|
90
|
+
parsed_header = _b64json(pr_header) if pr_header else None
|
|
91
|
+
if isinstance(body, str):
|
|
92
|
+
try:
|
|
93
|
+
body_obj: Dict[str, Any] = json.loads(body)
|
|
94
|
+
except Exception:
|
|
95
|
+
body_obj = {"raw": body}
|
|
96
|
+
elif isinstance(body, dict):
|
|
97
|
+
body_obj = body
|
|
98
|
+
else:
|
|
99
|
+
body_obj = {}
|
|
100
|
+
|
|
101
|
+
source = parsed_header or body_obj
|
|
102
|
+
accepts = list(source.get("accepts") or body_obj.get("accepts") or [])
|
|
103
|
+
hint = catalog.price_of(path)
|
|
104
|
+
# Prefer extensions.priceUsd or amount/1e6 from preferred accept
|
|
105
|
+
price = hint
|
|
106
|
+
for a in accepts:
|
|
107
|
+
if a.get("network") == "eip155:8453":
|
|
108
|
+
try:
|
|
109
|
+
price = int(a.get("amount", 0)) / 1_000_000
|
|
110
|
+
except Exception:
|
|
111
|
+
pass
|
|
112
|
+
break
|
|
113
|
+
if price is None and accepts:
|
|
114
|
+
try:
|
|
115
|
+
price = int(accepts[0].get("amount", 0)) / 1_000_000
|
|
116
|
+
except Exception:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
msg = (
|
|
120
|
+
f"HTTP 402 Payment Required for {path}"
|
|
121
|
+
+ (f" (~${price} USDC via x402)" if price is not None else "")
|
|
122
|
+
+ f". Pass a payment_handler to ScraplingData, or open {catalog.DOCS_URL}."
|
|
123
|
+
)
|
|
124
|
+
return PaymentRequired(
|
|
125
|
+
status_code=status_code,
|
|
126
|
+
path=path,
|
|
127
|
+
body=body_obj,
|
|
128
|
+
payment_required_header=pr_header,
|
|
129
|
+
accepts=accepts,
|
|
130
|
+
price_usd_hint=price,
|
|
131
|
+
message=msg,
|
|
132
|
+
)
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: scrapling-data
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lead-magnet SDK for Scrapling Data: free address tools + x402 pay-per-call crypto intel for AI agents (USDC on Base, no API key).
|
|
5
|
+
Project-URL: Homepage, https://scraplingdata.xyz
|
|
6
|
+
Project-URL: Documentation, https://scraplingdata.xyz/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/scraplingdata
|
|
8
|
+
Project-URL: Hugging Face, https://huggingface.co/scraplingdata
|
|
9
|
+
Project-URL: OpenAPI, https://scraplingdata.xyz/openapi.json
|
|
10
|
+
Project-URL: llms.txt, https://scraplingdata.xyz/llms.txt
|
|
11
|
+
Project-URL: x402 manifest, https://scraplingdata.xyz/.well-known/x402
|
|
12
|
+
Project-URL: Issues, https://github.com/scraplingdata/scrapling-data-skill/issues
|
|
13
|
+
Author-email: Scrapling Data <scraplingdata@mail.grokbot.com>
|
|
14
|
+
License-Expression: MIT
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Keywords: ai-agents,base,crypto,defi,mcp,scrapling,usdc,wallet,x402
|
|
17
|
+
Classifier: Development Status :: 4 - Beta
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
26
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
27
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
28
|
+
Classifier: Typing :: Typed
|
|
29
|
+
Requires-Python: >=3.9
|
|
30
|
+
Requires-Dist: httpx>=0.27
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# scrapling-data
|
|
38
|
+
|
|
39
|
+
**Lead-magnet Python SDK** for [Scrapling Data](https://scraplingdata.xyz) — pay-per-call crypto & web intel for AI agents via **x402** (USDC on Base, no API key).
|
|
40
|
+
|
|
41
|
+
Install once. Get **free** local address tools and free store teasers. When you need the real answer, the same client speaks **HTTP 402 → pay → retry** against the live catalog.
|
|
42
|
+
|
|
43
|
+
- Site: https://scraplingdata.xyz
|
|
44
|
+
- Docs: https://scraplingdata.xyz/docs
|
|
45
|
+
- GitHub: https://github.com/scraplingdata
|
|
46
|
+
- Hugging Face: https://huggingface.co/scraplingdata
|
|
47
|
+
- MCP: https://scraplingdata.xyz/mcp
|
|
48
|
+
|
|
49
|
+
> This package is the **public thin SDK**. It does **not** include Scrapling's private scrapers, label builders, or secrets.
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install scrapling-data
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
(Until published: `pip install -e /path/to/scrapling-data`)
|
|
58
|
+
|
|
59
|
+
## 30-second sticky start
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Free local checksum + free wallet-label teaser + paid price menu
|
|
63
|
+
scrapling-data check 0x28C6c06298d514Db089934071355E5743bf21d60
|
|
64
|
+
|
|
65
|
+
# Lookalike / address-poisoning heuristic (local, free)
|
|
66
|
+
scrapling-data check 0x28C6c06298d514Db089934071355E5743bf21d60 \
|
|
67
|
+
--compare 0x28C6c06298d514Db089934071355E5743bf21d61
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from scrapling_data import ScraplingData, inspect_address, to_checksum_address
|
|
72
|
+
|
|
73
|
+
print(inspect_address("0x28c6c06298d514db089934071355e5743bf21d60"))
|
|
74
|
+
print(to_checksum_address("0x28c6c06298d514db089934071355e5743bf21d60"))
|
|
75
|
+
|
|
76
|
+
with ScraplingData() as sd:
|
|
77
|
+
print(sd.free_crypto_top10()) # free
|
|
78
|
+
print(sd.free_wallet_label("0x28C6c06298d514Db089934071355E5743bf21d60"))
|
|
79
|
+
# Paid — raises PaymentRequiredError with accepts[] unless you attach a signer:
|
|
80
|
+
# sd.counterparty("0x28C6c06298d514Db089934071355E5743bf21d60")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## What's free vs paid
|
|
84
|
+
|
|
85
|
+
| Layer | What you get | Cost |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| **Local** | EVM validate, EIP-55 checksum, ENS/Solana detect, lookalike/poison heuristic, chain id hints | Free, offline |
|
|
88
|
+
| **Free API** | `/v1/free/*` teasers (wallet-label sample, hack-tracker headline, top10, yields, 3×/day risk-check, …) | Free (rate-limited) |
|
|
89
|
+
| **Paid API** | Full counterparty, risk-check, hack-tracker, yields, market-pulse, tokenized assets, whales, DD, … | From **$0.0009** USDC via x402 |
|
|
90
|
+
|
|
91
|
+
### Sticky funnel (by design)
|
|
92
|
+
|
|
93
|
+
1. `scrapling-data check <addr>` → valid + checksum + free label teaser.
|
|
94
|
+
2. See the **paid menu** with live catalog prices.
|
|
95
|
+
3. Call `ScraplingData(...).counterparty(addr)` (or risk-check / hack-tracker / …).
|
|
96
|
+
4. Without a wallet: clear **402** payload + docs link. With `payment_handler`: auto-retry with `PAYMENT-SIGNATURE`.
|
|
97
|
+
|
|
98
|
+
## Paying with x402
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from scrapling_data import ScraplingData, PaymentRequiredError
|
|
102
|
+
|
|
103
|
+
def my_signer(payment):
|
|
104
|
+
"""Return a PAYMENT-SIGNATURE string. Use your own wallet / x402 client.
|
|
105
|
+
Never put a private key in this library or in source control."""
|
|
106
|
+
accept = payment.preferred_accept()
|
|
107
|
+
# ... sign accept with your viem / eth-account / Coinbase AgentKit / x402 SDK ...
|
|
108
|
+
raise NotImplementedError("wire your signer here")
|
|
109
|
+
|
|
110
|
+
sd = ScraplingData(payment_handler=my_signer)
|
|
111
|
+
try:
|
|
112
|
+
print(sd.counterparty("0x28C6c06298d514Db089934071355E5743bf21d60"))
|
|
113
|
+
except PaymentRequiredError as e:
|
|
114
|
+
print(e.payment.to_dict()) # accepts, amount, payTo, how_to_pay
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Also supported: `pass_token=` (day/month pass), `api_key=` (`Bearer sk_scrapling_...`), `trial_code=`.
|
|
118
|
+
|
|
119
|
+
Day pass **$2** · month pass **$25** · settlement payTo (EVM): `0x72C80C01fD89Bac74f47cf604b61ACd119c9BCdb`.
|
|
120
|
+
|
|
121
|
+
## Highlight paid routes (catalog hints)
|
|
122
|
+
|
|
123
|
+
Prices from live `/llms.txt` + OpenAPI (Nov 2026). **402 body is authoritative.**
|
|
124
|
+
|
|
125
|
+
| Route | USD | Use |
|
|
126
|
+
| --- | ---: | --- |
|
|
127
|
+
| `/v1/lookup/address-poisoning` | 0.0009 | Live Base USDC lookalike flags |
|
|
128
|
+
| `/v1/bundle/market-pulse` | 0.001 | Whole-market pulse |
|
|
129
|
+
| `/v1/wallet-label` | 0.002 | Full label + sources |
|
|
130
|
+
| `/v1/check/counterparty` | 0.004 | safe / caution / block |
|
|
131
|
+
| `/v1/intel/risk-check` | 0.0045 | MALICIOUS…UNKNOWN (3 free/day) |
|
|
132
|
+
| `/v1/wallet/profile` | 0.005 | Who is this wallet? |
|
|
133
|
+
| `/v1/lookup/tokenized-stocks` | 0.005 | Ondo / xStocks |
|
|
134
|
+
| `/v1/lookup/hack-tracker` | 0.03 | Stolen-funds tracker |
|
|
135
|
+
| `/v1/trust/agent` | 0.04 | Agent trust bundle |
|
|
136
|
+
| `/v1/dd/report` | 1.00 | Full due-diligence |
|
|
137
|
+
| `/v1/bundles/stolen-funds/download` | 12 | Pack download (not on passes) |
|
|
138
|
+
|
|
139
|
+
Full machine list: https://scraplingdata.xyz/llms.txt · https://scraplingdata.xyz/openapi.json
|
|
140
|
+
|
|
141
|
+
## CLI
|
|
142
|
+
|
|
143
|
+
```text
|
|
144
|
+
scrapling-data check <address> [--compare 0x...] [--offline] [--json]
|
|
145
|
+
scrapling-data catalog [--max-price 0.01]
|
|
146
|
+
scrapling-data ping
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## For agents / MCP
|
|
150
|
+
|
|
151
|
+
See [`AGENT.md`](./AGENT.md). Remote MCP (same store): `https://scraplingdata.xyz/mcp`.
|
|
152
|
+
|
|
153
|
+
## License
|
|
154
|
+
|
|
155
|
+
MIT © Scrapling Data
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
scrapling_data/__init__.py,sha256=n2gZr1PXI-s-UvavQLncZ0cE4i7TQss4pP6oGONfObQ,1049
|
|
2
|
+
scrapling_data/__main__.py,sha256=HRUOs4hOqMu9Jt_tfRlb3NSIVXO-bnqJR0LY3FAJOJk,62
|
|
3
|
+
scrapling_data/_keccak.py,sha256=RcYmiWGZzewR9FFX0V0XNsmXrKJjGycqTfbxuxpIcc0,2449
|
|
4
|
+
scrapling_data/catalog.py,sha256=e_9mohIQYuLEjItES4nLkD6fW1RD_kfPHQ3I9tDzCJ0,5341
|
|
5
|
+
scrapling_data/cli.py,sha256=jVHCx1OMv0A5ftX_h20NllnW7RsRyfz-0cozI6FVE80,6835
|
|
6
|
+
scrapling_data/client.py,sha256=snlcQtOY4ds9Frjb2vjXzVCfe2ACWKvj3MQ1PokDb7g,9700
|
|
7
|
+
scrapling_data/local.py,sha256=h4dBns7YlLGSrcTbDCVs0Kov-4j0Dpb0XrzZVdyn-6o,9881
|
|
8
|
+
scrapling_data/x402.py,sha256=FktPScAnTKZ36ofx9RntD0oUAOBquMvbjldVpqSs_dY,4432
|
|
9
|
+
scrapling_data-0.1.0.dist-info/METADATA,sha256=InPgciy8fxg1qmd8FYA37gEVAL3WW5W271gu7k-R1Zo,6382
|
|
10
|
+
scrapling_data-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
11
|
+
scrapling_data-0.1.0.dist-info/entry_points.txt,sha256=1XAeUoEdpg5h6EdRhDUVHLA3lRABenbcIsEgcZPjWTo,59
|
|
12
|
+
scrapling_data-0.1.0.dist-info/licenses/LICENSE,sha256=VleBzJFNM4YHe1ugxQJwiCyTnD2tgnhj_6ODkVD03oo,1071
|
|
13
|
+
scrapling_data-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Scrapling Data
|
|
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.
|