FATF Travel Rule Compliance Setup
FATF Recommendation 16 (Travel Rule) requires that when transferring virtual assets between VASPs, information about sender and recipient must be transmitted. This is the most technically complex requirement in the FATF R15-16 package: there is no single protocol, several competing solutions, and the "sunrise issue" when one VASP is compliant but another is not.
What Travel Rule Requires
Originator information (sender):
- Name
- Account number (blockchain address or account ID)
- Physical address / date of birth / national ID number (one of)
Beneficiary information (recipient):
- Name
- Account number
Threshold: FATF recommends $1,000/€1,000. Reality: different jurisdictions set different thresholds. EU TFR: €0 (all transfers). US FinCEN: $3,000. Most others: $1,000.
Architectural Solutions
Travel Rule Messaging Providers
Not every VASP builds its own infrastructure — there are specialized messaging networks:
Notabene: largest coverage (500+ VASP members), SSI-based (Self-Sovereign Identity), RESTful API.
Sygna Bridge: strong in Asia (BiTSO, OKX, Huobi use it), good for Asian market compliance.
Veriscope (Shyft Network): blockchain-based Travel Rule, decentralized.
TRP (Travel Rule Protocol, SWIFT): for large banks with crypto arms.
OpenVASP: open protocol, peer-to-peer, doesn't require hub. Less adoption, but technically interesting.
Notabene Integration
import { Notabene } from "@notabene/javascript-sdk";
const notabene = new Notabene({
audience: "https://api.notabene.id",
clientId: NOTABENE_CLIENT_ID,
clientSecret: NOTABENE_CLIENT_SECRET,
vaspDID: MY_VASP_DID,
});
// Create outgoing Travel Rule transfer
async function createTravelRuleTransfer(withdrawal: Withdrawal): Promise<string> {
const transfer = await notabene.transfers.create({
transactionAsset: withdrawal.asset,
transactionAmount: withdrawal.amount.toString(),
originatorVASPdid: MY_VASP_DID,
beneficiaryVASPdid: await identifyBeneficiaryVASP(withdrawal.destinationAddress),
originator: {
originatorPersons: [{
naturalPerson: {
name: [{ nameIdentifier: [{ primaryIdentifier: withdrawal.userLastName,
secondaryIdentifier: withdrawal.userFirstName }] }],
},
geographicAddress: [{ streetName: withdrawal.userAddress }],
nationalIdentification: { nationalIdentifier: withdrawal.userIdNumber },
}],
accountNumber: [MY_VASP_ADDRESS_MAPPING[withdrawal.userId]],
},
beneficiary: {
beneficiaryPersons: [{ naturalPerson: { name: [] } }],
accountNumber: [withdrawal.destinationAddress],
},
transactionBlockchainInfo: {
origin: withdrawal.fromAddress,
destination: withdrawal.destinationAddress,
},
});
return transfer.id;
}
VASP Identification by Address
Key task: determine if destination address belongs to another VASP (hosted wallet) or is an unhosted wallet.
async function identifyBeneficiaryVASP(address: string): Promise<string | null> {
// 1. Notabene VASP lookup (database of VASP addresses)
const vaspLookup = await notabene.addresses.lookup({ address });
if (vaspLookup.vasp) return vaspLookup.vasp.did;
// 2. Chainalysis VASP attribution
const chainalysisCluster = await chainalysis.getCluster(address);
if (chainalysisCluster?.type === "exchange" || chainalysisCluster?.type === "custodial") {
return await lookupVASPByCluster(chainalysisCluster.name);
}
// 3. If not identified — unhosted wallet
return null;
}
Unhosted Wallet Policy
FATF allows simplified approach for transfers to unhosted wallets (user personal wallets). But many regulators (EU, Switzerland) require additional measures:
async function handleUnhostedWalletWithdrawal(
userId: string,
destinationAddress: string,
amount: number
): Promise<void> {
const usdAmount = await convertToUSD(amount);
if (usdAmount >= UNHOSTED_WALLET_VERIFICATION_THRESHOLD) {
// Require proof of wallet ownership
const ownershipProof = await requestWalletOwnershipProof(userId, destinationAddress);
if (!ownershipProof.verified) {
throw new Error("Wallet ownership verification failed");
}
// Record in travel rule file (without sending — no receiving VASP)
await db.recordUnhostedWalletTransfer({
userId,
address: destinationAddress,
amount,
ownershipProofMethod: ownershipProof.method,
verifiedAt: new Date(),
});
}
await executeWithdrawal(userId, destinationAddress, amount);
}
// Wallet ownership verification — message signing
async function requestWalletOwnershipProof(
userId: string,
address: string
): Promise<OwnershipProof> {
const challenge = crypto.randomBytes(32).toString("hex");
// Store challenge, wait for signature from user
await db.storeWalletChallenge(userId, address, challenge);
// User signs challenge with their wallet
// Verification happens in another endpoint
return { pending: true, challenge };
}
Sunrise Issue
"Sunrise issue" — situation when receiving VASP doesn't support Travel Rule. Options:
- Don't send until confirmation — strictly compliant, poor UX.
- Best efforts — send Travel Rule data if we can, log attempts. Accepted by most regulators as temporary measure.
- Delay + retry — keep transaction pending, retry requests to receiving VASP at intervals.
Recommendation: best efforts approach with full logging of all attempts and responses.
Technical Stack
| Component | Solution |
|---|---|
| Travel Rule messaging | Notabene SDK |
| VASP identification | Notabene + Chainalysis |
| Wallet ownership proof | EIP-191 message signing |
| Records storage | PostgreSQL + encryption |
| Compliance dashboard | React admin panel |
FATF Travel Rule compliance setup with Notabene integration, unhosted wallet policy and compliance dashboard: 3-5 weeks.







