BchainPay logoBchainPay
ComplianceFATFTravel RuleVASPStablecoins

FATF Travel Rule for crypto payment gateways: what VASPs must send

FATF Travel Rule for crypto gateways: $1,000 threshold, IVMS101 payloads, TRISA counterparty lookup and the audit log regulators actually inspect.

By Cipher · Founding engineer, BchainPay9 min read
Illustration for FATF Travel Rule for crypto payment gateways: what VASPs must send

The Financial Action Task Force (FATF) Recommendation 16 -- the Travel Rule -- requires entities that transfer value on behalf of another person to pass originator and beneficiary identifying data alongside every qualifying transaction. Banks have lived with this since 1996. In 2019, FATF explicitly extended it to Virtual Asset Service Providers (VASPs). In 2026, regulators in the EU, UK, Singapore, Switzerland, Canada, and the US treat it as a baseline compliance requirement.

This post is for engineers building payment gateways: exact data format (IVMS101), wire protocols, a TypeScript integration pattern, and the hard cases FATF guidance glosses over.

Does the Travel Rule apply to your gateway?#

Before writing any code, confirm you are in scope. FATF defines a VASP as any entity that exchanges, transfers, safeguards, or administers virtual assets on behalf of another person. A payment gateway that temporarily holds USDC in a float account before settling to a merchant is a VASP. So is a custodial on-ramp, a fiat off-ramp, and any service that generates and controls deposit addresses on behalf of customers.

Non-custodial infrastructure -- RPC nodes, open-source wallet software, indexers -- falls outside scope. The dividing line is custody: if funds flow through an account you control, you are a VASP.

Apply a four-question test per transfer:

  1. Is your platform a regulated VASP/CASP in the relevant jurisdiction?
  2. Is the originating address custodied by a known counterparty VASP?
  3. Is the destination address custodied by a known counterparty VASP?
  4. Does the transfer amount exceed the applicable threshold?

Questions 2 and 3 drive whether you transmit, request, or simply retain Travel Rule data. Question 4 determines when obligations kick in.

Thresholds by jurisdiction#

FATF's standard de minimis is $1,000 USD (or equivalent). Several key regimes differ:

Jurisdiction VASP-to-VASP threshold Self-hosted wallet
FATF standard $1,000 $1,000
EU (TFR, Reg 2023/1113) €0 — no floor €1,000 ownership verification
Switzerland (FINMA) CHF 1,000 CHF 1,000
Singapore (MAS PSN02) SGD 1,500 SGD 1,500
UK (FCA MLR 2017) £1,000 £1,000

The EU rule is the most demanding: for CASP-to-CASP transfers, Travel Rule data must accompany every transfer with no floor at all. The €1,000 figure applies only to the enhanced obligation for self-hosted wallets (ownership verification), not to the general inter-VASP info requirement.

If you serve EU customers, code for the zero-threshold path.

The data you must send: IVMS101#

IVMS101 (Interoperability and Virtual Assets Messaging Standard 101) is the widely adopted common data model for Travel Rule messages, published by the Joint Working Group on interVASP Messaging Standards. It defines four top-level objects: originatingVASP, beneficiaryVASP, originator, and beneficiary. Transport metadata -- amount, asset, transaction hash -- sits above IVMS101 in the wire protocol layer, not inside it.

A canonical IVMS101 payload for a natural-person originator:

{
  "originatingVASP": {
    "originatingVASP": {
      "legalPerson": {
        "name": [
          {
            "nameIdentifier": [
              {
                "legalPersonName": "BchainPay Inc.",
                "legalPersonNameIdentifierType": "LEGL"
              }
            ]
          }
        ],
        "nationalIdentification": {
          "nationalIdentifier": "5493001KJTIIGC8Y1R12",
          "nationalIdentifierType": "LEIX"
        },
        "geographicAddress": [
          { "addressType": "BIZZ", "country": "US" }
        ]
      }
    }
  },
  "originator": {
    "originatorPersons": [
      {
        "naturalPerson": {
          "name": [
            {
              "nameIdentifier": [
                {
                  "primaryIdentifier": "Smith",
                  "secondaryIdentifier": "Alice",
                  "nameIdentifierType": "LEGL"
                }
              ]
            }
          ]
        }
      }
    ],
    "accountNumber": ["0xA1b2C3d4E5f6c9d8E7f6a5b4c3d2e1f0"]
  },
  "beneficiary": {
    "beneficiaryPersons": [
      {
        "naturalPerson": {
          "name": [
            {
              "nameIdentifier": [
                {
                  "primaryIdentifier": "Jones",
                  "secondaryIdentifier": "Bob",
                  "nameIdentifierType": "LEGL"
                }
              ]
            }
          ]
        }
      }
    ],
    "accountNumber": ["0xB9c8D7e6F5a4b3c2D1e0f9a8B7c6d5e4"]
  }
}

