blockintql 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,17 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Block6IQ
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.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: blockintql
3
+ Version: 1.0.0
4
+ Summary: BlockINTQL — Sovereign Blockchain Intelligence CLI
5
+ Home-page: https://blockintql.com
6
+ Author: Block6IQ
7
+ Author-email: joe@block6iq.com
8
+ Keywords: blockchain bitcoin ethereum forensics compliance aml kyc intelligence agents
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Security
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: click>=8.0.0
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: rich>=13.0.0
20
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: keywords
27
+ Dynamic: license-file
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # BlockINTQL CLI
33
+
34
+ Sovereign blockchain intelligence from the command line.
35
+ Built for AI agents, compliance teams, and developers.
36
+
37
+ ## Install
38
+
39
+ pip install blockintql
40
+
41
+ ## Setup
42
+
43
+ blockintql auth --api-key biq_sk_live_YOUR_KEY
44
+
45
+ Get an API key at blockintql.com
46
+
47
+ ## Usage
48
+
49
+ # Screen before accepting payment
50
+ blockintql screen --address 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
51
+
52
+ # Enrich with your own Chainalysis/TRM key
53
+ blockintql screen --address 0x123... --chain ethereum \
54
+ --provider chainalysis --provider-key $KEY
55
+
56
+ # Natural language intelligence
57
+ blockintql query "is this address linked to Lazarus Group?"
58
+
59
+ # Multi-agent analysis
60
+ blockintql analyze "check if these wallets transacted with each other" \
61
+ --address 0x123... --address 0x456...
62
+
63
+ # OP_RETURN identity search
64
+ blockintql profile --identifier @lazarus_trader
65
+
66
+ # Trace funds FIFO/LIFO
67
+ blockintql trace --txid abc123... --hops 5
68
+
69
+ # List attribution providers
70
+ blockintql providers
71
+
72
+ # Install skills into agent context
73
+ blockintql skills --install >> CLAUDE.md
74
+
75
+ ## Agent Mode
76
+
77
+ All commands support --agent for machine-readable JSON:
78
+
79
+ RESULT=$(blockintql screen --address $PAYMENT_DEST --agent)
80
+ SAFE=$(echo $RESULT | jq -r '.safe')
81
+
82
+ if [ "$SAFE" = "false" ]; then
83
+ echo "Payment blocked"
84
+ exit 1
85
+ fi
86
+
87
+ ## x402 Autonomous Payments
88
+
89
+ Configure once, pay per screen automatically:
90
+
91
+ blockintql pay --wallet-type cdp \
92
+ --cdp-key-id $CDP_KEY_ID \
93
+ --cdp-private-key $CDP_PRIVATE_KEY \
94
+ --auto-pay
95
+
96
+ Every screen auto-pays $0.001 USDC on Base to:
97
+ 0x32984663A11b9d7634Bf35835AE32B5A031637D5
98
+
99
+ ## Attribution Providers
100
+
101
+ Bring your own key — we never see your data:
102
+
103
+ chainalysis --provider chainalysis --provider-key $KEY
104
+ trm --provider trm --provider-key $KEY
105
+ elliptic --provider elliptic --provider-key $KEY
106
+ arkham --provider arkham --provider-key $KEY
107
+ metamask --provider metamask (free, no key needed)
108
+ generic --provider generic --provider-url https://your-api.com/screen/{address}
109
+
110
+ ## Privacy Guarantee
111
+
112
+ Your attribution provider key never leaves your machine.
113
+
114
+ Provider API calls are made directly from the CLI on your local machine.
115
+ BlockINTQL servers only receive the address being screened — never your
116
+ provider key, never the raw provider response.
117
+
118
+ Verify this by reading the source:
119
+ blockintql/providers.py — all provider calls are direct HTTP from CLI
120
+ blockintql/cli.py — only address + chain sent to BlockINTQL API
121
+
122
+ Open source. Verify yourself: github.com/block6iq/blockintql-cli
123
+
124
+ ## MCP Server
125
+
126
+ For AI agents using MCP (Model Context Protocol):
127
+
128
+ https://blockintql-mcp-385334043904.us-central1.run.app/mcp
129
+
130
+ ## Powered By
131
+
132
+ - Sovereign Bitcoin node — fully synced, 942,000+ blocks
133
+ - Sovereign Ethereum node — fully synced, 24,000,000+ blocks
134
+ - 50,000+ OP_RETURN identity signals mined from the Bitcoin blockchain
135
+ - BlockINTAI — autonomous multi-agent analytics engine
136
+ - BlockINTQL — sovereign blockchain query language
137
+
138
+ Block6IQ — block6iq.com
@@ -0,0 +1,107 @@
1
+ # BlockINTQL CLI
2
+
3
+ Sovereign blockchain intelligence from the command line.
4
+ Built for AI agents, compliance teams, and developers.
5
+
6
+ ## Install
7
+
8
+ pip install blockintql
9
+
10
+ ## Setup
11
+
12
+ blockintql auth --api-key biq_sk_live_YOUR_KEY
13
+
14
+ Get an API key at blockintql.com
15
+
16
+ ## Usage
17
+
18
+ # Screen before accepting payment
19
+ blockintql screen --address 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
20
+
21
+ # Enrich with your own Chainalysis/TRM key
22
+ blockintql screen --address 0x123... --chain ethereum \
23
+ --provider chainalysis --provider-key $KEY
24
+
25
+ # Natural language intelligence
26
+ blockintql query "is this address linked to Lazarus Group?"
27
+
28
+ # Multi-agent analysis
29
+ blockintql analyze "check if these wallets transacted with each other" \
30
+ --address 0x123... --address 0x456...
31
+
32
+ # OP_RETURN identity search
33
+ blockintql profile --identifier @lazarus_trader
34
+
35
+ # Trace funds FIFO/LIFO
36
+ blockintql trace --txid abc123... --hops 5
37
+
38
+ # List attribution providers
39
+ blockintql providers
40
+
41
+ # Install skills into agent context
42
+ blockintql skills --install >> CLAUDE.md
43
+
44
+ ## Agent Mode
45
+
46
+ All commands support --agent for machine-readable JSON:
47
+
48
+ RESULT=$(blockintql screen --address $PAYMENT_DEST --agent)
49
+ SAFE=$(echo $RESULT | jq -r '.safe')
50
+
51
+ if [ "$SAFE" = "false" ]; then
52
+ echo "Payment blocked"
53
+ exit 1
54
+ fi
55
+
56
+ ## x402 Autonomous Payments
57
+
58
+ Configure once, pay per screen automatically:
59
+
60
+ blockintql pay --wallet-type cdp \
61
+ --cdp-key-id $CDP_KEY_ID \
62
+ --cdp-private-key $CDP_PRIVATE_KEY \
63
+ --auto-pay
64
+
65
+ Every screen auto-pays $0.001 USDC on Base to:
66
+ 0x32984663A11b9d7634Bf35835AE32B5A031637D5
67
+
68
+ ## Attribution Providers
69
+
70
+ Bring your own key — we never see your data:
71
+
72
+ chainalysis --provider chainalysis --provider-key $KEY
73
+ trm --provider trm --provider-key $KEY
74
+ elliptic --provider elliptic --provider-key $KEY
75
+ arkham --provider arkham --provider-key $KEY
76
+ metamask --provider metamask (free, no key needed)
77
+ generic --provider generic --provider-url https://your-api.com/screen/{address}
78
+
79
+ ## Privacy Guarantee
80
+
81
+ Your attribution provider key never leaves your machine.
82
+
83
+ Provider API calls are made directly from the CLI on your local machine.
84
+ BlockINTQL servers only receive the address being screened — never your
85
+ provider key, never the raw provider response.
86
+
87
+ Verify this by reading the source:
88
+ blockintql/providers.py — all provider calls are direct HTTP from CLI
89
+ blockintql/cli.py — only address + chain sent to BlockINTQL API
90
+
91
+ Open source. Verify yourself: github.com/block6iq/blockintql-cli
92
+
93
+ ## MCP Server
94
+
95
+ For AI agents using MCP (Model Context Protocol):
96
+
97
+ https://blockintql-mcp-385334043904.us-central1.run.app/mcp
98
+
99
+ ## Powered By
100
+
101
+ - Sovereign Bitcoin node — fully synced, 942,000+ blocks
102
+ - Sovereign Ethereum node — fully synced, 24,000,000+ blocks
103
+ - 50,000+ OP_RETURN identity signals mined from the Bitcoin blockchain
104
+ - BlockINTAI — autonomous multi-agent analytics engine
105
+ - BlockINTQL — sovereign blockchain query language
106
+
107
+ Block6IQ — block6iq.com
@@ -0,0 +1,2 @@
1
+ """BlockINTQL — Sovereign Blockchain Intelligence CLI"""
2
+ __version__ = "1.0.0"
@@ -0,0 +1,415 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ BlockINTQL CLI
4
+
5
+ PRIVACY ARCHITECTURE:
6
+ BlockINTQL API receives: address + chain ONLY
7
+ Provider API receives: address + your key (direct from your machine)
8
+ BlockINTQL NEVER sees: your provider key or raw provider response
9
+
10
+ Verify this by reading the source. Open source: github.com/block6iq/blockintql-cli
11
+ """
12
+
13
+ import sys, os, json
14
+ import click
15
+ import httpx
16
+ from typing import Optional
17
+ from rich.console import Console
18
+ from rich.table import Table
19
+ from rich.panel import Panel
20
+ from rich import box
21
+ from .providers import get_provider, list_providers
22
+
23
+ API_BASE = os.environ.get("BLOCKINTQL_API_URL", "https://btc-index-api-385334043904.us-central1.run.app")
24
+ CONFIG_FILE = os.path.expanduser("~/.blockintql/config.json")
25
+ console = Console()
26
+ err_console = Console(stderr=True)
27
+
28
+ def load_config():
29
+ if os.path.exists(CONFIG_FILE):
30
+ with open(CONFIG_FILE) as f: return json.load(f)
31
+ return {}
32
+
33
+ def save_config(config):
34
+ os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
35
+ with open(CONFIG_FILE, "w") as f: json.dump(config, f, indent=2)
36
+
37
+ def get_api_key():
38
+ return os.environ.get("BLOCKINTQL_API_KEY") or load_config().get("api_key")
39
+
40
+ def get_headers():
41
+ key = get_api_key()
42
+ if not key:
43
+ err_console.print("[red]No API key.[/] Run: blockintql auth --api-key YOUR_KEY")
44
+ sys.exit(1)
45
+ return {"Authorization": f"Bearer {key}", "Content-Type": "application/json"}
46
+
47
+ def api_get(path, params=None):
48
+ """Query BlockINTQL API — sends address+chain ONLY, never provider keys."""
49
+ try:
50
+ r = httpx.get(f"{API_BASE}{path}", headers=get_headers(), params=params, timeout=30)
51
+ r.raise_for_status()
52
+ return r.json()
53
+ except Exception as e:
54
+ return {"error": str(e)}
55
+
56
+ def api_post(path, body):
57
+ """Query BlockINTQL API — sends address+chain ONLY, never provider keys."""
58
+ try:
59
+ r = httpx.post(f"{API_BASE}{path}", headers=get_headers(), json=body, timeout=60)
60
+ r.raise_for_status()
61
+ return r.json()
62
+ except Exception as e:
63
+ return {"error": str(e)}
64
+
65
+ def enrich_with_provider(result, address, chain, provider_name, provider_key):
66
+ """
67
+ PRIVACY: Runs entirely on your local machine.
68
+ Calls provider API directly — key never sent to BlockINTQL.
69
+ Only the merged verdict (no raw provider data) is shown to user.
70
+ """
71
+ if not provider_name or not provider_key:
72
+ return result
73
+ provider = get_provider(provider_name, provider_key)
74
+ if not provider:
75
+ err_console.print(f"[yellow]Unknown provider: {provider_name}[/]")
76
+ return result
77
+
78
+ # PRIVACY: This call goes directly to provider API from your machine
79
+ pd = provider.get_address_risk(address, chain)
80
+
81
+ if "error" in pd.get("raw", {}):
82
+ return result
83
+
84
+ # Merge — take higher risk score
85
+ result["risk_score"] = max(pd.get("risk_score", 0), result.get("risk_score", 0))
86
+ if pd.get("entity_name") and not result.get("entity"):
87
+ result["entity"] = pd["entity_name"]
88
+ if pd.get("sanctions_hit"):
89
+ result["verdict"] = "BLOCK"
90
+ result["safe"] = False
91
+ result.setdefault("risk_indicators", []).append("SANCTIONS")
92
+ # Store provider summary (not raw response) for display
93
+ result["provider_data"] = {
94
+ "provider": provider_name,
95
+ "entity_name": pd.get("entity_name"),
96
+ "entity_category": pd.get("entity_category"),
97
+ "risk_score": pd.get("risk_score", 0),
98
+ "risk_indicators": pd.get("risk_indicators", []),
99
+ "sanctions_hit": pd.get("sanctions_hit", False),
100
+ }
101
+ return result
102
+
103
+ def verdict_color(v):
104
+ return {"CLEAR": "green", "CAUTION": "yellow", "BLOCK": "red"}.get(str(v).upper(), "white")
105
+
106
+ def output(data, agent, quiet):
107
+ if agent or not sys.stdout.isatty():
108
+ click.echo(json.dumps(data, indent=2, default=str))
109
+ return
110
+ if "error" in data:
111
+ err_console.print(f"[red]Error:[/] {data['error']}")
112
+ return
113
+ if "verdict" in data and "risk_score" in data:
114
+ v = data["verdict"]
115
+ color = verdict_color(v)
116
+ console.print(Panel(f"[bold {color}]{v}[/] {'✅' if data.get('safe') else '❌'}",
117
+ title="BlockINTQL Verdict", border_style=color))
118
+ if not quiet:
119
+ t = Table(box=box.SIMPLE, show_header=False)
120
+ t.add_column("", style="dim", width=22)
121
+ t.add_column("")
122
+ t.add_row("Address", data.get("address",""))
123
+ t.add_row("Chain", data.get("chain",""))
124
+ t.add_row("Risk Score", f"{data.get('risk_score',0)}/100")
125
+ t.add_row("Entity", data.get("entity") or "Unknown")
126
+ if data.get("risk_indicators"):
127
+ t.add_row("Risk Indicators", ", ".join(data["risk_indicators"]))
128
+ if data.get("action"):
129
+ t.add_row("Action", data["action"])
130
+ if data.get("provider_data"):
131
+ pd = data["provider_data"]
132
+ t.add_row("─"*15, "─"*25)
133
+ t.add_row(f"[dim]{pd.get('provider','').upper()} (local)[/]", "")
134
+ if pd.get("entity_name"): t.add_row(" Entity", pd["entity_name"])
135
+ t.add_row(" Risk", f"{pd.get('risk_score',0)}/100")
136
+ if pd.get("sanctions_hit"): t.add_row(" Sanctions", "[red]⚠️ HIT[/]")
137
+ console.print(t)
138
+ if data.get("narrative"):
139
+ console.print(Panel(data["narrative"], title="Analysis", border_style="dim"))
140
+ return
141
+ if "profile" in data:
142
+ found = data.get("found", False)
143
+ console.print(Panel(
144
+ f"[bold]{data['identifier']}[/] ({data.get('identifier_type','')})\n"
145
+ f"{'[green]Found[/]' if found else '[dim]Not found[/]'}",
146
+ title="BlockINTQL Profile", border_style="blue" if found else "dim"))
147
+ if not quiet and found:
148
+ p = data.get("profile", {})
149
+ t = Table(box=box.SIMPLE, show_header=False)
150
+ t.add_column("", style="dim", width=25)
151
+ t.add_column("")
152
+ if p.get("entity_name"): t.add_row("Entity", p["entity_name"])
153
+ t.add_row("Risk Score", f"{p.get('risk_score',0)}/100")
154
+ if p.get("linked_bitcoin_addresses"):
155
+ t.add_row("Linked BTC", "\n".join(p["linked_bitcoin_addresses"][:5]))
156
+ if p.get("linked_identifiers"):
157
+ t.add_row("Linked IDs", "\n".join(
158
+ [f"{l['identifier']} ({l['type']})" for l in p["linked_identifiers"][:5]]))
159
+ console.print(t)
160
+ return
161
+ if not quiet:
162
+ console.print_json(json.dumps(data, default=str))
163
+
164
+ provider_opts = [
165
+ click.option("--provider", "-p", default=None,
166
+ type=click.Choice(["chainalysis","trm","elliptic","arkham","metamask","generic"]),
167
+ help="Attribution provider (key stays on your machine)"),
168
+ click.option("--provider-key", default=None, envvar="BLOCKINTQL_PROVIDER_KEY",
169
+ help="Provider API key — never sent to BlockINTQL"),
170
+ click.option("--provider-url", default=None,
171
+ help="Custom provider URL template (use {address} placeholder)"),
172
+ ]
173
+
174
+ def with_provider(f):
175
+ for opt in reversed(provider_opts): f = opt(f)
176
+ return f
177
+
178
+ @click.group()
179
+ @click.version_option("1.0.0", prog_name="blockintql")
180
+ def cli():
181
+ """BlockINTQL — Sovereign Blockchain Intelligence CLI
182
+
183
+ Your provider key never leaves your machine.
184
+ BlockINTQL only receives the address being screened.
185
+ """
186
+ pass
187
+
188
+ @cli.command()
189
+ @click.option("--api-key", required=True)
190
+ @click.option("--provider", default=None)
191
+ @click.option("--provider-key", default=None)
192
+ def auth(api_key, provider, provider_key):
193
+ """Save API key and optional default provider."""
194
+ config = load_config()
195
+ config["api_key"] = api_key
196
+ if provider: config["default_provider"] = provider
197
+ if provider_key: config["default_provider_key"] = provider_key
198
+ save_config(config)
199
+ console.print("[green]✅ Saved.[/]")
200
+
201
+ @cli.command()
202
+ @click.option("--address", "-a", required=True)
203
+ @click.option("--chain", "-c", default="bitcoin", type=click.Choice(["bitcoin","ethereum"]))
204
+ @click.option("--context", default="")
205
+ @with_provider
206
+ @click.option("--agent", is_flag=True)
207
+ @click.option("--quiet", "-q", is_flag=True)
208
+ def verdict(address, chain, context, provider, provider_key, provider_url, agent, quiet):
209
+ """Get a CLEAR/CAUTION/BLOCK verdict.
210
+
211
+ \b
212
+ Privacy: BlockINTQL receives address+chain only.
213
+ Provider key stays on your machine.
214
+
215
+ \b
216
+ Examples:
217
+ blockintql verdict --address 1A1zP1e...
218
+ blockintql verdict --address 0x123... --provider chainalysis --provider-key $KEY
219
+ """
220
+ config = load_config()
221
+ provider = provider or config.get("default_provider")
222
+ provider_key = provider_key or config.get("default_provider_key")
223
+ if not quiet and not agent:
224
+ p_info = f" + {provider} (local)" if provider else ""
225
+ console.print(f"[dim]Screening {address[:20]}...{p_info}[/]")
226
+
227
+ # STEP 1: BlockINTQL gets address+chain ONLY
228
+ result = api_post("/v1/verdict", {"address": address, "chain": chain, "context": context})
229
+
230
+ # STEP 2: Provider called directly from YOUR machine — key never sent to BlockINTQL
231
+ if provider and provider_key and "error" not in result:
232
+ result = enrich_with_provider(result, address, chain, provider, provider_key)
233
+
234
+ output(result, agent, quiet)
235
+
236
+ @cli.command()
237
+ @click.option("--address", "-a", required=True)
238
+ @click.option("--chain", "-c", default="bitcoin", type=click.Choice(["bitcoin","ethereum"]))
239
+ @with_provider
240
+ @click.option("--agent", is_flag=True)
241
+ @click.option("--quiet", "-q", is_flag=True)
242
+ def screen(address, chain, provider, provider_key, provider_url, agent, quiet):
243
+ """Screen a counterparty before transacting.
244
+
245
+ \b
246
+ Privacy: Your provider key never touches BlockINTQL servers.
247
+ Provider is called directly from your machine.
248
+
249
+ \b
250
+ Examples:
251
+ blockintql screen --address 1A1zP1e...
252
+ blockintql screen --address 0x123... --provider trm --provider-key $KEY
253
+ """
254
+ config = load_config()
255
+ provider = provider or config.get("default_provider")
256
+ provider_key = provider_key or config.get("default_provider_key")
257
+ if not quiet and not agent:
258
+ p_info = f" + {provider} (local)" if provider else ""
259
+ console.print(f"[dim]Screening {address[:20]}...{p_info}[/]")
260
+
261
+ # STEP 1: BlockINTQL gets address+chain ONLY
262
+ result = api_post("/v1/screen", {"address": address, "chain": chain})
263
+
264
+ # STEP 2: Provider called directly from YOUR machine — key never sent to BlockINTQL
265
+ if provider and provider_key and "error" not in result:
266
+ result = enrich_with_provider(result, address, chain, provider, provider_key)
267
+
268
+ output(result, agent, quiet)
269
+
270
+ @cli.command()
271
+ @click.argument("query", required=False)
272
+ @click.option("--address", "-a", multiple=True)
273
+ @click.option("--chain", "-c", default="ethereum", type=click.Choice(["bitcoin","ethereum","both"]))
274
+ @click.option("--format", "fmt", default="full", type=click.Choice(["full","graph","narrative"]))
275
+ @click.option("--agent", is_flag=True)
276
+ @click.option("--quiet", "-q", is_flag=True)
277
+ def analyze(query, address, chain, fmt, agent, quiet):
278
+ """Run autonomous multi-agent analysis."""
279
+ if not query and not address:
280
+ raise click.UsageError("Provide a QUERY or --address")
281
+ if not quiet and not agent:
282
+ console.print("[dim]Running autonomous analysis...[/]")
283
+ result = api_post("/v1/analyze", {"query": query or "", "addresses": list(address),
284
+ "chain": chain, "output_format": fmt})
285
+ output(result, agent, quiet)
286
+
287
+ @cli.command()
288
+ @click.option("--identifier", "-i", required=True)
289
+ @click.option("--type", "id_type", default="auto",
290
+ type=click.Choice(["auto","email","telegram","twitter","phone",
291
+ "btc_address","eth_address","pgp_fingerprint"]))
292
+ @click.option("--agent", is_flag=True)
293
+ @click.option("--quiet", "-q", is_flag=True)
294
+ def profile(identifier, id_type, agent, quiet):
295
+ """Search OP_RETURN identity graph — unique on-chain data."""
296
+ if not quiet and not agent:
297
+ console.print(f"[dim]Searching identity graph...[/]")
298
+ result = api_get("/v1/profile/search", {"identifier": identifier, "type": id_type})
299
+ output(result, agent, quiet)
300
+
301
+ @cli.command()
302
+ @click.option("--txid", "-t", required=True)
303
+ @click.option("--hops", default=5)
304
+ @click.option("--method", default="fifo", type=click.Choice(["fifo","lifo"]))
305
+ @click.option("--agent", is_flag=True)
306
+ @click.option("--quiet", "-q", is_flag=True)
307
+ def trace(txid, hops, method, agent, quiet):
308
+ """Trace funds with FIFO/LIFO accounting."""
309
+ if not quiet and not agent:
310
+ console.print(f"[dim]Tracing {txid[:20]}... ({hops} hops)[/]")
311
+ result = api_post("/v1/trace", {"txid": txid, "hops": hops, "method": method})
312
+ output(result, agent, quiet)
313
+
314
+ @cli.command()
315
+ @click.argument("query")
316
+ @click.option("--agent", is_flag=True)
317
+ @click.option("--quiet", "-q", is_flag=True)
318
+ def query(query, agent, quiet):
319
+ """Natural language blockchain intelligence."""
320
+ if not quiet and not agent: console.print("[dim]Processing...[/]")
321
+ result = api_post("/v1/intelligence/search", {"query": query})
322
+ output(result, agent, quiet)
323
+
324
+ @cli.command()
325
+ @click.option("--agent", is_flag=True)
326
+ def providers(agent):
327
+ """List attribution providers — all called locally, keys never leave your machine."""
328
+ data = list_providers()
329
+ if agent or not sys.stdout.isatty():
330
+ click.echo(json.dumps(data, indent=2))
331
+ return
332
+ t = Table(title="Attribution Providers (all local — keys never sent to BlockINTQL)",
333
+ box=box.ROUNDED, border_style="blue")
334
+ t.add_column("Provider", style="bold yellow")
335
+ t.add_column("Description")
336
+ t.add_column("Key Required")
337
+ for p in data:
338
+ t.add_row(p["name"], p["description"], "No" if p["name"] in ("metamask","generic") else "Yes")
339
+ console.print(t)
340
+
341
+ @cli.command()
342
+ @click.option("--install", is_flag=True)
343
+ @click.option("--agent", is_flag=True)
344
+ def skills(install, agent):
345
+ """List capabilities or install into agent context."""
346
+ if install:
347
+ r = httpx.get(f"{API_BASE}/skills/skill.md", timeout=10)
348
+ click.echo(r.text)
349
+ return
350
+ if agent or not sys.stdout.isatty():
351
+ click.echo(json.dumps({
352
+ "commands": ["verdict","screen","analyze","profile","trace","query","providers"],
353
+ "providers": [p["name"] for p in list_providers()],
354
+ "privacy": "Provider keys never leave your machine",
355
+ "mcp_server": "https://blockintql-mcp-385334043904.us-central1.run.app/mcp",
356
+ "source": "https://github.com/block6iq/blockintql-cli",
357
+ }, indent=2))
358
+ return
359
+ t = Table(title="BlockINTQL CLI", box=box.ROUNDED, border_style="blue")
360
+ t.add_column("Command", style="bold yellow", width=12)
361
+ t.add_column("Description")
362
+ t.add_column("Example")
363
+ rows = [
364
+ ("verdict","CLEAR/CAUTION/BLOCK","blockintql verdict --address 1ABC..."),
365
+ ("screen","Screen + provider","blockintql screen --address 0x123... --provider trm --provider-key $KEY"),
366
+ ("analyze","Multi-agent analysis",'blockintql analyze "check for sanctions"'),
367
+ ("profile","OP_RETURN identity","blockintql profile --identifier @handle"),
368
+ ("trace","FIFO/LIFO tracing","blockintql trace --txid abc123..."),
369
+ ("query","Natural language",'blockintql query "is this safe?"'),
370
+ ("providers","List providers","blockintql providers"),
371
+ ("skills","Agent skills","blockintql skills --install >> CLAUDE.md"),
372
+ ]
373
+ for r in rows: t.add_row(*r)
374
+ console.print(t)
375
+ console.print("\n[dim]Provider keys stay on your machine. BlockINTQL only sees the address.[/]")
376
+ console.print("[dim]Source: github.com/block6iq/blockintql-cli[/]")
377
+
378
+ @cli.command()
379
+ @click.option("--wallet-type", default="cdp", type=click.Choice(["cdp","privatekey"]))
380
+ @click.option("--cdp-key-id", default=None, envvar="BLOCKINTQL_CDP_KEY_ID")
381
+ @click.option("--cdp-private-key", default=None, envvar="BLOCKINTQL_CDP_PRIVATE_KEY")
382
+ @click.option("--private-key", default=None, envvar="BLOCKINTQL_PRIVATE_KEY")
383
+ @click.option("--auto-pay", is_flag=True)
384
+ @click.option("--max-payment", default=0.10)
385
+ def pay(wallet_type, cdp_key_id, cdp_private_key, private_key, auto_pay, max_payment):
386
+ """Configure x402 auto-payment — $0.001 USDC per screen on Base."""
387
+ config = load_config()
388
+ payment_config = {"type": wallet_type, "auto_pay": auto_pay, "max_payment_usd": max_payment}
389
+ if wallet_type == "cdp":
390
+ if not cdp_key_id or not cdp_private_key:
391
+ err_console.print("[red]CDP requires --cdp-key-id and --cdp-private-key[/]")
392
+ return
393
+ payment_config.update({"cdp_key_id": cdp_key_id, "cdp_private_key": cdp_private_key})
394
+ elif wallet_type == "privatekey":
395
+ if not private_key:
396
+ err_console.print("[red]Requires --private-key[/]")
397
+ return
398
+ payment_config["private_key"] = private_key
399
+ config["payment"] = payment_config
400
+ save_config(config)
401
+ console.print(f"[green]✅ Payment wallet configured ({wallet_type})[/]")
402
+ console.print(f"[green]✅ Auto-pay: {'enabled' if auto_pay else 'disabled'} | Max: ${max_payment}[/]")
403
+ console.print(f"[dim]Payments → 0x32984663A11b9d7634Bf35835AE32B5A031637D5 (Base)[/]")
404
+
405
+ @cli.command()
406
+ @click.option("--agent", is_flag=True)
407
+ def status(agent):
408
+ """Check node health."""
409
+ output(api_get("/health"), agent, False)
410
+
411
+ def main():
412
+ cli()
413
+
414
+ if __name__ == "__main__":
415
+ main()
@@ -0,0 +1,228 @@
1
+ """BlockINTQL Provider Plugin System"""
2
+
3
+ import httpx
4
+ from abc import ABC, abstractmethod
5
+
6
+
7
+ class AttributionProvider(ABC):
8
+ name: str = "unknown"
9
+ description: str = ""
10
+ def __init__(self, api_key: str):
11
+ self.api_key = api_key
12
+ @abstractmethod
13
+ def get_address_risk(self, address: str, chain: str = "bitcoin") -> dict:
14
+ pass
15
+ def normalize(self, raw: dict) -> dict:
16
+ return {"entity_name": None, "entity_category": None, "risk_score": 0,
17
+ "risk_indicators": [], "sanctions_hit": False, "provider": self.name, "raw": raw}
18
+
19
+
20
+ class ChainalysisProvider(AttributionProvider):
21
+ name = "chainalysis"
22
+ description = "Chainalysis KYT — industry standard blockchain analytics"
23
+ def get_address_risk(self, address: str, chain: str = "bitcoin") -> dict:
24
+ asset = {"bitcoin": "BITCOIN", "ethereum": "ETHEREUM"}.get(chain, "BITCOIN")
25
+ try:
26
+ r = httpx.post(f"https://api.chainalysis.com/api/kyt/v2/users/demo_user/transfers",
27
+ headers={"Token": self.api_key, "Content-Type": "application/json"},
28
+ json={"network": asset, "asset": asset, "transferReference": address, "direction": "received"},
29
+ timeout=15)
30
+ if r.status_code not in (200, 201):
31
+ return self.normalize({"error": f"HTTP {r.status_code}"})
32
+ data = r.json()
33
+ risk = data.get("riskScore", "unknown")
34
+ cluster = data.get("cluster", {})
35
+ risk_map = {"low": 10, "medium": 50, "high": 80, "severe": 100}
36
+ result = self.normalize(data)
37
+ result.update({"entity_name": cluster.get("name"), "entity_category": cluster.get("category"),
38
+ "risk_score": risk_map.get(str(risk).lower(), 0),
39
+ "risk_indicators": data.get("exposures", []),
40
+ "sanctions_hit": any(e.get("category") == "sanctions" for e in data.get("exposures", []))})
41
+ return result
42
+ except Exception as e:
43
+ return self.normalize({"error": str(e)})
44
+
45
+
46
+ class TRMProvider(AttributionProvider):
47
+ name = "trm"
48
+ description = "TRM Labs — blockchain risk intelligence"
49
+ def get_address_risk(self, address: str, chain: str = "bitcoin") -> dict:
50
+ blockchain = {"bitcoin": "bitcoin", "ethereum": "ethereum"}.get(chain, "bitcoin")
51
+ try:
52
+ r = httpx.post(f"https://api.trmlabs.com/public/v2/screening/addresses",
53
+ headers={"Authorization": f"Basic {self.api_key}", "Content-Type": "application/json"},
54
+ json=[{"address": address, "chain": blockchain}], timeout=15)
55
+ if r.status_code != 200:
56
+ return self.normalize({"error": f"HTTP {r.status_code}"})
57
+ data = r.json()
58
+ item = data[0] if isinstance(data, list) and data else {}
59
+ risk_details = item.get("addressRiskIndicators", [])
60
+ risk_score = item.get("riskScore", 0)
61
+ result = self.normalize(data)
62
+ result.update({"entity_name": item.get("addressSummary", {}).get("name"),
63
+ "entity_category": item.get("addressSummary", {}).get("type"),
64
+ "risk_score": float(risk_score) * 100 if risk_score <= 1 else float(risk_score),
65
+ "risk_indicators": [r.get("riskType") for r in risk_details if r.get("riskType")],
66
+ "sanctions_hit": any(r.get("riskType") == "SANCTIONS" for r in risk_details)})
67
+ return result
68
+ except Exception as e:
69
+ return self.normalize({"error": str(e)})
70
+
71
+
72
+ class EllipticProvider(AttributionProvider):
73
+ name = "elliptic"
74
+ description = "Elliptic — blockchain analytics and financial crime compliance"
75
+ def get_address_risk(self, address: str, chain: str = "bitcoin") -> dict:
76
+ asset = {"bitcoin": "bitcoin", "ethereum": "ethereum"}.get(chain, "bitcoin")
77
+ try:
78
+ r = httpx.post("https://aml-api.elliptic.co/v2/wallet/synchronous",
79
+ headers={"x-access-key": self.api_key, "Content-Type": "application/json"},
80
+ json={"subject": {"asset": asset, "type": "address", "hash": address}, "type": "wallet_exposure"},
81
+ timeout=20)
82
+ if r.status_code != 200:
83
+ return self.normalize({"error": f"HTTP {r.status_code}"})
84
+ data = r.json()
85
+ risk_score = data.get("risk_score_detail", {}).get("risk_score", 0)
86
+ result = self.normalize(data)
87
+ result.update({"risk_score": float(risk_score) * 100 if risk_score <= 1 else float(risk_score),
88
+ "sanctions_hit": data.get("risk_score_detail", {}).get("rule_triggered_name") == "OFAC SDN"})
89
+ return result
90
+ except Exception as e:
91
+ return self.normalize({"error": str(e)})
92
+
93
+
94
+ class ArkhamProvider(AttributionProvider):
95
+ name = "arkham"
96
+ description = "Arkham Intelligence — entity intelligence platform"
97
+ def get_address_risk(self, address: str, chain: str = "bitcoin") -> dict:
98
+ try:
99
+ r = httpx.get(f"https://api.arkhamintelligence.com/intelligence/address/{address}",
100
+ headers={"API-Key": self.api_key}, timeout=15)
101
+ if r.status_code != 200:
102
+ return self.normalize({"error": f"HTTP {r.status_code}"})
103
+ data = r.json()
104
+ entity = data.get("arkhamEntity", {})
105
+ entity_type = entity.get("type", "")
106
+ risk_map = {"exchange": 10, "defi": 15, "mixer": 90, "sanctions": 100, "scam": 95, "hack": 95, "darknet": 90}
107
+ result = self.normalize(data)
108
+ result.update({"entity_name": entity.get("name"), "entity_category": entity_type,
109
+ "risk_score": risk_map.get(entity_type.lower(), 20),
110
+ "sanctions_hit": entity_type.lower() == "sanctions"})
111
+ return result
112
+ except Exception as e:
113
+ return self.normalize({"error": str(e)})
114
+
115
+
116
+ class MetaMaskRiskProvider(AttributionProvider):
117
+ name = "metamask"
118
+ description = "MetaMask Transaction Insight — free, no API key needed"
119
+ def __init__(self, api_key: str = ""):
120
+ self.api_key = api_key
121
+ def get_address_risk(self, address: str, chain: str = "ethereum") -> dict:
122
+ if chain != "ethereum":
123
+ return self.normalize({"error": "MetaMask only supports Ethereum"})
124
+ try:
125
+ r = httpx.get(f"https://risk-api.metamask.io/v1/chains/1/addresses/{address}", timeout=10)
126
+ if r.status_code != 200:
127
+ return self.normalize({"error": f"HTTP {r.status_code}"})
128
+ data = r.json()
129
+ risk_score = 90 if data.get("result") == "Malicious" else 50 if data.get("result") == "Warning" else 0
130
+ indicators = ["FLAGGED_MALICIOUS"] if risk_score == 90 else ["WARNING"] if risk_score == 50 else []
131
+ result = self.normalize(data)
132
+ result.update({"risk_score": risk_score, "risk_indicators": indicators})
133
+ return result
134
+ except Exception as e:
135
+ return self.normalize({"error": str(e)})
136
+
137
+
138
+ PROVIDERS = {
139
+ "chainalysis": ChainalysisProvider,
140
+ "trm": TRMProvider,
141
+ "elliptic": EllipticProvider,
142
+ "arkham": ArkhamProvider,
143
+ "metamask": MetaMaskRiskProvider,
144
+ }
145
+
146
+ def get_provider(name: str, api_key: str):
147
+ cls = PROVIDERS.get(name.lower())
148
+ return cls(api_key) if cls else None
149
+
150
+ def list_providers() -> list:
151
+ return [{"name": k, "description": v.description} for k, v in PROVIDERS.items()]
152
+
153
+
154
+ class GenericProvider(AttributionProvider):
155
+ """
156
+ Generic provider — point to any REST API that returns risk data.
157
+
158
+ Usage:
159
+ blockintql screen --address 1ABC... \
160
+ --provider generic \
161
+ --provider-key $API_KEY \
162
+ --provider-url https://api.yourprovider.com/screen/{address} \
163
+ --provider-field risk_score
164
+ """
165
+ name = "generic"
166
+ description = "Generic — any REST API that returns risk data"
167
+
168
+ def __init__(self, api_key: str, url_template: str = None,
169
+ risk_field: str = "risk_score",
170
+ entity_field: str = "entity",
171
+ auth_header: str = "Authorization",
172
+ auth_prefix: str = "Bearer"):
173
+ self.api_key = api_key
174
+ self.url_template = url_template
175
+ self.risk_field = risk_field
176
+ self.entity_field = entity_field
177
+ self.auth_header = auth_header
178
+ self.auth_prefix = auth_prefix
179
+
180
+ def get_address_risk(self, address: str, chain: str = "bitcoin") -> dict:
181
+ if not self.url_template:
182
+ return self.normalize({"error": "No --provider-url specified"})
183
+ try:
184
+ url = self.url_template.replace("{address}", address).replace("{chain}", chain)
185
+ r = httpx.get(url,
186
+ headers={self.auth_header: f"{self.auth_prefix} {self.api_key}".strip()},
187
+ timeout=15)
188
+ if r.status_code != 200:
189
+ return self.normalize({"error": f"HTTP {r.status_code}"})
190
+ data = r.json()
191
+ # Try to extract risk score from nested path e.g. "result.risk.score"
192
+ risk_score = 0
193
+ parts = self.risk_field.split(".")
194
+ val = data
195
+ for p in parts:
196
+ val = val.get(p, 0) if isinstance(val, dict) else 0
197
+ try:
198
+ risk_score = float(val)
199
+ if risk_score <= 1:
200
+ risk_score *= 100
201
+ except:
202
+ pass
203
+ # Extract entity name
204
+ entity_val = data
205
+ for p in self.entity_field.split("."):
206
+ entity_val = entity_val.get(p) if isinstance(entity_val, dict) else None
207
+ result = self.normalize(data)
208
+ result.update({"entity_name": str(entity_val) if entity_val else None,
209
+ "risk_score": risk_score})
210
+ return result
211
+ except Exception as e:
212
+ return self.normalize({"error": str(e)})
213
+
214
+
215
+ # Add generic to registry
216
+ PROVIDERS["generic"] = GenericProvider
217
+
218
+ # ── PRIVACY GUARANTEE ─────────────────────────────────────────────────────────
219
+ #
220
+ # Provider API calls are made DIRECTLY from this CLI on the user's machine.
221
+ # Provider keys and raw responses NEVER touch BlockINTQL servers.
222
+ # BlockINTQL only receives: address, chain, and the final merged verdict.
223
+ #
224
+ # You can verify this by reading the source code above.
225
+ # The BlockINTQL API endpoint called is /v1/verdict or /v1/screen —
226
+ # neither endpoint accepts or logs provider keys.
227
+ #
228
+ # Open source. Verify yourself: github.com/block6iq/blockintql-cli
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: blockintql
3
+ Version: 1.0.0
4
+ Summary: BlockINTQL — Sovereign Blockchain Intelligence CLI
5
+ Home-page: https://blockintql.com
6
+ Author: Block6IQ
7
+ Author-email: joe@block6iq.com
8
+ Keywords: blockchain bitcoin ethereum forensics compliance aml kyc intelligence agents
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Topic :: Security
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.8
15
+ Description-Content-Type: text/markdown
16
+ License-File: LICENSE
17
+ Requires-Dist: click>=8.0.0
18
+ Requires-Dist: httpx>=0.27.0
19
+ Requires-Dist: rich>=13.0.0
20
+ Dynamic: author
21
+ Dynamic: author-email
22
+ Dynamic: classifier
23
+ Dynamic: description
24
+ Dynamic: description-content-type
25
+ Dynamic: home-page
26
+ Dynamic: keywords
27
+ Dynamic: license-file
28
+ Dynamic: requires-dist
29
+ Dynamic: requires-python
30
+ Dynamic: summary
31
+
32
+ # BlockINTQL CLI
33
+
34
+ Sovereign blockchain intelligence from the command line.
35
+ Built for AI agents, compliance teams, and developers.
36
+
37
+ ## Install
38
+
39
+ pip install blockintql
40
+
41
+ ## Setup
42
+
43
+ blockintql auth --api-key biq_sk_live_YOUR_KEY
44
+
45
+ Get an API key at blockintql.com
46
+
47
+ ## Usage
48
+
49
+ # Screen before accepting payment
50
+ blockintql screen --address 1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa
51
+
52
+ # Enrich with your own Chainalysis/TRM key
53
+ blockintql screen --address 0x123... --chain ethereum \
54
+ --provider chainalysis --provider-key $KEY
55
+
56
+ # Natural language intelligence
57
+ blockintql query "is this address linked to Lazarus Group?"
58
+
59
+ # Multi-agent analysis
60
+ blockintql analyze "check if these wallets transacted with each other" \
61
+ --address 0x123... --address 0x456...
62
+
63
+ # OP_RETURN identity search
64
+ blockintql profile --identifier @lazarus_trader
65
+
66
+ # Trace funds FIFO/LIFO
67
+ blockintql trace --txid abc123... --hops 5
68
+
69
+ # List attribution providers
70
+ blockintql providers
71
+
72
+ # Install skills into agent context
73
+ blockintql skills --install >> CLAUDE.md
74
+
75
+ ## Agent Mode
76
+
77
+ All commands support --agent for machine-readable JSON:
78
+
79
+ RESULT=$(blockintql screen --address $PAYMENT_DEST --agent)
80
+ SAFE=$(echo $RESULT | jq -r '.safe')
81
+
82
+ if [ "$SAFE" = "false" ]; then
83
+ echo "Payment blocked"
84
+ exit 1
85
+ fi
86
+
87
+ ## x402 Autonomous Payments
88
+
89
+ Configure once, pay per screen automatically:
90
+
91
+ blockintql pay --wallet-type cdp \
92
+ --cdp-key-id $CDP_KEY_ID \
93
+ --cdp-private-key $CDP_PRIVATE_KEY \
94
+ --auto-pay
95
+
96
+ Every screen auto-pays $0.001 USDC on Base to:
97
+ 0x32984663A11b9d7634Bf35835AE32B5A031637D5
98
+
99
+ ## Attribution Providers
100
+
101
+ Bring your own key — we never see your data:
102
+
103
+ chainalysis --provider chainalysis --provider-key $KEY
104
+ trm --provider trm --provider-key $KEY
105
+ elliptic --provider elliptic --provider-key $KEY
106
+ arkham --provider arkham --provider-key $KEY
107
+ metamask --provider metamask (free, no key needed)
108
+ generic --provider generic --provider-url https://your-api.com/screen/{address}
109
+
110
+ ## Privacy Guarantee
111
+
112
+ Your attribution provider key never leaves your machine.
113
+
114
+ Provider API calls are made directly from the CLI on your local machine.
115
+ BlockINTQL servers only receive the address being screened — never your
116
+ provider key, never the raw provider response.
117
+
118
+ Verify this by reading the source:
119
+ blockintql/providers.py — all provider calls are direct HTTP from CLI
120
+ blockintql/cli.py — only address + chain sent to BlockINTQL API
121
+
122
+ Open source. Verify yourself: github.com/block6iq/blockintql-cli
123
+
124
+ ## MCP Server
125
+
126
+ For AI agents using MCP (Model Context Protocol):
127
+
128
+ https://blockintql-mcp-385334043904.us-central1.run.app/mcp
129
+
130
+ ## Powered By
131
+
132
+ - Sovereign Bitcoin node — fully synced, 942,000+ blocks
133
+ - Sovereign Ethereum node — fully synced, 24,000,000+ blocks
134
+ - 50,000+ OP_RETURN identity signals mined from the Bitcoin blockchain
135
+ - BlockINTAI — autonomous multi-agent analytics engine
136
+ - BlockINTQL — sovereign blockchain query language
137
+
138
+ Block6IQ — block6iq.com
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ blockintql/__init__.py
5
+ blockintql/cli.py
6
+ blockintql/providers.py
7
+ blockintql.egg-info/PKG-INFO
8
+ blockintql.egg-info/SOURCES.txt
9
+ blockintql.egg-info/dependency_links.txt
10
+ blockintql.egg-info/entry_points.txt
11
+ blockintql.egg-info/requires.txt
12
+ blockintql.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ blockintql = blockintql.cli:main
@@ -0,0 +1,3 @@
1
+ click>=8.0.0
2
+ httpx>=0.27.0
3
+ rich>=13.0.0
@@ -0,0 +1 @@
1
+ blockintql
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,32 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name="blockintql",
5
+ version="1.0.0",
6
+ description="BlockINTQL — Sovereign Blockchain Intelligence CLI",
7
+ long_description=open("README.md").read(),
8
+ long_description_content_type="text/markdown",
9
+ author="Block6IQ",
10
+ author_email="joe@block6iq.com",
11
+ url="https://blockintql.com",
12
+ packages=find_packages(),
13
+ install_requires=[
14
+ "click>=8.0.0",
15
+ "httpx>=0.27.0",
16
+ "rich>=13.0.0",
17
+ ],
18
+ entry_points={
19
+ "console_scripts": [
20
+ "blockintql=blockintql.cli:main",
21
+ ],
22
+ },
23
+ python_requires=">=3.8",
24
+ classifiers=[
25
+ "Development Status :: 4 - Beta",
26
+ "Intended Audience :: Developers",
27
+ "Topic :: Security",
28
+ "License :: OSI Approved :: MIT License",
29
+ "Programming Language :: Python :: 3",
30
+ ],
31
+ keywords="blockchain bitcoin ethereum forensics compliance aml kyc intelligence agents",
32
+ )