examples/resolve-ambiguous.json
{
"_comment": "Same symbol, no chain. Must return status=ambiguous with the candidate list and exit 3 — never a guess. The server behaves the same way: an ambiguous lookup yields no currency at all, so the transaction ends up with no resolved asset and no amountInDefaultCurrency.",
"queries": [
{ "code": "USDT" },
{ "code": "USDC" }
]
}
examples/resolve-fiat.json
{
"_comment": "Fiat is a membership test — run with --type fiat against the fiat catalogue. No chain, no contract, no ambiguity; matching is case-insensitive.",
"queries": [
{ "code": "eur" },
{ "code": "USD" }
]
}
examples/resolve-native.json
{
"_comment": "Native coins. An empty cryptoChain is a value meaning 'the native coin of its own chain'. Sending BTC/BTC is the common client mistake and normalises to the same asset — but do not rely on that, send the chain empty.",
"queries": [
{ "code": "BTC" },
{ "code": "BTC", "chain": "BTC" },
{ "code": "ETH", "chain": "ETHEREUM" }
]
}
examples/resolve-usdt-tron.json
{
"_comment": "The common case. USDT names many different assets; the chain is what makes it one. TRON, TRC20 and TRX all normalise to TRX.",
"queries": [
{
"code": "USDT",
"chain": "TRON"
}
]
}
references/crypto-catalogue.md
# Crypto currency catalogue — reference
## Shape
`GET /resources/kyt/currency?type=crypto` returns a flat array. Fields visible
over the public API:
| Field | Meaning |
|---|---|
| `currencyCode` | Ticker symbol. **Not unique.** |
| `cryptoChain` | Native currency ticker of the chain (`ETH`, `BTC`, `TRX`, `TON`). Empty means the entry *is* the native coin. |
| `contractAddress` | Token contract, where applicable. |
| `chainId` | Numeric chain id (CAIP-2 style), e.g. `1` Ethereum, `56` BNB. |
| `name` | Human-readable asset name. |
| `coinmarketcapId` | CoinMarketCap UCID. |
| `coingeckoId` | CoinGecko id. |
The stored records carry more — per-provider vocabularies (`chainalysisAsset`,
`ellipticAsset`, `trmLabsChain`, `merkleBlockchainId`, `crystalCurrency`,
`fireblocksAssetId`) and Travel Rule protocol vocabularies (`gtrTicker`,
`gtrNetwork`, `sygnaCurrencyId`, `codeCurrency`, `codeNetwork`) — but these are
not returned to API clients. Their coverage still matters, see below.
## Scale and collisions
The catalogue holds thousands of assets across a hundred-plus chains, and over
a thousand symbols appear on more than one chain. `USDC` and `USDT` lead with
more than a dozen entries each, `ETH` has several, then a long tail of
memecoins reusing tickers.
Exact counts move every week — the listing job below adds assets continuously —
so derive them from the catalogue you just fetched rather than quoting a
figure. The invariant is the one that matters: **any integration that keys
assets by symbol alone will eventually hit a collision.**
## How the server resolves a lookup
Chain strings are normalised first: trim → uppercase → alias table. An empty
chain becomes an internal "native" sentinel. Then, in order:
1. **`(code, chain, contract)`** — exact match.
2. **`(code, chain)`** — tried when step 1 misses. If this hits, the server
logs `Currency matched without contract address — contractAddress likely
invalid` **and returns the match anyway**. A wrong contract address is
therefore swallowed silently from the caller's point of view.
3. **`code == chain` → retry as native.** Handles the common client mistake of
sending `BTC`/`BTC` or `ETH`/`ETH` instead of `BTC`/empty.
At every step, **more than one hit returns nothing**: the server logs
`Multiple currencies found` and yields no currency rather than choosing. Design
for "unresolved", not for "wrong pick".
## Chain aliases
Applied after uppercasing, before lookup. Anything not listed is used verbatim.
| Alias | Canonical |
|---|---|
| `ETHEREUM` | `ETH` |
| `BITCOIN` | `BTC` |
| `MATIC`, `POLYGON`, `POL_POLYGON` | `POL` |
| `BSC`, `BEP20` | `BNB` |
| `TRON`, `TRC20` | `TRX` |
| `SOLANA` | `SOL` |
| `STELLAR` | `XLM` |
| `AVAXC`, `CAVAX`, `AVAX_C` | `AVAX` |
| `ARBI`, `ARBITRUM`, `ARBEVM` | `ARB` |
| `OPTIMISM` | `OP` |
| `APTOS` | `APT` |
| `MANTLE` | `MNT` |
| `PULSE` | `PLS` |
| `PLASMA` | `XPL` |
| `VECHAIN` | `VET` |
| `HYPEEVM`, `HYPEREVM` | `HYPE` |
| `SEIEVM` | `SEI` |
| `MONAD` | `MON` |
| `SONIC` | `S` |
| `ETH_BASE` | `BASE` |
Near-misses are not forgiven: `TRON20`, `ERC-20`, `ERC20`, `Polygon PoS` do not
resolve. Check this table before telling a user their asset is unsupported.
## Travel Rule protocol vocabularies
The counterparty's protocol has its own names for assets and networks, and the
mapping is stored per catalogue entry. Approximate coverage:
Only a minority of entries carry a mapping for any given protocol: Sygna
covers the most, then GTR, then CODE, all of them a small fraction of the
catalogue. An asset without a mapping for the protocol in use
cannot be expressed in the outgoing message; the exchange ends at
`notEnoughCounterpartyData` before delivery. This is an asset-support problem
wearing a data-validation error's clothes.
Coverage is refreshed by a weekly job that polls the providers' own listing
APIs for assets Sumsub does not yet support, so gaps close over time — but not
on demand, and not because a client asked.
## Fiat
`GET /resources/kyt/currency?type=fiat` (or no `type`) returns ISO 4217 codes
assembled live from the rates provider. Only `currencyCode` is meaningful;
`cryptoChain`, `contractAddress` and the ids are absent. Matching is
case-insensitive. There is no chain concept and no ambiguity.
## What is not available
- **No rates endpoint.** Sumsub converts to your default currency internally
and does not expose the rate or the timestamp used.
- **No public chain list.** Derive distinct `cryptoChain` values from the
crypto catalogue; the dedicated endpoint is dashboard-only.
- **No alias endpoint.** The table above is code-only.
scripts/get_currencies.sh
#!/usr/bin/env bash
# Fetch the Sumsub currency catalogue.
# Usage: get_currencies.sh [crypto|fiat]
# Defaults to crypto. Omitting the type server-side yields the fiat list.
# The crypto catalogue is ~10k entries and static — cache it for the session
# rather than re-fetching per lookup.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
: "${SUMSUB_APP_TOKEN:?}" "${SUMSUB_SECRET_KEY:?}"
TYPE="${1:-crypto}"
case "$TYPE" in
crypto|fiat) ;;
*) echo "error: type must be 'crypto' or 'fiat' (got '$TYPE')" >&2; exit 2 ;;
esac
exec bash "$SCRIPT_DIR/sumsub_curl.sh" GET "/resources/kyt/currency?type=${TYPE}"
scripts/resolve_currency.py
#!/usr/bin/env python3
"""Resolve a currency against a Sumsub catalogue, mirroring the server's own rules.
Usage:
get_currencies.sh crypto > crypto.json
resolve_currency.py --catalogue crypto.json --code USDT --chain TRON
resolve_currency.py --catalogue fiat.json --type fiat --code eur
# or a batch: {"queries":[{"code":"USDT","chain":"TRX"}, ...]} on stdin
resolve_currency.py --catalogue crypto.json < queries.json
Exit codes: 0 resolved, 1 not found, 3 ambiguous, 2 usage/input error.
The point of this script is that it refuses to guess. The server returns *no
currency* when a lookup matches several rows, so a resolver that silently picks
the first hit would disagree with production.
"""
import argparse
import json
import sys
# Mirrors KytCurrencyModel.ALIASES_TO_CRYPTO_CHAINS. Applied after upper+trim.
CHAIN_ALIASES = {
"ETHEREUM": "ETH",
"BITCOIN": "BTC",
"MATIC": "POL",
"POLYGON": "POL",
"POL_POLYGON": "POL",
"BSC": "BNB",
"BEP20": "BNB",
"TRON": "TRX",
"TRC20": "TRX",
"SOLANA": "SOL",
"STELLAR": "XLM",
"AVAXC": "AVAX",
"CAVAX": "AVAX",
"AVAX_C": "AVAX",
"ARBI": "ARB",
"ARBITRUM": "ARB",
"ARBEVM": "ARB",
"OPTIMISM": "OP",
"APTOS": "APT",
"MANTLE": "MNT",
"PULSE": "PLS",
"PLASMA": "XPL",
"VECHAIN": "VET",
"HYPEEVM": "HYPE",
"HYPEREVM": "HYPE",
"SEIEVM": "SEI",
"MONAD": "MON",
"SONIC": "S",
"ETH_BASE": "BASE",
}
NATIVE = "" # empty cryptoChain means "the native coin of its own chain"
def norm(s):
return s.strip().upper() if isinstance(s, str) else ""
def normalize_chain(raw):
v = norm(raw)
return CHAIN_ALIASES.get(v, v)
def load_catalogue(path):
with open(path) as fh:
data = json.load(fh)
if isinstance(data, dict):
data = data.get("list", {}).get("items", data.get("items", []))
if not isinstance(data, list):
raise SystemExit("error: catalogue must be a JSON array (or {list:{items:[]}})")
return data
def resolve_fiat(catalogue, code):
code_n = norm(code)
hits = [c for c in catalogue if norm(c.get("currencyCode")) == code_n]
if not hits:
return {"status": "not_found", "reason": "no fiat currency with that code"}
return {
"status": "resolved",
"send": {"currencyCode": hits[0].get("currencyCode"), "currencyType": "fiat"},
"entry": hits[0],
}
def resolve_crypto(catalogue, code, chain, contract):
code_n = norm(code)
by_code = [c for c in catalogue if norm(c.get("currencyCode")) == code_n]
if not by_code:
return {"status": "not_found", "reason": "no asset with that symbol"}
chain_n = normalize_chain(chain)
# The server treats code == chain as the native coin (the BTC/BTC mistake).
if chain_n == code_n:
chain_n = NATIVE
contract_n = norm(contract)
def chain_of(entry):
return normalize_chain(entry.get("cryptoChain") or "")
if chain is None:
candidates = by_code
else:
candidates = [c for c in by_code if chain_of(c) == chain_n]
if not candidates:
return {
"status": "not_found",
"reason": "symbol exists but not on that chain",
"chain_normalised_to": chain_n or "(native)",
"available_chains": sorted(
{(c.get("cryptoChain") or "(native)") for c in by_code}
),
}
if contract_n:
exact = [c for c in candidates if norm(c.get("contractAddress")) == contract_n]
if exact:
candidates = exact
else:
# Mirrors the server: it falls back to (code, chain) and keeps going,
# logging that the contract is probably wrong. Surface that, loudly.
return {
"status": "resolved" if len(candidates) == 1 else "ambiguous",
"warning": (
"contractAddress did not match any entry; the server would fall "
"back to (code, chain) and accept the match silently. Verify it."
),
"send": _send(candidates[0]) if len(candidates) == 1 else None,
"entry": candidates[0] if len(candidates) == 1 else None,
"candidates": _summarise(candidates) if len(candidates) > 1 else None,
}
if len(candidates) > 1:
return {
"status": "ambiguous",
"reason": (
"%d assets share this symbol; the server returns NO currency for an "
"ambiguous lookup. Pin it with cryptoChain (and contractAddress if "
"the symbol repeats on one chain)." % len(candidates)
),
"candidates": _summarise(candidates),
}
return {"status": "resolved", "send": _send(candidates[0]), "entry": candidates[0]}
def _send(entry):
out = {"currencyCode": entry.get("currencyCode"), "currencyType": "crypto"}
chain = entry.get("cryptoChain") or ""
out["cryptoParams"] = {"cryptoChain": chain} if chain else {}
if not chain:
out["_note"] = "native coin — send cryptoChain empty or omit cryptoParams"
return out
def _summarise(entries):
return [
{
"currencyCode": e.get("currencyCode"),
"cryptoChain": e.get("cryptoChain") or "(native)",
"contractAddress": e.get("contractAddress"),
"chainId": e.get("chainId"),
"name": e.get("name"),
}
for e in entries[:25]
]
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--catalogue", required=True, help="JSON from get_currencies.sh")
ap.add_argument("--type", choices=("crypto", "fiat"), default="crypto")
ap.add_argument("--code")
ap.add_argument("--chain", default=None, help="omit entirely to search all chains")
ap.add_argument("--contract", default=None)
args = ap.parse_args()
catalogue = load_catalogue(args.catalogue)
if args.code:
queries = [{"code": args.code, "chain": args.chain, "contract": args.contract}]
else:
try:
payload = json.load(sys.stdin)
except json.JSONDecodeError as e:
raise SystemExit("error: no --code given and stdin is not valid JSON: %s" % e)
queries = payload.get("queries") if isinstance(payload, dict) else payload
if not isinstance(queries, list) or not queries:
raise SystemExit("error: expected {'queries': [...]} or a non-empty array")
results = []
for q in queries:
if args.type == "fiat":
r = resolve_fiat(catalogue, q.get("code"))
else:
r = resolve_crypto(catalogue, q.get("code"), q.get("chain"), q.get("contract"))
r["query"] = q
results.append(r)
json.dump(results if len(results) > 1 else results[0], sys.stdout, indent=2, ensure_ascii=False)
sys.stdout.write("\n")
statuses = {r["status"] for r in results}
if "ambiguous" in statuses:
return 3
if "not_found" in statuses:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
scripts/sumsub_curl.sh
#!/usr/bin/env bash
# Sign + send a single Sumsub API request.
#
# Usage:
# SUMSUB_APP_TOKEN=sbx:... SUMSUB_SECRET_KEY=... \
# sumsub_curl.sh METHOD PATH_WITH_QUERY [BODY_FILE_OR_-]
#
# Refuses non-sandbox tokens unless SUMSUB_ALLOW_PROD=1.
set -euo pipefail
if [[ $# -lt 2 ]]; then
sed -n '2,12p' "$0" >&2
exit 2
fi
METHOD="$(printf '%s' "$1" | tr '[:lower:]' '[:upper:]')"
PATH_Q="$2"
BODY_ARG="${3-}"
: "${SUMSUB_APP_TOKEN:?set SUMSUB_APP_TOKEN}"
: "${SUMSUB_SECRET_KEY:?set SUMSUB_SECRET_KEY}"
BASE="${SUMSUB_BASE:-https://api.sumsub.com}"
if [[ "${SUMSUB_APP_TOKEN}" != sbx:* && "${SUMSUB_ALLOW_PROD:-0}" != "1" ]]; then
echo "error: SUMSUB_APP_TOKEN does not look like a sandbox token (expected 'sbx:' prefix)." >&2
echo " Production credentials must not be shared with this skill." >&2
exit 3
fi
if [[ "${PATH_Q}" != /* ]]; then
echo "error: PATH must start with '/'" >&2
exit 2
fi
# Materialise the body to a temp file so we can sign the *exact* bytes we send.
BODY_FILE="$(mktemp)"
trap 'rm -f "${BODY_FILE}"' EXIT
if [[ -z "${BODY_ARG}" ]]; then
: >"${BODY_FILE}"
elif [[ "${BODY_ARG}" == "-" ]]; then
cat >"${BODY_FILE}"
else
cp "${BODY_ARG}" "${BODY_FILE}"
fi
TS="$(date -u +%s)"
SIG="$(
{ printf '%s%s%s' "${TS}" "${METHOD}" "${PATH_Q}"; cat "${BODY_FILE}"; } \
| openssl dgst -sha256 -hmac "${SUMSUB_SECRET_KEY}" -hex \
| awk '{print $NF}'
)"
CURL_ARGS=(
-sS -X "${METHOD}"
-H "X-App-Token: ${SUMSUB_APP_TOKEN}"
-H "X-App-Access-Ts: ${TS}"
-H "X-App-Access-Sig: ${SIG}"
-H "X-Agent-Source: sumsub-skills"
-H "X-Agent-Source-Ver: 1.4.1"
-H "Accept: application/json"
)
# Only attach Content-Type + body for methods that send one.
if [[ -s "${BODY_FILE}" ]]; then
CURL_ARGS+=(-H "Content-Type: application/json" --data-binary "@${BODY_FILE}")
fi
RESP_FILE="$(mktemp)"
trap 'rm -f "${BODY_FILE}" "${RESP_FILE}"' EXIT
HTTP_STATUS="$(curl "${CURL_ARGS[@]}" -w "%{http_code}" -o "${RESP_FILE}" "${BASE}${PATH_Q}")"
cat "${RESP_FILE}"
if [[ "${HTTP_STATUS}" -ge 400 ]]; then
echo "HTTP ${HTTP_STATUS}" >&2
exit 1
fi
SKILL.md
---
name: sumsub-resolve-currency
description: Resolve and validate the currency fields Sumsub expects on a transaction — `info.currencyCode`, `info.currencyType`, `info.cryptoParams.cryptoChain` and the `amountInDefaultCurrency` / `defaultCurrencyCode` pair. Reads the catalogue from `GET /resources/kyt/currency?type=crypto|fiat` and resolves a (symbol, chain, contract) triple to exactly one asset, normalising chain aliases and refusing ambiguous input rather than guessing. TRIGGER when the user asks "which currency codes / chains does Sumsub support", "is USDT on TRON supported", "what do I put in cryptoChain", "why is my currency not recognised", "why is amountInDefaultCurrency wrong / missing", or is about to submit a transaction with a token whose symbol exists on several networks. SKIP for building the transaction itself (use `sumsub-create-transaction`), for currency-based scoring rules (use `sumsub-create-kyt-rules`), and for FX rates or historical conversion — Sumsub exposes no rates endpoint.
allowed-tools: Read, Write, Bash
---
# Sumsub — Resolve currency and chain
One endpoint answers both halves, and the two halves look nothing alike:
| `type=fiat` | `type=crypto` |
|---|---|
| Flat list of ISO 4217 codes, fetched live | Thousands of assets across a hundred-plus chains |
| No chain, no contract, no ambiguity | Over a thousand symbols exist on **more than one** chain |
| Resolving = "is it in the list" | Resolving = `(symbol, chain, [contract])` → exactly one asset |
Most of this skill is about the crypto side, because that is where transactions
silently go wrong. Fiat is a membership test.
## Endpoint
| Verb | Path | Purpose |
|---|---|---|
| `GET` | `/resources/kyt/currency?type=crypto` | The crypto catalogue. Static server-side; safe to cache for a session. |
| `GET` | `/resources/kyt/currency?type=fiat` | Fiat codes, assembled live from the rates provider. |
Omit `type` and you get the fiat list. There is **no public endpoint for the
chain list** — derive it from the crypto catalogue's distinct `cryptoChain`
values. There is **no rates endpoint**: Sumsub converts internally and does not
expose the rate it used.
Each crypto entry carries `currencyCode`, `cryptoChain`, `contractAddress`,
`chainId`, `name`, `coinmarketcapId`, `coingeckoId`.
## Auth — App Token + secret (sandbox only)
Signing per [the authentication reference](https://docs.sumsub.com/reference/authentication);
mechanics in [`sumsub-api-auth`](../sumsub-api-auth/SKILL.md).
> **⚠️ Sandbox tokens only.** The helper script refuses tokens that don't start
> with `sbx:` unless `SUMSUB_ALLOW_PROD=1`. This endpoint is read-only, but the
> same rule applies across these skills — get a sandbox pair at
> <https://cockpit.sumsub.com/checkus/home?sbx=true> (**Connect Sumsub to your AI agent** -> **Build & configure** -> **Generate token**).
| Var | Example |
|---|---|
| `SUMSUB_APP_TOKEN` | `sbx:...` |
| `SUMSUB_SECRET_KEY` | The paired secret. |
| `SUMSUB_BASE` | Optional. Defaults to `https://api.sumsub.com`. |
## The rule that matters
**A symbol is not an asset.** `USDT` and `USDC` each name more than a dozen
different assets in the catalogue; `ETH` several. The identity of a crypto
asset is the pair `(currencyCode, cryptoChain)`, plus `contractAddress` when
even that is not unique.
📘 **An empty `cryptoChain` is a value, not a gap.** It means *the native coin
of its own chain* — `BTC` with no chain is bitcoin; `BTC` with `cryptoChain:
BTC` is the same thing (the server normalises that common mistake); `BTC` on
some wrapped chain is a different asset entirely.
🚧 **Ambiguity does not resolve to a default — it resolves to nothing.** When a
lookup matches more than one row the server logs `Multiple currencies found`
and returns no currency at all. So the symptom of a missing chain is not "wrong
rate" but "currency not recognised", with everything downstream that depends on
it silently degraded.
## Procedure
1. **Decide the type.** If the code is three uppercase letters and appears in
the fiat list, treat it as fiat unless the user says otherwise. Beware of
overlaps in intent — a user saying "USD" almost never means a token called
USD.
2. **Fetch the catalogue** with `${CLAUDE_SKILL_DIR}/scripts/get_currencies.sh crypto`
(or `fiat`). Cache it for the session; the crypto list is large and static.
3. **Resolve** with `${CLAUDE_SKILL_DIR}/scripts/resolve_currency.py` — pass the
catalogue and the query. It uppercases and trims, expands chain aliases,
treats `code == chain` as the native coin, and on ambiguity **fails with the
candidate list** instead of picking one.
4. **Report the exact fields to send**, never just "supported":
```json
"info": {
"currencyCode": "USDT",
"currencyType": "crypto",
"cryptoParams": { "cryptoChain": "TRX" }
}
```
5. **Address the conversion** — see below. This is the part callers forget.
## `amountInDefaultCurrency` — read this before skipping it
`info.amountInDefaultCurrency` + `info.defaultCurrencyCode` express the
transfer in your reporting currency. They look optional. They are not, in three
ways.
🚧 **Travel Rule thresholds are compared against `amountInDefaultCurrency`.**
If it is absent, the threshold comparison is **skipped entirely** and the
Travel Rule flow runs regardless of how small the transfer is. Omitting the
field does not lose precision — it changes behaviour. The same applies to the
unhosted-wallet verification threshold.
🚧 **The value is frozen at ingest.** Sumsub computes it once, when the
transaction is created, and stores it. A rate that was wrong at that moment
stays wrong on the record; correcting it later means a backfill, not a
re-read.
📘 **If the asset does not resolve, there is nothing to convert.** An
unrecognised `(symbol, chain)` pair means no conversion happens — which is the
second reason a missing `cryptoChain` is expensive.
**Recommendation:** if you know the value your own systems used, send
`amountInDefaultCurrency` and `defaultCurrencyCode` explicitly. Your books and
Sumsub's record then agree by construction, and you are not depending on a
third-party rate at an instant you do not control.
## Chain aliases
Chain names are uppercased, trimmed, then mapped through a fixed alias table
before lookup. `ETHEREUM`, `BSC`, `TRC20`, `MATIC`, `POLYGON`, `BEP20`,
`TRON`, `SOLANA`, `AVAX_C` and about two dozen more resolve to their canonical
chain. Anything outside the table is used verbatim, so a near-miss like
`TRON20` or `ERC-20` simply does not match.
The table is not exposed by any endpoint — the full list is in
[`references/crypto-catalogue.md`](references/crypto-catalogue.md). When a
user's chain string fails to resolve, check it against the aliases before
concluding the asset is unsupported.
## Travel Rule specifics
Beyond the threshold behaviour above, the asset you pick can decide whether an
exchange is possible at all. Each catalogue entry carries the counterparty
protocols' own vocabularies, and coverage is thin.
Only a minority of catalogue entries carry a mapping for any given protocol —
Sygna covers the most, then GTR, then CODE, and all three are a small fraction
of the whole. Coverage grows as the weekly listing job picks up new assets, so
check rather than assume.
An asset with no mapping for the protocol your counterparty speaks cannot be
expressed in their message. The exchange fails validation before anything is
sent — surfacing as `notEnoughCounterpartyData`, which reads like a data
problem rather than an asset-support problem. If a Travel Rule exchange fails
that way on an exotic token, check the vocabulary coverage before debugging the
payload. See [`sumsub-integrate-travel-rule`](../sumsub-integrate-travel-rule/SKILL.md).
## Outputs
- **Resolved** — the matched entry, and the exact `currencyCode` / `currencyType` / `cryptoChain` to send. Mention `contractAddress` when the symbol is a known collision, so the caller can pin it.
- **Ambiguous** — the candidate rows with their chains and contracts, and the question the user must answer. Never pick one.
- **Not found** — say whether the *symbol* is unknown or only the *chain* is, and check the alias table before declaring it unsupported.
## Worked examples
- [`examples/resolve-usdt-tron.json`](examples/resolve-usdt-tron.json) — the common case: a collided symbol pinned by chain.
- [`examples/resolve-ambiguous.json`](examples/resolve-ambiguous.json) — the same symbol with no chain; must fail, not guess.
- [`examples/resolve-native.json`](examples/resolve-native.json) — a native coin, showing the empty-chain semantics.
- [`examples/resolve-fiat.json`](examples/resolve-fiat.json) — the membership test.
## See also
- [`references/crypto-catalogue.md`](references/crypto-catalogue.md) — catalogue fields, the alias table, the server's resolution ladder, collision statistics.
- [`sumsub-create-transaction`](../sumsub-create-transaction/SKILL.md) — where the resolved fields go.
- [`sumsub-integrate-travel-rule`](../sumsub-integrate-travel-rule/SKILL.md) — thresholds and protocol vocabularies.