前回の記事「ブロックチェーンAMLの基礎 — 資金決済法・犯収法・FATFトラベルルール」では、日本の暗号資産・ステーブルコイン事業者が直面する規制レイヤーを整理しました。
今回はもう一段踏み込み、暗号資産交換業者・電子決済手段等取扱業者が実際に KYT / AML スクリーニングを設計・実装・運用する 際の具体的なパターンを、コードと図で解説します。
💡 KYT について
本記事の「KYT」は Know Your Transaction(取引モニタリング/取引時確認の取引層拡張) を指す一般用語として用います。FATF ガイダンス、FinCEN、JVCEA(日本暗号資産取引業協会)、金融庁の監督指針でも同様の意味で汎用的に使われる業界用語です。Chainalysis 社の製品「Chainalysis KYT」など特定ベンダの製品名とは別物です。
対象読者:
- 暗号資産交換業者/電子決済手段等取扱業者のコンプライアンス・エンジニアリング責任者
- ステーブルコイン関連サービス(JPYC、USDT、USDC 等)を扱うプロダクト開発者
- 金融機関の AML 部門で暗号資産対応を検討している方
TL;DR
KYT / AML スクリーニングは 3 つの独立した評価層 に分解して設計するのが実務的です。
| 層 | 評価対象 | 発火タイミング | レイテンシ要求 |
|---|---|---|---|
| L1: 顧客スクリーニング | ユーザー本人(KYC 後) | アカウント開設時+定期再確認 | 分〜時間 |
| L2: トランザクション スクリーニング(KYT) | 入出金の個々のオンチェーン取引 | 送金実行の直前/直後 | 100ms〜500ms |
| L3: 関係先(カウンターパーティ) スクリーニング | 送金元/受取先アドレスとその周辺 | L2 の中で合わせて実行 | L2 と同じ |
それぞれ別のデータソースと判定ロジックを持つので、独立したモジュールとして疎結合に実装するのが保守性と精度の両面で有利です。
1. 3 層モデルの全体像
L1 は 遅延許容(分〜時間)、L2〜L3 は リアルタイム(100-500ms) という特性差があるため、ストレージとキャッシュ戦略も別物にします。
2. L1 顧客スクリーニングの設計
データモデル
PostgreSQL 前提で最小構成。
CREATE TABLE users (
id UUID PRIMARY KEY,
full_name TEXT NOT NULL,
full_name_kana TEXT,
date_of_birth DATE NOT NULL,
nationality CHAR(2) NOT NULL,
address TEXT,
kyc_level SMALLINT NOT NULL DEFAULT 0, -- 0=未完了, 1=初回, 2=強化
aml_risk_tier TEXT CHECK (aml_risk_tier IN ('low', 'medium', 'high')),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE user_screening_results (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
screening_type TEXT NOT NULL, -- 'sanctions', 'pep', 'adverse_media'
source TEXT NOT NULL, -- 'OFAC', 'FATF', 'World-Check', ...
matched BOOLEAN NOT NULL,
match_score NUMERIC(3,2), -- ファジーマッチスコア
matched_entity JSONB, -- マッチ先の詳細
checked_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_uscreen_user_type ON user_screening_results(user_id, screening_type, checked_at DESC);
ファジーマッチの閾値
人名マッチは完全一致ではなく、Jaro-Winkler + 発音表記の正規化 を使います。
from rapidfuzz.fuzz import WRatio
from pykakasi import kakasi
def normalize_name(name: str) -> str:
"""漢字・ひらがな・カタカナ・ローマ字を統一的に正規化"""
kks = kakasi()
kks.setMode("J", "a") # 漢字→ローマ字
kks.setMode("H", "a") # ひらがな→ローマ字
kks.setMode("K", "a") # カタカナ→ローマ字
conv = kks.getConverter()
return conv.do(name).lower().replace(" ", "")
def fuzzy_name_match(candidate: str, targets: list[str], threshold: int = 88) -> list[tuple[str, float]]:
cand_norm = normalize_name(candidate)
hits = []
for t in targets:
score = WRatio(cand_norm, normalize_name(t))
if score >= threshold:
hits.append((t, score))
return sorted(hits, key=lambda x: -x[1])
閾値は 88〜92 が実務的な良い塩梅です。下げすぎると誤検知が爆発し、上げすぎると Yamamoto/Yamaguchi のような似た苗字で漏れが出ます。
再確認の頻度
改正犯収法では 取引関係の継続性確認 が求められ、顧客の属性変化(住所、取引パターン等)を検知する仕組みが必要です。
- 低リスク顧客: 年1回
- 中リスク顧客: 半年に1回
- 高リスク顧客: 3か月に1回
- 重要イベント(大口送金、国外送金、新制裁指定への該当可能性): 即時
3. L2 KYT — 送金時のリアルタイム判定
コアデータフロー
レイテンシー予算
- 合計: 300ms 以下を目標
- OFAC SDN: ≤ 10ms(Redis でメモリアクセス)
- Neo4j: ≤ 80ms(1-3 ホップ BFS、インデックスあり)
- ML 推論: ≤ 100ms(軽量モデル、バッチ推論回避)
- PostgreSQL 書き込み: ≤ 30ms(async commit)
- その他: ≤ 80ms(ネットワーク、JSON 変換等)
並列化の実装パターン
Python asyncio で並列化する典型例:
import asyncio
from dataclasses import dataclass
@dataclass
class KytVerdict:
level: str # 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
score: int
reasons: list[str]
async def evaluate_kyt(tx: TxRequest) -> KytVerdict:
# 並列に評価
sanctions_t, graph_t, ml_t = await asyncio.gather(
check_sanctions(tx.receiver, tx.chain),
check_graph_proximity(tx.receiver, tx.chain),
check_ml_anomaly(tx),
)
reasons: list[str] = []
score = 100
# 制裁 (CRITICAL)
if sanctions_t.matched:
score -= 100
reasons.append(f"OFAC SDN 一致: {sanctions_t.list_name}")
# グラフ近接度 (HIGH)
if graph_t.distance <= 2 and graph_t.category in ("darknet_market", "mixing"):
score -= 40
reasons.append(f"{graph_t.category} まで {graph_t.distance} ホップ")
# ML 異常 (MEDIUM-HIGH)
if ml_t.anomaly_score > 0.85:
score -= 30
reasons.append(f"ML 異常スコア {ml_t.anomaly_score:.2f}")
elif ml_t.anomaly_score > 0.65:
score -= 15
reasons.append(f"ML 注意レベル {ml_t.anomaly_score:.2f}")
level = (
"CRITICAL" if score < 40 else
"HIGH" if score < 70 else
"MEDIUM" if score < 90 else
"LOW"
)
return KytVerdict(level=level, score=max(0, score), reasons=reasons)
asyncio.gather で並列化すると、全体レイテンシは一番遅い子の時間 になります。上記例では Neo4j の 80ms。
ベロシティ チェック
短時間での大量送金を検知する:
-- 過去 24h の送金 USD 合算(受取先単位)
SELECT
receiver_address,
SUM(amount_usd) AS total_usd,
COUNT(*) AS tx_count
FROM transactions
WHERE user_id = $1
AND created_at > NOW() - INTERVAL '24 hours'
GROUP BY receiver_address
HAVING SUM(amount_usd) > 10000
OR COUNT(*) > 50;
実務では受取先単位だけでなく、ユーザー単位の合算・受取先クラスタ単位の合算 も別途チェックします(ストラクチャリング対策)。
4. L3 カウンターパーティ — 受取先のリスク評価
Known Entities データセットの運用
OFAC SDN だけでは不十分であるため、以下の追加ソースを統合管理します。
| カテゴリ | 出典例 | エントリ数(目安) |
|---|---|---|
| sanctioned_entity | OFAC SDN, UN 制裁, EU 制裁 | 数百〜数千 |
| darknet_market | WalletExplorer, Chainabuse, 当局公表 | 数十〜数百 |
| mixing | Tornado Cash 派生、CoinJoin プール | 数百 |
| scam / fraud_shop | ChainAbuse, BitcoinAbuse, 独自調査 | 千〜数万 |
| gambling | 業界公開 + 独自調査 | 数百 |
| no_kyc_exchange | 独自調査 | 数十〜百 |
PostgreSQL の 正規化テーブル + Redis による O(1) 照合キャッシュ が鉄板構成。
CREATE TABLE known_entities (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
address TEXT NOT NULL,
chain TEXT NOT NULL,
category TEXT NOT NULL,
label TEXT NOT NULL,
source TEXT,
first_seen DATE,
sanctioned_at DATE,
active BOOLEAN DEFAULT TRUE,
UNIQUE(chain, address)
);
CREATE INDEX idx_ke_lookup ON known_entities(chain, address) WHERE active = TRUE;
CREATE INDEX idx_ke_category ON known_entities(category) WHERE active = TRUE;
起動時に全件を Redis にロードし、マッチ後に Redis を TTL 付き更新(例: 1時間)で整合性を担保します。
Neo4j でのグラフ近接度
直接一致(O(1))で引っかからない場合、1-3 ホップのグラフ近接度で評価します。
MATCH (t:Address {address: $addr, chain: $chain})
MATCH path = (t)-[:TX*1..3]-(neighbor:Address)
WHERE neighbor.category IN ['sanctioned_entity', 'darknet_market', 'mixing']
AND NOT (neighbor = t)
WITH
neighbor,
length(path) AS hops,
reduce(amount = 0.0, r IN relationships(path) | amount + coalesce(r.amount_usd, 0)) AS total_usd
RETURN neighbor.category, neighbor.label, min(hops) AS min_hops, sum(total_usd) AS exposure_usd
ORDER BY min_hops ASC, exposure_usd DESC
LIMIT 10;
ホップ数と露出額の両方を返すと、「制裁対象に 2 ホップ 100 万円」vs「ダークネット市場に 1 ホップ 1 万円」のような 質の差 を判断できます。
ML 異常スコアの組み合わせ
Neo4j サブグラフを特徴量に変換して、以下のアンサンブルで判定:
実装 Tips:
- Isolation Forest: scikit-learn で十分、ONNX 化で推論 5ms
- AutoEncoder: PyTorch Lightning、ONNX Runtime で 10-30ms
- GraphSAGE: PyTorch Geometric、CPU 推論で 50-100ms
- モデルが一部失敗しても残りで判定できるよう 失敗時は重みを 0 に、残り重みを正規化 して継続
5. トラベルルールの実装統合
KYT の途中で閾値を超えた送金を検知したら、トラベルルール プロトコルに引き渡します。
async def maybe_trigger_travel_rule(tx: TxRequest, verdict: KytVerdict) -> None:
if tx.amount_usd < 3000.0:
return # しきい値未満
# 受取先 VASP の特定(既知取引所 DB で照合)
receiver_vasp = await lookup_vasp(tx.receiver, tx.chain)
if receiver_vasp is None:
# Self-custody wallet の可能性高 — トラベルルールの適用範囲外だが記録は残す
await log_below_threshold_selfcustody(tx)
return
# IVMS 101 メッセージ生成
ivms = build_ivms_101(
originator=tx.user,
beneficiary=receiver_vasp.get_beneficiary(tx.receiver),
amount=tx.amount, asset=tx.asset_symbol, network=tx.chain,
)
# Notabene / Sumsub / TRP 等のプロトコルへ
await travel_rule_provider.send(ivms, receiver_vasp=receiver_vasp)
VASP 特定の実務
受取先が取引所のホットウォレットかどうかを高速判定するために、取引所レジストリ を持っておきます。
CREATE TABLE exchange_hot_wallets (
id UUID PRIMARY KEY,
exchange_name TEXT NOT NULL, -- 'Binance', 'Coinbase', 'bitFlyer', ...
chain TEXT NOT NULL,
address TEXT NOT NULL,
japan_fsa_status TEXT, -- 'registered', 'not_registered', 'enforcement_action'
travel_rule_protocol TEXT, -- 'notabene', 'sumsub', 'trp', 'veriscope', null
UNIQUE(chain, address)
);
日本法域では、受取側 VASP の金融庁登録状況 によって要件の重さが変わります。未登録交換業者への送金は自動で HIGH 判定 + 追加確認 をかけるのが安全です。
6. パフォーマンス最適化のコツ
実運用で詰まりやすいポイント:
1) OFAC SDN は Redis メモリ展開
ファイル I/O や DB クエリは避け、SET/SISMEMBER で 1ms 以下に。更新は週次クロンでフル再構築(数秒で終わる)。
2) Neo4j クエリの上限パス数制御
MATCH (t)-[:TX*1..3] は指数爆発しやすいので、LIMIT とパス上の amount_usd > threshold フィルタを必ず入れる。
3) ML 推論はバッチ化しない
KYT は 1 TX 単位で呼ばれるので、バッチ推論のレイテンシメリットはなくむしろ悪化。ONNX Runtime の InferenceSession を再利用してシングル推論を最適化。
4) 判定結果のキャッシュ
同じ (user_id, receiver_address, chain) の組み合わせは短期間(5-10 分)キャッシュ可能。UI の「見積もり」系呼び出しで爆撃されない対策。
5) PostgreSQL は async commit
KYT 結果の保存は監査上の要請で必須だが、同期コミットすると 20-50ms 取られる。synchronous_commit = off または レプリケーション先のみ fsync で大幅短縮。
7. 監査・運用の観点
運用に入ると発生する典型課題:
閾値チューニング
初期閾値は保守的(= 検知漏れより誤検知を許容)に設定し、週次で誤検知レビュー → 閾値を少しずつ緩める サイクルを回します。これはロードマップ必須です。
疑わしい取引の届出 (SAR) 準備
CREATE TABLE sar_drafts (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
trigger_tx_id UUID,
triggered_at TIMESTAMPTZ NOT NULL,
verdict_level TEXT NOT NULL,
reasons JSONB NOT NULL,
narrative_draft TEXT, -- AIで下書き生成
status TEXT NOT NULL DEFAULT 'pending', -- pending / submitted / rejected
reviewed_by UUID,
reviewed_at TIMESTAMPTZ,
jafic_submission_id TEXT -- 国家公安委員会提出 ID
);
LLM を使って narrative(疑わしい取引の内容記述)を自動下書きすると、届出作成時間が 80% 削減 できた事例もあります。最終レビューと提出は人間が行います。
7 年保存の運用
- 取引記録・判定結果・通信ログ・KYC 書類・サンクション スクリーニング履歴
- S3 / Azure Blob の Object Lock + 暗号化 + アクセス ログ で改竄不能保管
- 期限切れ後の確実な削除プロセスも設計(GDPR / 個人情報保護法との兼ね合い)
8. 運用のメトリクス
ダッシュボードに出すべき KPI:
| 指標 | 目標値 | アラート閾値 |
|---|---|---|
| KYT p95 レイテンシ | < 300ms | > 500ms で PagerDuty |
| 誤検知率(CRITICAL のうち手動レビューで OK 判定) | < 10% | > 25% |
| 未レビュー CRITICAL 件数 | 0 | > 5 件 |
| OFAC リスト更新遅延 | < 24h | > 48h |
| 疑わしい取引の届出 SLA(検知→提出) | < 7 日 | > 30 日 |
9. 国産のマルチチェーン AML ツールとして
筆者は株式会社refinancier として、上記の内容を組み込んだ ChainAnalyzer を運営しています。
- 9チェーン対応(Bitcoin・Ethereum・Polygon・Base・Arbitrum・Optimism・Avalanche・Solana の 8 チェーン即時対応 + BNB Smart Chain は Enterprise ロールアウト中; いつの間にか増えてしまいました)
- ステーブルコイン特化 AML(JPYC・USDT・USDC・PYUSD・FDUSD)
- FATF トラベルルール対応のしきい値検知
- Known Entities レジストリ(10 カテゴリ)
- Neo4j グラフ近接度 + ML 3モデル アンサンブル
- Case Management(複数アドレス調査ケース)
- KYT 風カスタム アラート(ユーザー定義ルール)
- 個人タグ(「自社ウォレット」等のユーザー固有ラベル)
- 日本語ネイティブ UI・ドキュメント
- FISC 準拠セキュリティ統制
- Azure Japan East ホスティング
- 無料から Enterprise 個別見積まで
エンタープライズ向けのご相談は お問い合わせページ からお願いします。
まとめ
- KYT / AML スクリーニングは L1 顧客 / L2 トランザクション / L3 カウンターパーティ の 3 層で独立に設計
- L2〜L3 はリアルタイム性が肝。OFAC は Redis メモリ化、Neo4j はホップ数制御、ML は ONNX シングル推論
- トラベルルールは $3,000 相当閾値で発火、VASP 特定 → IVMS 101 → Notabene / Sumsub 等
- 運用は 誤検知率の定期レビュー + 疑わしい取引の届出 (SAR) ドラフト自動化 で回す
- 国産ツールはまだ空白。エンジニアとして入り込むチャンスの大きい領域です
次回はおそらく 「Neo4j で資金フロー調査 — アドレス ポイズニング検知の仕組み」 を書く予定です。
ご質問・ご意見はコメントでお気軽に。LGTM / ストックも励みになります。
参考リンク:
- 金融庁: 暗号資産・電子決済手段関係
- FATF: Travel Rule Guidance (英語)
- IVMS 101 仕様(英語)
- Notabene: Travel Rule Protocol
- ChainAnalyzer — 国産マルチチェーン AML
ChainAnalyzer では、Solana / EVM / Bitcoin を含むマルチチェーンの
AMLリスクスコアリング、制裁リスト照合、ウォレットドレイナー検知、
トランザクション追跡を提供しています。
- 公式サイト: https://chain-analyzer.com/ja
- MCP Server: https://github.com/rascal-3/chainanalyzer-mcp
- 技術ドキュメント: https://chain-analyzer.com/ja/docs