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})` : "")
);
}
}
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 });
}
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>
);
}
UX tips
- Show score + label (“CEX / Bitfinex”) — raw numbers alone create support tickets.
- Cache enrich results for a few minutes per address while the user edits amount.
- 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).
- 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)