---
name: trustin-dart-api
description: Screen blockchain addresses and transactions for AML risk with TrustIn DART, and manage the rulesets and rules that drive the verdicts. Use when asked to check an address or transaction, read a screening result, or create, clone, edit or highlight compliance rules.
---

# TrustIn DART API

TrustIn DART is a blockchain AML screening API. Give it a wallet address (KYA) or a transaction hash (KYT) and it traces the funds up to five hops through labeled counterparties, matches every fund-flow path against a ruleset, and returns a 0–100 fund-attribution score with a verdict — accept, review, edd or block — and the exact rules behind it. Every screening is saved as an immutable snapshot. The same API lets a customer manage the rulesets and rules that drive the verdicts.

## Before you start

- You need the user's API key (created in the console under Account › API Keys). Never invent one; ask for it. Send it as the `X-Api-Key` header.
- Base URL: `https://test-api.width.live` (test environment). For production use `https://api.width.live` — its ruleset ids differ.
- Backup domains: `https://test-api.trustin.bond` (test) and `https://api.trustin.bond` (production). Every endpoint is served on two domains: https://test-api.width.live and its backup https://test-api.trustin.bond — the same service, the same data and the same API keys. Use the backup when the primary cannot be reached (the name does not resolve or the connection is refused). Do not resend there a request that got a response or timed out: it may already have run, and a screening would run, and be billed, twice.
- Every response is `{"code": 0, "msg": "success", "data": …}`; on error `code` is -1 and `msg` names the problem. Status codes: 400 invalid field, 401 key, 403 builtin read-only, 404 not yours / missing, 409 duplicate rule_code, 429 back off.
- Prefer `"mode": "async"` for screenings and poll `GET /api/v3/screen/result/{job_id}?type=kya` every 2–5 seconds until `status` is `COMPLETE`.
- If an MCP client is available, connect to `https://test-api.width.live/mcp` with `Authorization: Bearer <key>` and use the tools instead of raw HTTP.

## How to think about it

Two rulesets shape a screening. The path-matching ruleset (ruleset_type kya for address screening, kyt_in / kyt_out for the two sides of a transaction) holds rules; each rule fires when a fund-flow path — or, for a rule with max_hops -1, the screened address's own labels — meets all of its conditions, and a hit carries the rule's risk_level and action into the result. The scoring ruleset (ruleset_type scoring) holds a scoring_config: for each hop band and direction a base, per-severity weights, and verdict bands; the score sums, over the distinct inbound edges that carried tainted funds, edge amount ÷ total inflow × base × severity weight; an address whose own labels hit a self-hit rule overrides the sum with the self-hit score (100 by default).

Builtin rulesets — AML KYA Default v1.3, AML KYT-IN Default, AML KYT-OUT Default, the fund-attribution scoring default — are visible to everyone and read-only. Anything a customer creates or clones is theirs to edit. The normal way to customise: clone the builtin, edit the copy, pass the copy's id as ruleset_id (or scoring_ruleset_id) when screening. Screening with ruleset_id 0 uses the builtin default.