A few field semantics worth knowing:

  • legalPersonNameIdentifierType: "LEGL" = registered legal name; SHRT = short name; TRAD = trading name.
  • nationalIdentifierType: "LEIX" = LEI (ISO 17442 20-character code). Also valid: RAID, DUNS, MISC. LEI is not strictly mandatory by the IVMS101 spec but is expected in practice -- every regulated VASP should have one.
  • accountNumber is an array; a customer can legitimately have multiple deposit addresses on file.
  • For a legal-entity originator, replace naturalPerson with legalPerson and supply name and nationalIdentification at that level.

The beneficiaryVASP block mirrors originatingVASP and identifies the receiving platform. You populate it after counterparty VASP discovery.

Wire protocols: TRISA, TRP, and hosted platforms#

FATF mandates information exchange but not the mechanism. Three approaches cover most real flows in 2026:

Approach Model Typical latency Coverage
TRISA (protocol) mTLS peer-to-peer; each VASP runs a node 200 ms – 2 s ~100+ VASPs; directory via TRISA Global Trust Directory
TRP (protocol) REST + W3C DIDs; Fireblocks-backed 50 ms – 1 s Strong among institutional CeFi
Notabene / Sygna / 21A (SaaS platforms) Hosted intermediary; implement TRISA/TRP on your behalf 200 ms – 500 ms Largest combined directory; 70-80% counterparty reach

TRISA and TRP are open protocols you can implement yourself. Notabene, Sygna, and 21Analytics are commercial platforms that implement one or more of those protocols and manage counterparty directories for you. For a new gateway the practical path is: start with a SaaS platform for broad coverage, add a direct TRISA node connection if you have high volume with a specific counterparty.

Integration pattern (TypeScript, REST)#

interface TravelRuleTransfer {
  asset: string;
  amountMinorUnits: bigint;
  fromAddress: string;
  toAddress: string;
  txHash: string;
  senderKyc: KycRecord;
  recipientKyc: KycRecord;
}
 
interface TrResult {
  id: string;
  status: "ACCEPTED" | "REJECTED" | "PENDING" | "WAITING";
}
 
async function sendTravelRuleMessage(
  transfer: TravelRuleTransfer
): Promise<TrResult> {
  const counterpartyDid = await resolveBeneficiaryVasp(transfer.toAddress);
 
  const payload = {
    transactionAsset: transfer.asset,
    transactionAmount: transfer.amountMinorUnits.toString(),
    transactionBlockchainInfo: {
      origin: transfer.fromAddress,
      destination: transfer.toAddress,
      txHash: transfer.txHash,
    },
    originatorVaspDid: process.env.OWN_VASP_DID!,
    beneficiaryVaspDid: counterpartyDid,
    ivms101: buildIvms101(transfer),          // canonical payload above
  };
 
  const res = await fetch(`${process.env.TR_PLATFORM_BASE}/v1/transfers`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.TR_PLATFORM_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });
 
  if (!res.ok) {
    throw new TravelRuleError(await res.text(), res.status);
  }
  return res.json();
}
 
async function resolveBeneficiaryVasp(address: string): Promise<string> {
  // Most platforms expose a directory lookup endpoint
  const res = await fetch(
    `${process.env.TR_PLATFORM_BASE}/v1/vasps/lookup?address=${encodeURIComponent(address)}`,
    { headers: { Authorization: `Bearer ${process.env.TR_PLATFORM_TOKEN}` } }
  );
  if (!res.ok) return "UNKNOWN";
  const { did } = await res.json();
  return did ?? "UNKNOWN";
}

Plug the check into the payment intent pipeline after address screening (OFAC post) and before chain submission:

const TRAVEL_RULE_THRESHOLD_USD = Number(process.env.TR_THRESHOLD_USD ?? "1000");
 
async function handlePaymentIntent(intent: PaymentIntent) {
  const usdValue = await toUSD(intent.amount, intent.asset);
 
  // EU CASPs: use 0 for VASP-to-VASP; others: 1000
  const threshold = isEuJurisdiction(intent) ? 0 : TRAVEL_RULE_THRESHOLD_USD;
 
  if (usdValue >= threshold) {
    const sender = await requireKyc(intent.senderId);
    const recipient = await requireKyc(intent.recipientId);
 
    const tr = await sendTravelRuleMessage(
      buildTransfer(intent, sender, recipient)
    );
 
    await db.intents.update(intent.id, {
      travelRuleRef: tr.id,
      travelRuleStatus: tr.status,
    });
 
    if (tr.status === "REJECTED") {
      return { status: "blocked", reason: "travel_rule_counterparty_rejected" };
    }
  }
 
  return submitToChain(intent);
}

