DEV Community

PublicAML
PublicAML

Posted on Originally published at intelapi.publicaml.org

Free Crypto KYT API — Enrich Sandbox, Sanction Checks, and Wallet Pre-Send Code

Free Crypto KYT API — Enrich Sandbox, Sanction Checks, and Wallet Pre-Send Code

Interactive console + docs live at intelapi.publicaml.org. Same payload, same JSON your backend gets. This post turns that API into shippable code.

  • Base URL: https://intelapi.publicaml.org
  • Auth (public enrich path): none
  • Chains: ETH, BTC, TRON, BSC

Core: POST /v1/enrich

curl -sS -X POST 'https://intelapi.publicaml.org/v1/enrich' \
  -H 'Content-Type: application/json' \
  -d '{
    "addresses":[{"wallet_address":"0x67d40ee1a85bf4a4bb7ffae16de985e8427b6b45","chain":"ETH"}],
    "include":["aml_score","category"],
    "top_n":5
  }' | jq '.entities[0] | {label, category, sanctioned, aml_score}'
Enter fullscreen mode Exit fullscreen mode

Live shape for that OFAC example: sanctioned: true, aml_score: 100, label Chatex (OFAC Sanctioned).

Rule #1 from the docs: sanctioned: true overrides everything else — block even if category looks like cex.

Optional include[]: aml_score | category | counterparties | source_of_funds. Cheap probe:

curl -sS -X POST 'https://intelapi.publicaml.org/v1/enrich' \
  -H 'Content-Type: application/json' \
  -d '{"addresses":[{"wallet_address":"0x9696f59e4d72e237be84ffd425dcad154bf96976","chain":"ETH"}],"include":["category"]}'
Enter fullscreen mode Exit fullscreen mode

JavaScript — decision helper for Send

const BASE = "https://intelapi.publicaml.org";
const BAD = new Set(["mixer","sanction","scam","phishing","romance","hack","darknet","ransomware","terrorism"]);

export async function enrichAddress(wallet_address, chain = "ETH", include) {
  const res = await fetch(`${BASE}/v1/enrich`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      addresses: [{ wallet_address, chain }],
      include: include ?? ["aml_score","category","counterparties","source_of_funds"],
      top_n: 10,
    }),
  });
  if (!res.ok) throw new Error(`enrich ${res.status}`);
  const data = await res.json();
  const entity = data.entities?.[0];
  if (!entity) throw new Error(JSON.stringify(data.not_found ?? "empty"));
  return entity;
}

export function decideSend(entity) {
  if (entity.sanctioned === true) return { action: "block", reason: "sanctioned" };
  const score = Number(entity.aml_score ?? 0);
  if (score >= 60) return { action: "review", reason: "high", score };      // docs: high
  if (score >= 25) return { action: "monitor", reason: "medium", score };    // docs: medium
  const bad = (entity.counterparties || []).filter(cp => BAD.has(String(cp.category||"").toLowerCase()));
  if (bad.length) return { action: "review", reason: "bad_cp", bad };
  const sof = entity.source_of_funds || {};
  if (sof.risk_summary === "high" || (sof.top_sources||[]).some(s => s.sanctioned)) {
    return { action: "review", reason: "sof" };
  }
  return { action: "allow", score, label: entity.label, category: entity.category };
}

// before wagmi sendTransaction / ethers send
export async function assertSendable(to, chain = "ETH") {
  const entity = await enrichAddress(to, chain);
  const d = decideSend(entity);
  if (d.action === "block") throw new Error(`KYT block: ${entity.label}`);
  return { entity, decision: d };
}
Enter fullscreen mode Exit fullscreen mode

Python — CLI for desks / bots

import json, sys, requests
BASE = "https://intelapi.publicaml.org"
BAD = {"mixer","sanction","scam","phishing","romance","hack","darknet","ransomware","terrorism"}

def enrich(wallet, chain="ETH"):
    r = requests.post(f"{BASE}/v1/enrich", json={
        "addresses":[{"wallet_address": wallet, "chain": chain}],
        "include":["aml_score","category","counterparties","source_of_funds"],
        "top_n": 10,
    }, timeout=60)
    r.raise_for_status()
    data = r.json()
    if not data.get("entities"):
        raise SystemExit(data.get("not_found"))
    return data["entities"][0]

def decide(e):
    if e.get("sanctioned") is True: return {"action":"block","reason":"sanctioned"}
    score = float(e.get("aml_score") or 0)
    if score >= 60: return {"action":"review","score":score}
    if score >= 25: return {"action":"monitor","score":score}
    if any(str(cp.get("category") or "").lower() in BAD for cp in e.get("counterparties") or []):
        return {"action":"review","reason":"bad_cp"}
    return {"action":"allow","score":score,"label":e.get("label")}

if __name__ == "__main__":
    e = enrich(sys.argv[1], sys.argv[2] if len(sys.argv)>2 else "ETH")
    print(json.dumps({"decision": decide(e), "label": e.get("label"), "aml_score": e.get("aml_score"), "sanctioned": e.get("sanctioned")}, indent=2))
Enter fullscreen mode Exit fullscreen mode

More endpoints worth wiring

# edges between two addresses
curl -sS -X POST 'https://intelapi.publicaml.org/v1/transactions' \
  -H 'Content-Type: application/json' \
  -d '{"from":"0x4d5e63f1981444a4923f94fea61596b53a057113","to":"0xdd3d3c05a672b8a1112e158cd2fc6f577c2b6e1f","chain":"BSC","limit":20}'

# targeted counterparty / category query
curl -sS -X POST 'https://intelapi.publicaml.org/v1/counterparties' \
  -H 'Content-Type: application/json' \
  -d '{"wallet_address":"0x9696f59e4d72e237be84ffd425dcad154bf96976","chain":"ETH","category":"mixer"}'

# dataset coverage
curl -sS 'https://intelapi.publicaml.org/stats' | jq '{clusters, counterparties}'
Enter fullscreen mode Exit fullscreen mode

Also on the docs page: /v1/entities, /v1/cluster-mappings, /v1/address-transactions, /health.

Field cheat sheet

Signal Action
sanctioned: true refuse
aml_score >= 60 high → review
aml_score 25..59 medium → monitor
dirty counterparties[].category investigate
cluster_size > 1000 treat as entity via cluster_id

Open the sandbox, paste an address, hit Copy as curl, then mirror that JSON in your wallet preflight. Product site: publicaml.org.

Top comments (0)