Rule fields:
- rule_code — your identifier, unique within the ruleset; names the hit in results and snapshots.
- name, category, description, reference — free text.
- risk_level — low | medium | high | critical (drives the score's severity weight).
- action — alert | review | edd | block | allow (the disposition a hit recommends).
- direction — inbound | outbound | both.
- min_hops / max_hops — path length the rule applies to; max_hops -1 = the address's own labels, no path.
- conditions — array of {parameter, operator, value}; all must hold (see Rule conditions below).
- is_highlighted — highlighted (★) rules are surfaced first in results; presentation only.
- highlight_tags — scopes the ★: non-empty means only hits whose counterparty label (any of its four category levels) contains one of these labels (case-insensitive) are highlighted; empty means every hit of the rule is. Kept independently of is_highlighted (it survives the ★ being switched off).
- sort_order — display order.

Rule conditions — each is {"parameter", "operator", "value"}; a rule fires only when all of its conditions hold.

Condition parameters (label = tested against the counterparty's labels, or the screened address's own in a max_hops -1 rule; path = tested against the fund-flow path):
- primary_category (label, string; == != IN NOT_IN CONTAINS) — Top-level category of a label. Values: One of the primary categories under Label values, e.g. Sanctions, Obfuscation, Cybercrime. Exact match, case-sensitive. Example: {"parameter":"primary_category","operator":"==","value":"Sanctions"}
- secondary_category (label, string; == != IN NOT_IN CONTAINS) — Second-level category of a label, under its primary category. Values: See Label values, e.g. Prohibited Entity, Mixers, Phishing. Exact match, case-sensitive. Example: {"parameter":"secondary_category","operator":"IN","value":["Mixers","Privacy Wallet"]}
- tertiary_category (label, string; == != IN NOT_IN CONTAINS) — Third level: the entity behind the label — for a sanctions label, the list it is on. Values: Entity names as the data source spells them, e.g. Htx, Huionepay, Xinbi, Tornado_cash, US-OFAC, UK-OFSI. Case-insensitive. Example: {"parameter":"tertiary_category","operator":"==","value":"huionepay"}
- quaternary_category (label, string; == != IN NOT_IN CONTAINS) — Fourth level: the address type within the entity. Often empty. Values: e.g. htx-hotwallet, htx-deposit, xinbi-address, us-ofac-sanctions. Case-insensitive. Example: {"parameter":"quaternary_category","operator":"CONTAINS","value":"hotwallet"}
- risk_level (label, string; == != IN NOT_IN CONTAINS) — The risk level the data source gives a label — not the rule's own risk_level field. Values: low, medium, high, critical (lower case). Example: {"parameter":"risk_level","operator":"IN","value":["high","critical"]}
- risk_score (label, number; == != > < >= <=) — The highest label risk_level as a number. Values: critical 90, high 70, medium 50, low 20; 0 when there is no label. Example: {"parameter":"risk_score","operator":">=","value":70}
- hops (path, number; == != > < >= <=) — Path length: 1 is a direct counterparty, 2 one address in between, and so on. Values: 1 to 5. Example: {"parameter":"hops","operator":"<=","value":2}
- inflow_hops (path, number; == != > < >= <=) — Path length, checked on inbound paths only. Values: 1 to 5. Example: {"parameter":"inflow_hops","operator":"<=","value":3}
- outflow_hops (path, number; == != > < >= <=) — Path length, checked on outbound paths only. Values: 1 to 5. Example: {"parameter":"outflow_hops","operator":"<=","value":3}
- amount (path, number; == != > < >= <=) — The path's smallest transfer — the most risk-tainted funds that could have travelled the whole path. A hit reports it as exposureAmount. Values: Amount in USDT/USDC, e.g. 10000. Example: {"parameter":"amount","operator":">","value":10000}
- inflow_total_amount (path, number; == != > < >= <=) — The same figure as amount, checked on inbound paths only. Despite the name it is not a total. Values: Amount in USDT/USDC. Example: {"parameter":"inflow_total_amount","operator":">","value":1000}
- outflow_total_amount (path, number; == != > < >= <=) — The same figure as amount, checked on outbound paths only. Despite the name it is not a total. Values: Amount in USDT/USDC. Example: {"parameter":"outflow_total_amount","operator":">","value":1000}
- risk_rate (path, number; == != > < >= <=) — The path's smallest transfer divided by its last transfer, as a percentage. The last transfer is what the screened address received on that path (inbound) or what the counterparty received (outbound). A hit reports it as riskRatePct. Values: Percent, 0 to 100: 10 means 10%. Example: {"parameter":"risk_rate","operator":">=","value":10}
- direction (path, string; == != IN NOT_IN CONTAINS) — The path's direction. The rule's own direction field does the same and is the usual choice. Values: inbound, outbound. Example: {"parameter":"direction","operator":"==","value":"inbound"}
- chain_name (path, string; == != IN NOT_IN CONTAINS) — The chain the path is on. Values: A chain_name from GET /api/v3/chains written in lower case, e.g. tron, ethereum, bsc. A capitalised value such as Tron never matches. Example: {"parameter":"chain_name","operator":"==","value":"tron"}

Condition operators (aliases are case-insensitive):
- == — Equal; value: String or number. Also accepted: =, eq, equals, is
- != — Not equal; value: String or number. Also accepted: <>, ne, not, not_equals
- IN — Equal to one of the listed values; value: JSON array of strings
- NOT_IN — Equal to none of the listed values; value: JSON array of strings. Also accepted: not in, nin
- CONTAINS — Contains the value as a substring; value: String. Also accepted: like
- > — Greater than; value: Number. Also accepted: gt
- < — Less than; value: Number. Also accepted: lt
- >= — Greater than or equal; value: Number. Also accepted: gte, ge
- <= — Less than or equal; value: Number. Also accepted: lte, le

Label values — categories, primary: secondary. The common categories, not a closed list: the data source adds new ones over time. The labels on real results are the authoritative spelling — subjectTags for the screened address, hits[].pathNodes[].tags along each path.
- Sanctions: Sanctioned Entity, Prohibited Entity, Sanctioned Jurisdiction
- Terrorism Financing: Terrorism Financing
- Other Financial Crimes: Money Laundering, Tax Evasion
- Public Freezing Action: Law Enforcement Freeze/Seizure
- Illicit Markets: Human Trafficking, Illegal Services, Child Sexual Abuse Material (CSAM)
- Cybercrime: Hacker/Thief, Ransomware, Phishing, Ponzi, Pig Butchering, Rug Pull, Honeypot, Fake Presale, Black/Grey Industry
- Obfuscation: Mixers, Privacy Wallet, Privacy Token, Centralized Bridge, Crypto ATM/Kiosk
- Gambling: Online Casino, Sports Betting, Unlicensed Gambling
- High-Risk Entities: CEX in High-Risk Jurisdiction, Sanctioned CEX, CEX with Weak AML/CFT Controls, OTC Desk
- Exchanges & DeFi: CEX, DEX, DeFi Protocol, Lending Protocol, Bridges, NFT Marketplaces
- Other Entities: TradFi, Individual, Merchant, Payments, Escrow
Entity and address-type levels (tertiary_category, quaternary_category) come from the data source as it names things, for example Htx / htx-hotwallet, US-OFAC / us-ofac-sanctions, Blacklisted / blacklisted-address, Xinbi / xinbi-address. They compare case-insensitively.

Condition examples (direction, hop range and conditions of a rule):
- Direct exposure to a sanctioned address: {"direction":"both","min_hops":1,"max_hops":1,"conditions":[{"parameter":"primary_category","operator":"==","value":"Sanctions"}]} — Funds came from or went to a sanctioned address with nothing in between.
- The screened address is itself on the OFAC list: {"direction":"both","min_hops":0,"max_hops":-1,"conditions":[{"parameter":"primary_category","operator":"==","value":"Sanctions"},{"parameter":"tertiary_category","operator":"==","value":"us-ofac"}]} — max_hops -1 tests the screened address's own labels instead of a path; only label conditions count there.
- Mixer funds two hops upstream, at least 10% of what arrived: {"direction":"inbound","min_hops":2,"max_hops":2,"conditions":[{"parameter":"secondary_category","operator":"==","value":"Mixers"},{"parameter":"risk_rate","operator":">=","value":10}]} — Fires on an inbound path whose source is a mixer when the path's smallest transfer is at least 10% of what the screened address received on it.
- Any path to one entity: {"direction":"both","min_hops":1,"max_hops":5,"conditions":[{"parameter":"tertiary_category","operator":"IN","value":["huionepay","xinbi"]}]} — Entity names ignore case, so huionepay matches the label Huionepay.
- Cybercrime proceeds above 1,000 USDT within three hops: {"direction":"inbound","min_hops":1,"max_hops":3,"conditions":[{"parameter":"secondary_category","operator":"IN","value":["Hacker/Thief","Ransomware","Phishing","Pig Butchering"]},{"parameter":"amount","operator":">","value":1000}]} — amount is the path's smallest transfer, so all of the 1,000 could have come from the source.
- Sending to a high-risk counterparty on Tron: {"direction":"outbound","min_hops":1,"max_hops":2,"conditions":[{"parameter":"risk_score","operator":">=","value":70},{"parameter":"chain_name","operator":"==","value":"tron"}]} — risk_score 70 or more means a label of high or critical risk. chain_name is written in lower case.

Condition rules of thumb:
- Every condition of a rule must hold. For "either A or B" list the values with IN, or write two rules.
- Label conditions test the counterparty at the far end of the path — the source on an inbound path, the destination on an outbound one — not the addresses in between. In a rule with max_hops -1 they test the screened address's own labels instead; such a rule ignores path conditions and needs at least one label condition.
- An address can carry several labels, and each label condition passes when any one of them matches, so two conditions may be met by different labels. For the same reason != and NOT_IN pass as soon as one label differs — they do not mean "none of its labels". A counterparty with no labels fails every category and risk_level condition, != and NOT_IN included; its risk_score is 0.
- Case: primary_category, secondary_category, risk_level and direction match exactly, case included; tertiary_category and quaternary_category ignore case; chain_name must be written in lower case.
- An operator that does not fit the parameter is not rejected but never matches: >, <, >=, <= on a text parameter, or IN, NOT_IN, CONTAINS on a number. IN and NOT_IN need a JSON array — a single string never matches.
- inflow_hops and inflow_total_amount are checked only on inbound paths, outflow_hops and outflow_total_amount only on outbound ones; on the other side they are skipped, neither passing nor failing. A rule whose conditions are all skipped does not fire.
- Writes are validated: a condition without a parameter or value, with an operator not listed here, or with a parameter not listed here is rejected with 400, and the message names conditions[i]. An unknown parameter a rule already holds is kept when the rule is updated. The legacy spellings target.tags.*, path.node.tags.*, path.amount, path.risk_amount_usd, path.hops_total and fund_ratio are still accepted.

## Recipes

Common workflows:

1. Screen an address synchronously: POST /api/v3/screen/kya {"chain_name":"Tron","address":"T…"} → data.score.verdict, data.hits, data.exposures.
2. Screen asynchronously (recommended for deep hops or busy addresses): add "mode":"async" → data.job_id; then GET /api/v3/screen/result/{job_id}?type=kya until status is COMPLETE; poll no faster than every 2 seconds. The job computes immediately — polling only reads.
   Every screening is recorded as a snapshot: a sync response carries data.snapshotId, a COMPLETE poll data.snapshot_id. GET /api/v3/snapshots/{snapshot_id} reads it back later — parameters and result.
3. Customise the rules: POST /api/v3/rulesets/clone {"source_ruleset_id": <builtin kya id>, "name": "Ours"} → data.id; POST /api/v3/rulesets/rule/create {"ruleset_id": <id>, "rule_code": "MY_MIXER_EXPOSURE_H2", "name": "…", "risk_level": "high", "action": "review", "direction": "inbound", "min_hops": 2, "max_hops": 2, "conditions": [{"parameter":"primary_category","operator":"==","value":"Obfuscation"},{"parameter":"risk_rate","operator":">=","value":10}]}; then screen with "ruleset_id": <id>.
4. Read before you change: POST /api/v3/rulesets/list {"ruleset_type":"kya"} to find ids; POST /api/v3/rulesets/rule/list {"ruleset_id": <id>} for the catalogue.
5. Highlight what matters: POST /api/v3/rulesets/rule/highlight {"ruleset_id": <id>, "rule_ids": [...], "is_highlighted": true}. To highlight only hits touching one entity, add "highlight_tags": ["Huione"] — omit the field to keep each rule's existing scope, or send [] to clear it back to unscoped.

Pitfalls: builtin rulesets cannot be edited (403) — clone them; ids differ between the test and production environments; a sync screening can take minutes on deep hops — prefer async; a rule needs at least one condition; deleting a ruleset never rewrites past snapshots.

## Endpoints

- `GET /api/v3/chains` — Supported Chains (v3)
- `POST /api/v3/screen/kya` — KYA Screen (v3)
- `POST /api/v3/screen/kyt` — KYT Screen (v3)
- `POST /api/v3/transaction-transfers` — List Transaction Transfers (v3)
- `GET /api/v3/screen/result/{jobId}` — Get Screen Result (async)
- `GET /api/v3/snapshots/{snapshotId}` — Get Snapshot (v3)
- `POST /api/v3/rulesets/list` — List Rulesets
- `POST /api/v3/rulesets/get` — Get Ruleset
- `POST /api/v3/rulesets/create` — Create Ruleset
- `POST /api/v3/rulesets/update` — Update Ruleset
- `POST /api/v3/rulesets/delete` — Delete Ruleset
- `POST /api/v3/rulesets/clone` — Clone Ruleset
- `POST /api/v3/rulesets/rule/list` — List Rules
- `POST /api/v3/rulesets/rule/get` — Get Rule
- `POST /api/v3/rulesets/rule/create` — Create Rule
- `POST /api/v3/rulesets/rule/update` — Update Rule
- `POST /api/v3/rulesets/rule/delete` — Delete Rule
- `POST /api/v3/rulesets/rule/highlight` — Highlight Rules
- `POST /api/v3/adverse-media/screen` — Screen a Person
- `GET /api/v3/adverse-media/result/{screeningId}` — Get Screening Result
- `POST /api/v3/adverse-media/list` — List Screenings

Full reference with request and response examples: https://test-v2.width.live/llms-full.txt · OpenAPI: https://test-v2.width.live/openapi.json
