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.

GET /api/report/wallet?address={address}&chain={chain}

Headers

HeaderDescription
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

ParameterTypeDescription
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:

ETH ARB BASE BSC TRON

Response

StatusContent-TypeBody
200application/pdfGenerated PDF bytes. Header: Content-Disposition: attachment; filename="aml-report-<address>.pdf".
400application/jsonINVALID_ADDRESS (bad format), UNSUPPORTED_CHAIN (address family ≠ chain), BAD_REQUEST (missing param).
401application/jsonMissing or invalid X-apikey — rejected before this endpoint runs.
403application/jsonKey has no access to this endpoint.
404application/jsonADDRESS_NOT_FOUND — no upstream data for the address.
429application/jsonRate limit exceeded — Retry-After header and retry_after_seconds in the body say when to retry.
500application/jsonPDF_FAILED — Chromium failed to render the report.
App-level errors (400/404/500) are JSON — { "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.pdf
COPY
curl -H "X-apikey: <key>" \
  "https://api.alpha-aml.com/api/report/wallet?address=TXyz1234567890abcdefgh&chain=TRON" \
  -o aml-report.pdf
COPY

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.

GET /api/report/txhash?txHash={txHash}&chain={chain}

Headers

HeaderDescription
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

ParameterTypeDescription
txHashrequiredstringTransaction hash. EVM chains: 0x + 64 hex. TRON: 64 hex, no 0x. Wrong shape for the chain → INVALID_TX_HASH.
chainrequiredstringChain to resolve the transaction on. One of the supported values below.
event_indexoptionalintegerFilter: 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_contractoptionalstringFilter: keep only token transfers of this contract address.
senderoptionalstringFilter / assertion: keep only transfers sent from this address.
recipientoptionalstringFilter / assertion: keep only transfers received by this address.

Supported chain values:

ETH ARB BASE BSC TRON

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.

SituationResult
No filters — exactly one native transfer, no token transfersThat native transfer is used.
No filters — no native transfer, exactly one token transferThat token transfer is used.
No filters — a native transfer and token transfers, or more than one token transfer409 MULTIPLE_TRANSFERS — add a filter.
No filters — no transfers at all409 NO_TRANSFERS.
Filters given — no transfer matches409 NO_MATCH.
Filters given — more than one transfer still matches409 MULTIPLE_TRANSFERS — narrow further.
Filters given — exactly one matchThat transfer is used.

Response

StatusContent-TypeBody
200application/jsonTransfer metadata + risk scores (fields below).
400application/jsonINVALID_TX_HASH, UNSUPPORTED_CHAIN, INVALID_ADDRESS, BAD_REQUEST.
401application/jsonMissing or invalid X-apikey — rejected before this endpoint runs.
403application/jsonKey has no access to this endpoint.
404application/jsonUNKNOWN_HASH, ADDRESS_NOT_FOUND.
409application/jsonPENDING_TRANSACTION, TRANSACTION_FAILED, NO_TRANSFERS, MULTIPLE_TRANSFERS, NO_MATCH.
429application/jsonRate limit exceeded — Retry-After header and retry_after_seconds in the body say when to retry.
502application/jsonRPC_ERROR — RPC provider unreachable or returned an error.
Schema 1.3.1 — one breaking change. The top-level 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)