Un-hosted wallets: the hard case#

When the destination address belongs to a self-hosted (non-custodial) wallet, there is no counterparty VASP inbox to receive your IVMS101 message. FATF's 2021 updated guidance and national implementations diverge here:

  • FATF baseline: collect and retain originator data; apply enhanced due diligence above the threshold.
  • EU TFR (Art. 14-15): for transfers to or from a self-hosted address above €1,000, take adequate measures to verify the address is controlled by the originator or beneficiary. A signed-message challenge is the standard implementation.
async function requestOwnershipProof(
  walletAddress: string,
  userId: string
): Promise<OwnershipChallenge> {
  const nonce = `bchainpay-tr-${crypto.randomUUID()}`;
 
  await db.ownershipChallenges.insert({
    userId,
    walletAddress,
    nonce,
    expiresAt: new Date(Date.now() + 600_000),  // 10 min
  });
 
  // Front-end prompts: eth_sign(nonce) or Solana signMessage(nonce)
  // POST /v1/wallets/verify validates the returned signature on-chain
  return { nonce, expiresInSeconds: 600 };
}

Store the signed challenge and the verified wallet-address association in your compliance record; regulators will ask for it on audits.

Structuring detection#

Linked transactions that individually fall below the threshold but are clearly part of a single economic event must be treated as one. Build a sliding-window aggregator: sum all transfers from the same originator to the same destination within a 24-hour window, and trigger the Travel Rule check when the aggregate crosses the threshold.

Sunrise issue and fallback policy#

Many VASPs have not yet built Travel Rule receiving infrastructure. When counterparty VASP lookup returns "UNKNOWN":

  1. Hold and retry: pause the transaction for up to 10 minutes while retrying the directory and the TRISA Global Trust Directory.
  2. Out-of-band attempt: look up the counterparty's published Travel Rule contact address. Send via a secure, encrypted channel only -- never transmit PII over plain email, Slack, or general support tickets.
  3. Reject above your ceiling: for amounts above $10,000 with no counterparty resolution after the hold window, decline the transfer and ask the sender to use an alternative destination.

Encode the policy in config so compliance can adjust thresholds without a deployment:

{
  "travelRule": {
    "thresholdUSD": 1000,
    "euCasp2CaspNoFloor": true,
    "unknownVasp": {
      "holdMs": 600000,
      "maxAmountAutoRelease": 10000,
      "secureEmailFallback": true
    }
  }
}

Audit log#

Regulators require a durable record of every Travel Rule message sent and received. Minimum retention is 5 years under FATF guidance and major national regimes; confirm local law, which can be longer.

CREATE TABLE travel_rule_records (
  id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  payment_intent_id     UUID NOT NULL REFERENCES payment_intents(id),
  direction             TEXT NOT NULL CHECK (direction IN ('OUTBOUND','INBOUND')),
  counterparty_vasp_did TEXT,
  ivms101_encrypted     BYTEA NOT NULL,     -- AES-256-GCM, KMS-managed key
  protocol              TEXT NOT NULL CHECK (
    protocol IN ('TRISA','TRP','NOTABENE','SYGNA','EMAIL','HELD')
  ),
  tr_status             TEXT NOT NULL CHECK (
    tr_status IN ('ACCEPTED','REJECTED','PENDING','WAITING','UNRESOLVED')
  ),
  amount_usd            NUMERIC(18,2) NOT NULL,
  created_at            TIMESTAMPTZ NOT NULL DEFAULT now()
);
 
CREATE INDEX ON travel_rule_records (payment_intent_id);
CREATE INDEX ON travel_rule_records (created_at);

Store ivms101_encrypted rather than plaintext -- this object contains full PII. Encrypt with the same KMS key used for your hot-wallet secrets (see the key management post).

Key takeaways#

  • A gateway that holds funds in transit is a VASP; the Travel Rule applies.
  • EU CASPs must send IVMS101 on all CASP-to-CASP transfers with no floor; most other jurisdictions use the $1,000 or equivalent threshold.
  • IVMS101 is the canonical data model: originatingVASP, beneficiaryVASP, originator, beneficiary. Amount and asset live in the wire protocol layer above it, not inside the IVMS101 object.
  • SaaS platforms (Notabene, Sygna) offer the widest counterparty coverage fastest; TRISA and TRP are the underlying open protocols.
  • Un-hosted wallets require originator data retention; EU TFR Art. 14-15 mandates ownership verification above €1,000.
  • Build a hold / retry / reject policy for sunrise cases; never transmit PII over unencrypted channels.
  • Audit-log every message for at least 5 years, with PII encrypted at rest using your KMS-managed key.

Try it yourself

Spin up a sandbox merchant in under 60 seconds.

One REST endpoint, signed webhooks, five chains. No credit card required.

Related reading