2025年8月、JPYC株式会社は資金移動業登録を受け、同年10月に国内初の資金移動業型円建てステーブルコイン「JPYC」 として稼働を開始しました。Ethereum・Polygon・Avalanche の 3 チェーンで発行され、1 JPYC = 1 円で相互交換できる「電子決済手段」として、B2B 送金・EC 決済・クリエイター経済 など多様なユースケースに使われ始めています。
この記事では、JPYC を B2B 送金のレールに使う サービスを設計する際に、AML・コンプライアンス観点で押さえるべき実装を、具体コードとともに整理します。前回予定していた記事とは異なります(細かすぎるため)。
TL;DR
- JPYC は 電子決済手段(2023 年改正資金決済法)として規制対象
- 発行体 JPYC 株式会社は Elliptic と提携して AML 体制を構築済み
-
利用事業者側(JPYC で送金する B2B サービス)にも AML 責務あり
- 資金決済法(事業スキームにより登録要否が変わる)
- 犯罪による収益の移転防止に関する法律(犯収法)
- FATF トラベルルール(VASP 向けは 日本は犯収法で ¥100,000 超; 当プラットフォームでは$1,000を閾値としています)で送金者・受取者情報共有
- 実装の肝: 受取先スクリーニング → 閾値検知 → トラベルルール発火 → 証跡保全
1. JPYC のネットワーク基礎
発行チェーン
| チェーン | コントラクト タイプ | 特徴 |
|---|---|---|
| Ethereum | ERC-20 | 王道、ガス代高、EVM ツール豊富 |
| Polygon | ERC-20 (L2) | 低ガス代、国内決済向けに人気 |
| Avalanche | ERC-20 (C-Chain) | 高速、対応エコシステムは限定 |
3 チェーン共通の特徴:
- 1 JPYC = 1 JPY の固定レート
- 発行体が 裏付資産(日本円預金・国債) で全額担保
- 償還(JPYC → JPY 日本円) は発行体経由
典型的な B2B 送金フロー
2. 想定されるユースケース
JPYC を B2B 送金レールに使う代表的なパターン:
- クリエイター報酬支払い: デザイナー・翻訳者・開発者への報酬を JPYC で即日払い(海外在住含む)
- 中小企業間の仕入代金決済: 銀行振込より低コスト・24時間即時決済
- 国際送金の一部代替: 日本→海外の送金で JPYC → USDC → 現地 CEX 出金のルート
- EC 受取: 国内 EC 事業者が JPYC 決済を受け入れ
- サブスクリプション型 BtoB: SaaS 事業者が月次 JPYC 請求
それぞれ AML 要件の濃淡が違う。B2B 仕入代金は閾値超過が日常的、クリエイター報酬は少額多数、といった感じです。
3. 受取先スクリーニングの実装
B2B 送金で最もクリティカルなのが 受取先アドレスのリスク評価 です。
最小構成
from dataclasses import dataclass
@dataclass
class ScreeningResult:
level: str # CRITICAL / HIGH / MEDIUM / LOW
reasons: list[str]
can_proceed: bool
async def screen_receiver(
receiver: str, chain: str, amount_jpy: int
) -> ScreeningResult:
reasons: list[str] = []
level = "LOW"
# 1. OFAC SDN 即時照合
if await is_ofac_sanctioned(receiver, chain):
return ScreeningResult(
level="CRITICAL",
reasons=["OFAC SDN 一致"],
can_proceed=False,
)
# 2. 国内捜査公開リスト照合(警察庁公開の凍結対象等)
if await is_domestic_watchlist(receiver, chain):
return ScreeningResult(
level="CRITICAL",
reasons=["国内凍結対象リスト一致"],
can_proceed=False,
)
# 3. ScamDB / Known Entities 照合
entity = await lookup_known_entity(receiver, chain)
if entity:
if entity.category in ("darknet_market", "sanctioned_entity"):
return ScreeningResult(
level="CRITICAL",
reasons=[f"{entity.category}: {entity.label}"],
can_proceed=False,
)
if entity.category in ("mixing", "scam", "fraud_shop"):
reasons.append(f"{entity.category}: {entity.label}")
level = "HIGH"
# 4. Neo4j グラフ近接度(1-3 ホップ)
proximity = await check_graph_proximity(receiver, chain)
if proximity.has_risk_neighbor and proximity.min_hops <= 2:
if level == "LOW":
level = "MEDIUM"
reasons.append(f"risky neighbor: {proximity.category} at {proximity.min_hops} hops")
# 5. ML 異常スコア
ml_score = await compute_ml_anomaly(receiver, chain)
if ml_score > 0.85:
level = "HIGH" if level != "CRITICAL" else level
reasons.append(f"ML anomaly {ml_score:.2f}")
return ScreeningResult(
level=level,
reasons=reasons,
can_proceed=(level != "CRITICAL"),
)
OFAC SDN のインメモリキャッシュ
受取先照合は 送金のたび に走るので高速が必須。
# 起動時にファイルから全件ロード
OFAC_CRYPTO_ADDRESSES: set[str] = set()
async def bootstrap_ofac():
global OFAC_CRYPTO_ADDRESSES
addresses = await download_ofac_sdn_crypto_list()
OFAC_CRYPTO_ADDRESSES = {a.lower() for a in addresses}
async def is_ofac_sanctioned(address: str, chain: str) -> bool:
return address.lower() in OFAC_CRYPTO_ADDRESSES
更新は 毎週水曜の OFAC 定例更新 に合わせてクロン実行。
4. 閾値検知とトラベルルール
JPYC の金額 → USD 換算
JPYC は JPY ペッグなので、USD 換算だけで済みます。
async def to_usd_jpyc(amount_jpyc: int) -> float:
"""amount_jpyc は 6 decimals (JPYC の decimals)"""
jpy = amount_jpyc / (10 ** 6) # JPYC = JPY 1:1
jpy_to_usd = await get_fx_rate_jpy_to_usd() # 例: 1/150.0
return jpy * jpy_to_usd
FX レートは 10 分キャッシュで十分。
トラベルルール 発火
閾値を超えた送金は、IVMS 101 メッセージを生成して受取側 VASP へ 送ります。日本の事業者なら犯収法の ¥100,000 超 が一次基準(当プラットフォームでは実際には閾値として$1,000を採用)。
# 日本の犯収法に基づく一次閾値(円建て)
TRAVEL_RULE_THRESHOLD_JPY = 100_000
# FATF VASP 向け推奨閾値(海外 VASP 接続用、保守的に併用)
TRAVEL_RULE_THRESHOLD_USD = 1000.0
async def maybe_fire_travel_rule(
tx: JpycTxRequest,
) -> None:
jpy = tx.amount_jpyc / (10 ** 6) # JPYC = JPY 1:1
usd = await to_usd_jpyc(tx.amount_jpyc)
if jpy <= TRAVEL_RULE_THRESHOLD_JPY and usd <= TRAVEL_RULE_THRESHOLD_USD:
return # 両閾値以下
# 受取先が VASP か self-custody かを判定
vasp = await lookup_vasp_wallet(tx.receiver, tx.chain)
if vasp is None:
# セルフカストディ → トラベルルール対象外だが記録のみ残す
await log_self_custody_transfer(tx)
return
# VASP 間送金 → IVMS 101 メッセージ作成
ivms = build_ivms_101(
originator={
"name": tx.sender.legal_name,
"address": tx.sender.address,
"birthDate": tx.sender.birthdate,
"nationalIdentifier": tx.sender.kyc_id,
},
beneficiary={
"name": vasp.beneficiary_info(tx.receiver).legal_name,
"accountNumber": tx.receiver, # = ウォレットアドレス
},
asset={"symbol": "JPYC", "chain": tx.chain},
amount=tx.amount_jpyc,
)
# Notabene / Sumsub / TRP 等のプロトコル経由で送信
await travel_rule_provider.send(ivms, receiver_vasp=vasp)
VASP 特定の精度が肝
受取先が取引所のホットウォレット か 個人ウォレット かを正確に判定できないと、トラベルルールの発火可否がブレる。
CREATE TABLE exchange_hot_wallets (
chain TEXT NOT NULL,
address TEXT NOT NULL,
exchange_name TEXT NOT NULL,
japan_fsa_status TEXT, -- 'registered' / 'not_registered' / 'enforcement_action'
travel_rule_protocol TEXT, -- 'notabene' / 'sumsub' / 'trp' / null
PRIMARY KEY (chain, address)
);
CREATE INDEX idx_exchange_wallets_lookup ON exchange_hot_wallets(chain, address);
主要国内外取引所のホット ウォレット は OSINT 統合で初期投入 + 定期更新:
- 国内: bitFlyer, Coincheck, bitbank, GMO コイン, BITPOINT 等
- 海外: Binance, Coinbase, Kraken, Bybit, OKX, Bitfinex 等
5. ストラクチャリング(小分け回避)対策
閾値回避のために ¥100,000 未満を分割送金 するパターンには、24-72 時間の合算 ベロシティ チェック を入れます。
-- 過去 24h で同一ユーザー→同一受取先の合算
SELECT SUM(amount_jpy) AS total_jpy, COUNT(*) AS tx_count
FROM transactions
WHERE sender_user_id = $1
AND receiver_address = $2
AND created_at > NOW() - INTERVAL '24 hours';
async def check_structuring(
user_id: str, receiver: str, chain: str
) -> bool:
"""合算で ¥100,000 超えるなら True(= トラベルルール発火 + レビュー)"""
total_jpy = await sum_transfers_24h(user_id, receiver, chain)
return total_jpy > TRAVEL_RULE_THRESHOLD_JPY
さらに保守的にやるなら:
- 同一送金者 → 複数受取先(関連アドレス)の合算
- 同一受取先クラスタ への合算
6. 証跡保全(犯収法 7 年義務)
犯収法では 確認記録・取引記録の 7 年保存 が義務。JPYC 送金でも同様。
保存すべき項目
CREATE TABLE jpyc_transfers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tx_hash TEXT UNIQUE NOT NULL,
chain TEXT NOT NULL, -- 'ethereum' / 'polygon' / 'avalanche'
block_number BIGINT NOT NULL,
sender_user_id UUID REFERENCES users(id),
sender_address TEXT NOT NULL,
receiver_address TEXT NOT NULL,
amount_jpyc BIGINT NOT NULL, -- 6 decimals
amount_jpy NUMERIC(20, 0) NOT NULL,
amount_usd NUMERIC(20, 6),
screening_result JSONB NOT NULL,
travel_rule_fired BOOLEAN DEFAULT FALSE,
travel_rule_message_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_jpyc_sender ON jpyc_transfers(sender_user_id, created_at DESC);
CREATE INDEX idx_jpyc_receiver ON jpyc_transfers(receiver_address);
改竄不能な保管(対応予定)
- Tamper-evident ログ: S3 Object Lock / Azure Immutable Blob に日次ダンプ
- 監査エクスポート: 月次で金融庁・会計監査人向けの CSV エクスポート機能
- 削除前の警告: 7 年経過時に削除候補通知、人間承認後に物理削除
7. 発行体 / 利用事業者 の役割分担
JPYC 発行体と、JPYC を業務利用する事業者では AML 責務のスコープが違います。
| 項目 | JPYC 株式会社(発行体) | 利用事業者(B2B送金サービス等) |
|---|---|---|
| 発行時 KYC | 必須(高強度) | — |
| 顧客 KYC | — | 必須 |
| 送金モニタリング | 発行 / 償還レベルで把握 | 送金実行のたび |
| 制裁照合 | 発行 / 償還時 | 送金実行時 |
| トラベルルール | VASP 間で必須 | VASP であれば必須 |
| 疑わしい取引の届出 | 必要に応じて | 必要に応じて |
| 証跡保全 | 発行 / 償還記録 | 送金記録 |
利用事業者側は「JPYC 発行体が AML 対応してるから安心」ではなく、自前で送金レベルのモニタリングが必要。ここが今回の記事のメインテーマです。
8. チェーン別の実装注意点
Ethereum
- ガスが高い(通常 $10-30 / tx)→ B2B 少額送金には不向き
- Etherscan V2 API で TX 索引が簡単
Polygon
- 国内事業者が選ぶことが多い(低ガス $0.01 / tx)
- Etherscan V2 で
chainid=137として索引可能 - POL(旧 MATIC)ガス支払い
Avalanche
- C-Chain 使用、EVM 互換
- Routescan API(Etherscan 互換、無料)でインデックス
- 対応事業者は限定的
CHAIN_ETHERSCAN_ID = {
"ethereum": "1",
"polygon": "137",
"avalanche": "43114", # Routescan 経由
}
async def fetch_jpyc_transfers(chain: str, contract: str, since_block: int):
chain_id = CHAIN_ETHERSCAN_ID[chain]
# Avalanche は Routescan を使う
base_url = (
"https://api.routescan.io/v2/network/mainnet/evm/43114/etherscan/api"
if chain == "avalanche"
else "https://api.etherscan.io/v2/api"
)
params = {
"chainid": chain_id,
"module": "account",
"action": "tokentx",
"contractaddress": contract,
"startblock": str(since_block),
"apikey": ETHERSCAN_API_KEY,
}
async with httpx.AsyncClient() as client:
r = await client.get(base_url, params=params)
return r.json().get("result", [])
9. 実装のチェックリスト
JPYC B2B 送金サービスを立ち上げるときの AML チェックリスト:
- 暗号資産交換業 / 電子決済手段等取扱業者の登録要否を外部弁護士に確認
- 顧客 KYC / eKYC を実装(本人確認書類 + 実質的支配者確認)
- OFAC SDN リストの週次自動更新
- 国内警察庁公表の凍結対象リスト照合
- ScamDB / Known Entities レジストリ照合(ダークネット・ミキサー等)
- Neo4j グラフ近接度(1-3 ホップ)
- ML 異常スコア(IF + AE 程度で十分開始可能)
- 閾値検知(犯収法 ¥100,000 超 / FATF VASP 向け USD/EUR 1,000 相当超)
- トラベルルール プロトコル選定(Notabene / Sumsub / TRP 等)
- VASP 特定用の取引所ホットウォレット レジストリ
- ストラクチャリング対策の合算ベロシティ チェック
- 証跡保全(7 年、改竄不能)
- 疑わしい取引の届出(SAR)ドラフト生成
- 誤検知率の週次モニタリング
- FISC 準拠のセキュリティ統制(最低: 国内リージョン、TLS、暗号化、MFA、監査ログ)
- 外部脆弱性診断 / ペネトレーション テスト(年次)
10. 国産のマルチチェーン AML として
筆者は株式会社refinancier として、ChainAnalyzer を運営しています。JPYC を含むステーブルコインの B2B 送金 AML に必要な機能をワンパッケージで提供:
- JPYC 対応(Ethereum / Polygon / Avalanche 3 チェーン)
- 受取先 5 層スクリーニング(OFAC → ScamDB → Known Entities → Graph → ML)
- FATF トラベルルール 閾値検知とメタデータ生成
- Token Contract Monitor で JPYC コントラクト自体の異常パターン監視(P1-P5)
- Case Management で疑わしい送金を調査ケースとして記録
- Follow Mode で受取先から先の資金フロー自動追跡
- 日本語ネイティブ UI・ドキュメント・メール通知
- FISC 準拠(Azure Japan East ホスティング、暗号化、監査ログ)
- 無料プランから Enterprise 個別見積まで
まとめ
- JPYC 発行体は Elliptic と提携して AML 体制整備済み、利用事業者側にも別途 AML 責務あり
- 実装の肝は 受取先スクリーニング → 閾値検知 → トラベルルール → 証跡保全
- Polygon が低ガスで国内 B2B 送金に現実的、Avalanche は Routescan 経由で索引
- 7 年の取引記録保存、ストラクチャリング対策の合算ベロシティは必須
- JPYC B2B 送金サービス立ち上げ前に、外部弁護士 + AML ツール選定を
このシリーズはこれで一区切りです。次は個別トピック(例: ブリッジ経由の資金追跡、Neo4j でのコミュニティ検出、etc.)を不定期で書いていく予定です。
ChainAnalyzer 公式サイト の無料プランで、ぜひ実装のイメージを掴んでください。
参考リンク:
- JPYC 株式会社 公式サイト
- JPYC Ethereum コントラクト(Etherscan)
- 金融庁: 電子決済手段関係
- FATF Recommendation 16 (Travel Rule)
- InterVASP Messaging Standards (IVMS 101)
- 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