DEV Community

PublicAML
PublicAML

Posted on Originally published at publicaml.org

Add Pre-Transaction KYT Screening to wagmi / ethers (Free API)

Add Pre-Transaction KYT Screening to wagmi / ethers (Free API)

Most wallet UX bugs that lose user funds are not smart-contract bugs — they are wrong destination bugs. A 150ms KYT (Know Your Transaction) check on to before sendTransaction is cheap insurance.

Here is a pattern that works with ethers and wagmi, using the free PublicAML enrich API (publicaml.org).

Shared enrich helper

export type Risk = {
  amlScore: number;
  label?: string | null;
  category?: string | null;
};

export async function enrichEvmAddress(address: `0x${string}`): Promise<Risk> {
  const res = await fetch("https://intelapi.publicaml.org/v1/enrich", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      addresses: [{ wallet_address: address, chain: "ETH" }],
    }),
  });
  if (!res.ok) throw new Error(`enrich ${res.status}`);
  const entity = (await res.json()).entities?.[0];
  return {
    amlScore: Number(entity?.aml_score ?? 0),
    label: entity?.label,
    category: entity?.category,
  };
}

export function assertSendable(risk: Risk, hardLimit = 70) {
  if (risk.amlScore >= hardLimit) {
    throw new Error(
      `KYT block: score ${risk.amlScore}` +
        (risk.label ? ` (${risk.label})` : "")
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

ethers v6

import { BrowserProvider } from "ethers";

async function sendWithKyt(to: `0x${string}`, valueWei: bigint) {
  const risk = await enrichEvmAddress(to);
  if (risk.amlScore >= 40 && risk.amlScore < 70) {
    const ok = window.confirm(
      `Medium risk destination (score ${risk.amlScore}` +
        `${risk.label ? `, ${risk.label}` : ""}). Continue?`
    );
    if (!ok) return;
  }
  assertSendable(risk);

  const provider = new BrowserProvider(window.ethereum);
  const signer = await provider.getSigner();
  return signer.sendTransaction({ to, value: valueWei });
}
Enter fullscreen mode Exit fullscreen mode

wagmi (React)

Call enrich in the click handler before writeContract / sendTransaction:

import { useSendTransaction } from "wagmi";
import { parseEther } from "viem";
import { enrichEvmAddress, assertSendable } from "./kyt";

export function SendButton({ to }: { to: `0x${string}` }) {
  const { sendTransactionAsync, isPending } = useSendTransaction();

  async function onSend() {
    const risk = await enrichEvmAddress(to);
    // Optional: setRiskBanner(risk) for inline UI
    assertSendable(risk);
    await sendTransactionAsync({ to, value: parseEther("0.01") });
  }

  return (
    <button disabled={isPending} onClick={onSend}>
      Send with KYT
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode

UX tips

  1. Show score + label (“CEX / Bitfinex”) — raw numbers alone create support tickets.
  2. Cache enrich results for a few minutes per address while the user edits amount.
  3. Do not block the whole app if enrich fails — fail open with a visible “risk check unavailable” banner, or fail closed for high-value transfers (product choice).
  4. Same pattern works for token transfers: screen the recipient, not only the router.

Why free matters here

Commercial KYT often means waiting on sales. For open wallets and hackathon UX, a public enrich call unblocks shipping. PublicAML’s free tier is documented on the site (~1k req/h) — plenty for client-side gates with a small backend cache.

Ship the gate. Then tune thresholds. Users will never thank you for the scam they did not send to — but your support inbox will.

Top comments (0)