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.
@@ -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,3 @@
1
+ from scrapling_data.cli import main
2
+
3
+ raise SystemExit(main())
@@ -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())
@@ -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 "")
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ scrapling-data = scrapling_data.cli:main
@@ -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.