application/json — top level
versionstringResponse schema version ("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.
hashstringThe transaction hash (echo of txHash).
chainstringChain the lookup ran on (echo of chain).
block_numbernumberBlock the transfer was mined in.
block_hashstringHash of block_number. Omitted on TRON, which does not expose it on this lookup.
confirmationsnumberDepth of 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_indexnumberIndex of the log this report actually analysed — not an echo of the event_index request parameter. null for a native transfer, which emits no log.
event_index_basisstringNumbering scheme behind 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.
amountstringTransferred amount, decimal-adjusted, trailing zeros stripped (e.g. "490.14").
amount_rawstringTransferred amount in base units (wei / sun / token base units).
assetstring"native" for a coin transfer, otherwise the token contract address.
decimalsnumberDecimals used for amount — 18 (native EVM), 6 (native TRON), or the token's own.
token_symbolstringToken symbol if it could be read on-chain; omitted for native transfers or when unavailable.
senderstringThe selected transfer's from address.
receiverstringThe selected transfer's to address.
sender_risk_scorenumber0–100 score for 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_risk_scorenumber0–100 score for receiver. Same null semantics as above.
score_zonesarrayScore bands, carrying min_inclusive / max_exclusive. Use that pair for classification — the integer range_min / range_max pair leaves fractional scores unclassified.
sender_reportobjectFull report for the sender — fields below.
receiver_reportobjectSame shape, for the receiver.
sender_report / receiver_report — per-side report object
Each side carries the same report structure /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.
addressstringThe address this report is for.
report_idstringIdentifier of the exact report these numbers came from. Covers data_version, so a label sync changes it — a later standalone wallet lookup may legitimately return a different score.
statusstringcomplete / partial. partial means the score is not fully computed — pair it with reason.
coveragestringWhat the report covers, e.g. full / activity_only.
is_finalbooleanWhether this result is settled or may still change.
preliminarybooleanWhether the numeric score is preliminary.
reasonstringWhy a report is not complete, e.g. GRAPH_BEHIND_REQUESTED_BLOCK, CACHE_WARMING, COUNTERPARTY_LOOKUP_FAILED. Empty when complete.
model_versionstringRisk model version that produced the score.
data_versionstringDigest of the reference-data state. model_version alone cannot reproduce a score once labels sync; both together can.
generated_at_utcstringWhen this report was generated.
data_as_ofobjectFreshness: 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.
risk_scorenumber0–100, or null when not scored.
risk_levelstringTier label for risk_score.
blacklisted / blacklist_noteboolean / stringDirect blacklist match and its note.
score_breakdownobjectComponents (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.
post_graph_exposureobjectExposure found in the window after 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.
factorsarrayPer-finding detail: 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.
excluded_below_thresholdobjecttainted_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.
funding.scopeobjectdescribes, 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.
funding.distributionarrayPer row: 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.
entity_attributionobjectCompany/person attribution and address label. none_held means attribution is unknown — which is not the same as clean.
jurisdiction_riskobjectLinked jurisdictions, listed counts, applied penalty. A contributing factor, not a verdict; an empty list means none was found.
related_entitiesobjectNamed counterparties only; anonymous ones are not listed.
Scope of this endpoint. It identifies one transfer, pins the event it analysed, and scores the transfer's two addresses. It does not attribute the origin of the transferred amount itself — no allocation rule (proportional / FIFO) or time cut is applied to the selected transfer. See <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.

GET https://api.alpha-aml.com/api/engine/report-v1?address={address}&chain={chain}
Read 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

HeaderDescription
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

ParameterTypeDescription
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

ethereum tron

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 covers
describesstringaddress_inflows. The breakdown describes money arriving at this ADDRESS.
per_transferbooleanAlways false. These shares do NOT describe the funds of any single transfer, and a per-transfer origin breakdown is not available from this endpoint.
max_transfers_sampledintegerCap on the number of inflows inspected.
window_secondsintegerHow far back the sample reaches, in seconds. Converted to blocks per network, since Ethereum and Tron settle at different rates.
anchored_tostringaddress_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.
denominatorstringBase the shares are computed over.
denominator_valuenumberAbsolute value of that base.
report — identity, completeness and data freshness
report_idstringIdentifies this result: rpt_ + 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.
statusstringcomplete — analysis ran in full. partial — a component was unavailable; the score is preliminary. unavailable — no analysis was performed. Branch on this before reading score.
reasonstringPresent only when 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.
coveragestringfull — 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.
is_finalbooleanTrue only when status = complete. A final report is reproducible: identical inputs return a byte-identical payload.
preliminarybooleanInverse of is_final. A preliminary score is never presented as final.
model_versionstringIdentifies the scoring model and its configuration, e.g. 4.3.0-da50fad0. Two reports carrying the same value were produced by the same model against the same configuration. Changes when either changes.
generated_at_utcstringISO-8601 UTC timestamp of this response.
as_of_blockintegerEcho of the block parameter. Present only when a point-in-time query was requested.
data_as_of.chain_head_blockintegerHighest block indexed for this network.
data_as_of.graph_blockintegerBlock up to which the exposure graph is computed.
data_as_of.graph_lag_blocksintegerDifference between the two. Wallet activity is current to the chain head; graph exposure is current to graph_block. Index lag is therefore visible and never looks like an absent wallet history.
data_as_of.chain_head_utcstringISO-8601 UTC of chain_head_block.
data_as_of.graph_utcstringISO-8601 UTC of graph_block.
data_as_of.graph_lag_secondsintegerHead minus graph in SECONDS, or -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.
data_as_of.exposure_raised_to_blockintegerPresent only when stored exposure has been brought forward past 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.
data_as_of.exposure_checked_to_chain_headbooleanWhether value received after 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.
data_as_of.exposure_covers_requested_blockbooleanPresent only when 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.
data_as_of.graph_covers_requested_blockbooleanPresent only when 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.
data_versionstringVersion of the reference DATA, separate from 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 report
addressstringEchoes the requested address, in the network's native format. Base58 case is preserved for Tron.
blockchainstringUppercase network name, e.g. ETHEREUM, TRON.
descriptionstringKnown label for the address, where one is held. Empty otherwise.
entity_tagstringSet to Smart contract when the address is a contract. Useful for blocking plain transfers to token contracts, which are unrecoverable.
risk_assessment — the headline verdict
scorenumber | nullRisk score, 0–score_max. May be null when status != complete. Never read a missing or zero score as clean.
score_maxintegerScale maximum, currently 100. Always positive.
risk_levelstringVERY 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.
blacklistedbooleanTrue only when a reference list names this exact address with sufficient confidence. A real boolean, never a string. Necessary but not sufficient: an address can be unlisted and still score 100 on traced exposure.
blacklist_notestringHuman-readable basis for the flag.
risk_sources_countintegerNumber of distinct flagged sources this address is connected to.
risk_sources_notestringDescribes what the count refers to.
score_zones[]array{ 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 holdings
tokenstringSymbol, e.g. ETH, USDT.
contract_addressstringToken contract. Empty for the native coin.
balancenumberCurrent balance in token units.
wallet_statistics — activity
received_transactions_count
sent_transactions_count
total_transactions_count
integerTransfer 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.
first_transaction_utc
last_transaction_utc
stringFirst and last activity, ISO-8601 UTC. Empty when the address has no history.
wallet_age_daysintegerDays since first activity.
statusstringACTIVE or INACTIVE. INACTIVE with zero transactions means a new or unused address — unexamined, not cleared.
stablecoin_inflow_usd
stablecoin_outflow_usd
stablecoin_turnover_usd
numberVolume in USD-pegged stablecoins specifically, readable as dollars without holding a price date in mind.
funded_by_entity_type_distribution[] — where the money came from
labelstringCounterparty name where known, otherwise a structural description such as Unnamed high-volume service.
percentagenumberShare of the denominator. Share of value, not of transfer count, unless denominator says otherwise.
category_codestringTaxonomy code — see the category dictionary. Threshold on this, not on label.
category_labelstringDisplay name for the code.
entity_classstringendpoint or service. Empty for unattributed funders.
risk_bearingbooleanTrue when this funder carries a risk attribution. False for merely unattributed funders.
sourcestringList or registry that produced the attribution. Empty when unattributed.
transfer_countintegerNumber of transfers behind this share.
denominatorstringsampled_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_valuenumberAbsolute value of the base this share is computed over.
valuenumberAbsolute value of THIS row, in the unit named by denominator.
share_pct_exactnumberShare to five decimals. 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 summary
statusstringFlagged or Clear.
scoreintegerHighest severity among direct listings, or 0.
detailstringExplanation, including the contract case: a contract belonging to a recognised project is reported as such and not scored as a wallet.
risk_factors.reported_connected_addresses[] — flagged counterparties by severity band
severitystringBand: Severe / High / Medium / Low. Describes the connected source, not the address you queried.
scoreintegerHighest numeric severity in the band.
address_countintegerNumber of addresses in the band.
statusstringCurrently always Reported.
addresses[]array{ 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 decomposition
kindstringWhich signal produced it: blacklist, indirect_exposure, direct_counterparty, counterparty_venue, entity_risk, geo_risk, trusted_entity, operator_flagged, contract.
category_codestringStable taxonomy code — see the dictionary.
category_labelstringDisplay name for the code.
entity_classstringendpoint — the source is itself the risk. service — a business holding third-party funds.
list_typestringAuthority behind the match: sanctions, law_enforcement, hack_attribution, issuer_freeze, community_report, operator_rating, derived. This is what separates a government designation from a user complaint.
sourcestringThe list or registry that matched. proportional_graph means the finding comes from traced value rather than a named list.
entitystringDesignated entity or company, where known.
jurisdictionstringIssuing authority for a sanctions match, where held.
severityinteger0–100 severity of the source. Never reduced by distance: a source six hops away carries its full severity.
directnessstringdirect — this address is named. indirect — inferred from the graph. The two are never merged.
hopsintegerReal distance. 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_pctnumberShare of the denominator attributable to this finding.
amount_usdnumberAbsolute value attributable to this finding. Use alongside share_pct: a small share of a large balance is material, a small share of a small one is not.
denominatorstringWhat 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).
denominator_value_usdnumber | nullABSOLUTE value of the base, so a share can be verified and not merely compared: share_pct ≈ amount_usd / denominator_value_usd × 100. null when the denominator is not_applicable.
candidate_pointsnumberWhat this factor would contribute if it decided the score.
appliedbooleanWhether it reached the score. The combination is a max, so a real finding can be reported and still not be what set the number.
effective_pointsnumbercandidate_points when applied, otherwise 0.
exclusion_reasonstringWhy it was not applied, e.g. superseded_by:counterparty or recognised_project_contract_scored_zero. Empty when applied.
source_addressesstring[]The addresses the finding is built from, capped at a sample. A counterparty line groups by severity, so several addresses can merge into one row.
source_countintegerTrue number of sources, which may exceed the length of source_addresses.
contribution_pointsnumberUnchanged alias of candidate_points, kept for existing readers.
detailstringOne-sentence summary for a reviewer.
risk_factors.score_breakdown — what each component contributed
direct_listingnumberContribution from a listing naming this address.
counterpartynumberContribution from who funded it directly.
indirect_exposurenumberContribution from value traced through the graph.
concentrationnumberContribution from how concentrated that exposure is.
post_graph_exposurenumberContribution from value received after data_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.
activity_baselinenumberBaseline from activity alone. An address nothing is known against is unexamined, not cleared, so this is never zero for an active address.
entity_penaltyintegerAdded for risk carried by the linked company or person.
geo_penaltyintegerAdded for jurisdiction risk.
recognised_entity_credit_appliedbooleanTrue when a recognised-operator credit reduced the score, so a major exchange is not scored as dirty for handling everyone's funds.
counterparty_is_rated_servicebooleanTrue when the worst direct funder is a rated business rather than a crime source. In that case the finding is about counterparty selection, not about the funds being criminal.
treated_as_servicebooleanTrue when the address itself behaves as a high-throughput service, whose inferred exposure describes its depositors more than itself.
service_basisstringWhy that determination was made, e.g. named service, high-throughput service, not a service.
scorenumberThe final score, repeated for convenience.
degraded_lookups[]arrayComponents that failed. Non-empty implies status: partial. A failed lookup lowers the score mechanically, so it must not be read as a cleaner address.
decided_bystringWhich component set the score: direct_listing, counterparty, indirect_exposure, post_graph_exposure, activity_baseline, contract_allowlist or none.
decided_by_categorystringCategory code of the deciding finding.
decided_by_list_typestringList type of the deciding finding. operator_rating means our own rating of a service, NOT a sanctions match.
notestringComponents are not additive — the score is not their sum.
risk_factors.post_graph_exposure — exposure newer than the graph
statusstringfound, 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.
tainted_usdnumberValue received after graph_block attributable to illicit origin, counted proportionally: a sender that is 2 % contaminated contributes 2 % of what it sent.
share_of_received_pctnumberThat amount as a percentage of total received value, so it is comparable with the graph's own shares.
transfer_countintegerHow many transfers in this window came from such senders.
source_countintegerHow many distinct senders. source_addresses[] lists up to 20 of them.
senders_examinedintegerHow many senders in this window were checked, largest by value first.
category_codestringCategory of the worst origin behind those senders, from the same dictionary as every other factor.
from_block / last_block / last_utcinteger, stringThe window actually covered, and when the most recent such transfer landed.
notestringThe limits, stated: one hop, senders the last graph pass had already judged, the largest of them by value, priced assets only. It is a floor on exposure in this window, and the next graph pass resolves the rest.

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 scored
tainted_usdnumberTraced value that fell below the materiality threshold.
source_countintegerHow many sources it came from.
notestringExcluded never means hidden. Immaterial exposure is reported here so a reviewer can see it and apply their own threshold.
entity_attribution — who the address belongs to
attributionstringresolved 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.
notestringPlain-language restatement of the above.
entity.kindstringcompany or person. Present only when attribution = resolved.
entity.id / entity.namestringIdentifier and display name of the organisation or individual.
entity.risk_scoreinteger0–100 risk held against the entity itself, independent of this address. 0 when the entity is known but carries no risk.
entity.jurisdictions[]arrayCountries linked to the entity.
entity.reasons[]array{ source, weight, detail } — why the entity carries risk.
address_label.labelstringLabel held for this specific address, e.g. a named exchange wallet.
address_label.companystringOperator this address is attributed to.
address_label.countrystringCountries linked to the address, as a single string.
address_label.recognised_operatorbooleanTrue when the address is matched to a named, recognised operator. Being labelled is not the same as being reputable — a mixer pool is labelled too.
address_label.labelled_in_registrybooleanTrue when any label is held for the address, whatever its reputation.
address_label.behaves_as_servicebooleanTrue when the address behaves as a high-throughput service.
address_label.service_basisstringWhy, in words.
jurisdiction_risk — linked jurisdictions
jurisdictions[].countrystringCountry linked to the address or its owner.
jurisdictions[].tierstringhigh_risk, grey_risk, or not_listed.
jurisdictions[].weightintegerRelative weight of the tier. 0 for not_listed.
listed_countintegerHow many linked jurisdictions are on either list.
total_linked_countintegerTotal linked, including unlisted ones.
truncatedstringPresent when the list was shortened. Listed jurisdictions sort first, so the omitted tail is unlisted.
applied_penalty_pointsintegerPoints actually added to the score for jurisdiction.
basisstringWhich jurisdiction drove the penalty.
notestringJurisdiction is a contributing factor, not a verdict. An empty list means none was found.
related_entities — named counterparties
counterparties[].addressstringThe counterparty, in the network's native format.
counterparties[].directionstringreceived_from or sent_to.
counterparties[].name / .company / .countrystringIdentity of the counterparty as held.
counterparties[].category_code / .category_label / .entity_classstringIts taxonomy classification.
counterparties[].severityinteger0–100 severity if it carries a risk attribution, else 0.
counterparties[].recognisedbooleanTrue when matched to a recognised operator.
counterparties[].value_usdnumberTotal priced value exchanged with this counterparty.
named_counterparty_countintegerNumber returned.
risk_bearing_countintegerHow many carry a risk attribution.
recognised_countintegerHow many are recognised operators.
notestringNamed counterparties only; anonymous ones are not listed. Complements the funder percentages, which give proportions but not names.

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.

Entity classes. An 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.
CodeEntity classList typePropagatesCan set blacklistedMeaning
sanctionsendpointsanctionsyesyesNamed in a government sanctions designation.
sanctioned_exchangeservicesanctionsyesyesA trading venue that is itself under a designation. Receiving funds from it is the violation, whoever they belonged to.
hack_attributionendpointhack_attributionyesyesAn investigator attributed a specific theft or exploit to the address.
mixerendpointderivedyesyesA service whose function is to break the link between source and destination.
issuer_freezeendpointissuer_freezeyesyesA token issuer blocked the address on-chain. Operational action by the issuer, not a finding by an authority.
enforcement_actionendpointlaw_enforcementyesyesConnected 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_reportendpointcommunity_reportnonoA user-submitted report. Unverified, so it contributes to the score but never sets blacklisted.
high_risk_exchangeserviceoperator_ratingnonoA real venue rated elevated-risk — weak KYC, prior enforcement, jurisdiction concern. Not a designation.
unlicensed_exchangeserviceoperator_ratingnonoA venue operating without a required licence. A licensing failure, not a designation.
medium_reputation_serviceserviceoperator_ratingnonoA payment processor, card issuer or custodian rated elevated-risk.
exchange_depositservicederivednonoA per-customer deposit address attributed to an exchange.
trusted_entityservicederivednonoMatched to a named, recognised operator.
trusted_contractservicederivednonoA contract belonging to a recognised project. Scored as infrastructure.
public_personendpointderivednonoA publicly identified individual.
unknownendpointderivednonoNo attribution found. Unknown, not safe.

Status & Coverage Vocabulary

FieldValueMeaning for an integrator
statuscompleteAnalysis ran in full. score is final and the report is reproducible.
partialA component was unavailable. score is preliminary and may be understated. Treat as a hold, not a pass.
unavailableNo analysis performed. score is null. Never read as clean.
coveragefullThe address has history and it was analysed.
no_historyValid address, zero transfers. A new or unused address — unexamined, not cleared.
partial_historyHistory exists but was not fully covered.

Status Codes

HTTPBodyMeaning
200Report 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

Fail closed
1holdAny response other than 200, any body you cannot parse, and any status != "complete". A missing, null or zero score is never a pass.
2blockrisk_assessment.blacklisted = true, at any score.
3blockrisk_assessment.score >= 76 — the lower bound of the Very High band.
4blockwallet.entity_tag = "Smart contract" on a plain transfer. Tokens sent to a token contract are unrecoverable — a loss case, not a risk case.
5reviewA 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.
6warncoverage = "no_history". Unexamined, not proven clean.
7noteUse 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.
8noteDo not compare share_pct across factors — the bases differ. Divide amount_usd by denominator_value_usd instead.
9noteFor deposit screening, check 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

WindowPer keyAll keys combined
per second510
per minute50200
per day1 00010 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.

ResponseConsumes quotaNote
200yesA completed request.
400yesValidation runs after the rate-limit check, so a malformed address still costs one request. Validate client-side where you can.
503yesReturned from the same stage as 400.
401noRejected before the rate limiter.
403noRejected before the rate limiter.
429noA 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.
ResponseRetryAction
429yesHonour Retry-After. Safe to retry indefinitely; it is a throttle, not a failure.
503yesHonour Retry-After (also exposed as retry_after_seconds). Risk data is not loaded; the request has not been assessed.
500onceRetry once after a short pause. If it repeats, quote the request_id.
400noDeterministic. The same request will fail identically — fix the input.
401noFix the key or the header.
403noThe 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.

CodeHTTPTrigger
BAD_REQUEST400A required parameter is missing or blank, or has the wrong type.
INVALID_ADDRESS400address / sender / recipient / token_contract is not a valid EVM or TRON address.
UNSUPPORTED_CHAIN400chain is not supported, or an address's family does not match chain (e.g. a T… address with chain=ETH).
INVALID_TX_HASH400txHash is not a valid hash for the requested chain.
UNKNOWN_HASH404The RPC node does not know txHash.
ADDRESS_NOT_FOUND404The Alpha AML upstream has no data for the address.
PENDING_TRANSACTION409The transaction exists but is not yet mined.
TRANSACTION_FAILED409The transaction was mined but reverted.
NO_TRANSFERS409The transaction moved no native coin and emitted no token Transfer events.
MULTIPLE_TRANSFERS409More than one transfer is in scope — supply or narrow the filters.
NO_MATCH409No transfer matches the supplied filters.
PDF_FAILED500/report-v1 only — Chromium failed to render the PDF.
RPC_ERROR502The 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.

Note: The 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.

GET /api/engine/risk-grade?address={address}

Headers

HeaderDescription
X-apikeyrequired API key for authentication. Missing or invalid → {"error":"Unauthorized"}. Not a query parameter — ?apiKey= no longer works.

Query Parameters

ParameterTypeDescription
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)

{ address, riskGrade, status, time }
addressstringThe queried wallet address (echo).
riskGradestringRisk tier. One of: very-low, low, high, very-high.
statusstring"success" on a valid result.
timeintegerUnix timestamp (seconds) indicating when this risk grade was last computed for the address.
{
  "address": "0xd7632A8A3dC9287Ea25d2903df0cFb8702C49572",
  "riskGrade": "very-low",
  "status": "success",
  "time": 1751500800
}

Grade Values

The riskGrade field carries one of four tiers:

very-low
low
high
very-high
GradeInterpretation
very-lowAddress has very low risk exposure — minimal suspicious activity detected.
lowAddress has low risk — some indirect exposure but generally clean.
highAddress has high risk — notable exposure to flagged counterparties or patterns.
very-highAddress has very high risk — direct blacklist match or severe exposure.

Error Reference

All errors return a JSON body with an error field.

Error bodyTriggerHTTP 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