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.
- blockintql-1.0.0/LICENSE +17 -0
- blockintql-1.0.0/PKG-INFO +138 -0
- blockintql-1.0.0/README.md +107 -0
- blockintql-1.0.0/blockintql/__init__.py +2 -0
- blockintql-1.0.0/blockintql/cli.py +415 -0
- blockintql-1.0.0/blockintql/providers.py +228 -0
- blockintql-1.0.0/blockintql.egg-info/PKG-INFO +138 -0
- blockintql-1.0.0/blockintql.egg-info/SOURCES.txt +12 -0
- blockintql-1.0.0/blockintql.egg-info/dependency_links.txt +1 -0
- blockintql-1.0.0/blockintql.egg-info/entry_points.txt +2 -0
- blockintql-1.0.0/blockintql.egg-info/requires.txt +3 -0
- blockintql-1.0.0/blockintql.egg-info/top_level.txt +1 -0
- blockintql-1.0.0/setup.cfg +4 -0
- blockintql-1.0.0/setup.py +32 -0
blockintql-1.0.0/LICENSE
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blockintql
|
|
@@ -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
|
+
)
|