freezeradar 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- freezeradar-0.1.0/.gitignore +22 -0
- freezeradar-0.1.0/LICENSE +21 -0
- freezeradar-0.1.0/PKG-INFO +238 -0
- freezeradar-0.1.0/README.md +203 -0
- freezeradar-0.1.0/examples/addresses.csv +4 -0
- freezeradar-0.1.0/examples/celery_deposit_screening.py +61 -0
- freezeradar-0.1.0/examples/django_withdrawal_hook.py +43 -0
- freezeradar-0.1.0/examples/fastapi_withdrawal.py +48 -0
- freezeradar-0.1.0/openapi/v1.json +1 -0
- freezeradar-0.1.0/pyproject.toml +116 -0
- freezeradar-0.1.0/src/freezeradar/__init__.py +51 -0
- freezeradar-0.1.0/src/freezeradar/_http.py +139 -0
- freezeradar-0.1.0/src/freezeradar/_version.py +1 -0
- freezeradar-0.1.0/src/freezeradar/async_client.py +443 -0
- freezeradar-0.1.0/src/freezeradar/chains.py +182 -0
- freezeradar-0.1.0/src/freezeradar/cli.py +240 -0
- freezeradar-0.1.0/src/freezeradar/client.py +449 -0
- freezeradar-0.1.0/src/freezeradar/endpoints.py +19 -0
- freezeradar-0.1.0/src/freezeradar/errors.py +65 -0
- freezeradar-0.1.0/src/freezeradar/models.py +283 -0
- freezeradar-0.1.0/src/freezeradar/policy.py +146 -0
- freezeradar-0.1.0/src/freezeradar/py.typed +0 -0
- freezeradar-0.1.0/src/freezeradar/webhooks.py +86 -0
- freezeradar-0.1.0/tests/__init__.py +0 -0
- freezeradar-0.1.0/tests/conftest.py +8 -0
- freezeradar-0.1.0/tests/payloads.py +47 -0
- freezeradar-0.1.0/tests/test_async_client.py +53 -0
- freezeradar-0.1.0/tests/test_chains.py +49 -0
- freezeradar-0.1.0/tests/test_cli.py +69 -0
- freezeradar-0.1.0/tests/test_client.py +252 -0
- freezeradar-0.1.0/tests/test_models.py +83 -0
- freezeradar-0.1.0/tests/test_openapi.py +29 -0
- freezeradar-0.1.0/tests/test_policy.py +77 -0
- freezeradar-0.1.0/tests/test_webhooks.py +63 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.env
|
|
11
|
+
.env.*
|
|
12
|
+
!.env.example
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.coverage
|
|
17
|
+
.coverage.*
|
|
18
|
+
htmlcov/
|
|
19
|
+
.tox/
|
|
20
|
+
.python-version
|
|
21
|
+
*.log
|
|
22
|
+
.DS_Store
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FreezeRadar
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: freezeradar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: FreezeRadar Python SDK: address freeze-risk screening for exchanges, OTC desks, and compliance teams.
|
|
5
|
+
Project-URL: Homepage, https://freezeradar.com
|
|
6
|
+
Project-URL: Documentation, https://freezeradar.com/developers
|
|
7
|
+
Project-URL: Source, https://github.com/Freezeradar/python-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/Freezeradar/python-sdk/issues
|
|
9
|
+
Project-URL: OpenAPI, https://freezeradar.com/api/openapi/v1
|
|
10
|
+
Author-email: FreezeRadar <mehdiraized@gmail.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: aml,blacklist,compliance,freezeradar,sanctions,stablecoin,usdc,usdt
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: httpx<1,>=0.27.0
|
|
27
|
+
Requires-Dist: pydantic<3,>=2.5.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
32
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# freezeradar
|
|
37
|
+
|
|
38
|
+
Python SDK for [FreezeRadar](https://freezeradar.com) wallet-risk intelligence. Exchanges, OTC desks, and compliance teams can screen freezeable-asset counterparties (USDT, USDC, PAXG, XAUt, and others) for issuer blacklists, sanctions hits, and explainable freeze-risk scores.
|
|
39
|
+
|
|
40
|
+
The client talks to the live API documented at [freezeradar.com/developers](https://freezeradar.com/developers) and [GET /api/openapi/v1](https://freezeradar.com/api/openapi/v1). Models are generated from that OpenAPI document (snapshot in `openapi/v1.json`).
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install freezeradar
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Python 3.9+. Sync and async clients use [httpx](https://www.python-httpx.org/). Types use Pydantic v2.
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from freezeradar import FreezeRadar, AsyncFreezeRadar, Action
|
|
52
|
+
|
|
53
|
+
with FreezeRadar() as client: # public checks; no key required
|
|
54
|
+
check = client.check_address("TBZefVsyQpzzxc2WSCLbZBECvxVdzGqdtC")
|
|
55
|
+
print(check.chain, check.blacklisted, check.sanctioned, check.matches)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Business API methods (`create_address_screening`, `get_report`, full `screen_*`) need a Bearer key from the Developer Console:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export FREEZERADAR_API_KEY=fr_live_...
|
|
62
|
+
# optional
|
|
63
|
+
export FREEZERADAR_BASE_URL=https://freezeradar.com
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
client = FreezeRadar(api_key="fr_live_...") # or from the environment
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Quick start
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from freezeradar import FreezeRadar, Thresholds
|
|
74
|
+
|
|
75
|
+
client = FreezeRadar(
|
|
76
|
+
thresholds=Thresholds(block_score=75, review_score=40, high_value_amount=25_000),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Fast path: issuer blacklist + sanctions (public, IP rate-limited)
|
|
80
|
+
check = client.check_address("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", chain="ethereum")
|
|
81
|
+
|
|
82
|
+
# Batch (sequential sync / concurrent async). Same public endpoints.
|
|
83
|
+
results = client.check_addresses([
|
|
84
|
+
"TBZefVsyQpzzxc2WSCLbZBECvxVdzGqdtC",
|
|
85
|
+
{"address": "0x0000000000000000000000000000000000000000", "chain": "ethereum"},
|
|
86
|
+
])
|
|
87
|
+
|
|
88
|
+
# Withdrawal / deposit policy: allow | review | block
|
|
89
|
+
decision = client.screen_withdrawal(
|
|
90
|
+
"TLXDJUauN4vgN75tJtCbXiAm4mQaDTmTNY",
|
|
91
|
+
asset="USDT",
|
|
92
|
+
amount=12_000,
|
|
93
|
+
)
|
|
94
|
+
if decision.action.value == "block":
|
|
95
|
+
raise SystemExit(decision.reasons)
|
|
96
|
+
|
|
97
|
+
# Full scored report (Business API key, async job, polled)
|
|
98
|
+
job = client.create_address_screening("0x...", "ethereum", assets=["USDT"])
|
|
99
|
+
report = client.get_report(job.id) # waits until COMPLETED / FAILED
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Async:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
async with AsyncFreezeRadar() as client:
|
|
106
|
+
checks = await client.check_addresses(addresses, max_concurrency=5)
|
|
107
|
+
decision = await client.screen_deposit(from_address, asset="USDC", amount=500)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## CLI
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
freezeradar check TBZefVsyQpzzxc2WSCLbZBECvxVdzGqdtC
|
|
114
|
+
freezeradar check 0x... --chain ethereum --json --decide
|
|
115
|
+
freezeradar screen-file examples/addresses.csv --out results.csv
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
CSV columns: `address` (required), optional `chain`, `asset`, `amount`, `direction` (`withdrawal` or `deposit`).
|
|
119
|
+
|
|
120
|
+
## Endpoints used
|
|
121
|
+
|
|
122
|
+
Base URL: `https://freezeradar.com` (override with `FREEZERADAR_BASE_URL` or `base_url=`).
|
|
123
|
+
|
|
124
|
+
| SDK method | HTTP | Auth | Notes |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| `check_address` / `check_addresses` | `GET /api/widget/check` + `GET /api/sanctions/check` | none | Default synchronous screen. Widget is 30/min per IP; sanctions is 20/min per IP. Bitcoin uses sanctions only (no blacklist). |
|
|
127
|
+
| `check_address(..., use_quick_check=True)` | `POST /api/v1/quick-check` | Bearer | **Not live yet.** If the route 404s, the SDK falls back to the public pair. |
|
|
128
|
+
| `create_address_screening` | `POST /api/v1/address-screenings` | Bearer | Body: `address`, `chain`, optional `assets`, `scanMode`. |
|
|
129
|
+
| `create_transaction_screening` | `POST /api/v1/transaction-screenings` | Bearer | Body: `chain`, `transactionHash`, optional `policyId`. |
|
|
130
|
+
| `get_report` | `GET /api/v1/screenings/{id}` | Bearer | Polls `QUEUED` / `RUNNING` until `COMPLETED` or `FAILED`. |
|
|
131
|
+
| `list_chains` | `GET /api/chains` | none | Includes bitcoin (sanctions-only). |
|
|
132
|
+
| `list_assets` | `GET /api/assets` | none | |
|
|
133
|
+
| `get_stats` | `GET /api/stats` | none | |
|
|
134
|
+
| `list_events` | `GET /api/events` | none | |
|
|
135
|
+
|
|
136
|
+
`screen_withdrawal` / `screen_deposit` always run the synchronous address check, then apply `Thresholds`. If an API key is configured (or `full=True`), they also create an address screening and wait for `get_report`. Pass `full=False` to keep the hook in the public, low-latency path.
|
|
137
|
+
|
|
138
|
+
Authentication for Business routes: `Authorization: Bearer <key>`. Public routes send the header only when a key is present.
|
|
139
|
+
|
|
140
|
+
Retries: 429 / 5xx / transport errors, exponential backoff, `Retry-After` honored. Rate-limit headers (`X-RateLimit-Limit|Remaining|Reset`) are stored on the client. Timeouts default to 30s.
|
|
141
|
+
|
|
142
|
+
Chain detection: Tron (`T` + Base58), EVM (`0x` + 40 hex; defaults to `ethereum` unless you pass `bsc` / `polygon` / `base` / `arbitrum` / `optimism` / `avalanche`), Solana (Base58 32–44), Bitcoin (1… / 3… / bc1…, sanctions only). Explicit `chain` is validated against the address family.
|
|
143
|
+
|
|
144
|
+
## Planned endpoint the main app must expose
|
|
145
|
+
|
|
146
|
+
`POST /api/v1/quick-check` is **not** in the live OpenAPI spec. Enable it from the SDK with `FreezeRadar(use_quick_check=True)` or `check_address(..., use_quick_check=True)`. Until the route exists, those calls fall back to widget + sanctions.
|
|
147
|
+
|
|
148
|
+
The main FreezeRadar app should add:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
POST https://freezeradar.com/api/v1/quick-check
|
|
152
|
+
Authorization: Bearer <Business API key>
|
|
153
|
+
Content-Type: application/json
|
|
154
|
+
|
|
155
|
+
{
|
|
156
|
+
"address": "T...",
|
|
157
|
+
"chain": "tron"
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`chain` optional when it can be detected. Response:
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"ok": true,
|
|
166
|
+
"data": {
|
|
167
|
+
"address": "T...",
|
|
168
|
+
"chain": "tron",
|
|
169
|
+
"blacklisted": true,
|
|
170
|
+
"sanctioned": false,
|
|
171
|
+
"matches": [
|
|
172
|
+
{ "source": "OFAC", "entityName": "...", "entitySlug": "..." }
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
That is the union of `/api/widget/check` and `/api/sanctions/check` in one authenticated, quota-metered call — the shape exchanges need for `check_address` without burning two IP rate limits. Native bulk (`POST /api/v1/address-checks` taking `{ "addresses": [...] }`) is also not published; `check_addresses` fans out to the public GETs today.
|
|
179
|
+
|
|
180
|
+
There is no public PDF `get_report` route. `get_report` in this SDK is the structured `GET /api/v1/screenings/{id}` object.
|
|
181
|
+
|
|
182
|
+
## Decisions
|
|
183
|
+
|
|
184
|
+
`Thresholds` (all overridable):
|
|
185
|
+
|
|
186
|
+
| Field | Default | Effect |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| `block_if_blacklisted` | `True` | Issuer blacklist → block |
|
|
189
|
+
| `block_if_sanctioned` | `True` | Sanctions match → block |
|
|
190
|
+
| `block_score` | `75` | `overallRisk` ≥ this → block |
|
|
191
|
+
| `review_score` | `40` | `overallRisk` ≥ this → review |
|
|
192
|
+
| `block_categories` | `HIGH`, `CRITICAL` | |
|
|
193
|
+
| `review_categories` | `MEDIUM` | |
|
|
194
|
+
| `high_value_amount` | unset | If `amount` ≥ this, use `high_value_review_score` |
|
|
195
|
+
| `high_value_review_score` | `25` | |
|
|
196
|
+
|
|
197
|
+
Transaction screening `verdict` values `CLEAR` / `REVIEW` / `BLOCK` are mapped onto the same actions when a transaction report is present.
|
|
198
|
+
|
|
199
|
+
## Webhooks
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
from freezeradar import verify_signature
|
|
203
|
+
|
|
204
|
+
raw = verify_signature(request.body, request.headers, webhook_secret)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Signature is `sha256=` + HMAC-SHA256 of `{timestamp}.{raw_body}` using headers `x-freezeradar-timestamp` and `x-freezeradar-signature`, as documented on `/developers`. Deliveries are retried up to 5 times with backoff; dedupe on event `id` and order by timestamp.
|
|
208
|
+
|
|
209
|
+
## Examples
|
|
210
|
+
|
|
211
|
+
- `examples/fastapi_withdrawal.py` — FastAPI withdrawal screen that 403s on `block`
|
|
212
|
+
- `examples/django_withdrawal_hook.py` — Django JSON view
|
|
213
|
+
- `examples/celery_deposit_screening.py` — deposit task that holds / queues / clears a credit
|
|
214
|
+
- `examples/addresses.csv` — input for `freezeradar screen-file`
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
pip install -e ".[dev]"
|
|
220
|
+
ruff check src tests examples
|
|
221
|
+
ruff format src tests examples
|
|
222
|
+
mypy
|
|
223
|
+
pytest
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
CI runs that matrix on Python 3.9–3.13. The `publish.yml` workflow uses PyPI trusted publishing on GitHub Release (OIDC). It does not publish from CI on push.
|
|
227
|
+
|
|
228
|
+
## What you need to do manually
|
|
229
|
+
|
|
230
|
+
1. **Do not ship a real API key** in this repo. Create Business keys in the FreezeRadar Developer Console and inject `FREEZERADAR_API_KEY` at runtime.
|
|
231
|
+
2. **PyPI trusted publishing:** create a PyPI project `freezeradar`, add a pending publisher for GitHub org `Freezeradar`, repo `python-sdk`, workflow `publish.yml`, environment `pypi`. Create the GitHub Environment `pypi` (optional protection rules). Then cut a GitHub Release when you actually want a package on PyPI.
|
|
232
|
+
3. **Expose `POST /api/v1/quick-check`** on the main app (schema above) so `use_quick_check=True` stops falling back. Optionally add a bulk address-check route if exchange customers outgrow per-IP public GETs.
|
|
233
|
+
4. **Grant this SDK repo access** to private docs if you want models regenerated from internal types rather than `/api/openapi/v1`.
|
|
234
|
+
5. Results are explainable risk signals, not legal advice, AML certification, or an instruction to freeze funds.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# freezeradar
|
|
2
|
+
|
|
3
|
+
Python SDK for [FreezeRadar](https://freezeradar.com) wallet-risk intelligence. Exchanges, OTC desks, and compliance teams can screen freezeable-asset counterparties (USDT, USDC, PAXG, XAUt, and others) for issuer blacklists, sanctions hits, and explainable freeze-risk scores.
|
|
4
|
+
|
|
5
|
+
The client talks to the live API documented at [freezeradar.com/developers](https://freezeradar.com/developers) and [GET /api/openapi/v1](https://freezeradar.com/api/openapi/v1). Models are generated from that OpenAPI document (snapshot in `openapi/v1.json`).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install freezeradar
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Python 3.9+. Sync and async clients use [httpx](https://www.python-httpx.org/). Types use Pydantic v2.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from freezeradar import FreezeRadar, AsyncFreezeRadar, Action
|
|
17
|
+
|
|
18
|
+
with FreezeRadar() as client: # public checks; no key required
|
|
19
|
+
check = client.check_address("TBZefVsyQpzzxc2WSCLbZBECvxVdzGqdtC")
|
|
20
|
+
print(check.chain, check.blacklisted, check.sanctioned, check.matches)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Business API methods (`create_address_screening`, `get_report`, full `screen_*`) need a Bearer key from the Developer Console:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
export FREEZERADAR_API_KEY=fr_live_...
|
|
27
|
+
# optional
|
|
28
|
+
export FREEZERADAR_BASE_URL=https://freezeradar.com
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
client = FreezeRadar(api_key="fr_live_...") # or from the environment
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from freezeradar import FreezeRadar, Thresholds
|
|
39
|
+
|
|
40
|
+
client = FreezeRadar(
|
|
41
|
+
thresholds=Thresholds(block_score=75, review_score=40, high_value_amount=25_000),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Fast path: issuer blacklist + sanctions (public, IP rate-limited)
|
|
45
|
+
check = client.check_address("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", chain="ethereum")
|
|
46
|
+
|
|
47
|
+
# Batch (sequential sync / concurrent async). Same public endpoints.
|
|
48
|
+
results = client.check_addresses([
|
|
49
|
+
"TBZefVsyQpzzxc2WSCLbZBECvxVdzGqdtC",
|
|
50
|
+
{"address": "0x0000000000000000000000000000000000000000", "chain": "ethereum"},
|
|
51
|
+
])
|
|
52
|
+
|
|
53
|
+
# Withdrawal / deposit policy: allow | review | block
|
|
54
|
+
decision = client.screen_withdrawal(
|
|
55
|
+
"TLXDJUauN4vgN75tJtCbXiAm4mQaDTmTNY",
|
|
56
|
+
asset="USDT",
|
|
57
|
+
amount=12_000,
|
|
58
|
+
)
|
|
59
|
+
if decision.action.value == "block":
|
|
60
|
+
raise SystemExit(decision.reasons)
|
|
61
|
+
|
|
62
|
+
# Full scored report (Business API key, async job, polled)
|
|
63
|
+
job = client.create_address_screening("0x...", "ethereum", assets=["USDT"])
|
|
64
|
+
report = client.get_report(job.id) # waits until COMPLETED / FAILED
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Async:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
async with AsyncFreezeRadar() as client:
|
|
71
|
+
checks = await client.check_addresses(addresses, max_concurrency=5)
|
|
72
|
+
decision = await client.screen_deposit(from_address, asset="USDC", amount=500)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## CLI
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
freezeradar check TBZefVsyQpzzxc2WSCLbZBECvxVdzGqdtC
|
|
79
|
+
freezeradar check 0x... --chain ethereum --json --decide
|
|
80
|
+
freezeradar screen-file examples/addresses.csv --out results.csv
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
CSV columns: `address` (required), optional `chain`, `asset`, `amount`, `direction` (`withdrawal` or `deposit`).
|
|
84
|
+
|
|
85
|
+
## Endpoints used
|
|
86
|
+
|
|
87
|
+
Base URL: `https://freezeradar.com` (override with `FREEZERADAR_BASE_URL` or `base_url=`).
|
|
88
|
+
|
|
89
|
+
| SDK method | HTTP | Auth | Notes |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| `check_address` / `check_addresses` | `GET /api/widget/check` + `GET /api/sanctions/check` | none | Default synchronous screen. Widget is 30/min per IP; sanctions is 20/min per IP. Bitcoin uses sanctions only (no blacklist). |
|
|
92
|
+
| `check_address(..., use_quick_check=True)` | `POST /api/v1/quick-check` | Bearer | **Not live yet.** If the route 404s, the SDK falls back to the public pair. |
|
|
93
|
+
| `create_address_screening` | `POST /api/v1/address-screenings` | Bearer | Body: `address`, `chain`, optional `assets`, `scanMode`. |
|
|
94
|
+
| `create_transaction_screening` | `POST /api/v1/transaction-screenings` | Bearer | Body: `chain`, `transactionHash`, optional `policyId`. |
|
|
95
|
+
| `get_report` | `GET /api/v1/screenings/{id}` | Bearer | Polls `QUEUED` / `RUNNING` until `COMPLETED` or `FAILED`. |
|
|
96
|
+
| `list_chains` | `GET /api/chains` | none | Includes bitcoin (sanctions-only). |
|
|
97
|
+
| `list_assets` | `GET /api/assets` | none | |
|
|
98
|
+
| `get_stats` | `GET /api/stats` | none | |
|
|
99
|
+
| `list_events` | `GET /api/events` | none | |
|
|
100
|
+
|
|
101
|
+
`screen_withdrawal` / `screen_deposit` always run the synchronous address check, then apply `Thresholds`. If an API key is configured (or `full=True`), they also create an address screening and wait for `get_report`. Pass `full=False` to keep the hook in the public, low-latency path.
|
|
102
|
+
|
|
103
|
+
Authentication for Business routes: `Authorization: Bearer <key>`. Public routes send the header only when a key is present.
|
|
104
|
+
|
|
105
|
+
Retries: 429 / 5xx / transport errors, exponential backoff, `Retry-After` honored. Rate-limit headers (`X-RateLimit-Limit|Remaining|Reset`) are stored on the client. Timeouts default to 30s.
|
|
106
|
+
|
|
107
|
+
Chain detection: Tron (`T` + Base58), EVM (`0x` + 40 hex; defaults to `ethereum` unless you pass `bsc` / `polygon` / `base` / `arbitrum` / `optimism` / `avalanche`), Solana (Base58 32–44), Bitcoin (1… / 3… / bc1…, sanctions only). Explicit `chain` is validated against the address family.
|
|
108
|
+
|
|
109
|
+
## Planned endpoint the main app must expose
|
|
110
|
+
|
|
111
|
+
`POST /api/v1/quick-check` is **not** in the live OpenAPI spec. Enable it from the SDK with `FreezeRadar(use_quick_check=True)` or `check_address(..., use_quick_check=True)`. Until the route exists, those calls fall back to widget + sanctions.
|
|
112
|
+
|
|
113
|
+
The main FreezeRadar app should add:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
POST https://freezeradar.com/api/v1/quick-check
|
|
117
|
+
Authorization: Bearer <Business API key>
|
|
118
|
+
Content-Type: application/json
|
|
119
|
+
|
|
120
|
+
{
|
|
121
|
+
"address": "T...",
|
|
122
|
+
"chain": "tron"
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`chain` optional when it can be detected. Response:
|
|
127
|
+
|
|
128
|
+
```json
|
|
129
|
+
{
|
|
130
|
+
"ok": true,
|
|
131
|
+
"data": {
|
|
132
|
+
"address": "T...",
|
|
133
|
+
"chain": "tron",
|
|
134
|
+
"blacklisted": true,
|
|
135
|
+
"sanctioned": false,
|
|
136
|
+
"matches": [
|
|
137
|
+
{ "source": "OFAC", "entityName": "...", "entitySlug": "..." }
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
That is the union of `/api/widget/check` and `/api/sanctions/check` in one authenticated, quota-metered call — the shape exchanges need for `check_address` without burning two IP rate limits. Native bulk (`POST /api/v1/address-checks` taking `{ "addresses": [...] }`) is also not published; `check_addresses` fans out to the public GETs today.
|
|
144
|
+
|
|
145
|
+
There is no public PDF `get_report` route. `get_report` in this SDK is the structured `GET /api/v1/screenings/{id}` object.
|
|
146
|
+
|
|
147
|
+
## Decisions
|
|
148
|
+
|
|
149
|
+
`Thresholds` (all overridable):
|
|
150
|
+
|
|
151
|
+
| Field | Default | Effect |
|
|
152
|
+
|---|---|---|
|
|
153
|
+
| `block_if_blacklisted` | `True` | Issuer blacklist → block |
|
|
154
|
+
| `block_if_sanctioned` | `True` | Sanctions match → block |
|
|
155
|
+
| `block_score` | `75` | `overallRisk` ≥ this → block |
|
|
156
|
+
| `review_score` | `40` | `overallRisk` ≥ this → review |
|
|
157
|
+
| `block_categories` | `HIGH`, `CRITICAL` | |
|
|
158
|
+
| `review_categories` | `MEDIUM` | |
|
|
159
|
+
| `high_value_amount` | unset | If `amount` ≥ this, use `high_value_review_score` |
|
|
160
|
+
| `high_value_review_score` | `25` | |
|
|
161
|
+
|
|
162
|
+
Transaction screening `verdict` values `CLEAR` / `REVIEW` / `BLOCK` are mapped onto the same actions when a transaction report is present.
|
|
163
|
+
|
|
164
|
+
## Webhooks
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from freezeradar import verify_signature
|
|
168
|
+
|
|
169
|
+
raw = verify_signature(request.body, request.headers, webhook_secret)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Signature is `sha256=` + HMAC-SHA256 of `{timestamp}.{raw_body}` using headers `x-freezeradar-timestamp` and `x-freezeradar-signature`, as documented on `/developers`. Deliveries are retried up to 5 times with backoff; dedupe on event `id` and order by timestamp.
|
|
173
|
+
|
|
174
|
+
## Examples
|
|
175
|
+
|
|
176
|
+
- `examples/fastapi_withdrawal.py` — FastAPI withdrawal screen that 403s on `block`
|
|
177
|
+
- `examples/django_withdrawal_hook.py` — Django JSON view
|
|
178
|
+
- `examples/celery_deposit_screening.py` — deposit task that holds / queues / clears a credit
|
|
179
|
+
- `examples/addresses.csv` — input for `freezeradar screen-file`
|
|
180
|
+
|
|
181
|
+
## Development
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
pip install -e ".[dev]"
|
|
185
|
+
ruff check src tests examples
|
|
186
|
+
ruff format src tests examples
|
|
187
|
+
mypy
|
|
188
|
+
pytest
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
CI runs that matrix on Python 3.9–3.13. The `publish.yml` workflow uses PyPI trusted publishing on GitHub Release (OIDC). It does not publish from CI on push.
|
|
192
|
+
|
|
193
|
+
## What you need to do manually
|
|
194
|
+
|
|
195
|
+
1. **Do not ship a real API key** in this repo. Create Business keys in the FreezeRadar Developer Console and inject `FREEZERADAR_API_KEY` at runtime.
|
|
196
|
+
2. **PyPI trusted publishing:** create a PyPI project `freezeradar`, add a pending publisher for GitHub org `Freezeradar`, repo `python-sdk`, workflow `publish.yml`, environment `pypi`. Create the GitHub Environment `pypi` (optional protection rules). Then cut a GitHub Release when you actually want a package on PyPI.
|
|
197
|
+
3. **Expose `POST /api/v1/quick-check`** on the main app (schema above) so `use_quick_check=True` stops falling back. Optionally add a bulk address-check route if exchange customers outgrow per-IP public GETs.
|
|
198
|
+
4. **Grant this SDK repo access** to private docs if you want models regenerated from internal types rather than `/api/openapi/v1`.
|
|
199
|
+
5. Results are explainable risk signals, not legal advice, AML certification, or an instruction to freeze funds.
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT. See `LICENSE`.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Celery task that screens a deposit sender before crediting a balance.
|
|
2
|
+
|
|
3
|
+
Run the worker with ``FREEZERADAR_API_KEY`` set if you want full Business
|
|
4
|
+
API reports (``full=True``). Public blacklist/sanctions checks work without a key.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from freezeradar import Action, FreezeRadar, Thresholds
|
|
12
|
+
|
|
13
|
+
# from celery import shared_task
|
|
14
|
+
|
|
15
|
+
client = FreezeRadar(
|
|
16
|
+
thresholds=Thresholds(block_score=80, review_score=35, high_value_amount=25_000),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def screen_deposit_task(
|
|
21
|
+
from_address: str,
|
|
22
|
+
asset: str = "USDT",
|
|
23
|
+
amount: Optional[float] = None,
|
|
24
|
+
chain: Optional[str] = None,
|
|
25
|
+
credit_id: Optional[str] = None,
|
|
26
|
+
) -> Dict[str, Any]:
|
|
27
|
+
"""Replace with ``@shared_task`` in a real Celery app."""
|
|
28
|
+
decision = client.screen_deposit(
|
|
29
|
+
from_address,
|
|
30
|
+
asset=asset,
|
|
31
|
+
amount=amount,
|
|
32
|
+
chain=chain,
|
|
33
|
+
full=False,
|
|
34
|
+
)
|
|
35
|
+
result = {
|
|
36
|
+
"credit_id": credit_id,
|
|
37
|
+
"action": decision.action.value,
|
|
38
|
+
"reasons": decision.reasons,
|
|
39
|
+
"chain": decision.chain,
|
|
40
|
+
"blacklisted": decision.blacklisted,
|
|
41
|
+
"sanctioned": decision.sanctioned,
|
|
42
|
+
"overall_risk": decision.overall_risk,
|
|
43
|
+
}
|
|
44
|
+
if decision.action is Action.BLOCK:
|
|
45
|
+
# Hold the credit and open a compliance case.
|
|
46
|
+
result["credit_status"] = "held"
|
|
47
|
+
elif decision.action is Action.REVIEW:
|
|
48
|
+
result["credit_status"] = "pending_review"
|
|
49
|
+
else:
|
|
50
|
+
result["credit_status"] = "clear"
|
|
51
|
+
return result
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
from celery import shared_task # type: ignore[import-not-found]
|
|
56
|
+
|
|
57
|
+
screen_incoming_deposit = shared_task(name="freezeradar.screen_incoming_deposit")(
|
|
58
|
+
screen_deposit_task
|
|
59
|
+
)
|
|
60
|
+
except ImportError:
|
|
61
|
+
screen_incoming_deposit = screen_deposit_task
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Django withdrawal screening view.
|
|
2
|
+
|
|
3
|
+
Wire this into your withdrawals app. The SDK is the only FreezeRadar dependency;
|
|
4
|
+
Django itself is assumed to already be installed in that service.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from freezeradar import Action, FreezeRadar, Thresholds
|
|
13
|
+
|
|
14
|
+
client = FreezeRadar(
|
|
15
|
+
thresholds=Thresholds(block_if_blacklisted=True, block_if_sanctioned=True),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def screen_withdrawal_view(request: Any) -> Any:
|
|
20
|
+
# from django.http import JsonResponse
|
|
21
|
+
from django.http import JsonResponse # type: ignore[import-not-found]
|
|
22
|
+
|
|
23
|
+
if request.method != "POST":
|
|
24
|
+
return JsonResponse({"error": "POST required"}, status=405)
|
|
25
|
+
body = json.loads(request.body.decode("utf-8"))
|
|
26
|
+
decision = client.screen_withdrawal(
|
|
27
|
+
body["to_address"],
|
|
28
|
+
asset=body.get("asset", "USDT"),
|
|
29
|
+
amount=float(body.get("amount", 0)),
|
|
30
|
+
chain=body.get("chain"),
|
|
31
|
+
full=False,
|
|
32
|
+
)
|
|
33
|
+
status = 200 if decision.action is not Action.BLOCK else 403
|
|
34
|
+
return JsonResponse(
|
|
35
|
+
{
|
|
36
|
+
"action": decision.action.value,
|
|
37
|
+
"reasons": decision.reasons,
|
|
38
|
+
"chain": decision.chain,
|
|
39
|
+
"blacklisted": decision.blacklisted,
|
|
40
|
+
"sanctioned": decision.sanctioned,
|
|
41
|
+
},
|
|
42
|
+
status=status,
|
|
43
|
+
)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""FastAPI withdrawal hook.
|
|
2
|
+
|
|
3
|
+
Install extras in your service: ``pip install freezeradar fastapi``
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from fastapi import FastAPI, HTTPException
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
|
|
13
|
+
from freezeradar import Action, FreezeRadar, Thresholds
|
|
14
|
+
|
|
15
|
+
app = FastAPI()
|
|
16
|
+
client = FreezeRadar(
|
|
17
|
+
thresholds=Thresholds(block_score=75, review_score=40, high_value_amount=50_000),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class WithdrawalRequest(BaseModel):
|
|
22
|
+
to_address: str
|
|
23
|
+
asset: str = "USDT"
|
|
24
|
+
amount: float
|
|
25
|
+
chain: Optional[str] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.post("/withdrawals/screen")
|
|
29
|
+
def screen_withdrawal(body: WithdrawalRequest) -> dict[str, object]:
|
|
30
|
+
decision = client.screen_withdrawal(
|
|
31
|
+
body.to_address,
|
|
32
|
+
asset=body.asset,
|
|
33
|
+
amount=body.amount,
|
|
34
|
+
chain=body.chain,
|
|
35
|
+
full=False,
|
|
36
|
+
)
|
|
37
|
+
if decision.action is Action.BLOCK:
|
|
38
|
+
raise HTTPException(
|
|
39
|
+
status_code=403,
|
|
40
|
+
detail={"action": decision.action.value, "reasons": decision.reasons},
|
|
41
|
+
)
|
|
42
|
+
return {
|
|
43
|
+
"action": decision.action.value,
|
|
44
|
+
"reasons": decision.reasons,
|
|
45
|
+
"chain": decision.chain,
|
|
46
|
+
"blacklisted": decision.blacklisted,
|
|
47
|
+
"sanctioned": decision.sanctioned,
|
|
48
|
+
}
|