Risk Score API
Wallet and transaction risk lookups backed by the Alpha AML upstream API. Returns a styled PDF report for an address, or JSON risk metadata for the two parties of a transaction.
Base URL
https://api.alpha-aml.com
GET /api/report/wallet
Returns a PDF AML risk report for the given wallet address and chain.
Headers
| Header | Description |
|---|---|
| X-apikeyrequired | Caller JWT. Per-key rate limits apply (10/s, 100/min, 2000/day). Not a query parameter — sending it as ?apiKey= no longer works. |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| addressrequired | string | Wallet address to check. Blank or missing → 400. |
| chainoptional | string | Blockchain the address belongs to. If omitted, upstream API attempts auto-detection. When supplied, an address whose family does not match the chain is rejected with UNSUPPORTED_CHAIN. |
Supported chain values:
Response
| Status | Content-Type | Body |
|---|---|---|
| 200 | application/pdf | Generated PDF bytes. Header: Content-Disposition: attachment; filename="aml-report-<address>.pdf". |
| 400 | application/json | INVALID_ADDRESS (bad format), UNSUPPORTED_CHAIN (address family ≠ chain), BAD_REQUEST (missing param). |
| 401 | application/json | Missing or invalid X-apikey — rejected before this endpoint runs. |
| 403 | application/json | Key has no access to this endpoint. |
| 404 | application/json | ADDRESS_NOT_FOUND — no upstream data for the address. |
| 429 | application/json | Rate limit exceeded — Retry-After header and retry_after_seconds in the body say when to retry. |
| 500 | application/json | PDF_FAILED — Chromium failed to render the report. |
{ "error": { "code", "field", "message" } }. Auth/rate-limit errors (401/403/429) come from the SDK layer before the request reaches this endpoint. Every response — success or error — carries an X-Request-Id header for tracing. A successful call still returns the raw PDF. See the Error Reference for app-level codes.curl Examples
curl -H "X-apikey: <key>" \ "https://api.alpha-aml.com/api/report/wallet?address=0x7f367cc41522ce07553e823bf3be79a889debe1b&chain=ETH" \ -o aml-report.pdfCOPY
curl -H "X-apikey: <key>" \ "https://api.alpha-aml.com/api/report/wallet?address=TXyz1234567890abcdefgh&chain=TRON" \ -o aml-report.pdfCOPY
GET /api/report/txhash
Resolves a transaction hash to a single transfer, pins the exact log event it analysed, and returns a full risk report for both parties of that transfer — scores plus their provenance (report_id, model and data versions, freshness, score breakdown and per-finding factors). This endpoint does not generate a PDF — use /api/report/wallet with the returned sender / receiver for the styled report.
Headers
| Header | Description |
|---|---|
| X-apikeyrequired | Caller JWT. Per-key rate limits apply (10/s, 100/min, 2000/day). Not a query parameter — sending it as ?apiKey= no longer works. |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| txHashrequired | string | Transaction hash. EVM chains: 0x + 64 hex. TRON: 64 hex, no 0x. Wrong shape for the chain → INVALID_TX_HASH. |
| chainrequired | string | Chain to resolve the transaction on. One of the supported values below. |
| event_indexoptional | integer | Filter: keep only the token transfer whose log index equals this value (the number shown under a transaction's event logs on a block explorer). Native transfers have no log index. |
| token_contractoptional | string | Filter: keep only token transfers of this contract address. |
| senderoptional | string | Filter / assertion: keep only transfers sent from this address. |
| recipientoptional | string | Filter / assertion: keep only transfers received by this address. |
Supported chain values:
Transfer Selection
Exactly one transfer is scored. The optional parameters filter the transaction's transfers — the native value transfer (if any) plus every ERC-20 / TRC-20 Transfer log event.
| Situation | Result |
|---|---|
| No filters — exactly one native transfer, no token transfers | That native transfer is used. |
| No filters — no native transfer, exactly one token transfer | That token transfer is used. |
| No filters — a native transfer and token transfers, or more than one token transfer | 409 MULTIPLE_TRANSFERS — add a filter. |
| No filters — no transfers at all | 409 NO_TRANSFERS. |
| Filters given — no transfer matches | 409 NO_MATCH. |
| Filters given — more than one transfer still matches | 409 MULTIPLE_TRANSFERS — narrow further. |
| Filters given — exactly one match | That transfer is used. |
Response
| Status | Content-Type | Body |
|---|---|---|
| 200 | application/json | Transfer metadata + risk scores (fields below). |
| 400 | application/json | INVALID_TX_HASH, UNSUPPORTED_CHAIN, INVALID_ADDRESS, BAD_REQUEST. |
| 401 | application/json | Missing or invalid X-apikey — rejected before this endpoint runs. |
| 403 | application/json | Key has no access to this endpoint. |
| 404 | application/json | UNKNOWN_HASH, ADDRESS_NOT_FOUND. |
| 409 | application/json | PENDING_TRANSACTION, TRANSACTION_FAILED, NO_TRANSFERS, MULTIPLE_TRANSFERS, NO_MATCH. |
| 429 | application/json | Rate limit exceeded — Retry-After header and retry_after_seconds in the body say when to retry. |
| 502 | application/json | RPC_ERROR — RPC provider unreachable or returned an error. |
funding_entity_types array was removed. The same data now lives at sender_report.funding.distribution (and the receiver's is available too, which it was not before). Everything else in 1.2.15 is unchanged; all other additions are new fields — among them <side>_report.post_graph_exposure and <side>_report.data_as_of.exposure_checked_to_chain_head, which carry the engine's post-graph exposure check through per side.Response Fields (200 OK JSON)
"1.3.1") — not a risk-model version. The scoring model and reference data behind each side are reported per side as model_version / data_version.txHash).chain).block_number. Omitted on TRON, which does not expose it on this lookup.block_number, derived from the engine's chain_head_block. That head can trail the live chain, so this is conservative and never over-reports.event_index request parameter. null for a native transfer, which emits no log.event_index, because the chains differ: evm_block_log_index (chain-native block-level logIndex, the number explorers show) · tron_log_array_position (position in the transaction's log array — TronGrid exposes no chain-native index) · none_native_transfer."490.14")."native" for a coin transfer, otherwise the token contract address.amount — 18 (native EVM), 6 (native TRON), or the token's own.from address.to address.sender. null means NOT SCORED — it is not a low score and must not be read as low risk. See sender_report.status for why.receiver. Same null semantics as above.min_inclusive / max_exclusive. Use that pair for classification — the integer range_min / range_max pair leaves fractional scores unclassified./api/engine/report-v1 returns for that address — see its Response Fields for full field semantics and Rate Limits & Retries for quota behaviour. Summarised below.data_version, so a label sync changes it — a later standalone wallet lookup may legitimately return a different score.complete / partial. partial means the score is not fully computed — pair it with reason.full / activity_only.GRAPH_BEHIND_REQUESTED_BLOCK, CACHE_WARMING, COUNTERPARTY_LOOKUP_FAILED. Empty when complete.model_version alone cannot reproduce a score once labels sync; both together can.chain_head_block / chain_head_utc, graph_block / graph_utc, graph_lag_blocks / graph_lag_seconds, requested_block (this transfer's block), graph_covers_requested_block — whether the exposure graph actually reached the block being checked — and exposure_checked_to_chain_head, whether value received after graph_block was examined at request time. Activity is current to chain_head_block; traced exposure only to graph_block, and the gap between the two is what post_graph_exposure below covers.null when not scored.risk_score.direct_listing, counterparty, indirect_exposure, concentration, activity_baseline, entity_penalty, geo_penalty) plus decided_by, decided_by_category, decided_by_list_type. Components are not additive — the score is the strongest of them, and decided_by names the one that set it. decided_by_list_type: operator_rating means our own rating of a service, not a sanctions match.data_as_of.graph_block, up to chain_head_block — value received from addresses the last graph pass had already judged, checked at request time. This is the window a freshly-mined transfer falls into, so when screening a deposit by its hash this is the field that says whether value newer than the graph was examined rather than simply absent from the assessment. Carries status, tainted_usd, share_of_received_pct, transfer_count, source_count, senders_examined, source_addresses, category_code, from_block / last_block / last_utc, window and note. Treat it as a floor on exposure in that window, not the whole of it: hop-1 only, limited to senders already judged, to the largest senders_examined of them by value, and to priced assets — the next graph pass traces the rest. status: "none" with empty values means the window was examined and nothing matched, which is not the same as not having been checked — data_as_of.exposure_checked_to_chain_head distinguishes the two. See report-v1’s risk_factors.post_graph_exposure for a populated example.kind, category_code / category_label, entity_class, list_type, source, entity, jurisdiction, severity, directness, hops, share_pct, amount_usd, denominator / denominator_value_usd, source_addresses / source_count, candidate_points / effective_points, applied, exclusion_reason, detail. A finding can be reported with applied: false — real, but not what set the score.tainted_usd, source_count, rule. Exposure below the materiality threshold, reported separately from a true zero. The test is on the absolute USD figure, not the share.describes, per_transfer, max_transfers_sampled, window_seconds, anchored_to, denominator / denominator_value. per_transfer: false means the shares describe a bounded sample of this address's inflows — not the funds of this transfer. Do not multiply amount by a share to get a risk figure.label, percentage (rounded to 1 dp), share_pct_exact (5 dp — a small real share prints as 0.0 in percentage), value (absolute, in the unit named by denominator), denominator / denominator_value, category_code / category_label, entity_class, risk_bearing, source, transfer_count.none_held means attribution is unknown — which is not the same as clean.<side>_report.funding.scope.per_transfer.Example Response
{
"version": "1.3.1",
"hash": "0x7660631c30ca9befe961e2910b30c92fbe910a7a7156e41337912c616240cd16",
"chain": "eth",
"block_number": 23612901,
"block_hash": "0x9f2c4b8e1d3a77c0e5b9a2f84c16d05e7a3b9c1f2e8d4a60b7c3f51e9d82a04c",
"confirmations": 14207,
"event_index": 95,
"event_index_basis": "evm_block_log_index",
"amount": "26500.97",
"amount_raw": "26500970000",
"asset": "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
"decimals": 6,
"token_symbol": "USDC",
"sender": "0x935f64b44b5c48a1539c4ada5161d27ace4205b5",
"receiver": "0x7547463fe4d6b3a5efc8540af5f7cc0665475bd8",
"sender_risk_score": 55,
"receiver_risk_score": 24.75,
"score_zones": [
{ "label": "Very Low", "range_min": 0, "range_max": 25, "min_inclusive": 0.0, "max_exclusive": 26.0 },
{ "label": "High", "range_min": 51, "range_max": 75, "min_inclusive": 51.0, "max_exclusive": 76.0 }
],
"sender_report": {
"address": "0x935f64b44b5c48a1539c4ada5161d27ace4205b5",
"report_id": "rpt_4c1f09a7be83d215cc90aa47e1f2b6d8",
"status": "complete",
"coverage": "full",
"is_final": true,
"preliminary": false,
"reason": "",
"model_version": "4.3.0-da50fad0",
"data_version": "b866bd64",
"generated_at_utc": "2026-10-20T08:17:24Z",
"data_as_of": {
"chain_head_block": 26232545,
"graph_block": 26231130,
"graph_lag_blocks": 1415,
"graph_lag_seconds": 17052,
"requested_block": 23612901,
"graph_covers_requested_block": true,
"exposure_checked_to_chain_head": true
},
"risk_score": 55,
"risk_level": "HIGH RISK",
"blacklisted": false,
"score_breakdown": {
"direct_listing": 0.0,
"counterparty": 55.0,
"indirect_exposure": 21.4,
"activity_baseline": 18.7,
"score": 55.0,
"decided_by": "counterparty",
"decided_by_category": "medium_reputation_service",
"decided_by_list_type": "operator_rating",
"note": "Components are not additive; the score is the strongest of them."
},
"post_graph_exposure": {
"status": "none",
"tainted_usd": 0.0,
"share_of_received_pct": 0.0,
"transfer_count": 0,
"source_count": 0,
"senders_examined": 64,
"source_addresses": [],
"category_code": "",
"from_block": 26231131,
"last_block": 26232545,
"last_utc": "",
"window": "after graph_block, to chain_head_block"
},
"factors": [
{
"kind": "counterparty",
"category_code": "medium_reputation_service",
"entity_class": "service",
"list_type": "operator_rating",
"entity": "Wirex",
"directness": "direct",
"hops": 1,
"share_pct": 28.1,
"amount_usd": 41922.63,
"denominator": "sampled_inflow_usd",
"denominator_value_usd": 149190.85,
"applied": true,
"effective_points": 55.0,
"source_count": 387,
"detail": "Counterparty is a service we rate as medium-reputation. Our own operator rating, not a sanctions match."
}
],
"excluded_below_threshold": {
"tainted_usd": 0.0,
"source_count": 0,
"rule": "absolute_traced_value_usd"
},
"funding": {
"scope": {
"describes": "address_inflows",
"per_transfer": false,
"max_transfers_sampled": 200000,
"window_seconds": 18000000,
"anchored_to": "address_last_activity",
"denominator": "sampled_inflow_usd",
"denominator_value": 149190.85
},
"distribution": [
{
"label": "Wirex",
"percentage": 28.1,
"share_pct_exact": 28.10442,
"value": 41922.63,
"category_code": "medium_reputation_service",
"entity_class": "service",
"risk_bearing": true,
"transfer_count": 387,
"denominator": "sampled_inflow_usd",
"denominator_value": 149190.85
}
]
},
"entity_attribution": { "attribution": "none_held" },
"jurisdiction_risk": { "listed_count": 0 },
"related_entities": { "named_counterparty_count": 1 }
},
"receiver_report": {
"address": "0x7547463fe4d6b3a5efc8540af5f7cc0665475bd8",
"report_id": "rpt_0b93ae51cc7f4428d6150e39ab77c214",
"status": "complete",
"coverage": "full",
"is_final": true,
"model_version": "4.3.0-da50fad0",
"data_version": "b866bd64",
"data_as_of": { "graph_covers_requested_block": true, "requested_block": 23612901 },
"risk_score": 24.75,
"risk_level": "VERY LOW RISK",
"score_breakdown": { "score": 24.75, "decided_by": "activity_baseline" },
"post_graph_exposure": { "status": "none", "senders_examined": 12, "tainted_usd": 0.0 },
"factors": [],
"funding": { "scope": { "per_transfer": false }, "distribution": [] }
}
}
Example — one side not scored
When the exposure graph has not yet reached the transfer's block, that side comes back partial with a null score. A null score is "not computed", never "low risk" — do not treat it as a pass.
{
"version": "1.3.1",
"block_number": 26232540,
"confirmations": 5,
"event_index": 95,
"sender_risk_score": 55,
"receiver_risk_score": null,
"receiver_report": {
"status": "partial",
"coverage": "activity_only",
"is_final": false,
"preliminary": true,
"reason": "GRAPH_BEHIND_REQUESTED_BLOCK",
"data_as_of": {
"graph_block": 26231130,
"requested_block": 26232540,
"graph_covers_requested_block": false
},
"risk_score": null,
"risk_level": null
}
}
curl Examples
curl -H "X-apikey: <key>" \ "https://api.alpha-aml.com/api/report/txhash?txHash=0x3421c1fb88ab5962400c904f874ed3ba850c2121e2216433c6c8034872b48d9f&chain=ETH"COPY
# disambiguate a multi-transfer tx by event log index curl -H "X-apikey: <key>" \ "https://api.alpha-aml.com/api/report/txhash?txHash=0x97ee11e7360dfff10a7e7f5878a5efe1bd008e913111f8224212bc5da6366489&chain=ETH&event_index=63"COPY
curl -H "X-apikey: <key>" \ "https://api.alpha-aml.com/api/report/txhash?txHash=e512d7f0eeeba031d9442a0ae8673a2ea1ede7d2dfe943cf86fcc0528dc4c937&chain=TRON"COPY
GET /api/engine/report-v1
Returns a full risk assessment for a single address: score, the findings behind it, attribution for the address and its counterparties, and metadata describing how complete and how current the analysis is.
report.status before risk_assessment.score.
A score is only an assessment when status is complete; otherwise it is
preliminary or absent. score is null whenever
status is not complete.Headers
| Header | Description |
|---|---|
| X-apikeyrequired | API key. Header only — a key supplied as ?apiKey=
returns 401. |
| User-Agentoptional | Send an identifying value such as your-app/1.0. Some default library
agents are filtered at the edge. |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| addressrequired | string | Address to score. Validated for the requested network before any scoring runs; a malformed address is rejected rather than scored. |
| chainoptional | string | Accepts ethereum / ETH / eth and
tron / TRON. Defaults to ethereum when omitted.
Never substituted when supplied: a Tron address sent with chain=ethereum
returns INVALID_ADDRESS. |
| blockoptional | integer | Point-in-time evaluation. Scores the address using only events at or before this
block, and echoes it back as report.as_of_block. Omit for current state.
Statistics, counterparties and related entities are all cut at this block.
Must be positive and at or below the indexed head, otherwise
INVALID_BLOCK / BLOCK_IN_FUTURE. If the exposure graph
has not reached it the report comes back partial, with one of two reasons.
DEEP_TRACE_PENDING_EXPOSURE_CURRENT means that block’s exposure
is already counted and only the full-depth trace is outstanding — the
figure is usable and a retry gains nothing until the next full pass.
GRAPH_BEHIND_REQUESTED_BLOCK means it is not covered yet —
activity is answerable, exposure is not; retry in about ten minutes.
data_as_of.exposure_covers_requested_block is the boolean form of
the same distinction. |
Supported Networks
curl Example
curl -H "X-apikey: <key>" -A "your-app/1.0" \ "https://api.alpha-aml.com/api/engine/report-v1?address=0x08723392ed15743cc38513c4925f5e6be5c17243&chain=ethereum"COPY
Full Response Example (200 OK)
Long arrays are trimmed for readability. Every key the endpoint can return is shown.
{
"report": {
"report_id": "rpt_eee33b79440cf7f7e1ac2e491e8fd217",
"status": "complete",
"coverage": "full",
"is_final": true,
"preliminary": false,
"model_version": "4.3.0-da50fad0",
"data_version": "3780a61d",
"generated_at_utc": "2026-10-21T16:36:25Z",
"data_as_of": {
"chain_head_block": 26242211,
"chain_head_utc": "2026-10-21T16:36:11Z",
"graph_block": 26238303,
"graph_utc": "2026-10-21T03:32:59Z",
"graph_lag_blocks": 3908,
"graph_lag_seconds": 46992,
"exposure_checked_to_chain_head": true,
"note": "Activity is current to chain_head_block; traced exposure is current to graph_block. Value received after graph_block from addresses already known to be exposed is checked at request time and reported under risk_factors.post_graph_exposure; deeper paths opened by such a transfer are resolved at the next graph pass."
}
},
"wallet": {
"address": "0x08723392ed15743cc38513c4925f5e6be5c17243",
"blockchain": "ETHEREUM",
"description": "Lazarus Group Ronin Hack ETH (OFAC SDN - May 6 #2)",
"entity_tag": "Lazarus Group"
},
"risk_assessment": {
"score": 100.0,
"score_max": 100,
"risk_level": "VERY HIGH RISK",
"blacklisted": true,
"blacklist_note": "Address found in 6 reference list(s)",
"risk_sources_count": 11,
"risk_sources_note": "flagged connected addresses",
"score_zones": [
{
"label": "Very Low",
"range_min": 0,
"range_max": 25,
"min_inclusive": 0.0,
"max_exclusive": 26.0
},
{
"label": "Low",
"range_min": 26,
"range_max": 40,
"min_inclusive": 26.0,
"max_exclusive": 41.0
}
]
},
"token_balances": [
{
"token": "ETH",
"contract_address": "",
"balance": 3.2972312
}
],
"wallet_statistics": {
"received_transactions_count": 13,
"sent_transactions_count": 4,
"total_transactions_count": 17,
"first_transaction_utc": "2022-05-04T07:32:25Z",
"last_transaction_utc": "2026-08-10T05:51:59Z",
"wallet_age_days": 1631,
"status": "ACTIVE",
"stablecoin_inflow_usd": 0.0,
"stablecoin_outflow_usd": 0.0,
"stablecoin_turnover_usd": 0.0
},
"funded_by_entity_type_distribution": [
{
"label": "Smart contract",
"percentage": 66.7,
"category_code": "unknown",
"category_label": "Unknown attribution",
"entity_class": "",
"risk_bearing": false,
"source": "",
"transfer_count": 2,
"denominator": "sampled_inflow_transfers",
"denominator_value": 3.0,
"value": 2.0,
"share_pct_exact": 66.66667
},
{
"label": "Unnamed active wallet",
"percentage": 33.3,
"category_code": "unknown",
"category_label": "Unknown attribution",
"entity_class": "",
"risk_bearing": false,
"source": "",
"transfer_count": 1,
"denominator": "sampled_inflow_transfers",
"denominator_value": 3.0,
"value": 1.0,
"share_pct_exact": 33.33333
}
],
"funding_scope": {
"describes": "address_inflows",
"per_transfer": false,
"max_transfers_sampled": 200000,
"window_seconds": 18000000,
"anchored_to": "address_last_activity",
"denominator": "sampled_inflow_transfers",
"denominator_value": 3.0,
"note": "Shares describe this address's sampled inflows, NOT the funds of any single transfer. A per-transfer origin breakdown is not available from this endpoint."
},
"risk_factors": {
"blacklists": {
"status": "Flagged",
"score": 100,
"detail": "Address found in 6 reference list(s)"
},
"reported_connected_addresses": [
{
"severity": "Severe",
"score": 100,
"address_count": 3,
"status": "Reported",
"addresses": [
{
"address": "0x08723392ed15743cc38513c4925f5e6be5c17243",
"hops": 0
},
{
"address": "0x098b716b8aaf21512996dc57eb0615e2383e2f96",
"hops": 1
}
]
}
],
"factors": [
{
"kind": "blacklist",
"category_code": "sanctions",
"category_label": "Sanctions designation",
"entity_class": "endpoint",
"list_type": "sanctions",
"source": "OFAC",
"entity": "",
"jurisdiction": "",
"severity": 100,
"directness": "direct",
"hops": 0,
"share_pct": 100.0,
"amount_usd": null,
"denominator": "not_applicable",
"denominator_value_usd": null,
"candidate_points": 100.0,
"applied": true,
"effective_points": 100.0,
"exclusion_reason": "",
"source_addresses": [
"0x08723392ed15743cc38513c4925f5e6be5c17243"
],
"source_count": 1,
"contribution_points": 100.0,
"detail": "Address is named directly in OFAC (sanctions)"
},
{
"kind": "blacklist",
"category_code": "sanctions",
"category_label": "Sanctions designation",
"entity_class": "endpoint",
"list_type": "sanctions",
"source": "ofac_sanctions_list",
"entity": "",
"jurisdiction": "",
"severity": 100,
"directness": "direct",
"hops": 0,
"share_pct": 100.0,
"amount_usd": null,
"denominator": "not_applicable",
"denominator_value_usd": null,
"candidate_points": 100.0,
"applied": true,
"effective_points": 100.0,
"exclusion_reason": "",
"source_addresses": [
"0x08723392ed15743cc38513c4925f5e6be5c17243"
],
"source_count": 1,
"contribution_points": 100.0,
"detail": "Address is named directly in ofac_sanctions_list (sanctions)"
}
],
"score_breakdown": {
"direct_listing": 100.0,
"counterparty": 100.0,
"indirect_exposure": 0.75,
"concentration": 0.0,
"post_graph_exposure": 0.0,
"activity_baseline": 16.88,
"entity_penalty": 75,
"geo_penalty": 19,
"recognised_entity_credit_applied": true,
"counterparty_is_rated_service": false,
"treated_as_service": true,
"service_basis": "named service",
"score": 100.0,
"degraded_lookups": [],
"decided_by": "direct_listing",
"decided_by_category": "sanctions",
"decided_by_list_type": "sanctions",
"note": "Components are not additive; the score is the strongest of them. decided_by names the one that set it; factors marked applied=false were found and reported but did not change the score."
},
"post_graph_exposure": {
"status": "none",
"tainted_usd": 0.0,
"share_of_received_pct": 0.0,
"transfer_count": 0,
"source_count": 0,
"senders_examined": 0,
"source_addresses": [],
"category_code": "",
"from_block": 0,
"last_block": 0,
"last_utc": "",
"window": "after graph_block, to chain_head_block",
"note": "Hop-1 only, limited to senders the last graph pass had already judged, to the largest senders_examined of them by value, and to priced assets. It is therefore a floor on exposure in this window, not the whole of it; the next pass traces the rest. An empty result means the window was examined and nothing matched."
},
"excluded_below_threshold": {
"tainted_usd": 0.0,
"source_count": 0,
"rule": "absolute_traced_value_usd",
"note": "Traced value below the materiality threshold. The test is on the absolute USD figure, not on the share. Such exposure is reported here and still contributes, but on the conservative curve."
}
},
"entity_attribution": {
"attribution": "resolved",
"note": "This address is attributed to the entity below.",
"entity": {
"kind": "company",
"id": "Lazarus Group",
"name": "Lazarus Group",
"risk_score": 75,
"jurisdictions": [
"North Korea",
"South Korea",
"United States"
],
"reasons": [
{
"source": "geo_risk",
"weight": 75,
"detail": "jurisdiction North Korea"
}
]
},
"address_label": {
"label": "Lazarus Group Ronin Hack ETH (OFAC SDN - May 6 #2)",
"company": "Lazarus Group",
"country": "North Korea, South Korea, United States, Japan, China, Malaysia, Indonesia, India, Bangladesh, Philippines, United Kingdom, Germany, France, Russia, Australia, Kuwait",
"recognised_operator": false,
"labelled_in_registry": true,
"behaves_as_service": true,
"service_basis": "named service"
}
},
"jurisdiction_risk": {
"jurisdictions": [
{
"country": "North Korea",
"tier": "high_risk",
"weight": 75
},
{
"country": "South Korea",
"tier": "not_listed",
"weight": 0
},
{
"country": "United States",
"tier": "not_listed",
"weight": 0
}
],
"listed_count": 1,
"total_linked_count": 16,
"truncated": "showing 8 of 16, highest-weighted first",
"applied_penalty_points": 19,
"basis": "North Korea is a high-risk jurisdiction",
"note": "Jurisdiction is a contributing factor, not a verdict. An empty list means no jurisdiction was found for this address."
},
"related_entities": {
"counterparties": [
{
"address": "0x098b716b8aaf21512996dc57eb0615e2383e2f96",
"direction": "received_from",
"name": "Sanctions",
"company": "",
"country": "",
"category_code": "issuer_freeze",
"category_label": "Issuer freeze",
"entity_class": "endpoint",
"severity": 90,
"recognised": false,
"value_usd": 37785900.0
}
],
"named_counterparty_count": 1,
"risk_bearing_count": 1,
"recognised_count": 1,
"note": "Named counterparties only; anonymous ones are not listed."
}
}
COPY
Response Fields
funding_scope — what the funding breakdown coversaddress_inflows. The breakdown describes money arriving at this ADDRESS.false. These shares do NOT describe the funds of any single transfer, and a per-transfer origin breakdown is not available from this endpoint.address_last_activity, or requested_block when block was supplied. The window is anchored to the address rather than to the chain head, so a wallet that stopped transacting a year ago still returns a breakdown.report — identity, completeness and data freshnessrpt_ + 32 hex. Deterministic — identical inputs return an identical id, and it changes whenever anything affecting the answer changes (new data, new model version, different block). Re-reading a finished report is therefore distinguishable from a fresh recalculation. Not a random UUID.complete — analysis ran in full. partial — a component was unavailable; the score is preliminary. unavailable — no analysis was performed. Branch on this before reading score.status != complete. Current values: CACHE_WARMING, DEEP_TRACE_PENDING_EXPOSURE_CURRENT, GRAPH_BEHIND_REQUESTED_BLOCK, COUNTERPARTY_LOOKUP_FAILED, ENTITY_GEO_LOOKUP_FAILED, RELATED_ENTITIES_LOOKUP_FAILED, AGGREGATES_NOT_BUILT. Treat unknown values as partial. DEEP_TRACE_PENDING_EXPOSURE_CURRENT is the one partial that needs no retry: the requested block’s exposure is already counted and only the full-depth trace is outstanding, so re-querying before the next full pass gains nothing.full — the address has history and it was analysed. no_history — a valid address with zero transfers; distinct from "could not analyse". partial_history — history exists but was not fully covered.status = complete. A final report is reproducible: identical inputs return a byte-identical payload.is_final. A preliminary score is never presented as final.4.3.0-da50fad0. Two reports carrying the same value were produced by the same model against the same configuration. Changes when either changes.block parameter. Present only when a point-in-time query was requested.graph_block. Index lag is therefore visible and never looks like an absent wallet history.chain_head_block.graph_block.-1 when unknown. A lag in blocks is not comparable across networks - Ethereum settles every ~12 s, Tron every ~3 s - so a deposit-freshness decision should use this.graph_block by an incremental pass, with exposure_raised_to_utc beside it. Not the same claim as graph_block: that one means traced to full depth, this one means raised where value from a known source arrived.graph_block was examined at request time. When true, a deposit newer than the graph is not simply missing from the assessment — see risk_factors.post_graph_exposure.block was supplied. Whether that block’s exposure has been counted. This is usually the question being asked about a fresh transfer, and it turns true within minutes — long before graph_covers_requested_block, which waits for the next full pass.block was supplied. Whether the exposure graph had reached that block. false with exposure_covers_requested_block: true means the exposure IS counted and only the full-depth trace is outstanding — see report.reason.model_version. Label and sanctions data move independently of the code, so a rerun after a sync can differ from the same model. Two reports sharing both versions were computed from the same code, settings and reference data.wallet — subject of the reportETHEREUM, TRON.Smart contract when the address is a contract. Useful for blocking plain transfers to token contracts, which are unrecoverable.risk_assessment — the headline verdict0–score_max. May be null when status != complete. Never read a missing or zero score as clean.100. Always positive.VERY LOW RISK / LOW RISK / HIGH RISK / VERY HIGH RISK. Note there is no MEDIUM string: scores 41–50 return LOW RISK. Threshold on score, not on this label.{ label, range_min, range_max, min_inclusive, max_exclusive }. Bands: Very Low, Low, Medium, High, Very High. Classify on min_inclusive / max_exclusive: the integer pair leaves gaps, so a score of 25.45 belongs to neither 0-25 nor 26-40. range_min / range_max are retained unchanged for existing readers.token_balances[] — current holdingsETH, USDT.wallet_statistics — activitysent_transactions_count
total_transactions_countintegerTransfer events (native coin + ERC-20 / TRC-10 / TRC-20) where the address is sender or receiver, over all history. Not external transaction count and not internal calls.
last_transaction_utcstringFirst and last activity, ISO-8601 UTC. Empty when the address has no history.
ACTIVE or INACTIVE. INACTIVE with zero transactions means a new or unused address — unexamined, not cleared.stablecoin_outflow_usd
stablecoin_turnover_usdnumberVolume in USD-pegged stablecoins specifically, readable as dollars without holding a price date in mind.
funded_by_entity_type_distribution[] — where the money came fromUnnamed high-volume service.denominator says otherwise.endpoint or service. Empty for unattributed funders.sampled_inflow_usd, or sampled_inflow_transfers when nothing received carries a price. Note the name. This section sums a bounded recent SAMPLE of inflows, so its base is smaller than the factor denominators and a category share here can legitimately be lower than one factor’s share within it. Previously labelled total_priced_inflow_usd, which was wrong for a sampled base.denominator.percentage rounds to one, so a small but real share prints as 0.0 and reads as excluded when it is only below display precision. Use this pair to tell rounding from exclusion.risk_factors.blacklists — direct listing summaryFlagged or Clear.risk_factors.reported_connected_addresses[] — flagged counterparties by severity bandSevere / High / Medium / Low. Describes the connected source, not the address you queried.Reported.{ address, hops }. hops is the real graph distance with no smoothing; 0 means the queried address itself.risk_factors.factors[] — one entry per finding the decompositionblacklist, indirect_exposure, direct_counterparty, counterparty_venue, entity_risk, geo_risk, trusted_entity, operator_flagged, contract.endpoint — the source is itself the risk. service — a business holding third-party funds.sanctions, law_enforcement, hack_attribution, issuer_freeze, community_report, operator_rating, derived. This is what separates a government designation from a user complaint.proportional_graph means the finding comes from traced value rather than a named list.direct — this address is named. indirect — inferred from the graph. The two are never merged.0 = the address itself. -1 = a path exists but was not resolved because an intervening service breaks the chain of custody; the severity is still reported.share_pct: a small share of a large balance is material, a small share of a small one is not.share_pct is a share OF. These are genuinely different bases and must not be compared across factors: total_priced_inflow_usd (current activity total), graph_priced_inflow_usd (the inflow the graph priced at its own watermark, used by indirect_exposure), incoming_transfer_count (no inflow is priced), or not_applicable (a direct listing is an identification, not a proportion).share_pct ≈ amount_usd / denominator_value_usd × 100. null when the denominator is not_applicable.candidate_points when applied, otherwise 0.superseded_by:counterparty or recognised_project_contract_scored_zero. Empty when applied.source_addresses.candidate_points, kept for existing readers.risk_factors.score_breakdown — what each component contributeddata_as_of.graph_block from addresses the graph had already judged. Checked at request time, so a deposit that arrived since the last graph pass is scored rather than invisible.named service, high-throughput service, not a service.status: partial. A failed lookup lowers the score mechanically, so it must not be read as a cleaner address.direct_listing, counterparty, indirect_exposure, post_graph_exposure, activity_baseline, contract_allowlist or none.operator_rating means our own rating of a service, NOT a sanctions match.risk_factors.post_graph_exposure — exposure newer than the graphfound, none, unavailable or not_applicable. none means the window was examined and nothing matched — not that it was skipped. unavailable means the check did not complete: the score is a floor, report.status is partial, and it must not be read as a low-risk answer. not_applicable means it was not examined, and not_applicable_reason says why; today the only reason is address_is_a_contract, a contract being assessed as machinery rather than as a counterparty.graph_block attributable to illicit origin, counted proportionally: a sender that is 2 % contaminated contributes 2 % of what it sent.source_addresses[] lists up to 20 of them.Populated example — 0xb6ef1290065488a40322033ceecca50c18414d86, whose entire funding history is one transfer received after the last graph pass, from an address the graph already held at severity 90. It scores 90 on that alone, and decided_by is post_graph_exposure:
{
"status": "found",
"tainted_usd": 544.0,
"share_of_received_pct": 100.0,
"transfer_count": 1,
"source_count": 1,
"senders_examined": 1,
"source_addresses": ["0x0ba9bf6ece534ef7d43ec50f6f08c899ddecebbe"],
"category_code": "issuer_freeze",
"from_block": 26249051,
"last_block": 26249051,
"last_utc": "2026-10-22T15:27:59Z",
"window": "after graph_block, to chain_head_block"
}
risk_factors.excluded_below_threshold — exposure found but not scoredentity_attribution — who the address belongs toresolved or none_held. Branch on this. A company or person is held for a small minority of addresses, so none_held is the normal answer — it means none was found, not that none exists, and never that the address is clean.company or person. Present only when attribution = resolved.0 when the entity is known but carries no risk.{ source, weight, detail } — why the entity carries risk.jurisdiction_risk — linked jurisdictionshigh_risk, grey_risk, or not_listed.0 for not_listed.related_entities — named counterpartiesreceived_from or sent_to.Category Dictionary
Codes appearing in category_code throughout the response. Codes are additive:
new ones may appear, existing ones are never renamed or redefined. Treat an unrecognised
code as entity_class: endpoint with the severity reported alongside it.
endpoint is itself the risk, and exposure propagates from it. A
service is a business holding third-party funds — its payouts are its
customers' money, so its rating is returned as a counterparty finding and never as a
claim that the funds are criminal. sanctioned_exchange is the deliberate
exception: a designated venue propagates like an endpoint.| Code | Entity class | List type | Propagates | Can set blacklisted | Meaning |
|---|---|---|---|---|---|
| sanctions | endpoint | sanctions | yes | yes | Named in a government sanctions designation. |
| sanctioned_exchange | service | sanctions | yes | yes | A trading venue that is itself under a designation. Receiving funds from it is the violation, whoever they belonged to. |
| hack_attribution | endpoint | hack_attribution | yes | yes | An investigator attributed a specific theft or exploit to the address. |
| mixer | endpoint | derived | yes | yes | A service whose function is to break the link between source and destination. |
| issuer_freeze | endpoint | issuer_freeze | yes | yes | A token issuer blocked the address on-chain. Operational action by the issuer, not a finding by an authority. |
| enforcement_action | endpoint | law_enforcement | yes | yes | Connected to a published regulator or law-enforcement action — seizure, arrest, court order — that is not itself a designation. Defined but not yet populated; treat its absence as "not covered". |
| community_report | endpoint | community_report | no | no | A user-submitted report. Unverified, so it contributes to the score but never sets blacklisted. |
| high_risk_exchange | service | operator_rating | no | no | A real venue rated elevated-risk — weak KYC, prior enforcement, jurisdiction concern. Not a designation. |
| unlicensed_exchange | service | operator_rating | no | no | A venue operating without a required licence. A licensing failure, not a designation. |
| medium_reputation_service | service | operator_rating | no | no | A payment processor, card issuer or custodian rated elevated-risk. |
| exchange_deposit | service | derived | no | no | A per-customer deposit address attributed to an exchange. |
| trusted_entity | service | derived | no | no | Matched to a named, recognised operator. |
| trusted_contract | service | derived | no | no | A contract belonging to a recognised project. Scored as infrastructure. |
| public_person | endpoint | derived | no | no | A publicly identified individual. |
| unknown | endpoint | derived | no | no | No attribution found. Unknown, not safe. |
Status & Coverage Vocabulary
| Field | Value | Meaning for an integrator |
|---|---|---|
| status | complete | Analysis ran in full. score is final and the report is reproducible. |
partial | A component was unavailable. score is preliminary and may be understated. Treat as a hold, not a pass. | |
unavailable | No analysis performed. score is null. Never read as clean. | |
| coverage | full | The address has history and it was analysed. |
no_history | Valid address, zero transfers. A new or unused address — unexamined, not cleared. | |
partial_history | History exists but was not fully covered. |
Status Codes
| HTTP | Body | Meaning |
|---|---|---|
| 200 | Report | A report was produced. Read report.status to tell a completed
analysis from a partial one. |
| 400 | { code, field, message } |
INVALID_ADDRESS, UNSUPPORTED_CHAIN,
INVALID_BLOCK or BLOCK_IN_FUTURE. The request could not
be accepted, so no score is returned. |
| 401 | { error, message, request_id } |
Missing or invalid X-apikey. |
| 403 | { error, message, request_id } |
The key is valid but not granted access to this endpoint. |
| 429 | { error, message, request_id } |
Rate limit exceeded. Retry-After gives the wait in seconds. |
| 503 | { status, reason, score, retry_after_seconds } |
Risk data is not loaded, so no assessment can be made.
status is unavailable, reason is
CACHE_WARMING and score is null.
Retry-After gives the wait in seconds. This is not an assessment and
must not be treated as one. |
Recommended Integration Policy
200, any body you cannot parse, and any status != "complete". A missing, null or zero score is never a pass.risk_assessment.blacklisted = true, at any score.risk_assessment.score >= 76 — the lower bound of the Very High band.wallet.entity_tag = "Smart contract" on a plain transfer. Tokens sent to a token contract are unrecoverable — a loss case, not a risk case.funded_by_entity_type_distribution[] entry whose category_code you do not accept, above a share you choose. A rated venue contributes to the score without reaching the top band on its own, so threshold on the category where that matters.coverage = "no_history". Unexamined, not proven clean.score, never risk_level, in logic — there is no MEDIUM label, so 41–50 reads LOW RISK. Classify with score_zones[].min_inclusive / max_exclusive.share_pct across factors — the bases differ. Divide amount_usd by denominator_value_usd instead.data_as_of.graph_lag_seconds for how far the traced graph reaches, and risk_factors.post_graph_exposure for what was found after it at request time. A transfer newer than graph_block has not been traced to full depth, but value received from an address already known to be exposed is still counted.Rate Limits & Retries
Applies to every /api/engine/* endpoint. Limits are enforced per API key and,
independently, across all keys.
Limits
| Window | Per key | All keys combined |
|---|---|---|
| per second | 5 | 10 |
| per minute | 50 | 200 |
| per day | 1 000 | 10 000 |
The three windows apply together: a burst of 5 in one second is accepted, and a sixth in the
same second is not, even with the daily allowance untouched. Because the combined limit is
shared, a key can receive 429 without having exceeded its own quota. Higher
limits can be set per key on request.
Which responses consume quota
Checks run in this order: authentication → rate limit → input validation → scoring. Anything rejected before the rate-limit step costs nothing.
| Response | Consumes quota | Note |
|---|---|---|
| 200 | yes | A completed request. |
| 400 | yes | Validation runs after the rate-limit check, so a malformed address still costs one request. Validate client-side where you can. |
| 503 | yes | Returned from the same stage as 400. |
| 401 | no | Rejected before the rate limiter. |
| 403 | no | Rejected before the rate limiter. |
| 429 | no | A refused request consumes nothing, so a retry starts from the same allowance. |
Retrying
429 carries Retry-After in seconds.
The value is the real time until the exhausted window refills, not a fixed constant. Wait at
least that long; a retry sooner is refused again and the wait does not grow.| Response | Retry | Action |
|---|---|---|
| 429 | yes | Honour Retry-After. Safe to retry indefinitely; it is a throttle, not a failure. |
| 503 | yes | Honour Retry-After (also exposed as retry_after_seconds). Risk data is not loaded; the request has not been assessed. |
| 500 | once | Retry once after a short pause. If it repeats, quote the request_id. |
| 400 | no | Deterministic. The same request will fail identically — fix the input. |
| 401 | no | Fix the key or the header. |
| 403 | no | The key is not granted this endpoint. Contact us. |
A retry is an independent request: it re-reads current data and returns a fresh
report_id if anything affecting the answer has changed, or the same one if
nothing has. Retries are not deduplicated, so each one that reaches the scoring stage counts
against the quota.
Usage Records
Every request that reaches the rate-limit stage is recorded with its request_id,
endpoint, response status and execution time. That log is the record of usage, and it counts
retries individually — nothing is deduplicated. Commercial terms are set by agreement
and are not expressed in the API.
Diagnostics
Every error body carries a request_id. Quote it when reporting a problem —
it identifies the exact request in our logs.
{ "error": "Too Many Requests", "message": "Per-user rate limit exceeded", "request_id": "27905ac8-79f1-4e88-bc5d-d87c4ff88ed7" }
COPY
Concurrency
Parallel requests are independent and never interleave: each response corresponds only to its
own address, which is echoed in wallet.address. Keep concurrency at
or below the per-second limit; beyond that the surplus is refused rather than queued.
Error Reference
Both /api/report endpoints return app-level errors as JSON:
{ "error": { "code": "INVALID_ADDRESS", "field": "address", "message": "Not a valid address: zzz" } }
field is present only when the error concerns a specific parameter.
| Code | HTTP | Trigger |
|---|---|---|
BAD_REQUEST | 400 | A required parameter is missing or blank, or has the wrong type. |
INVALID_ADDRESS | 400 | address / sender / recipient / token_contract is not a valid EVM or TRON address. |
UNSUPPORTED_CHAIN | 400 | chain is not supported, or an address's family does not match chain (e.g. a T… address with chain=ETH). |
INVALID_TX_HASH | 400 | txHash is not a valid hash for the requested chain. |
UNKNOWN_HASH | 404 | The RPC node does not know txHash. |
ADDRESS_NOT_FOUND | 404 | The Alpha AML upstream has no data for the address. |
PENDING_TRANSACTION | 409 | The transaction exists but is not yet mined. |
TRANSACTION_FAILED | 409 | The transaction was mined but reverted. |
NO_TRANSFERS | 409 | The transaction moved no native coin and emitted no token Transfer events. |
MULTIPLE_TRANSFERS | 409 | More than one transfer is in scope — supply or narrow the filters. |
NO_MATCH | 409 | No transfer matches the supplied filters. |
PDF_FAILED | 500 | /report-v1 only — Chromium failed to render the PDF. |
RPC_ERROR | 502 | The RPC provider or the upstream Alpha AML API was unreachable or returned an error. |
Error Response Examples
// Bad address format
{ "error": { "code": "INVALID_ADDRESS", "field": "address", "message": "Not a valid address: zzz" } }
// TRON address on an EVM chain
{ "error": { "code": "UNSUPPORTED_CHAIN", "field": "sender", "message": "Unsupported or mismatched chain: eth" } }
// Ambiguous transaction
{ "error": { "code": "MULTIPLE_TRANSFERS", "message": "Transaction has 0 native and 25 token transfer(s) — pass event_index / token_contract / sender / recipient to pick one" } }
// Unknown hash
{ "error": { "code": "UNKNOWN_HASH", "field": "txHash", "message": "Transaction not found: 0x…" } }
COPY
Risk Grade API
Fast, API-key authenticated risk grade for any on-chain wallet address. Returns a single grade value indicating the address's risk tier.
time field in responses is a Unix timestamp (seconds) indicating when the risk grade was last computed for the given address.Base URL
https://api.alpha-aml.com
GET /api/engine/risk-grade
Returns the current risk grade for a wallet address.
Headers
| Header | Description |
|---|---|
| X-apikeyrequired | API key for authentication. Missing or invalid → {"error":"Unauthorized"}. Not a query parameter — ?apiKey= no longer works. |
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| addressrequired | string | Wallet address to grade. Must be a valid on-chain address. Missing → {"error":"Address is required"}. |
curl Example
curl -H "X-apikey: <key>" \ "https://api.alpha-aml.com/api/engine/risk-grade?address=0xd7632A8A3dC9287Ea25d2903df0cFb8702C49572"COPY
Success Response (200)
very-low, low, high, very-high."success" on a valid result.{
"address": "0xd7632A8A3dC9287Ea25d2903df0cFb8702C49572",
"riskGrade": "very-low",
"status": "success",
"time": 1751500800
}
Grade Values
The riskGrade field carries one of four tiers:
| Grade | Interpretation |
|---|---|
very-low | Address has very low risk exposure — minimal suspicious activity detected. |
low | Address has low risk — some indirect exposure but generally clean. |
high | Address has high risk — notable exposure to flagged counterparties or patterns. |
very-high | Address has very high risk — direct blacklist match or severe exposure. |
Error Reference
All errors return a JSON body with an error field.
| Error body | Trigger | HTTP Status |
|---|---|---|
{"error":"Unauthorized"} |
Missing, invalid, or expired X-apikey header |
401 |
{"error":"Address is required"} |
address parameter is missing or empty |
400 |
{"error":"Unsupported address format"} |
address provided but not a valid on-chain address format |
400 |
{"error":"Rate limit exceeded"} |
API key quota exhausted | 429 |
Error Response Examples
// Unauthorized
{ "error": "Unauthorized" }
// Missing address
{ "error": "Address is required" }
// Bad format
{ "error": "Unsupported address format" }
// Rate limited
{ "error": "Rate limit exceeded" }
COPY